Rect Widget Showcase
Demonstrates the Rect class and the Cached Layout Pattern.
Rect is the fundamental geometry primitive for TUI layout. This example shows:
-
Rect attributes: x, y, width, height
-
Rect#contains? for hit testing mouse clicks
-
Layout.split returning cached rects for reuse
-
The layout caching pattern: compute in draw, reuse in handle_input
Controls:
←/→: Adjust sidebar width ↑/↓: Navigate menu items Mouse: Click panels to test Rect#contains? q: Quit
Source Code
# frozen_string_literal: true #-- # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com> # SPDX-License-Identifier: AGPL-3.0-or-later #++ $LOAD_PATH.unshift File.expand_path("../../lib", __dir__) require "ratatui_ruby" # Rect Widget Showcase # # Demonstrates the Rect class and the Cached Layout Pattern. # # Rect is the fundamental geometry primitive for TUI layout. This example shows: # - Rect attributes: x, y, width, height # - Rect#contains? for hit testing mouse clicks # - Layout.split returning cached rects for reuse # - The layout caching pattern: compute in draw, reuse in handle_input # # Controls: # ←/→: Adjust sidebar width # ↑/↓: Navigate menu items # Mouse: Click panels to test Rect#contains? # q: Quit class WidgetRect MENU_ITEMS = ["Dashboard", "Analytics", "Settings", "Logs", "Help"].freeze def initialize @sidebar_width = 20 @selected_index = 0 @last_action = "Click any panel to test Rect#contains?" @click_count = 0 @sidebar_rect = nil @main_rect = nil @controls_rect = nil end def run RatatuiRuby.run do |tui| @tui = tui @hotkey_style = @tui.style(modifiers: [:bold, :underlined]) @label_style = @tui.style(modifiers: [:bold]) @dim_style = @tui.style(fg: :dark_gray) loop do render break if handle_input == :quit end end end private def render @tui.draw do |frame| @main_rect, @controls_rect = @tui.layout_split( frame.area, direction: :vertical, constraints: [ @tui.constraint_fill(1), @tui.constraint_length(8), ] ) @sidebar_rect, @content_rect = @tui.layout_split( @main_rect, direction: :horizontal, constraints: [ @tui.constraint_length(@sidebar_width), @tui.constraint_fill(1), ] ) render_sidebar(frame) render_content(frame) render_controls(frame) end end private def render_sidebar(frame) sidebar = @tui.list( items: MENU_ITEMS, selected_index: @selected_index, highlight_style: @tui.style(fg: :black, bg: :white, modifiers: [:bold]), highlight_symbol: "> ", block: @tui.block(title: "Menu", borders: [:all]) ) frame.render_widget(sidebar, @sidebar_rect) end private def render_content(frame) text_content = [ @tui.text_line(spans: [ @tui.text_span(content: "Active View: ", style: @label_style), @tui.text_span(content: MENU_ITEMS[@selected_index], style: @tui.style(fg: :green)), ]), "", @tui.text_line(spans: [ @tui.text_span(content: "Rect Attributes ", style: @label_style), @tui.text_span(content: "(from Layout.split):", style: @dim_style), ]), " Sidebar: Rect(x:#{@sidebar_rect.x}, y:#{@sidebar_rect.y}, " \ "width:#{@sidebar_rect.width}, height:#{@sidebar_rect.height})", " Content: Rect(x:#{@content_rect.x}, y:#{@content_rect.y}, " \ "width:#{@content_rect.width}, height:#{@content_rect.height})", "", @tui.text_line(spans: [ @tui.text_span(content: "Hit Testing ", style: @label_style), @tui.text_span(content: "(Rect#contains?):", style: @dim_style), ]), " Clicks: #{@click_count} | #{@last_action}", ] paragraph = @tui.paragraph( text: text_content, block: @tui.block(title: "Content", borders: [:all]) ) frame.render_widget(paragraph, @content_rect) end private def render_controls(frame) controls = @tui.block( title: "Controls", borders: [:all], children: [ @tui.paragraph( text: [ @tui.text_line(spans: [ @tui.text_span(content: "LAYOUT", style: @label_style), @tui.text_span(content: " "), @tui.text_span(content: "←", style: @hotkey_style), @tui.text_span(content: ": Shrink sidebar "), @tui.text_span(content: "→", style: @hotkey_style), @tui.text_span(content: ": Expand sidebar "), @tui.text_span(content: "(width: #{@sidebar_width})"), ]), @tui.text_line(spans: [ @tui.text_span(content: "NAVIGATION", style: @label_style), @tui.text_span(content: " "), @tui.text_span(content: "↑↓", style: @hotkey_style), @tui.text_span(content: ": Select menu item "), @tui.text_span(content: "q", style: @hotkey_style), @tui.text_span(content: ": Quit"), ]), "", @tui.text_line(spans: [ @tui.text_span(content: "HIT TESTING", style: @label_style), @tui.text_span(content: " Click any panel → Rect#contains?(x, y) determines which rect was hit."), ]), ] ), ] ) frame.render_widget(controls, @controls_rect) 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: "left" @sidebar_width = [@sidebar_width - 2, 10].max @last_action = "Layout changed: sidebar_width=#{@sidebar_width}" nil in type: :key, code: "right" @sidebar_width = [@sidebar_width + 2, 40].min @last_action = "Layout changed: sidebar_width=#{@sidebar_width}" nil in type: :key, code: "up" @selected_index = (@selected_index - 1) % MENU_ITEMS.size @last_action = "Selected: #{MENU_ITEMS[@selected_index]}" nil in type: :key, code: "down" @selected_index = (@selected_index + 1) % MENU_ITEMS.size @last_action = "Selected: #{MENU_ITEMS[@selected_index]}" nil in type: :mouse, kind: "down", x: click_x, y: click_y handle_click(click_x, click_y) nil else nil end end private def handle_click(x, y) @click_count += 1 if @sidebar_rect&.contains?(x, y) relative_y = y - @sidebar_rect.y - 1 if relative_y >= 0 && relative_y < MENU_ITEMS.size old_item = MENU_ITEMS[@selected_index] @selected_index = relative_y new_item = MENU_ITEMS[@selected_index] @last_action = "sidebar.contains?(#{x},#{y})=true → #{old_item}→#{new_item}" else @last_action = "sidebar.contains?(#{x},#{y})=true (empty area)" end elsif @content_rect&.contains?(x, y) @last_action = "content.contains?(#{x},#{y})=true" elsif @controls_rect&.contains?(x, y) @last_action = "controls.contains?(#{x},#{y})=true" else @last_action = "No rect contains (#{x},#{y})" end end end WidgetRect.new.run if __FILE__ == $0