Color Picker Example

Color Picker

This example demonstrates how to build a Feature-Rich Interactive Application using ratatui_ruby.

It goes beyond simple widgets to show a complete, real-world architecture for handling: - Complex State Management (Input validation, clipboard interaction) - Mouse Interaction & Hit Testing - Dynamic Layouts - Modal Dialogs

Architecture: Component-Based

This app uses a Strict Component-Based Architecture where every UI element encapsulates its own Rendering, State, and Event Handling.

The Component Contract

Every component implements this duck-type interface:

# Renders the component into the given area
# Caches `area` for hit testing
def render(tui, frame, area)
  @area = area
  # ... render using frame.render_widget
end

# Processes events; returns a symbolic signal or nil
def handle_event(event) -> Symbol | nil
  # Returns :consumed, :submitted, :copy_requested, etc.
end

# Optional: time-based updates
def tick
end

1. The MainContainer (Orchestrator)

The MainContainer class (main_container.rb) owns all child components and orchestrates the UI:

2. Self-Contained Components

Each UI element is a self-contained component:

3. The App (Minimal Runner)

The App class (app.rb) is a thin runner: - Creates the MainContainer. - Runs the main loop: tick β†’ render β†’ poll β†’ handle_event. - Checks for quit events.

Key Features Showcased

πŸ–±οΈ Encapsulated Hit Testing

Components cache their render area (@area) during render. In handle_event, they check @area&.contains?(x, y) to detect clicks. The container never calculates coordinatesβ€”hit testing is fully encapsulated.

πŸ”² Modal Dialogs via Chain of Responsibility

When CopyDialog is active, the MainContainer offers it events first. If it returns :consumed, event propagation stops. This creates modal behavior without explicit flags in the app.

πŸ“‘ Symbolic Signals (Mediator Pattern)

Components return semantic symbols instead of just :consumed: - Input returns :submitted when the user presses Enter. - ExportPane returns :copy_requested when clicked.

The MainContainer interprets these signals to coordinate cross-component communication:

result = @input.handle_event(event)
case result
when :submitted
  @palette.update_color(@input.parsed_color)
  return :consumed
end

⏱️ Lifecycle Hooks (tick)

Components can have time-based updates. Controls#tick delegates to Clipboard#tick to decrement the feedback timer.

Problem Solving: What You Can Learn

Read this example if you are trying to solve: 1. β€œHow do I structure a larger app?” β†’ Use the Component Contract and a Container for orchestration. 2. β€œHow do I handle mouse clicks?” β†’ Cache @area during render; check contains? in handle_event. 3. β€œHow do I make a popup?” β†’ Use Chain of Responsibility: the active modal gets events first. 4. β€œHow do I coordinate between components?” β†’ Use symbolic signals and the Mediator pattern. 5. β€œHow do I validate input?” β†’ Encapsulate validation inside the Input component.

Usage

ruby examples/app_color_picker/app.rb

Comparison: Choosing an Architecture

Complex applications require structured state habits. This Color Picker and the App All Events example demonstrate two different approaches.

The Tool Approach (Color Picker)

Tools require interaction. Users click buttons and drag sliders. Components need to know where they exist on screen for hit testing. The Container orchestrates cross-component effects.

This example uses a Component-Based pattern. Each component owns its own state, rendering, and event handling. The Container routes events and mediates communication.

Use this pattern for forms, editors, and mouse-driven tools.

The Dashboard Approach (AppAllEvents)

Dashboards display data. They rarely require complex mouse interaction. Model-View-Update works best there. State is immutable. Logic is pure. Updates are predictable. This simplifies testing.

Use that pattern for logs, monitors, and data viewers.

Read the source code β†’