ratatui_ruby 0.3.0 → 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.
Files changed (302) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +15 -13
  3. data/.builds/ruby-3.3.yml +15 -13
  4. data/.builds/ruby-3.4.yml +15 -13
  5. data/.builds/ruby-4.0.0.yml +18 -17
  6. data/AGENTS.md +85 -37
  7. data/CHANGELOG.md +193 -1
  8. data/README.md +19 -17
  9. data/REUSE.toml +0 -5
  10. data/doc/application_architecture.md +116 -0
  11. data/doc/application_testing.md +12 -7
  12. data/doc/contributors/better_dx.md +543 -0
  13. data/doc/contributors/design/ruby_frontend.md +1 -1
  14. data/doc/contributors/developing_examples.md +203 -0
  15. data/doc/contributors/documentation_style.md +97 -0
  16. data/doc/contributors/dwim_dx.md +366 -0
  17. data/doc/contributors/example_analysis.md +82 -0
  18. data/doc/custom.css +14 -0
  19. data/doc/event_handling.md +119 -0
  20. data/doc/images/all_events.png +0 -0
  21. data/doc/images/analytics.png +0 -0
  22. data/doc/images/block_padding.png +0 -0
  23. data/doc/images/block_titles.png +0 -0
  24. data/doc/images/box_demo.png +0 -0
  25. data/doc/images/calendar_demo.png +0 -0
  26. data/doc/images/cell_demo.png +0 -0
  27. data/doc/images/chart_demo.png +0 -0
  28. data/doc/images/custom_widget.png +0 -0
  29. data/doc/images/flex_layout.png +0 -0
  30. data/doc/images/gauge_demo.png +0 -0
  31. data/doc/images/hit_test.png +0 -0
  32. data/doc/images/line_gauge_demo.png +0 -0
  33. data/doc/images/list_demo.png +0 -0
  34. data/doc/images/list_styles.png +0 -0
  35. data/doc/images/login_form.png +0 -0
  36. data/doc/images/map_demo.png +0 -0
  37. data/doc/images/mouse_events.png +0 -0
  38. data/doc/images/popup_demo.png +0 -0
  39. data/doc/images/quickstart_dsl.png +0 -0
  40. data/doc/images/quickstart_lifecycle.png +0 -0
  41. data/doc/images/ratatui_logo_demo.png +0 -0
  42. data/doc/images/readme_usage.png +0 -0
  43. data/doc/images/rich_text.png +0 -0
  44. data/doc/images/scroll_text.png +0 -0
  45. data/doc/images/scrollbar_demo.png +0 -0
  46. data/doc/images/sparkline_demo.png +0 -0
  47. data/doc/images/table_flex.png +0 -0
  48. data/doc/images/table_select.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/index.md +1 -0
  51. data/doc/interactive_design.md +121 -0
  52. data/doc/quickstart.md +147 -72
  53. data/examples/all_events/app.rb +169 -0
  54. data/examples/all_events/app.rbs +7 -0
  55. data/examples/all_events/test_app.rb +139 -0
  56. data/examples/analytics/app.rb +258 -0
  57. data/examples/analytics/app.rbs +7 -0
  58. data/examples/analytics/test_app.rb +132 -0
  59. data/examples/block_padding/app.rb +63 -0
  60. data/examples/block_padding/app.rbs +7 -0
  61. data/examples/block_padding/test_app.rb +31 -0
  62. data/examples/block_titles/app.rb +61 -0
  63. data/examples/block_titles/app.rbs +7 -0
  64. data/examples/block_titles/test_app.rb +34 -0
  65. data/examples/box_demo/app.rb +216 -0
  66. data/examples/box_demo/app.rbs +7 -0
  67. data/examples/box_demo/test_app.rb +88 -0
  68. data/examples/calendar_demo/app.rb +101 -0
  69. data/examples/calendar_demo/app.rbs +7 -0
  70. data/examples/calendar_demo/test_app.rb +108 -0
  71. data/examples/cell_demo/app.rb +108 -0
  72. data/examples/cell_demo/app.rbs +7 -0
  73. data/examples/cell_demo/test_app.rb +36 -0
  74. data/examples/chart_demo/app.rb +203 -0
  75. data/examples/chart_demo/app.rbs +7 -0
  76. data/examples/chart_demo/test_app.rb +102 -0
  77. data/examples/custom_widget/app.rb +51 -0
  78. data/examples/custom_widget/app.rbs +7 -0
  79. data/examples/custom_widget/test_app.rb +30 -0
  80. data/examples/flex_layout/app.rb +156 -0
  81. data/examples/flex_layout/app.rbs +7 -0
  82. data/examples/flex_layout/test_app.rb +65 -0
  83. data/examples/gauge_demo/app.rb +182 -0
  84. data/examples/gauge_demo/app.rbs +7 -0
  85. data/examples/gauge_demo/test_app.rb +120 -0
  86. data/examples/hit_test/app.rb +175 -0
  87. data/examples/hit_test/app.rbs +7 -0
  88. data/examples/hit_test/test_app.rb +102 -0
  89. data/examples/line_gauge_demo/app.rb +190 -0
  90. data/examples/line_gauge_demo/app.rbs +7 -0
  91. data/examples/line_gauge_demo/test_app.rb +129 -0
  92. data/examples/list_demo/app.rb +253 -0
  93. data/examples/list_demo/app.rbs +12 -0
  94. data/examples/list_demo/test_app.rb +237 -0
  95. data/examples/list_styles/app.rb +140 -0
  96. data/examples/list_styles/app.rbs +7 -0
  97. data/examples/list_styles/test_app.rb +157 -0
  98. data/examples/{login_form.rb → login_form/app.rb} +12 -16
  99. data/examples/login_form/app.rbs +7 -0
  100. data/examples/login_form/test_app.rb +51 -0
  101. data/examples/map_demo/app.rb +90 -0
  102. data/examples/map_demo/app.rbs +7 -0
  103. data/examples/map_demo/test_app.rb +149 -0
  104. data/examples/{mouse_events.rb → mouse_events/app.rb} +29 -27
  105. data/examples/mouse_events/app.rbs +7 -0
  106. data/examples/mouse_events/test_app.rb +53 -0
  107. data/examples/{popup_demo.rb → popup_demo/app.rb} +15 -17
  108. data/examples/popup_demo/app.rbs +7 -0
  109. data/examples/{test_popup_demo.rb → popup_demo/test_app.rb} +18 -26
  110. data/examples/quickstart_dsl/app.rb +36 -0
  111. data/examples/quickstart_dsl/app.rbs +7 -0
  112. data/examples/quickstart_dsl/test_app.rb +29 -0
  113. data/examples/quickstart_lifecycle/app.rb +39 -0
  114. data/examples/quickstart_lifecycle/app.rbs +7 -0
  115. data/examples/quickstart_lifecycle/test_app.rb +29 -0
  116. data/examples/ratatui_logo_demo/app.rb +79 -0
  117. data/examples/ratatui_logo_demo/app.rbs +7 -0
  118. data/examples/ratatui_logo_demo/test_app.rb +51 -0
  119. data/examples/ratatui_mascot_demo/app.rb +84 -0
  120. data/examples/ratatui_mascot_demo/app.rbs +7 -0
  121. data/examples/ratatui_mascot_demo/test_app.rb +47 -0
  122. data/examples/readme_usage/app.rb +29 -0
  123. data/examples/readme_usage/app.rbs +7 -0
  124. data/examples/readme_usage/test_app.rb +29 -0
  125. data/examples/rich_text/app.rb +141 -0
  126. data/examples/rich_text/app.rbs +7 -0
  127. data/examples/rich_text/test_app.rb +166 -0
  128. data/examples/scroll_text/app.rb +103 -0
  129. data/examples/scroll_text/app.rbs +7 -0
  130. data/examples/scroll_text/test_app.rb +110 -0
  131. data/examples/scrollbar_demo/app.rb +143 -0
  132. data/examples/scrollbar_demo/app.rbs +7 -0
  133. data/examples/scrollbar_demo/test_app.rb +77 -0
  134. data/examples/sparkline_demo/app.rb +240 -0
  135. data/examples/sparkline_demo/app.rbs +10 -0
  136. data/examples/sparkline_demo/test_app.rb +107 -0
  137. data/examples/table_flex/app.rb +65 -0
  138. data/examples/table_flex/app.rbs +7 -0
  139. data/examples/table_flex/test_app.rb +36 -0
  140. data/examples/table_select/app.rb +198 -0
  141. data/examples/table_select/app.rbs +7 -0
  142. data/examples/table_select/test_app.rb +180 -0
  143. data/examples/widget_style_colors/app.rb +104 -0
  144. data/examples/widget_style_colors/app.rbs +14 -0
  145. data/examples/widget_style_colors/test_app.rb +48 -0
  146. data/ext/ratatui_ruby/Cargo.lock +889 -115
  147. data/ext/ratatui_ruby/Cargo.toml +4 -3
  148. data/ext/ratatui_ruby/clippy.toml +7 -0
  149. data/ext/ratatui_ruby/extconf.rb +7 -0
  150. data/ext/ratatui_ruby/src/events.rs +218 -229
  151. data/ext/ratatui_ruby/src/lib.rs +38 -10
  152. data/ext/ratatui_ruby/src/rendering.rs +91 -10
  153. data/ext/ratatui_ruby/src/style.rs +281 -98
  154. data/ext/ratatui_ruby/src/terminal.rs +119 -25
  155. data/ext/ratatui_ruby/src/text.rs +171 -0
  156. data/ext/ratatui_ruby/src/widgets/barchart.rs +97 -24
  157. data/ext/ratatui_ruby/src/widgets/block.rs +31 -3
  158. data/ext/ratatui_ruby/src/widgets/calendar.rs +45 -44
  159. data/ext/ratatui_ruby/src/widgets/canvas.rs +46 -29
  160. data/ext/ratatui_ruby/src/widgets/chart.rs +69 -27
  161. data/ext/ratatui_ruby/src/widgets/clear.rs +3 -1
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +11 -4
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +218 -15
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +92 -0
  165. data/ext/ratatui_ruby/src/widgets/list.rs +91 -11
  166. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  167. data/ext/ratatui_ruby/src/widgets/overlay.rs +3 -2
  168. data/ext/ratatui_ruby/src/widgets/paragraph.rs +35 -13
  169. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +29 -0
  170. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +44 -0
  171. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +59 -7
  172. data/ext/ratatui_ruby/src/widgets/sparkline.rs +70 -6
  173. data/ext/ratatui_ruby/src/widgets/table.rs +173 -64
  174. data/ext/ratatui_ruby/src/widgets/tabs.rs +105 -5
  175. data/lib/ratatui_ruby/cell.rb +166 -0
  176. data/lib/ratatui_ruby/event/focus_gained.rb +49 -0
  177. data/lib/ratatui_ruby/event/focus_lost.rb +50 -0
  178. data/lib/ratatui_ruby/event/key.rb +211 -0
  179. data/lib/ratatui_ruby/event/mouse.rb +124 -0
  180. data/lib/ratatui_ruby/event/paste.rb +71 -0
  181. data/lib/ratatui_ruby/event/resize.rb +80 -0
  182. data/lib/ratatui_ruby/event.rb +79 -0
  183. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +45 -0
  184. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +27 -0
  185. data/lib/ratatui_ruby/schema/bar_chart.rb +228 -19
  186. data/lib/ratatui_ruby/schema/block.rb +186 -14
  187. data/lib/ratatui_ruby/schema/calendar.rb +74 -17
  188. data/lib/ratatui_ruby/schema/canvas.rb +215 -48
  189. data/lib/ratatui_ruby/schema/center.rb +49 -11
  190. data/lib/ratatui_ruby/schema/chart.rb +151 -41
  191. data/lib/ratatui_ruby/schema/clear.rb +41 -72
  192. data/lib/ratatui_ruby/schema/constraint.rb +82 -22
  193. data/lib/ratatui_ruby/schema/cursor.rb +27 -9
  194. data/lib/ratatui_ruby/schema/draw.rb +53 -0
  195. data/lib/ratatui_ruby/schema/gauge.rb +59 -15
  196. data/lib/ratatui_ruby/schema/layout.rb +95 -13
  197. data/lib/ratatui_ruby/schema/line_gauge.rb +78 -0
  198. data/lib/ratatui_ruby/schema/list.rb +93 -19
  199. data/lib/ratatui_ruby/schema/overlay.rb +34 -8
  200. data/lib/ratatui_ruby/schema/paragraph.rb +87 -30
  201. data/lib/ratatui_ruby/schema/ratatui_logo.rb +25 -0
  202. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +29 -0
  203. data/lib/ratatui_ruby/schema/rect.rb +64 -15
  204. data/lib/ratatui_ruby/schema/scrollbar.rb +132 -24
  205. data/lib/ratatui_ruby/schema/shape/label.rb +66 -0
  206. data/lib/ratatui_ruby/schema/sparkline.rb +122 -15
  207. data/lib/ratatui_ruby/schema/style.rb +49 -21
  208. data/lib/ratatui_ruby/schema/table.rb +119 -21
  209. data/lib/ratatui_ruby/schema/tabs.rb +75 -13
  210. data/lib/ratatui_ruby/schema/text.rb +90 -0
  211. data/lib/ratatui_ruby/session.rb +146 -0
  212. data/lib/ratatui_ruby/test_helper.rb +156 -13
  213. data/lib/ratatui_ruby/version.rb +1 -1
  214. data/lib/ratatui_ruby.rb +143 -23
  215. data/mise.toml +1 -1
  216. data/sig/ratatui_ruby/event.rbs +69 -0
  217. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -1
  218. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +16 -0
  219. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +13 -0
  220. data/sig/ratatui_ruby/schema/bar_chart.rbs +20 -2
  221. data/sig/ratatui_ruby/schema/block.rbs +5 -4
  222. data/sig/ratatui_ruby/schema/calendar.rbs +6 -2
  223. data/sig/ratatui_ruby/schema/canvas.rbs +52 -39
  224. data/sig/ratatui_ruby/schema/center.rbs +3 -3
  225. data/sig/ratatui_ruby/schema/chart.rbs +8 -5
  226. data/sig/ratatui_ruby/schema/constraint.rbs +8 -5
  227. data/sig/ratatui_ruby/schema/cursor.rbs +1 -1
  228. data/sig/ratatui_ruby/schema/draw.rbs +23 -0
  229. data/sig/ratatui_ruby/schema/gauge.rbs +4 -2
  230. data/sig/ratatui_ruby/schema/layout.rbs +11 -1
  231. data/sig/ratatui_ruby/schema/line_gauge.rbs +16 -0
  232. data/sig/ratatui_ruby/schema/list.rbs +5 -1
  233. data/sig/ratatui_ruby/schema/paragraph.rbs +4 -1
  234. data/{lib/ratatui_ruby/output.rb → sig/ratatui_ruby/schema/ratatui_logo.rbs} +3 -2
  235. data/sig/ratatui_ruby/{buffer.rbs → schema/ratatui_mascot.rbs} +4 -3
  236. data/sig/ratatui_ruby/schema/rect.rbs +2 -1
  237. data/sig/ratatui_ruby/schema/scrollbar.rbs +18 -2
  238. data/sig/ratatui_ruby/schema/sparkline.rbs +6 -2
  239. data/sig/ratatui_ruby/schema/table.rbs +8 -1
  240. data/sig/ratatui_ruby/schema/tabs.rbs +5 -1
  241. data/sig/ratatui_ruby/schema/text.rbs +22 -0
  242. data/tasks/resources/build.yml.erb +13 -24
  243. data/tasks/terminal_preview/app_screenshot.rb +35 -0
  244. data/tasks/terminal_preview/crash_report.rb +54 -0
  245. data/tasks/terminal_preview/example_app.rb +25 -0
  246. data/tasks/terminal_preview/launcher_script.rb +48 -0
  247. data/tasks/terminal_preview/preview_collection.rb +60 -0
  248. data/tasks/terminal_preview/preview_timing.rb +22 -0
  249. data/tasks/terminal_preview/safety_confirmation.rb +58 -0
  250. data/tasks/terminal_preview/saved_screenshot.rb +55 -0
  251. data/tasks/terminal_preview/system_appearance.rb +11 -0
  252. data/tasks/terminal_preview/terminal_window.rb +138 -0
  253. data/tasks/terminal_preview/window_id.rb +14 -0
  254. data/tasks/terminal_preview.rake +28 -0
  255. data/tasks/test.rake +1 -1
  256. metadata +175 -54
  257. data/doc/images/examples-analytics.rb.png +0 -0
  258. data/doc/images/examples-box_demo.rb.png +0 -0
  259. data/doc/images/examples-calendar_demo.rb.png +0 -0
  260. data/doc/images/examples-chart_demo.rb.png +0 -0
  261. data/doc/images/examples-custom_widget.rb.png +0 -0
  262. data/doc/images/examples-dashboard.rb.png +0 -0
  263. data/doc/images/examples-list_styles.rb.png +0 -0
  264. data/doc/images/examples-login_form.rb.png +0 -0
  265. data/doc/images/examples-map_demo.rb.png +0 -0
  266. data/doc/images/examples-mouse_events.rb.png +0 -0
  267. data/doc/images/examples-popup_demo.rb.gif +0 -0
  268. data/doc/images/examples-quickstart_lifecycle.rb.png +0 -0
  269. data/doc/images/examples-scroll_text.rb.png +0 -0
  270. data/doc/images/examples-scrollbar_demo.rb.png +0 -0
  271. data/doc/images/examples-stock_ticker.rb.png +0 -0
  272. data/doc/images/examples-system_monitor.rb.png +0 -0
  273. data/doc/images/examples-table_select.rb.png +0 -0
  274. data/examples/analytics.rb +0 -88
  275. data/examples/box_demo.rb +0 -71
  276. data/examples/calendar_demo.rb +0 -55
  277. data/examples/chart_demo.rb +0 -84
  278. data/examples/custom_widget.rb +0 -43
  279. data/examples/dashboard.rb +0 -72
  280. data/examples/list_styles.rb +0 -66
  281. data/examples/map_demo.rb +0 -58
  282. data/examples/quickstart_dsl.rb +0 -30
  283. data/examples/quickstart_lifecycle.rb +0 -40
  284. data/examples/readme_usage.rb +0 -21
  285. data/examples/scroll_text.rb +0 -74
  286. data/examples/scrollbar_demo.rb +0 -75
  287. data/examples/stock_ticker.rb +0 -93
  288. data/examples/system_monitor.rb +0 -94
  289. data/examples/table_select.rb +0 -70
  290. data/examples/test_analytics.rb +0 -65
  291. data/examples/test_box_demo.rb +0 -38
  292. data/examples/test_calendar_demo.rb +0 -66
  293. data/examples/test_dashboard.rb +0 -38
  294. data/examples/test_list_styles.rb +0 -61
  295. data/examples/test_login_form.rb +0 -63
  296. data/examples/test_map_demo.rb +0 -100
  297. data/examples/test_scroll_text.rb +0 -130
  298. data/examples/test_stock_ticker.rb +0 -39
  299. data/examples/test_system_monitor.rb +0 -40
  300. data/examples/test_table_select.rb +0 -37
  301. data/ext/ratatui_ruby/src/buffer.rs +0 -54
  302. data/lib/ratatui_ruby/dsl.rb +0 -64
@@ -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[:type] == :key && event[:code] == "esc"
44
+ break if event == :esc
45
45
 
46
46
  # 3. Construct View Tree
47
47
  ui = RatatuiRuby::Paragraph.new(text: "Time: #{Time.now}")