ratatui_ruby 0.3.1 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +14 -12
- data/.builds/ruby-3.3.yml +14 -12
- data/.builds/ruby-3.4.yml +14 -12
- data/.builds/ruby-4.0.0.yml +14 -12
- data/AGENTS.md +54 -13
- data/CHANGELOG.md +186 -1
- data/README.md +17 -15
- data/doc/application_architecture.md +116 -0
- data/doc/application_testing.md +12 -7
- data/doc/contributors/better_dx.md +543 -0
- data/doc/contributors/design/ruby_frontend.md +1 -1
- data/doc/contributors/developing_examples.md +203 -0
- data/doc/contributors/documentation_style.md +97 -0
- data/doc/contributors/dwim_dx.md +366 -0
- data/doc/contributors/example_analysis.md +82 -0
- data/doc/custom.css +14 -0
- data/doc/event_handling.md +119 -0
- data/doc/images/all_events.png +0 -0
- data/doc/images/analytics.png +0 -0
- data/doc/images/block_padding.png +0 -0
- data/doc/images/block_titles.png +0 -0
- data/doc/images/box_demo.png +0 -0
- data/doc/images/calendar_demo.png +0 -0
- data/doc/images/cell_demo.png +0 -0
- data/doc/images/chart_demo.png +0 -0
- data/doc/images/custom_widget.png +0 -0
- data/doc/images/flex_layout.png +0 -0
- data/doc/images/gauge_demo.png +0 -0
- data/doc/images/hit_test.png +0 -0
- data/doc/images/line_gauge_demo.png +0 -0
- data/doc/images/list_demo.png +0 -0
- data/doc/images/list_styles.png +0 -0
- data/doc/images/login_form.png +0 -0
- data/doc/images/map_demo.png +0 -0
- data/doc/images/mouse_events.png +0 -0
- data/doc/images/popup_demo.png +0 -0
- data/doc/images/quickstart_dsl.png +0 -0
- data/doc/images/quickstart_lifecycle.png +0 -0
- data/doc/images/ratatui_logo_demo.png +0 -0
- data/doc/images/readme_usage.png +0 -0
- data/doc/images/rich_text.png +0 -0
- data/doc/images/scroll_text.png +0 -0
- data/doc/images/scrollbar_demo.png +0 -0
- data/doc/images/sparkline_demo.png +0 -0
- data/doc/images/table_flex.png +0 -0
- data/doc/images/table_select.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/index.md +1 -0
- data/doc/interactive_design.md +121 -0
- data/doc/quickstart.md +147 -72
- data/examples/all_events/app.rb +169 -0
- data/examples/all_events/app.rbs +7 -0
- data/examples/all_events/test_app.rb +139 -0
- data/examples/analytics/app.rb +258 -0
- data/examples/analytics/app.rbs +7 -0
- data/examples/analytics/test_app.rb +132 -0
- data/examples/block_padding/app.rb +63 -0
- data/examples/block_padding/app.rbs +7 -0
- data/examples/block_padding/test_app.rb +31 -0
- data/examples/block_titles/app.rb +61 -0
- data/examples/block_titles/app.rbs +7 -0
- data/examples/block_titles/test_app.rb +34 -0
- data/examples/box_demo/app.rb +216 -0
- data/examples/box_demo/app.rbs +7 -0
- data/examples/box_demo/test_app.rb +88 -0
- data/examples/calendar_demo/app.rb +101 -0
- data/examples/calendar_demo/app.rbs +7 -0
- data/examples/calendar_demo/test_app.rb +108 -0
- data/examples/cell_demo/app.rb +108 -0
- data/examples/cell_demo/app.rbs +7 -0
- data/examples/cell_demo/test_app.rb +36 -0
- data/examples/chart_demo/app.rb +203 -0
- data/examples/chart_demo/app.rbs +7 -0
- data/examples/chart_demo/test_app.rb +102 -0
- data/examples/custom_widget/app.rb +51 -0
- data/examples/custom_widget/app.rbs +7 -0
- data/examples/custom_widget/test_app.rb +30 -0
- data/examples/flex_layout/app.rb +156 -0
- data/examples/flex_layout/app.rbs +7 -0
- data/examples/flex_layout/test_app.rb +65 -0
- data/examples/gauge_demo/app.rb +182 -0
- data/examples/gauge_demo/app.rbs +7 -0
- data/examples/gauge_demo/test_app.rb +120 -0
- data/examples/hit_test/app.rb +175 -0
- data/examples/hit_test/app.rbs +7 -0
- data/examples/hit_test/test_app.rb +102 -0
- data/examples/line_gauge_demo/app.rb +190 -0
- data/examples/line_gauge_demo/app.rbs +7 -0
- data/examples/line_gauge_demo/test_app.rb +129 -0
- data/examples/list_demo/app.rb +253 -0
- data/examples/list_demo/app.rbs +12 -0
- data/examples/list_demo/test_app.rb +237 -0
- data/examples/list_styles/app.rb +140 -0
- data/examples/list_styles/app.rbs +7 -0
- data/examples/list_styles/test_app.rb +157 -0
- data/examples/{login_form.rb → login_form/app.rb} +12 -16
- data/examples/login_form/app.rbs +7 -0
- data/examples/login_form/test_app.rb +51 -0
- data/examples/map_demo/app.rb +90 -0
- data/examples/map_demo/app.rbs +7 -0
- data/examples/map_demo/test_app.rb +149 -0
- data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
- data/examples/mouse_events/app.rbs +7 -0
- data/examples/mouse_events/test_app.rb +53 -0
- data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
- data/examples/popup_demo/app.rbs +7 -0
- data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
- data/examples/quickstart_dsl/app.rb +36 -0
- data/examples/quickstart_dsl/app.rbs +7 -0
- data/examples/quickstart_dsl/test_app.rb +29 -0
- data/examples/quickstart_lifecycle/app.rb +39 -0
- data/examples/quickstart_lifecycle/app.rbs +7 -0
- data/examples/quickstart_lifecycle/test_app.rb +29 -0
- data/examples/ratatui_logo_demo/app.rb +79 -0
- data/examples/ratatui_logo_demo/app.rbs +7 -0
- data/examples/ratatui_logo_demo/test_app.rb +51 -0
- data/examples/ratatui_mascot_demo/app.rb +84 -0
- data/examples/ratatui_mascot_demo/app.rbs +7 -0
- data/examples/ratatui_mascot_demo/test_app.rb +47 -0
- data/examples/readme_usage/app.rb +29 -0
- data/examples/readme_usage/app.rbs +7 -0
- data/examples/readme_usage/test_app.rb +29 -0
- data/examples/rich_text/app.rb +141 -0
- data/examples/rich_text/app.rbs +7 -0
- data/examples/rich_text/test_app.rb +166 -0
- data/examples/scroll_text/app.rb +103 -0
- data/examples/scroll_text/app.rbs +7 -0
- data/examples/scroll_text/test_app.rb +110 -0
- data/examples/scrollbar_demo/app.rb +143 -0
- data/examples/scrollbar_demo/app.rbs +7 -0
- data/examples/scrollbar_demo/test_app.rb +77 -0
- data/examples/sparkline_demo/app.rb +240 -0
- data/examples/sparkline_demo/app.rbs +10 -0
- data/examples/sparkline_demo/test_app.rb +107 -0
- data/examples/table_flex/app.rb +65 -0
- data/examples/table_flex/app.rbs +7 -0
- data/examples/table_flex/test_app.rb +36 -0
- data/examples/table_select/app.rb +198 -0
- data/examples/table_select/app.rbs +7 -0
- data/examples/table_select/test_app.rb +180 -0
- data/examples/widget_style_colors/app.rb +104 -0
- data/examples/widget_style_colors/app.rbs +14 -0
- data/examples/widget_style_colors/test_app.rb +48 -0
- data/ext/ratatui_ruby/Cargo.lock +889 -115
- data/ext/ratatui_ruby/Cargo.toml +4 -3
- data/ext/ratatui_ruby/clippy.toml +7 -0
- data/ext/ratatui_ruby/extconf.rb +7 -0
- data/ext/ratatui_ruby/src/events.rs +218 -229
- data/ext/ratatui_ruby/src/lib.rs +38 -10
- data/ext/ratatui_ruby/src/rendering.rs +90 -10
- data/ext/ratatui_ruby/src/style.rs +281 -98
- data/ext/ratatui_ruby/src/terminal.rs +119 -25
- data/ext/ratatui_ruby/src/text.rs +171 -0
- data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
- data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
- data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
- data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
- data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
- data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
- data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
- data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
- data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
- data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
- data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
- data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
- data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
- data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
- data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
- data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
- data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
- data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
- data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
- data/lib/ratatui_ruby/cell.rb +166 -0
- data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
- data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
- data/lib/ratatui_ruby/event/key.rb +211 -0
- data/lib/ratatui_ruby/event/mouse.rb +124 -0
- data/lib/ratatui_ruby/event/paste.rb +71 -0
- data/lib/ratatui_ruby/event/resize.rb +80 -0
- data/lib/ratatui_ruby/event.rb +79 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
- data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
- data/lib/ratatui_ruby/schema/block.rb +186 -14
- data/lib/ratatui_ruby/schema/calendar.rb +74 -17
- data/lib/ratatui_ruby/schema/canvas.rb +215 -48
- data/lib/ratatui_ruby/schema/center.rb +49 -11
- data/lib/ratatui_ruby/schema/chart.rb +151 -41
- data/lib/ratatui_ruby/schema/clear.rb +41 -72
- data/lib/ratatui_ruby/schema/constraint.rb +82 -22
- data/lib/ratatui_ruby/schema/cursor.rb +27 -9
- data/lib/ratatui_ruby/schema/draw.rb +53 -0
- data/lib/ratatui_ruby/schema/gauge.rb +59 -15
- data/lib/ratatui_ruby/schema/layout.rb +95 -13
- data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
- data/lib/ratatui_ruby/schema/list.rb +93 -19
- data/lib/ratatui_ruby/schema/overlay.rb +34 -8
- data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
- data/lib/ratatui_ruby/schema/rect.rb +64 -15
- data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
- data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
- data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
- data/lib/ratatui_ruby/schema/style.rb +49 -21
- data/lib/ratatui_ruby/schema/table.rb +119 -21
- data/lib/ratatui_ruby/schema/tabs.rb +75 -13
- data/lib/ratatui_ruby/schema/text.rb +90 -0
- data/lib/ratatui_ruby/session.rb +146 -0
- data/lib/ratatui_ruby/test_helper.rb +156 -13
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby.rb +143 -23
- data/sig/ratatui_ruby/event.rbs +69 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
- data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
- data/sig/ratatui_ruby/schema/block.rbs +5 -4
- data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
- data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
- data/sig/ratatui_ruby/schema/center.rbs +3 -3
- data/sig/ratatui_ruby/schema/chart.rbs +8 -5
- data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
- data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
- data/sig/ratatui_ruby/schema/draw.rbs +23 -0
- data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
- data/sig/ratatui_ruby/schema/layout.rbs +11 -1
- data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
- data/sig/ratatui_ruby/schema/list.rbs +5 -1
- data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
- data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
- data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
- data/sig/ratatui_ruby/schema/rect.rbs +2 -1
- data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
- data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
- data/sig/ratatui_ruby/schema/table.rbs +8 -1
- data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
- data/sig/ratatui_ruby/schema/text.rbs +22 -0
- data/tasks/resources/build.yml.erb +13 -11
- data/tasks/terminal_preview/app_screenshot.rb +35 -0
- data/tasks/terminal_preview/crash_report.rb +54 -0
- data/tasks/terminal_preview/example_app.rb +25 -0
- data/tasks/terminal_preview/launcher_script.rb +48 -0
- data/tasks/terminal_preview/preview_collection.rb +60 -0
- data/tasks/terminal_preview/preview_timing.rb +22 -0
- data/tasks/terminal_preview/safety_confirmation.rb +58 -0
- data/tasks/terminal_preview/saved_screenshot.rb +55 -0
- data/tasks/terminal_preview/system_appearance.rb +11 -0
- data/tasks/terminal_preview/terminal_window.rb +138 -0
- data/tasks/terminal_preview/window_id.rb +14 -0
- data/tasks/terminal_preview.rake +28 -0
- data/tasks/test.rake +1 -1
- metadata +174 -53
- data/doc/images/examples-analytics.rb.png +0 -0
- data/doc/images/examples-box_demo.rb.png +0 -0
- data/doc/images/examples-calendar_demo.rb.png +0 -0
- data/doc/images/examples-chart_demo.rb.png +0 -0
- data/doc/images/examples-custom_widget.rb.png +0 -0
- data/doc/images/examples-dashboard.rb.png +0 -0
- data/doc/images/examples-list_styles.rb.png +0 -0
- data/doc/images/examples-login_form.rb.png +0 -0
- data/doc/images/examples-map_demo.rb.png +0 -0
- data/doc/images/examples-mouse_events.rb.png +0 -0
- data/doc/images/examples-popup_demo.rb.gif +0 -0
- data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
- data/doc/images/examples-scroll_text.rb.png +0 -0
- data/doc/images/examples-scrollbar_demo.rb.png +0 -0
- data/doc/images/examples-stock_ticker.rb.png +0 -0
- data/doc/images/examples-system_monitor.rb.png +0 -0
- data/doc/images/examples-table_select.rb.png +0 -0
- data/examples/analytics.rb +0 -88
- data/examples/box_demo.rb +0 -71
- data/examples/calendar_demo.rb +0 -55
- data/examples/chart_demo.rb +0 -84
- data/examples/custom_widget.rb +0 -43
- data/examples/dashboard.rb +0 -72
- data/examples/list_styles.rb +0 -66
- data/examples/map_demo.rb +0 -58
- data/examples/quickstart_dsl.rb +0 -30
- data/examples/quickstart_lifecycle.rb +0 -40
- data/examples/readme_usage.rb +0 -21
- data/examples/scroll_text.rb +0 -74
- data/examples/scrollbar_demo.rb +0 -75
- data/examples/stock_ticker.rb +0 -93
- data/examples/system_monitor.rb +0 -94
- data/examples/table_select.rb +0 -70
- data/examples/test_analytics.rb +0 -65
- data/examples/test_box_demo.rb +0 -38
- data/examples/test_calendar_demo.rb +0 -66
- data/examples/test_dashboard.rb +0 -38
- data/examples/test_list_styles.rb +0 -61
- data/examples/test_login_form.rb +0 -63
- data/examples/test_map_demo.rb +0 -100
- data/examples/test_scroll_text.rb +0 -130
- data/examples/test_stock_ticker.rb +0 -39
- data/examples/test_system_monitor.rb +0 -40
- data/examples/test_table_select.rb +0 -37
- data/ext/ratatui_ruby/src/buffer.rs +0 -54
- data/lib/ratatui_ruby/dsl.rb +0 -64
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# Core Concepts
|
|
2
|
+
|
|
3
|
+
This guide explains the core concepts and patterns available in `ratatui_ruby` for structuring your terminal applications.
|
|
4
|
+
|
|
5
|
+
## 1. Lifecycle Management
|
|
6
|
+
|
|
7
|
+
Managing the terminal state is critical. You must enter "alternate screen" and "raw mode" on startup, and **always** restore the terminal on exit (even on errors), otherwise the user's terminal will be left in a broken state.
|
|
8
|
+
|
|
9
|
+
### `RatatuiRuby.run` (Recommended)
|
|
10
|
+
|
|
11
|
+
The `run` method acts as a **Context Manager**. It handles the initialization and restoration for you, ensuring the terminal is always restored even if your code raises an exception. We recommend using `run` for all applications, as it provides a safe sandbox for your TUI.
|
|
12
|
+
|
|
13
|
+
```ruby
|
|
14
|
+
RatatuiRuby.run do |tui|
|
|
15
|
+
loop do
|
|
16
|
+
# Your code here
|
|
17
|
+
tui.draw(...)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
# Terminal is restored here
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
### Manual Management (Advanced)
|
|
24
|
+
|
|
25
|
+
You can manage this manually if you need granular control, but use `ensure` blocks!
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
RatatuiRuby.init_terminal
|
|
29
|
+
begin
|
|
30
|
+
# Your code here
|
|
31
|
+
RatatuiRuby.draw(...)
|
|
32
|
+
ensure
|
|
33
|
+
RatatuiRuby.restore_terminal
|
|
34
|
+
end
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## 2. API Convenience
|
|
38
|
+
|
|
39
|
+
### Session API (Recommended)
|
|
40
|
+
|
|
41
|
+
The block yielded by `run` is a `RatatuiRuby::Session` instance (`tui`).
|
|
42
|
+
It provides factory methods for every widget class (converting snake_case to CamelCase) and aliases for module functions.
|
|
43
|
+
|
|
44
|
+
**Why use it?** It significantly reduces verbosity and repeated `RatatuiRuby::` namespacing, making the UI tree structure easier to read.
|
|
45
|
+
|
|
46
|
+
```ruby
|
|
47
|
+
RatatuiRuby.run do |tui|
|
|
48
|
+
loop do
|
|
49
|
+
layout = tui.layout(
|
|
50
|
+
direction: :horizontal,
|
|
51
|
+
constraints: [
|
|
52
|
+
RatatuiRuby::Constraint.length(20),
|
|
53
|
+
RatatuiRuby::Constraint.min(0)
|
|
54
|
+
],
|
|
55
|
+
children: [
|
|
56
|
+
tui.paragraph(
|
|
57
|
+
text: tui.text_line(spans: [
|
|
58
|
+
tui.text_span(content: "Side", style: tui.style(fg: :blue)),
|
|
59
|
+
tui.text_span(content: "bar")
|
|
60
|
+
]),
|
|
61
|
+
block: tui.block(borders: [:all], title: "Nav")
|
|
62
|
+
),
|
|
63
|
+
tui.paragraph(
|
|
64
|
+
text: "Main Content",
|
|
65
|
+
style: tui.style(fg: :green),
|
|
66
|
+
block: tui.block(borders: [:all], title: "Content")
|
|
67
|
+
)
|
|
68
|
+
]
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
tui.draw(layout)
|
|
72
|
+
|
|
73
|
+
event = tui.poll_event
|
|
74
|
+
break if event == "q" || event == :ctrl_c
|
|
75
|
+
end
|
|
76
|
+
end
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### Raw API
|
|
80
|
+
|
|
81
|
+
You can always use the raw module methods and classes directly. This is useful if you are building your own abstractions or prefer explicit class instantiation.
|
|
82
|
+
|
|
83
|
+
**Comparison:** Notice how much more verbose the same UI definition is.
|
|
84
|
+
|
|
85
|
+
```ruby
|
|
86
|
+
RatatuiRuby.run do
|
|
87
|
+
loop do
|
|
88
|
+
layout = RatatuiRuby::Layout.new(
|
|
89
|
+
direction: :horizontal,
|
|
90
|
+
constraints: [
|
|
91
|
+
RatatuiRuby::Constraint.length(20),
|
|
92
|
+
RatatuiRuby::Constraint.min(0)
|
|
93
|
+
],
|
|
94
|
+
children: [
|
|
95
|
+
RatatuiRuby::Paragraph.new(
|
|
96
|
+
text: RatatuiRuby::Text::Line.new(spans: [
|
|
97
|
+
RatatuiRuby::Text::Span.new(content: "Side", style: RatatuiRuby::Style.new(fg: :blue)),
|
|
98
|
+
RatatuiRuby::Text::Span.new(content: "bar")
|
|
99
|
+
]),
|
|
100
|
+
block: RatatuiRuby::Block.new(borders: [:all], title: "Nav")
|
|
101
|
+
),
|
|
102
|
+
RatatuiRuby::Paragraph.new(
|
|
103
|
+
text: "Main Content",
|
|
104
|
+
style: RatatuiRuby::Style.new(fg: :green),
|
|
105
|
+
block: RatatuiRuby::Block.new(borders: [:all], title: "Content")
|
|
106
|
+
)
|
|
107
|
+
]
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
RatatuiRuby.draw(layout)
|
|
111
|
+
|
|
112
|
+
event = RatatuiRuby.poll_event
|
|
113
|
+
break if event == "q" || event == :ctrl_c
|
|
114
|
+
end
|
|
115
|
+
end
|
|
116
|
+
```
|
data/doc/application_testing.md
CHANGED
|
@@ -44,8 +44,8 @@ Wrap your test assertions in `with_test_terminal`. This sets up a temporary, in-
|
|
|
44
44
|
|
|
45
45
|
```ruby
|
|
46
46
|
def test_rendering
|
|
47
|
-
#
|
|
48
|
-
with_test_terminal
|
|
47
|
+
# Uses default 80x24 terminal
|
|
48
|
+
with_test_terminal do
|
|
49
49
|
# 1. Instantiate your app/component
|
|
50
50
|
widget = RatatuiRuby::Paragraph.new(text: "Hello World")
|
|
51
51
|
|
|
@@ -82,13 +82,18 @@ assert_equal 2, pos[:y]
|
|
|
82
82
|
|
|
83
83
|
Injects a mock event into the event queue. This is the preferred way to simulate user input instead of stubbing `poll_event`.
|
|
84
84
|
|
|
85
|
+
> [!IMPORTANT]
|
|
86
|
+
> You must call `inject_event` inside a `with_test_terminal` block. Calling it outside leads to race conditions where events are flushed before the application starts.
|
|
87
|
+
|
|
85
88
|
```ruby
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
with_test_terminal do
|
|
90
|
+
# Simulate 'q' key press
|
|
91
|
+
inject_event("key", { code: "q" })
|
|
88
92
|
|
|
89
|
-
# Now poll_event will return the 'q' key event
|
|
90
|
-
event = RatatuiRuby.poll_event
|
|
91
|
-
assert_equal "q", event
|
|
93
|
+
# Now poll_event will return the 'q' key event
|
|
94
|
+
event = RatatuiRuby.poll_event
|
|
95
|
+
assert_equal "q", event.code
|
|
96
|
+
end
|
|
92
97
|
```
|
|
93
98
|
|
|
94
99
|
## Example
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
<!--
|
|
2
|
+
SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
|
|
3
|
+
SPDX-License-Identifier: CC-BY-SA-4.0
|
|
4
|
+
-->
|
|
5
|
+
|
|
6
|
+
# Improving DX for Layout & Hit Testing
|
|
7
|
+
|
|
8
|
+
## Problem Statement
|
|
9
|
+
|
|
10
|
+
Interactive TUI applications require hit testing: determining which UI region the user clicked. Current RatatuiRuby practice duplicates layout logic between rendering and input handling.
|
|
11
|
+
|
|
12
|
+
### Current Pattern (Duplication)
|
|
13
|
+
|
|
14
|
+
```ruby
|
|
15
|
+
def run
|
|
16
|
+
loop do
|
|
17
|
+
calculate_layout # Phase 1: Manually calculate rects
|
|
18
|
+
render # Phase 2: Build UI tree (repeating the same layout logic)
|
|
19
|
+
handle_input # Phase 3: Use cached rects from Phase 1
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def calculate_layout
|
|
24
|
+
full_area = RatatuiRuby::Rect.new(x: 0, y: 0, width: 80, height: 24)
|
|
25
|
+
|
|
26
|
+
@main_area, @control_area = RatatuiRuby::Layout.split(
|
|
27
|
+
full_area,
|
|
28
|
+
direction: :vertical,
|
|
29
|
+
constraints: [
|
|
30
|
+
RatatuiRuby::Constraint.fill(1),
|
|
31
|
+
RatatuiRuby::Constraint.length(7)
|
|
32
|
+
]
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
@left_rect, @right_rect = RatatuiRuby::Layout.split(
|
|
36
|
+
@main_area,
|
|
37
|
+
direction: :horizontal,
|
|
38
|
+
constraints: [
|
|
39
|
+
RatatuiRuby::Constraint.percentage(50),
|
|
40
|
+
RatatuiRuby::Constraint.percentage(50)
|
|
41
|
+
]
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def render
|
|
46
|
+
# Rebuilds the SAME layout internally, but we can't access those rects
|
|
47
|
+
layout = RatatuiRuby::Layout.new(
|
|
48
|
+
direction: :vertical,
|
|
49
|
+
constraints: [
|
|
50
|
+
RatatuiRuby::Constraint.fill(1),
|
|
51
|
+
RatatuiRuby::Constraint.length(7)
|
|
52
|
+
],
|
|
53
|
+
children: [...]
|
|
54
|
+
)
|
|
55
|
+
RatatuiRuby.draw(layout)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
def handle_input
|
|
59
|
+
event = RatatuiRuby.poll_event
|
|
60
|
+
if @left_rect&.contains?(event.x, event.y)
|
|
61
|
+
# hit test using cached rect
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
**Problems:**
|
|
67
|
+
1. **Duplication**: Layout constraints are written twice—once in `calculate_layout`, once in the UI tree.
|
|
68
|
+
2. **Fragility**: If layout changes in `render`, the cached rects in `@left_rect` become stale. The user must remember to update both places.
|
|
69
|
+
3. **Maintainability**: Adding new UI regions requires changes in two places and explicit rect caching.
|
|
70
|
+
4. **Performance**: Layout is calculated twice per frame (once manually, once internally during render).
|
|
71
|
+
|
|
72
|
+
### Ideal Pattern (No Duplication)
|
|
73
|
+
|
|
74
|
+
```ruby
|
|
75
|
+
def run
|
|
76
|
+
loop do
|
|
77
|
+
render # Single source of truth
|
|
78
|
+
break if handle_input == :quit
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def render
|
|
83
|
+
layout = RatatuiRuby::Layout.new(
|
|
84
|
+
direction: :vertical,
|
|
85
|
+
constraints: [
|
|
86
|
+
RatatuiRuby::Constraint.fill(1),
|
|
87
|
+
RatatuiRuby::Constraint.length(7)
|
|
88
|
+
],
|
|
89
|
+
children: [...]
|
|
90
|
+
)
|
|
91
|
+
|
|
92
|
+
@layout_info = RatatuiRuby.draw(layout) # Returns layout metadata
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def handle_input
|
|
96
|
+
event = RatatuiRuby.poll_event
|
|
97
|
+
# Query the layout info returned from draw()
|
|
98
|
+
if @layout_info[:left_panel]&.contains?(event.x, event.y)
|
|
99
|
+
# No manual caching needed; rects come from the same render pass
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## Proposed Solution
|
|
105
|
+
|
|
106
|
+
Extend `RatatuiRuby.draw()` to return a `LayoutInfo` object containing the rectangles where widgets were rendered.
|
|
107
|
+
|
|
108
|
+
### API Changes
|
|
109
|
+
|
|
110
|
+
#### Option A: Return a Plain Hash (Simpler)
|
|
111
|
+
|
|
112
|
+
```ruby
|
|
113
|
+
# Layout.rb with optional layout_id parameter
|
|
114
|
+
class Layout < Data
|
|
115
|
+
def self.new(
|
|
116
|
+
direction: :vertical,
|
|
117
|
+
constraints: [],
|
|
118
|
+
children: [],
|
|
119
|
+
flex: :legacy,
|
|
120
|
+
layout_id: nil # NEW: Optional semantic identifier
|
|
121
|
+
)
|
|
122
|
+
# ...
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
# Block.rb with optional layout_id parameter
|
|
127
|
+
class Block < Data
|
|
128
|
+
def self.new(
|
|
129
|
+
title: nil,
|
|
130
|
+
borders: [],
|
|
131
|
+
border_type: :rounded,
|
|
132
|
+
style: nil,
|
|
133
|
+
layout_id: nil # NEW: Optional semantic identifier
|
|
134
|
+
)
|
|
135
|
+
# ...
|
|
136
|
+
end
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
# Usage in app:
|
|
140
|
+
layout = RatatuiRuby::Layout.new(
|
|
141
|
+
direction: :vertical,
|
|
142
|
+
constraints: [...],
|
|
143
|
+
layout_id: :main, # Tag this layout
|
|
144
|
+
children: [
|
|
145
|
+
RatatuiRuby::Block.new(
|
|
146
|
+
title: "Left",
|
|
147
|
+
layout_id: :left_panel, # Tag this block
|
|
148
|
+
...
|
|
149
|
+
),
|
|
150
|
+
RatatuiRuby::Block.new(
|
|
151
|
+
title: "Right",
|
|
152
|
+
layout_id: :right_panel,
|
|
153
|
+
...
|
|
154
|
+
)
|
|
155
|
+
]
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
layout_info = RatatuiRuby.draw(layout)
|
|
159
|
+
|
|
160
|
+
# layout_info is a Hash:
|
|
161
|
+
# {
|
|
162
|
+
# left_panel: #<Rect x=0 y=0 width=40 height=24>,
|
|
163
|
+
# right_panel: #<Rect x=40 y=0 width=40 height=24>,
|
|
164
|
+
# main: #<Rect x=0 y=0 width=80 height=24>
|
|
165
|
+
# }
|
|
166
|
+
|
|
167
|
+
if layout_info[:left_panel]&.contains?(event.x, event.y)
|
|
168
|
+
handle_left_click
|
|
169
|
+
end
|
|
170
|
+
```
|
|
171
|
+
|
|
172
|
+
#### Option B: Return a LayoutInfo Class (More Structured)
|
|
173
|
+
|
|
174
|
+
```ruby
|
|
175
|
+
class LayoutInfo
|
|
176
|
+
attr_reader :rects # Hash of layout_id => Rect
|
|
177
|
+
|
|
178
|
+
def [](key)
|
|
179
|
+
rects[key]
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
def get(key)
|
|
183
|
+
rects[key]
|
|
184
|
+
end
|
|
185
|
+
|
|
186
|
+
def contains?(key, x, y)
|
|
187
|
+
rects[key]&.contains?(x, y)
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
|
|
191
|
+
# Usage:
|
|
192
|
+
layout_info = RatatuiRuby.draw(layout)
|
|
193
|
+
if layout_info.contains?(:left_panel, event.x, event.y)
|
|
194
|
+
handle_left_click
|
|
195
|
+
end
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Recommendation:** Start with Option A (Plain Hash). It's simpler, aligns with RatatuiRuby's minimal design, and can evolve to Option B if needed.
|
|
199
|
+
|
|
200
|
+
### Implementation Sketch
|
|
201
|
+
|
|
202
|
+
#### Ruby Side
|
|
203
|
+
|
|
204
|
+
1. **Add `layout_id` parameter** to `Layout` and `Block` (and optionally other container widgets like `Center`, `Overlay`).
|
|
205
|
+
2. **Update `.rbs` type signatures** to document the new optional parameter.
|
|
206
|
+
3. **Update `RatatuiRuby.draw()` signature** to return `Hash[Symbol | String, Rect] | nil` (or return both render status and layout info as needed).
|
|
207
|
+
|
|
208
|
+
```ruby
|
|
209
|
+
# sig/ratatui_ruby/ratatui_ruby.rbs
|
|
210
|
+
def self.draw: (widget, ?return_layout: bool) -> (nil | Hash[Symbol | String, Rect])
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
#### Rust Side
|
|
214
|
+
|
|
215
|
+
1. **Track layout IDs during render:** When the Rust renderer encounters a widget with a `layout_id`, record its rendered rectangle.
|
|
216
|
+
2. **Return layout info as a Ruby Hash:** Construct a Ruby Hash mapping `layout_id` (String or Symbol) to `Rect` objects.
|
|
217
|
+
3. **Wire into `lib.rs`:** Modify the `draw` function to return this Hash instead of `nil`.
|
|
218
|
+
|
|
219
|
+
**Pseudo-code for `rendering.rs`:**
|
|
220
|
+
|
|
221
|
+
```rust
|
|
222
|
+
pub fn render_node(
|
|
223
|
+
frame: &mut Frame,
|
|
224
|
+
area: Rect,
|
|
225
|
+
node: Value,
|
|
226
|
+
layout_map: &mut HashMap<Value, Rect>, // Collect rects as we go
|
|
227
|
+
) -> Result<(), Error> {
|
|
228
|
+
// Extract layout_id if present
|
|
229
|
+
let layout_id: Option<Value> = node.funcall("layout_id", ()).ok();
|
|
230
|
+
|
|
231
|
+
if let Some(id) = layout_id {
|
|
232
|
+
layout_map.insert(id.clone(), area);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ... render the widget ...
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
// In lib.rs, wrap the result:
|
|
239
|
+
pub fn draw(node: Value) -> Result<Value, Error> {
|
|
240
|
+
let mut layout_map = HashMap::new();
|
|
241
|
+
render_node(&mut frame, full_area, node, &mut layout_map)?;
|
|
242
|
+
|
|
243
|
+
// Convert HashMap to Ruby Hash
|
|
244
|
+
let result_hash = RHash::new();
|
|
245
|
+
for (key, rect) in layout_map {
|
|
246
|
+
result_hash.aset(key, rect)?;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
Ok(result_hash.into())
|
|
250
|
+
}
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### Backward Compatibility
|
|
254
|
+
|
|
255
|
+
**No breaking changes:**
|
|
256
|
+
- `layout_id` is optional on all widgets.
|
|
257
|
+
- `RatatuiRuby.draw()` continues to render correctly.
|
|
258
|
+
- **Behavior**: If `layout_id` is omitted, that region is simply not included in the returned Hash.
|
|
259
|
+
- **Return value**: If no widgets have `layout_id`, returns an empty Hash (or `nil` if we want to preserve existing return type).
|
|
260
|
+
|
|
261
|
+
**Recommendation**: Return `nil` if `layout_id` is not used anywhere in the tree (preserves current behavior of returning nothing). Return a Hash if any widget has a `layout_id`.
|
|
262
|
+
|
|
263
|
+
## Example: Before and After
|
|
264
|
+
|
|
265
|
+
### Before (Current)
|
|
266
|
+
|
|
267
|
+
```ruby
|
|
268
|
+
class ColorPickerApp
|
|
269
|
+
def initialize
|
|
270
|
+
@input = "#F96302"
|
|
271
|
+
@current_color = parse_color(@input)
|
|
272
|
+
@error_message = ""
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def run
|
|
276
|
+
RatatuiRuby.run do
|
|
277
|
+
loop do
|
|
278
|
+
calculate_layout # Manual layout calculation
|
|
279
|
+
render
|
|
280
|
+
result = handle_input
|
|
281
|
+
break if result == :quit
|
|
282
|
+
end
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
|
|
286
|
+
def calculate_layout
|
|
287
|
+
terminal_size = RatatuiRuby.terminal_size
|
|
288
|
+
width, height = terminal_size
|
|
289
|
+
|
|
290
|
+
full_area = RatatuiRuby::Rect.new(x: 0, y: 0, width: width, height: height)
|
|
291
|
+
|
|
292
|
+
input_area, rest = RatatuiRuby::Layout.split(full_area,
|
|
293
|
+
direction: :vertical,
|
|
294
|
+
constraints: [
|
|
295
|
+
RatatuiRuby::Constraint.length(3),
|
|
296
|
+
RatatuiRuby::Constraint.fill(1)
|
|
297
|
+
]
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
color_area, control_area = RatatuiRuby::Layout.split(rest,
|
|
301
|
+
direction: :vertical,
|
|
302
|
+
constraints: [
|
|
303
|
+
RatatuiRuby::Constraint.length(14),
|
|
304
|
+
RatatuiRuby::Constraint.fill(1)
|
|
305
|
+
]
|
|
306
|
+
)
|
|
307
|
+
|
|
308
|
+
harmony_area, @export_area_rect = RatatuiRuby::Layout.split(color_area,
|
|
309
|
+
direction: :vertical,
|
|
310
|
+
constraints: [
|
|
311
|
+
RatatuiRuby::Constraint.length(7),
|
|
312
|
+
RatatuiRuby::Constraint.fill(1)
|
|
313
|
+
]
|
|
314
|
+
)
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def render
|
|
318
|
+
main_ui = RatatuiRuby::Layout.new(
|
|
319
|
+
direction: :vertical,
|
|
320
|
+
constraints: [
|
|
321
|
+
RatatuiRuby::Constraint.length(3),
|
|
322
|
+
RatatuiRuby::Constraint.length(14),
|
|
323
|
+
RatatuiRuby::Constraint.fill(1)
|
|
324
|
+
],
|
|
325
|
+
children: [
|
|
326
|
+
build_input_section,
|
|
327
|
+
build_color_section,
|
|
328
|
+
build_controls_section
|
|
329
|
+
]
|
|
330
|
+
)
|
|
331
|
+
RatatuiRuby.draw(main_ui)
|
|
332
|
+
end
|
|
333
|
+
|
|
334
|
+
def handle_input
|
|
335
|
+
event = RatatuiRuby.poll_event
|
|
336
|
+
case event
|
|
337
|
+
in {type: :mouse, kind: "down", button: "left", x:, y:}
|
|
338
|
+
if @export_area_rect&.contains?(x, y) # Using cached rect
|
|
339
|
+
@copy_dialog_text = @current_color.to_hex.upcase
|
|
340
|
+
@copy_dialog_active = true
|
|
341
|
+
end
|
|
342
|
+
# ...
|
|
343
|
+
end
|
|
344
|
+
end
|
|
345
|
+
end
|
|
346
|
+
```
|
|
347
|
+
|
|
348
|
+
**Problems:**
|
|
349
|
+
- `calculate_layout` duplicates the exact same layout structure as `render`.
|
|
350
|
+
- Changes to layout in `render` require manual updates to `calculate_layout`.
|
|
351
|
+
- Fragile: rect caching is manual and easy to forget.
|
|
352
|
+
|
|
353
|
+
### After (With `layout_id`)
|
|
354
|
+
|
|
355
|
+
```ruby
|
|
356
|
+
class ColorPickerApp
|
|
357
|
+
def initialize
|
|
358
|
+
@input = "#F96302"
|
|
359
|
+
@current_color = parse_color(@input)
|
|
360
|
+
@error_message = ""
|
|
361
|
+
@layout_info = {} # Will be populated by draw()
|
|
362
|
+
end
|
|
363
|
+
|
|
364
|
+
def run
|
|
365
|
+
RatatuiRuby.run do
|
|
366
|
+
loop do
|
|
367
|
+
render
|
|
368
|
+
result = handle_input
|
|
369
|
+
break if result == :quit
|
|
370
|
+
end
|
|
371
|
+
end
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def render
|
|
375
|
+
main_ui = RatatuiRuby::Layout.new(
|
|
376
|
+
direction: :vertical,
|
|
377
|
+
layout_id: :main, # Tag the main layout
|
|
378
|
+
constraints: [
|
|
379
|
+
RatatuiRuby::Constraint.length(3),
|
|
380
|
+
RatatuiRuby::Constraint.length(14),
|
|
381
|
+
RatatuiRuby::Constraint.fill(1)
|
|
382
|
+
],
|
|
383
|
+
children: [
|
|
384
|
+
build_input_section,
|
|
385
|
+
build_color_section(layout_id: :color_section), # Tag child layouts
|
|
386
|
+
build_controls_section
|
|
387
|
+
]
|
|
388
|
+
)
|
|
389
|
+
@layout_info = RatatuiRuby.draw(main_ui) || {} # Capture layout info
|
|
390
|
+
end
|
|
391
|
+
|
|
392
|
+
def build_color_section(layout_id: nil)
|
|
393
|
+
RatatuiRuby::Layout.new(
|
|
394
|
+
direction: :vertical,
|
|
395
|
+
layout_id: layout_id,
|
|
396
|
+
constraints: [
|
|
397
|
+
RatatuiRuby::Constraint.length(7),
|
|
398
|
+
RatatuiRuby::Constraint.fill(1)
|
|
399
|
+
],
|
|
400
|
+
children: [
|
|
401
|
+
build_harmonies,
|
|
402
|
+
RatatuiRuby::Block.new(
|
|
403
|
+
title: "Export Formats",
|
|
404
|
+
layout_id: :export_formats, # Tag the export block
|
|
405
|
+
borders: [:all],
|
|
406
|
+
children: [
|
|
407
|
+
build_export_content
|
|
408
|
+
]
|
|
409
|
+
)
|
|
410
|
+
]
|
|
411
|
+
)
|
|
412
|
+
end
|
|
413
|
+
|
|
414
|
+
def handle_input
|
|
415
|
+
event = RatatuiRuby.poll_event
|
|
416
|
+
case event
|
|
417
|
+
in {type: :mouse, kind: "down", button: "left", x:, y:}
|
|
418
|
+
if @layout_info[:export_formats]&.contains?(x, y) # No manual caching!
|
|
419
|
+
@copy_dialog_text = @current_color.to_hex.upcase
|
|
420
|
+
@copy_dialog_active = true
|
|
421
|
+
end
|
|
422
|
+
# ...
|
|
423
|
+
end
|
|
424
|
+
end
|
|
425
|
+
end
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
**Benefits:**
|
|
429
|
+
- **Single source of truth**: Layout is defined once in `render`, not duplicated in `calculate_layout`.
|
|
430
|
+
- **Automatic tracking**: As you modify the UI tree, rects are automatically updated by the same render pass.
|
|
431
|
+
- **No manual caching**: Use `@layout_info` directly from `draw()`.
|
|
432
|
+
- **Declarative**: Tag regions with semantic IDs, making hit testing code self-documenting.
|
|
433
|
+
|
|
434
|
+
## Design Alignment
|
|
435
|
+
|
|
436
|
+
### Immediate-Mode Rendering
|
|
437
|
+
|
|
438
|
+
This proposal **preserves** immediate-mode principles:
|
|
439
|
+
|
|
440
|
+
- **Every frame**, the app constructs a fresh UI tree from current state.
|
|
441
|
+
- **Every frame**, `draw()` consumes that tree and renders it.
|
|
442
|
+
- **Returns**: Layout metadata computed during the same render pass.
|
|
443
|
+
|
|
444
|
+
The key insight: Returning layout info is **not** retained state; it's a **by-product** of the render, computed fresh each frame.
|
|
445
|
+
|
|
446
|
+
### Data-Driven UI
|
|
447
|
+
|
|
448
|
+
Widgets remain immutable data objects; adding `layout_id` is just an optional annotation:
|
|
449
|
+
|
|
450
|
+
```ruby
|
|
451
|
+
# Still pure data:
|
|
452
|
+
widget = RatatuiRuby::Block.new(
|
|
453
|
+
title: "Foo",
|
|
454
|
+
layout_id: :my_widget, # Just metadata, not behavior
|
|
455
|
+
borders: [:all]
|
|
456
|
+
)
|
|
457
|
+
```
|
|
458
|
+
|
|
459
|
+
No rendering logic moves to Ruby.
|
|
460
|
+
|
|
461
|
+
### Rust Backend Alignment
|
|
462
|
+
|
|
463
|
+
In Rust Ratatui, the `Frame` tracks where widgets are rendered:
|
|
464
|
+
|
|
465
|
+
```rust
|
|
466
|
+
let mut frame = Terminal::new(backend)?;
|
|
467
|
+
frame.render_widget(widget, area); // Frame knows where this widget is now
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
Returning layout info from `draw()` mirrors this: the Rust backend knows where things ended up, and returns that information to Ruby.
|
|
471
|
+
|
|
472
|
+
## Alternatives Considered
|
|
473
|
+
|
|
474
|
+
### Alternative 1: Widgets Maintain Their Own State
|
|
475
|
+
|
|
476
|
+
Store rects on mutable widget objects. **Rejected** because:
|
|
477
|
+
- Violates immediate-mode and data-driven design.
|
|
478
|
+
- Requires mutable state tracking in Rust.
|
|
479
|
+
- Complicates the simplicity of immutable data objects.
|
|
480
|
+
|
|
481
|
+
### Alternative 2: Full Frame Object
|
|
482
|
+
|
|
483
|
+
Return a `Frame` object (similar to Ratatui's Frame) that tracks all rendering details. **Rejected** because:
|
|
484
|
+
- Over-engineered for the current need.
|
|
485
|
+
- RatatuiRuby is intentionally minimal.
|
|
486
|
+
- Overkill if the app only cares about hit testing a few regions.
|
|
487
|
+
|
|
488
|
+
### Alternative 3: Callback-Based Rendering
|
|
489
|
+
|
|
490
|
+
Allow widgets to register callbacks when rendered. **Rejected** because:
|
|
491
|
+
- Adds complexity and statefulness.
|
|
492
|
+
- Less idiomatic for Ruby.
|
|
493
|
+
- Harder to reason about in immediate-mode loop.
|
|
494
|
+
|
|
495
|
+
### Alternative 4: Hit Testing DSL
|
|
496
|
+
|
|
497
|
+
Provide a declarative hit testing layer separate from rendering. **Rejected** because:
|
|
498
|
+
- Duplicates layout info (still two sources of truth).
|
|
499
|
+
- Unnecessary indirection.
|
|
500
|
+
|
|
501
|
+
## Impact Assessment
|
|
502
|
+
|
|
503
|
+
### User-Facing Changes
|
|
504
|
+
|
|
505
|
+
- **New optional parameter**: `layout_id` on `Layout`, `Block`, and similar container widgets.
|
|
506
|
+
- **New return value**: `RatatuiRuby.draw()` optionally returns a Hash of rects.
|
|
507
|
+
- **Zero breaking changes**: Existing apps without `layout_id` work unchanged.
|
|
508
|
+
|
|
509
|
+
### Documentation Updates
|
|
510
|
+
|
|
511
|
+
- **Update RDoc** for `Layout` and `Block` to document `layout_id`.
|
|
512
|
+
- **Add example**: `examples/hit_test/app.rb` (or new example) showing the pattern.
|
|
513
|
+
- **Update `doc/interactive_design.md`** with the new approach.
|
|
514
|
+
|
|
515
|
+
### Testing
|
|
516
|
+
|
|
517
|
+
- **Unit tests (Rust)**: Verify that rects are collected and returned correctly.
|
|
518
|
+
- **Integration tests (Ruby)**: Verify hit testing works with returned layout info.
|
|
519
|
+
- **Example app**: Ensure the color picker and hit test examples demonstrate the pattern.
|
|
520
|
+
|
|
521
|
+
## Timeline & Scope
|
|
522
|
+
|
|
523
|
+
**Scope**: Pre-1.0 feature. Fits RatatuiRuby's design philosophy and solves a real pain point.
|
|
524
|
+
|
|
525
|
+
**Estimated effort**:
|
|
526
|
+
- Rust backend: 4–6 hours (add `layout_id` extraction, rect collection, Hash construction)
|
|
527
|
+
- Ruby side: 2–3 hours (add parameter to widget classes, update `.rbs`, docs)
|
|
528
|
+
- Testing & examples: 2–3 hours
|
|
529
|
+
- **Total**: ~10 hours
|
|
530
|
+
|
|
531
|
+
**Risk**: Low. The change is additive (optional parameter, new return value). Backward compatible.
|
|
532
|
+
|
|
533
|
+
## Recommendation
|
|
534
|
+
|
|
535
|
+
**Approve**. This proposal:
|
|
536
|
+
|
|
537
|
+
1. Eliminates a real pain point (layout duplication).
|
|
538
|
+
2. Aligns with immediate-mode and data-driven design.
|
|
539
|
+
3. Mirrors how Rust Ratatui works (Frame tracks layout).
|
|
540
|
+
4. Requires no breaking changes.
|
|
541
|
+
5. Is low-risk and achievable pre-1.0.
|
|
542
|
+
|
|
543
|
+
Implement as **Option A (Plain Hash)** first. It's simpler and sufficient for hit testing. Evolve to `LayoutInfo` class if more complex queries are needed later.
|
|
@@ -41,7 +41,7 @@ The application loop typically looks like this:
|
|
|
41
41
|
loop do
|
|
42
42
|
# 1. & 2. Handle events and update state
|
|
43
43
|
event = RatatuiRuby.poll_event
|
|
44
|
-
break if event
|
|
44
|
+
break if event == :esc
|
|
45
45
|
|
|
46
46
|
# 3. Construct View Tree
|
|
47
47
|
ui = RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}")
|