WidgetLayoutSplit

Demonstrates dynamic geometry management with interactive cycling.

Terminal screens vary in size. Hardcoded positions break when the window resizes. You need a way to organize space dynamically.

This demo showcases the Layout.split mechanism. It provides an interactive playground where you can toggle directions, all seven flex modes, and various constraint types (Fill, Length, Percentage, Min, Ratio) in real-time.

Use it to understand how to build responsive, fluid terminal interfaces that adapt to any window dimensions.

Example

Run the demo from the terminal:

ruby examples/widget_layout_split/app.rb

Source Code

# frozen_string_literal: true

#--
# SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
# SPDX-License-Identifier: MIT-0
#++

$LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
require "ratatui_ruby"

# Demonstrates dynamic geometry management with interactive cycling.
#
# Terminal screens vary in size. Hardcoded positions break when the window resizes. You need a way to organize space dynamically.
#
# This demo showcases the <tt>Layout.split</tt> mechanism. It provides an interactive playground where you can toggle directions, all seven flex modes, and various constraint types (Fill, Length, Percentage, Min, Ratio) in real-time.
#
# Use it to understand how to build responsive, fluid terminal interfaces that adapt to any window dimensions.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_layout_split/app.rb
#
# rdoc-image:/doc/images/widget_layout_split.png
class WidgetLayoutSplit
  DIRECTIONS = [
    { name: "Vertical", value: :vertical },
    { name: "Horizontal", value: :horizontal },
  ].freeze

  FLEX_MODES = [
    { name: "Legacy", value: :legacy },
    { name: "Start", value: :start },
    { name: "Center", value: :center },
    { name: "End", value: :end },
    { name: "Space Between", value: :space_between },
    { name: "Space Around", value: :space_around },
    { name: "Space Evenly", value: :space_evenly },
  ].freeze

  BLOCK_COLORS = %i[red green blue].freeze

  def initialize
    @direction_index = 0
    @flex_index = 0
    @constraint_index = 0
    @hotkey_style = nil
  end

  def run
    RatatuiRuby.run do |tui|
      @tui = tui
      @hotkey_style = tui.style(modifiers: [:bold, :underlined])
      @constraint_demos = build_constraint_demos

      loop do
        render
        break if handle_input == :quit
      end
    end
  end

  private def build_constraint_demos
    [
      {
        name: "Fill (1:2:1)",
        constraints: -> (dir) {
          [
            @tui.constraint_fill(1),
            @tui.constraint_fill(2),
            @tui.constraint_fill(1),
          ]
        },
      },
      {
        name: "Length (10/15/10)",
        constraints: -> (dir) {
          [
            @tui.constraint_length(10),
            @tui.constraint_length(15),
            @tui.constraint_length(10),
          ]
        },
      },
      {
        name: "Percentage (25/50/25)",
        constraints: -> (dir) {
          [
            @tui.constraint_percentage(25),
            @tui.constraint_percentage(50),
            @tui.constraint_percentage(25),
          ]
        },
      },
      {
        name: "Min (5/10/5)",
        constraints: -> (dir) {
          [
            @tui.constraint_min(5),
            @tui.constraint_min(10),
            @tui.constraint_min(5),
          ]
        },
      },
      {
        name: "Ratio (1:4, 2:4, 1:4)",
        constraints: -> (dir) {
          [
            @tui.constraint_ratio(1, 4),
            @tui.constraint_ratio(2, 4),
            @tui.constraint_ratio(1, 4),
          ]
        },
      },
      {
        name: -> (dir) { (dir == :vertical) ? "Mixed (Len 3, Fill, Pct 25)" : "Mixed (Len 20, Fill, Pct 25)" },
        constraints: -> (dir) {
          fixed = (dir == :vertical) ? 3 : 20
          [
            @tui.constraint_length(fixed),
            @tui.constraint_fill(1),
            @tui.constraint_percentage(25),
          ]
        },
      },
      {
        name: "Batch (from_percentages)",
        constraints: -> (_dir) {
          RatatuiRuby::Layout::Constraint.from_percentages([25, 50, 25])
        },
      },
    ]
  end

  private def current_direction
    DIRECTIONS[@direction_index][:value]
  end

  private def current_flex
    FLEX_MODES[@flex_index][:value]
  end

  private def current_constraints
    demo = @constraint_demos[@constraint_index]
    demo[:constraints].call(current_direction)
  end

  private def current_constraint_name
    demo = @constraint_demos[@constraint_index]
    name = demo[:name]
    name.respond_to?(:call) ? name.call(current_direction) : name
  end

  private def render
    @tui.draw do |frame|
      # Split into main content and control panel
      main_area, controls_area = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(6),
        ]
      )

      render_demo_area(frame, main_area)
      render_controls(frame, controls_area)
    end
  end

  private def render_demo_area(frame, area)
    # Split demo area into title and content
    title_area, content_area = @tui.layout_split(
      area,
      direction: :vertical,
      constraints: [
        @tui.constraint_length(1),
        @tui.constraint_fill(1),
      ]
    )

    # Render title
    title = @tui.paragraph(
      text: "Layout.split",
      style: @tui.style(modifiers: [:bold])
    )
    frame.render_widget(title, title_area)

    # Apply current layout settings to 3 colored blocks
    block_areas = @tui.layout_split(
      content_area,
      direction: current_direction,
      flex: current_flex,
      constraints: current_constraints
    )

    block_areas.each_with_index do |block_area, i|
      block = @tui.block(
        title: "Block #{i + 1}",
        borders: [:all],
        border_style: @tui.style(fg: BLOCK_COLORS[i % BLOCK_COLORS.length])
      )
      frame.render_widget(block, block_area)
    end
  end

  private def render_controls(frame, area)
    # Demonstrate Constraint#call - show computed sizes for 100 units
    constraints = current_constraints
    applied = constraints.map { |c| c.(100) } # proc-like invocation
    apply_str = "constraint.(100): #{applied.join(', ')}"

    controls = @tui.block(
      title: "Controls",
      borders: [:all],
      children: [
        @tui.paragraph(
          text: [
            # Row 1: Direction and Flex
            @tui.text_line(spans: [
              @tui.text_span(content: "d", style: @hotkey_style),
              @tui.text_span(content: ": Direction (#{DIRECTIONS[@direction_index][:name]})  "),
              @tui.text_span(content: "f", style: @hotkey_style),
              @tui.text_span(content: ": Flex (#{FLEX_MODES[@flex_index][:name]})"),
            ]),
            @tui.text_line(spans: [
              @tui.text_span(content: "c", style: @hotkey_style),
              @tui.text_span(content: ": Constraints (#{current_constraint_name})  "),
              @tui.text_span(content: "q", style: @hotkey_style),
              @tui.text_span(content: ": Quit"),
            ]),
            # Row 3: Apply demonstration
            @tui.text_line(spans: [
              @tui.text_span(content: apply_str, style: @tui.style(fg: :dark_gray)),
            ]),
          ]
        ),
      ]
    )
    frame.render_widget(controls, area)
  end

  private def handle_input
    case @tui.poll_event
    in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
      :quit
    in type: :key, code: "d"
      @direction_index = (@direction_index + 1) % DIRECTIONS.length
    in type: :key, code: "f"
      @flex_index = (@flex_index + 1) % FLEX_MODES.length
    in type: :key, code: "c"
      @constraint_index = (@constraint_index + 1) % @constraint_demos.length
    else
      nil
    end
  end
end

WidgetLayoutSplit.new.run if __FILE__ == $PROGRAM_NAME