WidgetBox

Demonstrates visual container attributes with interactive cycling.

Widgets often float in void. Without boundaries, interfaces become a chaotic mess of text. Users need structure to parse information.

This demo showcases the Block widget. It provides an interactive playground where you can cycle through different border types, colors, and title alignments in real-time.

Use it to understand how to define distinct areas and create visual hierarchy in your terminal interface.

Example

Run the demo from the terminal:

ruby examples/widget_box/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 visual container attributes with interactive cycling.
#
# Widgets often float in void. Without boundaries, interfaces become a chaotic mess of text. Users need structure to parse information.
#
# This demo showcases the <tt>Block</tt> widget. It provides an interactive playground where you can cycle through different border types, colors, and title alignments in real-time.
#
# Use it to understand how to define distinct areas and create visual hierarchy in your terminal interface.
#
# === Example
#
# Run the demo from the terminal:
#
#   ruby examples/widget_box/app.rb
#
# rdoc-image:/doc/images/widget_box.png
class WidgetBox
  def initialize
    # Border Types (ratatui native styles)
    @border_types = [
      { name: "Plain", type: :plain },
      { name: "Rounded", type: :rounded },
      { name: "Double", type: :double },
      { name: "Thick", type: :thick },
      { name: "Quadrant Inside", type: :quadrant_inside },
      { name: "Quadrant Outside", type: :quadrant_outside },
    ]
    @border_type_index = 0

    # Custom Border Sets
    # NOTE: We define these ONCE in initialize for efficiency.
    @border_sets = [
      { name: "None", set: nil },
      {
name: "Digits (Short)",
set: {
        tl: "1",
        tr: "2",
        bl: "3",
        br: "4",
        vl: "5",
        vr: "6",
        ht: "7",
        hb: "8",
      },
},
      {
name: "Letters (Long)",
set: {
        top_left: "A",
        top_right: "B",
        bottom_left: "C",
        bottom_right: "D",
        vertical_left: "E",
        vertical_right: "F",
        horizontal_top: "G",
        horizontal_bottom: "H",
      },
},
    ]
    @border_set_index = 0

    @colors = [
      { name: "Green", color: "green" },
      { name: "Red", color: "red" },
      { name: "Blue", color: "blue" },
      { name: "Yellow", color: "yellow" },
      { name: "Magenta", color: "magenta" },
    ]
    @color_index = 0

    @title_alignments = [
      { name: "Left", alignment: :left },
      { name: "Center", alignment: :center },
      { name: "Right", alignment: :right },
    ]
    @title_alignment_index = 0

    @styles = [
      { name: "Default", style: nil },
      { name: "Blue on White", style: { fg: "blue", bg: "white", modifiers: [:bold] } },
    ]
    @style_index = 0

    @title_styles = [
      { name: "Default", style: nil },
      { name: "Yellow Bold Underlined", style: { fg: "yellow", modifiers: [:bold, :underlined] } },
    ]
    @title_style_index = 0

    @border_styles = [
      { name: "Default (no border style)", style: nil },
      { name: "Bold Red", style: { fg: "red", modifiers: [:bold] } },
      { name: "Cyan Italic", style: { fg: "cyan", modifiers: [:italic] } },
      { name: "Magenta Dim", style: { fg: "magenta", modifiers: [:dim] } },
    ]
    @border_style_index = 0

    @hotkey_style = nil # Initialized in run when tui is available
  end

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

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

  private def render
    # Get current values
    border_type_config = @border_types[@border_type_index]
    border_set_config = @border_sets[@border_set_index]

    color_config = @colors[@color_index]
    title_alignment_config = @title_alignments[@title_alignment_index]
    style_config = @styles[@style_index]
    title_style_config = @title_styles[@title_style_index]
    border_style_config = @border_styles[@border_style_index]

    # 1. State/View
    # Use border_style if explicitly set; otherwise, only apply color picker
    # color when no content style is set (preserves original border_color behavior)
    effective_border_style = if border_style_config[:style]
      border_style_config[:style]
    elsif style_config[:style].nil?
      @tui.style(fg: color_config[:color])
    end

    # Show overridden status if border_set is active
    type_display = border_type_config[:name]
    if border_set_config[:set]
      type_display += " (Overridden)"
    end

    block = @tui.block(
      title: "Box",
      title_alignment: title_alignment_config[:alignment],
      title_style: title_style_config[:style],
      borders: [:all],
      border_style: effective_border_style,
      border_type: border_type_config[:type],
      border_set: border_set_config[:set],
      style: style_config[:style]
    )

    # Main content
    main_panel = @tui.paragraph(
      text: "Arrow Keys: Change Color\n\nCurrent Color: #{color_config[:name]}",
      block:,
      fg: style_config[:style] ? nil : color_config[:color],
      style: style_config[:style],
      alignment: :center
    )

    # Bottom control panel
    control_panel = @tui.block(
      title: "Controls",
      borders: [:all],
      children: [
        @tui.paragraph(
          text: [
            # Line 1: Main Controls
            @tui.text_line(spans: [
              @tui.text_span(content: "↑↓←→", style: @hotkey_style),
              @tui.text_span(content: ": Color (#{color_config[:name]})  "),
              @tui.text_span(content: "q", style: @hotkey_style),
              @tui.text_span(content: ": Quit"),
            ]),
            # Line 2: Borders
            @tui.text_line(spans: [
              @tui.text_span(content: "space", style: @hotkey_style),
              @tui.text_span(content: ": Border Type (#{type_display})  "),
              @tui.text_span(content: "c", style: @hotkey_style),
              @tui.text_span(content: ": Border Set (#{border_set_config[:name]})"),
            ]),
            # Line 3: Styles
            @tui.text_line(spans: [
              @tui.text_span(content: "s", style: @hotkey_style),
              @tui.text_span(content: ": Style (#{style_config[:name]})  "),
              @tui.text_span(content: "b", style: @hotkey_style),
              @tui.text_span(content: ": Border Style (#{border_style_config[:name]})"),
            ]),
            # Line 4: Title
            @tui.text_line(spans: [
              @tui.text_span(content: "enter", style: @hotkey_style),
              @tui.text_span(content: ": Align Title (#{title_alignment_config[:name]})  "),
              @tui.text_span(content: "t", style: @hotkey_style),
              @tui.text_span(content: ": Title Style (#{title_style_config[:name]})"),
            ]),
          ]
        ),
      ]
    )

    # 2. Render with Frame API
    @tui.draw do |frame|
      main_rect, control_rect = @tui.layout_split(
        frame.area,
        direction: :vertical,
        constraints: [
          @tui.constraint_fill(1),
          @tui.constraint_length(6),
        ]
      )
      frame.render_widget(main_panel, main_rect)
      frame.render_widget(control_panel, control_rect)
    end
  end

  private def handle_input
    # 3. Events
    case @tui.poll_event
    in { type: :key, code: "q" } | { type: :key, code: "c", modifiers: ["ctrl"] }
      :quit
    in type: :key, code: "up"
      @color_index = (@color_index - 1) % @colors.size
    in type: :key, code: "down"
      @color_index = (@color_index + 1) % @colors.size
    in type: :key, code: "left"
      @color_index = (@color_index - 1) % @colors.size
    in type: :key, code: "right"
      @color_index = (@color_index + 1) % @colors.size
    in type: :key, code: " "
      @border_type_index = (@border_type_index + 1) % @border_types.size
    in type: :key, code: "c"
      @border_set_index = (@border_set_index + 1) % @border_sets.size
    in type: :key, code: "enter"
      @title_alignment_index = (@title_alignment_index + 1) % @title_alignments.size
    in type: :key, code: "s"
      @style_index = (@style_index + 1) % @styles.size
    in type: :key, code: "t"
      @title_style_index = (@title_style_index + 1) % @title_styles.size
    in type: :key, code: "b"
      @border_style_index = (@border_style_index + 1) % @border_styles.size
    else
      nil
    end
  end
end

WidgetBox.new.run if __FILE__ == $0