ratatui_ruby 0.4.0 → 0.6.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 (441) hide show
  1. checksums.yaml +4 -4
  2. data/.builds/ruby-3.2.yml +1 -1
  3. data/.builds/ruby-3.3.yml +1 -1
  4. data/.builds/ruby-3.4.yml +1 -1
  5. data/.builds/ruby-4.0.0.yml +1 -1
  6. data/AGENTS.md +98 -176
  7. data/CHANGELOG.md +80 -6
  8. data/README.md +19 -7
  9. data/REUSE.toml +15 -0
  10. data/doc/application_architecture.md +179 -45
  11. data/doc/application_testing.md +80 -32
  12. data/doc/contributors/design/ruby_frontend.md +48 -8
  13. data/doc/contributors/design/rust_backend.md +1 -0
  14. data/doc/contributors/developing_examples.md +191 -48
  15. data/doc/contributors/documentation_style.md +7 -0
  16. data/doc/contributors/examples_audit/p1_high.md +21 -0
  17. data/doc/contributors/examples_audit/p2_moderate.md +81 -0
  18. data/doc/contributors/examples_audit.md +41 -0
  19. data/doc/contributors/index.md +2 -0
  20. data/doc/event_handling.md +21 -7
  21. data/doc/images/app_all_events.png +0 -0
  22. data/doc/images/app_color_picker.png +0 -0
  23. data/doc/images/app_login_form.png +0 -0
  24. data/doc/images/app_stateful_interaction.png +0 -0
  25. data/doc/images/verify_quickstart_dsl.png +0 -0
  26. data/doc/images/verify_quickstart_layout.png +0 -0
  27. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  28. data/doc/images/verify_readme_usage.png +0 -0
  29. data/doc/images/widget_barchart_demo.png +0 -0
  30. data/doc/images/widget_block_demo.png +0 -0
  31. data/doc/images/widget_box_demo.png +0 -0
  32. data/doc/images/widget_calendar_demo.png +0 -0
  33. data/doc/images/widget_canvas_demo.png +0 -0
  34. data/doc/images/widget_cell_demo.png +0 -0
  35. data/doc/images/widget_center_demo.png +0 -0
  36. data/doc/images/widget_chart_demo.png +0 -0
  37. data/doc/images/widget_gauge_demo.png +0 -0
  38. data/doc/images/widget_layout_split.png +0 -0
  39. data/doc/images/widget_line_gauge_demo.png +0 -0
  40. data/doc/images/widget_list_demo.png +0 -0
  41. data/doc/images/widget_overlay_demo.png +0 -0
  42. data/doc/images/widget_ratatui_logo_demo.png +0 -0
  43. data/doc/images/widget_ratatui_mascot_demo.png +0 -0
  44. data/doc/images/widget_render.png +0 -0
  45. data/doc/images/widget_rich_text.png +0 -0
  46. data/doc/images/widget_scroll_text.png +0 -0
  47. data/doc/images/widget_scrollbar_demo.png +0 -0
  48. data/doc/images/widget_sparkline_demo.png +0 -0
  49. data/doc/images/widget_style_colors.png +0 -0
  50. data/doc/images/widget_table_demo.png +0 -0
  51. data/doc/images/widget_table_flex.png +0 -0
  52. data/doc/images/widget_tabs_demo.png +0 -0
  53. data/doc/images/widget_text_width.png +0 -0
  54. data/doc/interactive_design.md +25 -30
  55. data/doc/quickstart.md +150 -130
  56. data/doc/terminal_limitations.md +92 -0
  57. data/examples/app_all_events/README.md +99 -0
  58. data/examples/app_all_events/app.rb +96 -0
  59. data/examples/app_all_events/model/app_model.rb +157 -0
  60. data/examples/app_all_events/model/event_color_cycle.rb +41 -0
  61. data/examples/app_all_events/model/event_entry.rb +92 -0
  62. data/examples/app_all_events/model/msg.rb +37 -0
  63. data/examples/app_all_events/model/timestamp.rb +54 -0
  64. data/examples/app_all_events/update.rb +73 -0
  65. data/examples/app_all_events/view/app_view.rb +78 -0
  66. data/examples/app_all_events/view/controls_view.rb +52 -0
  67. data/examples/app_all_events/view/counts_view.rb +59 -0
  68. data/examples/app_all_events/view/live_view.rb +70 -0
  69. data/examples/app_all_events/view/log_view.rb +55 -0
  70. data/examples/app_all_events/view.rb +7 -0
  71. data/examples/app_color_picker/README.md +134 -0
  72. data/examples/app_color_picker/app.rb +74 -0
  73. data/examples/app_color_picker/clipboard.rb +84 -0
  74. data/examples/app_color_picker/color.rb +191 -0
  75. data/examples/app_color_picker/controls.rb +90 -0
  76. data/examples/app_color_picker/copy_dialog.rb +166 -0
  77. data/examples/app_color_picker/export_pane.rb +126 -0
  78. data/examples/app_color_picker/harmony.rb +56 -0
  79. data/examples/app_color_picker/input.rb +174 -0
  80. data/examples/app_color_picker/main_container.rb +178 -0
  81. data/examples/app_color_picker/palette.rb +109 -0
  82. data/examples/app_login_form/README.md +47 -0
  83. data/examples/{login_form → app_login_form}/app.rb +38 -42
  84. data/examples/app_stateful_interaction/README.md +31 -0
  85. data/examples/app_stateful_interaction/app.rb +272 -0
  86. data/examples/timeout_demo.rb +43 -0
  87. data/examples/verify_quickstart_dsl/README.md +48 -0
  88. data/examples/{quickstart_dsl → verify_quickstart_dsl}/app.rb +17 -6
  89. data/examples/verify_quickstart_layout/README.md +71 -0
  90. data/examples/verify_quickstart_layout/app.rb +71 -0
  91. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  92. data/examples/verify_quickstart_lifecycle/app.rb +54 -0
  93. data/examples/verify_readme_usage/README.md +43 -0
  94. data/examples/verify_readme_usage/app.rb +40 -0
  95. data/examples/widget_barchart_demo/README.md +49 -0
  96. data/examples/widget_barchart_demo/app.rb +238 -0
  97. data/examples/widget_block_demo/README.md +34 -0
  98. data/examples/widget_block_demo/app.rb +256 -0
  99. data/examples/widget_box_demo/README.md +45 -0
  100. data/examples/{box_demo → widget_box_demo}/app.rb +99 -65
  101. data/examples/widget_calendar_demo/README.md +39 -0
  102. data/examples/widget_calendar_demo/app.rb +109 -0
  103. data/examples/widget_canvas_demo/README.md +27 -0
  104. data/examples/widget_canvas_demo/app.rb +123 -0
  105. data/examples/widget_cell_demo/README.md +36 -0
  106. data/examples/widget_cell_demo/app.rb +111 -0
  107. data/examples/widget_center_demo/README.md +29 -0
  108. data/examples/widget_center_demo/app.rb +116 -0
  109. data/examples/widget_chart_demo/README.md +41 -0
  110. data/examples/widget_chart_demo/app.rb +218 -0
  111. data/examples/widget_gauge_demo/README.md +41 -0
  112. data/examples/widget_gauge_demo/app.rb +212 -0
  113. data/examples/widget_layout_split/README.md +44 -0
  114. data/examples/widget_layout_split/app.rb +246 -0
  115. data/examples/widget_line_gauge_demo/README.md +41 -0
  116. data/examples/widget_line_gauge_demo/app.rb +217 -0
  117. data/examples/widget_list_demo/README.md +49 -0
  118. data/examples/widget_list_demo/app.rb +366 -0
  119. data/examples/widget_map_demo/README.md +39 -0
  120. data/examples/{map_demo → widget_map_demo}/app.rb +24 -21
  121. data/examples/widget_overlay_demo/app.rb +248 -0
  122. data/examples/widget_popup_demo/README.md +36 -0
  123. data/examples/widget_popup_demo/app.rb +104 -0
  124. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  125. data/examples/widget_ratatui_logo_demo/app.rb +103 -0
  126. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  127. data/examples/widget_ratatui_mascot_demo/app.rb +93 -0
  128. data/examples/widget_rect/README.md +38 -0
  129. data/examples/widget_rect/app.rb +205 -0
  130. data/examples/widget_render/README.md +37 -0
  131. data/examples/widget_render/app.rb +184 -0
  132. data/examples/widget_rich_text/README.md +35 -0
  133. data/examples/widget_rich_text/app.rb +166 -0
  134. data/examples/widget_scroll_text/README.md +37 -0
  135. data/examples/widget_scroll_text/app.rb +107 -0
  136. data/examples/widget_scrollbar_demo/README.md +37 -0
  137. data/examples/widget_scrollbar_demo/app.rb +153 -0
  138. data/examples/widget_sparkline_demo/README.md +42 -0
  139. data/examples/widget_sparkline_demo/app.rb +275 -0
  140. data/examples/widget_style_colors/README.md +34 -0
  141. data/examples/widget_style_colors/app.rb +19 -21
  142. data/examples/widget_table_demo/README.md +48 -0
  143. data/examples/widget_table_demo/app.rb +239 -0
  144. data/examples/widget_tabs_demo/README.md +41 -0
  145. data/examples/widget_tabs_demo/app.rb +181 -0
  146. data/examples/widget_text_width/README.md +35 -0
  147. data/examples/widget_text_width/app.rb +106 -0
  148. data/ext/ratatui_ruby/Cargo.lock +11 -4
  149. data/ext/ratatui_ruby/Cargo.toml +2 -1
  150. data/ext/ratatui_ruby/src/events.rs +359 -62
  151. data/ext/ratatui_ruby/src/frame.rs +227 -0
  152. data/ext/ratatui_ruby/src/lib.rs +110 -27
  153. data/ext/ratatui_ruby/src/rendering.rs +8 -4
  154. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  155. data/ext/ratatui_ruby/src/style.rs +138 -57
  156. data/ext/ratatui_ruby/src/terminal.rs +42 -22
  157. data/ext/ratatui_ruby/src/text.rs +14 -7
  158. data/ext/ratatui_ruby/src/widgets/barchart.rs +74 -54
  159. data/ext/ratatui_ruby/src/widgets/block.rs +7 -6
  160. data/ext/ratatui_ruby/src/widgets/canvas.rs +21 -3
  161. data/ext/ratatui_ruby/src/widgets/chart.rs +20 -10
  162. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  163. data/ext/ratatui_ruby/src/widgets/layout.rs +9 -4
  164. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  165. data/ext/ratatui_ruby/src/widgets/list.rs +211 -12
  166. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  167. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  168. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  169. data/ext/ratatui_ruby/src/widgets/paragraph.rs +1 -1
  170. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +19 -8
  171. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +17 -10
  172. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +97 -3
  173. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  174. data/ext/ratatui_ruby/src/widgets/sparkline.rs +14 -11
  175. data/ext/ratatui_ruby/src/widgets/table.rs +121 -5
  176. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  177. data/ext/ratatui_ruby/src/widgets/tabs.rs +11 -11
  178. data/lib/ratatui_ruby/cell.rb +7 -7
  179. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  180. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  181. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  182. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  183. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  184. data/lib/ratatui_ruby/event/key.rb +112 -52
  185. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  186. data/lib/ratatui_ruby/event/none.rb +43 -0
  187. data/lib/ratatui_ruby/event/paste.rb +1 -1
  188. data/lib/ratatui_ruby/event.rb +56 -4
  189. data/lib/ratatui_ruby/frame.rb +183 -0
  190. data/lib/ratatui_ruby/list_state.rb +88 -0
  191. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +13 -13
  192. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +1 -5
  193. data/lib/ratatui_ruby/schema/bar_chart.rb +217 -217
  194. data/lib/ratatui_ruby/schema/block.rb +163 -168
  195. data/lib/ratatui_ruby/schema/calendar.rb +66 -67
  196. data/lib/ratatui_ruby/schema/canvas.rb +63 -63
  197. data/lib/ratatui_ruby/schema/center.rb +46 -46
  198. data/lib/ratatui_ruby/schema/chart.rb +135 -143
  199. data/lib/ratatui_ruby/schema/clear.rb +42 -42
  200. data/lib/ratatui_ruby/schema/constraint.rb +76 -76
  201. data/lib/ratatui_ruby/schema/cursor.rb +30 -25
  202. data/lib/ratatui_ruby/schema/gauge.rb +54 -52
  203. data/lib/ratatui_ruby/schema/layout.rb +87 -87
  204. data/lib/ratatui_ruby/schema/line_gauge.rb +62 -62
  205. data/lib/ratatui_ruby/schema/list.rb +103 -80
  206. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  207. data/lib/ratatui_ruby/schema/overlay.rb +31 -31
  208. data/lib/ratatui_ruby/schema/paragraph.rb +80 -80
  209. data/lib/ratatui_ruby/schema/ratatui_logo.rb +10 -6
  210. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +10 -5
  211. data/lib/ratatui_ruby/schema/rect.rb +99 -56
  212. data/lib/ratatui_ruby/schema/scrollbar.rb +119 -119
  213. data/lib/ratatui_ruby/schema/shape/label.rb +1 -1
  214. data/lib/ratatui_ruby/schema/sparkline.rb +111 -110
  215. data/lib/ratatui_ruby/schema/style.rb +66 -46
  216. data/lib/ratatui_ruby/schema/table.rb +126 -115
  217. data/lib/ratatui_ruby/schema/tabs.rb +66 -67
  218. data/lib/ratatui_ruby/schema/text.rb +69 -1
  219. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  220. data/lib/ratatui_ruby/session/autodoc.rb +482 -0
  221. data/lib/ratatui_ruby/session.rb +55 -23
  222. data/lib/ratatui_ruby/table_state.rb +90 -0
  223. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  224. data/lib/ratatui_ruby/test_helper/snapshot.rb +390 -0
  225. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  226. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  227. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  228. data/lib/ratatui_ruby/test_helper.rb +66 -193
  229. data/lib/ratatui_ruby/version.rb +1 -1
  230. data/lib/ratatui_ruby.rb +100 -51
  231. data/{examples/sparkline_demo → sig/examples/app_all_events}/app.rbs +3 -2
  232. data/sig/examples/app_all_events/model/event_entry.rbs +16 -0
  233. data/sig/examples/app_all_events/model/events.rbs +15 -0
  234. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  235. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  236. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  237. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  238. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  239. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  240. data/sig/examples/app_all_events/view.rbs +8 -0
  241. data/sig/examples/app_all_events/view_state.rbs +15 -0
  242. data/{examples/list_demo → sig/examples/app_color_picker}/app.rbs +2 -2
  243. data/sig/examples/app_login_form/app.rbs +11 -0
  244. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  245. data/sig/examples/verify_quickstart_dsl/app.rbs +11 -0
  246. data/sig/examples/verify_quickstart_lifecycle/app.rbs +11 -0
  247. data/sig/examples/verify_readme_usage/app.rbs +11 -0
  248. data/sig/examples/widget_block_demo/app.rbs +32 -0
  249. data/sig/examples/widget_box_demo/app.rbs +11 -0
  250. data/sig/examples/widget_calendar_demo/app.rbs +11 -0
  251. data/sig/examples/widget_cell_demo/app.rbs +11 -0
  252. data/sig/examples/widget_chart_demo/app.rbs +11 -0
  253. data/{examples/gauge_demo → sig/examples/widget_gauge_demo}/app.rbs +4 -0
  254. data/sig/examples/widget_layout_split/app.rbs +10 -0
  255. data/sig/examples/widget_line_gauge_demo/app.rbs +11 -0
  256. data/sig/examples/widget_list_demo/app.rbs +12 -0
  257. data/sig/examples/widget_map_demo/app.rbs +11 -0
  258. data/sig/examples/widget_popup_demo/app.rbs +11 -0
  259. data/sig/examples/widget_ratatui_logo_demo/app.rbs +11 -0
  260. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +11 -0
  261. data/sig/examples/widget_rect/app.rbs +12 -0
  262. data/sig/examples/widget_render/app.rbs +10 -0
  263. data/sig/examples/widget_rich_text/app.rbs +11 -0
  264. data/sig/examples/widget_scroll_text/app.rbs +11 -0
  265. data/sig/examples/widget_scrollbar_demo/app.rbs +11 -0
  266. data/sig/examples/widget_sparkline_demo/app.rbs +10 -0
  267. data/{examples → sig/examples}/widget_style_colors/app.rbs +1 -1
  268. data/sig/examples/widget_table_demo/app.rbs +11 -0
  269. data/sig/examples/widget_text_width/app.rbs +10 -0
  270. data/sig/ratatui_ruby/event.rbs +11 -1
  271. data/sig/ratatui_ruby/frame.rbs +11 -0
  272. data/sig/ratatui_ruby/list_state.rbs +13 -0
  273. data/sig/ratatui_ruby/ratatui_ruby.rbs +5 -4
  274. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  275. data/sig/ratatui_ruby/schema/draw.rbs +4 -0
  276. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  277. data/sig/ratatui_ruby/schema/layout.rbs +1 -1
  278. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  279. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  280. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  281. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  282. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  283. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  284. data/sig/ratatui_ruby/schema/text.rbs +8 -6
  285. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  286. data/sig/ratatui_ruby/session.rbs +107 -0
  287. data/sig/ratatui_ruby/table_state.rbs +15 -0
  288. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  289. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  290. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  291. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  292. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  293. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  294. data/tasks/autodoc/examples.rb +79 -0
  295. data/tasks/autodoc/inventory.rb +63 -0
  296. data/tasks/autodoc/member.rb +56 -0
  297. data/tasks/autodoc/name.rb +19 -0
  298. data/tasks/autodoc/notice.rb +26 -0
  299. data/tasks/autodoc/rbs.rb +38 -0
  300. data/tasks/autodoc/rdoc.rb +45 -0
  301. data/tasks/autodoc.rake +53 -0
  302. data/tasks/bump/changelog.rb +3 -3
  303. data/tasks/bump/history.rb +2 -2
  304. data/tasks/bump/links.rb +67 -0
  305. data/tasks/doc.rake +600 -6
  306. data/tasks/example_viewer.html.erb +172 -0
  307. data/tasks/lint.rake +8 -4
  308. data/tasks/resources/index.html.erb +6 -0
  309. data/tasks/sourcehut.rake +70 -30
  310. data/tasks/terminal_preview/app_screenshot.rb +14 -6
  311. data/tasks/terminal_preview/crash_report.rb +7 -9
  312. data/tasks/terminal_preview/launcher_script.rb +4 -6
  313. data/tasks/terminal_preview/preview_collection.rb +4 -6
  314. data/tasks/terminal_preview/safety_confirmation.rb +3 -5
  315. data/tasks/terminal_preview/saved_screenshot.rb +10 -11
  316. data/tasks/terminal_preview/terminal_window.rb +7 -9
  317. data/tasks/test.rake +1 -1
  318. data/tasks/website/index_page.rb +3 -3
  319. data/tasks/website/version.rb +10 -10
  320. data/tasks/website/version_menu.rb +10 -12
  321. data/tasks/website/versioned_documentation.rb +49 -17
  322. data/tasks/website/website.rb +6 -8
  323. data/tasks/website.rake +4 -4
  324. metadata +232 -127
  325. data/LICENSES/BSD-2-Clause.txt +0 -9
  326. data/doc/contributors/better_dx.md +0 -543
  327. data/doc/contributors/example_analysis.md +0 -82
  328. data/doc/images/all_events.png +0 -0
  329. data/doc/images/block_padding.png +0 -0
  330. data/doc/images/block_titles.png +0 -0
  331. data/doc/images/box_demo.png +0 -0
  332. data/doc/images/calendar_demo.png +0 -0
  333. data/doc/images/cell_demo.png +0 -0
  334. data/doc/images/chart_demo.png +0 -0
  335. data/doc/images/flex_layout.png +0 -0
  336. data/doc/images/gauge_demo.png +0 -0
  337. data/doc/images/line_gauge_demo.png +0 -0
  338. data/doc/images/list_demo.png +0 -0
  339. data/doc/images/list_styles.png +0 -0
  340. data/doc/images/login_form.png +0 -0
  341. data/doc/images/quickstart_dsl.png +0 -0
  342. data/doc/images/quickstart_lifecycle.png +0 -0
  343. data/doc/images/readme_usage.png +0 -0
  344. data/doc/images/rich_text.png +0 -0
  345. data/doc/images/scroll_text.png +0 -0
  346. data/doc/images/scrollbar_demo.png +0 -0
  347. data/doc/images/sparkline_demo.png +0 -0
  348. data/doc/images/table_flex.png +0 -0
  349. data/doc/images/table_select.png +0 -0
  350. data/examples/all_events/app.rb +0 -169
  351. data/examples/all_events/app.rbs +0 -7
  352. data/examples/all_events/test_app.rb +0 -139
  353. data/examples/analytics/app.rb +0 -258
  354. data/examples/analytics/app.rbs +0 -7
  355. data/examples/analytics/test_app.rb +0 -132
  356. data/examples/block_padding/app.rb +0 -63
  357. data/examples/block_padding/app.rbs +0 -7
  358. data/examples/block_padding/test_app.rb +0 -31
  359. data/examples/block_titles/app.rb +0 -61
  360. data/examples/block_titles/app.rbs +0 -7
  361. data/examples/block_titles/test_app.rb +0 -34
  362. data/examples/box_demo/app.rbs +0 -7
  363. data/examples/box_demo/test_app.rb +0 -88
  364. data/examples/calendar_demo/app.rb +0 -101
  365. data/examples/calendar_demo/app.rbs +0 -7
  366. data/examples/calendar_demo/test_app.rb +0 -108
  367. data/examples/cell_demo/app.rb +0 -108
  368. data/examples/cell_demo/app.rbs +0 -7
  369. data/examples/cell_demo/test_app.rb +0 -36
  370. data/examples/chart_demo/app.rb +0 -203
  371. data/examples/chart_demo/app.rbs +0 -7
  372. data/examples/chart_demo/test_app.rb +0 -102
  373. data/examples/custom_widget/app.rb +0 -51
  374. data/examples/custom_widget/app.rbs +0 -7
  375. data/examples/custom_widget/test_app.rb +0 -30
  376. data/examples/flex_layout/app.rb +0 -156
  377. data/examples/flex_layout/app.rbs +0 -7
  378. data/examples/flex_layout/test_app.rb +0 -65
  379. data/examples/gauge_demo/app.rb +0 -182
  380. data/examples/gauge_demo/test_app.rb +0 -120
  381. data/examples/hit_test/app.rb +0 -175
  382. data/examples/hit_test/app.rbs +0 -7
  383. data/examples/hit_test/test_app.rb +0 -102
  384. data/examples/line_gauge_demo/app.rb +0 -190
  385. data/examples/line_gauge_demo/app.rbs +0 -7
  386. data/examples/line_gauge_demo/test_app.rb +0 -129
  387. data/examples/list_demo/app.rb +0 -253
  388. data/examples/list_demo/test_app.rb +0 -237
  389. data/examples/list_styles/app.rb +0 -140
  390. data/examples/list_styles/app.rbs +0 -7
  391. data/examples/list_styles/test_app.rb +0 -157
  392. data/examples/login_form/app.rbs +0 -7
  393. data/examples/login_form/test_app.rb +0 -51
  394. data/examples/map_demo/app.rbs +0 -7
  395. data/examples/map_demo/test_app.rb +0 -149
  396. data/examples/mouse_events/app.rb +0 -97
  397. data/examples/mouse_events/app.rbs +0 -7
  398. data/examples/mouse_events/test_app.rb +0 -53
  399. data/examples/popup_demo/app.rb +0 -103
  400. data/examples/popup_demo/app.rbs +0 -7
  401. data/examples/popup_demo/test_app.rb +0 -54
  402. data/examples/quickstart_dsl/app.rbs +0 -7
  403. data/examples/quickstart_dsl/test_app.rb +0 -29
  404. data/examples/quickstart_lifecycle/app.rb +0 -39
  405. data/examples/quickstart_lifecycle/app.rbs +0 -7
  406. data/examples/quickstart_lifecycle/test_app.rb +0 -29
  407. data/examples/ratatui_logo_demo/app.rb +0 -79
  408. data/examples/ratatui_logo_demo/app.rbs +0 -7
  409. data/examples/ratatui_logo_demo/test_app.rb +0 -51
  410. data/examples/ratatui_mascot_demo/app.rb +0 -84
  411. data/examples/ratatui_mascot_demo/app.rbs +0 -7
  412. data/examples/ratatui_mascot_demo/test_app.rb +0 -47
  413. data/examples/readme_usage/app.rb +0 -29
  414. data/examples/readme_usage/app.rbs +0 -7
  415. data/examples/readme_usage/test_app.rb +0 -29
  416. data/examples/rich_text/app.rb +0 -141
  417. data/examples/rich_text/app.rbs +0 -7
  418. data/examples/rich_text/test_app.rb +0 -166
  419. data/examples/scroll_text/app.rb +0 -103
  420. data/examples/scroll_text/app.rbs +0 -7
  421. data/examples/scroll_text/test_app.rb +0 -110
  422. data/examples/scrollbar_demo/app.rb +0 -143
  423. data/examples/scrollbar_demo/app.rbs +0 -7
  424. data/examples/scrollbar_demo/test_app.rb +0 -77
  425. data/examples/sparkline_demo/app.rb +0 -240
  426. data/examples/sparkline_demo/test_app.rb +0 -107
  427. data/examples/table_flex/app.rb +0 -65
  428. data/examples/table_flex/app.rbs +0 -7
  429. data/examples/table_flex/test_app.rb +0 -36
  430. data/examples/table_select/app.rb +0 -198
  431. data/examples/table_select/app.rbs +0 -7
  432. data/examples/table_select/test_app.rb +0 -180
  433. data/examples/widget_style_colors/test_app.rb +0 -48
  434. data/tasks/bump/comparison_links.rb +0 -41
  435. /data/doc/images/{analytics.png → app_analytics.png} +0 -0
  436. /data/doc/images/{custom_widget.png → app_custom_widget.png} +0 -0
  437. /data/doc/images/{mouse_events.png → app_mouse_events.png} +0 -0
  438. /data/doc/images/{map_demo.png → widget_map_demo.png} +0 -0
  439. /data/doc/images/{popup_demo.png → widget_popup_demo.png} +0 -0
  440. /data/doc/images/{hit_test.png → widget_rect.png} +0 -0
  441. /data/{doc/images/ratatui_logo_demo.png → exe/.gitkeep} +0 -0
@@ -0,0 +1,174 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "color"
7
+
8
+ # A self-contained text input component for color entry.
9
+ #
10
+ # Users type color values. They make mistakes—typos, invalid formats. The app
11
+ # needs to validate their input and show helpful error messages.
12
+ #
13
+ # This component encapsulates rendering, state, and event handling. It draws
14
+ # itself into the provided area, caches that area for hit testing, and handles
15
+ # keyboard events internally.
16
+ #
17
+ # === Component Contract
18
+ #
19
+ # - `render(tui, frame, area)`: Draws the input field; stores `area` for hit testing
20
+ # - `handle_event(event) -> Symbol | nil`: Returns `:consumed`, `:submitted`, or `nil`
21
+ #
22
+ # === Example
23
+ #
24
+ # input = Input.new
25
+ # input.render(tui, frame, area)
26
+ #
27
+ # result = input.handle_event(event)
28
+ # case result
29
+ # when :submitted
30
+ # palette.update_color(input.parsed_color)
31
+ # end
32
+ class Input
33
+ PRINTABLE_PATTERN = /[\w#,().\s%]/
34
+
35
+ # Creates a new Input with an optional initial value.
36
+ #
37
+ # [initial_value] String initial color input (default: <tt>"#F96302"</tt>)
38
+ def initialize(initial_value = "#F96302")
39
+ @value = initial_value
40
+ @error = ""
41
+ @parsed_color = nil
42
+ @area = nil
43
+ end
44
+
45
+ # Current input string.
46
+ attr_reader :value
47
+
48
+ # Error message from the last failed parse, or empty string.
49
+ attr_reader :error
50
+
51
+ # The last successfully parsed Color, or nil.
52
+ attr_reader :parsed_color
53
+
54
+ # The cached render area, for hit testing.
55
+ attr_reader :area
56
+
57
+ # Clears the current error message.
58
+ def clear_error
59
+ @error = ""
60
+ end
61
+
62
+ # Renders the input widget into the given area.
63
+ #
64
+ # Caches `area` for hit testing. Shows the current input value and positions
65
+ # the terminal's blinking cursor at the end of the text using
66
+ # `frame.set_cursor_position`. Displays the error message in red if set.
67
+ #
68
+ # [tui] Session or TUI factory object
69
+ # [frame] Frame object from RatatuiRuby.draw block
70
+ # [area] Rect area to draw into
71
+ #
72
+ # === Example
73
+ #
74
+ # input.render(tui, frame, input_area)
75
+ def render(tui, frame, area)
76
+ @area = area
77
+ widget = build_widget(tui)
78
+ frame.render_widget(widget, area)
79
+
80
+ # Position real blinking cursor at end of input text
81
+ cursor_x, cursor_y = cursor_position_in(area)
82
+ frame.set_cursor_position(cursor_x, cursor_y)
83
+ end
84
+
85
+ # Processes a keyboard event and updates internal state.
86
+ #
87
+ # Returns:
88
+ # - `:submitted` when Enter is pressed (caller should read `parsed_color`)
89
+ # - `:consumed` when the event was handled (typing, backspace)
90
+ # - `nil` when the event was ignored
91
+ #
92
+ # [event] Event from RatatuiRuby.poll_event
93
+ #
94
+ # === Example
95
+ #
96
+ # result = input.handle_event(event)
97
+ # if result == :submitted
98
+ # palette.update_color(input.parsed_color)
99
+ # end
100
+ def handle_event(event)
101
+ case event
102
+ in { type: :key, code: "enter" }
103
+ parse
104
+ :submitted
105
+ in { type: :key, code: "backspace" }
106
+ delete_char
107
+ :consumed
108
+ in { type: :paste, content: }
109
+ set(content)
110
+ parse
111
+ :submitted
112
+ in { type: :key, code: code }
113
+ append_char(code)
114
+ :consumed
115
+ else
116
+ nil
117
+ end
118
+ end
119
+
120
+ private def append_char(char)
121
+ @value += char if char.length == 1 && char.match?(PRINTABLE_PATTERN)
122
+ end
123
+
124
+ private def delete_char
125
+ @value = @value[0...-1]
126
+ end
127
+
128
+ private def set(text)
129
+ @value = text
130
+ end
131
+
132
+ private def parse
133
+ color = Color.parse(@value)
134
+ if color
135
+ clear_error
136
+ @parsed_color = color
137
+ else
138
+ @error = "Invalid color format. Try: #ff0000, rgb(255,0,0), red"
139
+ @parsed_color = nil
140
+ end
141
+ end
142
+
143
+ private def build_widget(tui)
144
+ input_lines = [
145
+ tui.text_line(spans: [
146
+ tui.text_span(content: @value),
147
+ ]),
148
+ ]
149
+
150
+ unless @error.empty?
151
+ input_lines << tui.text_line(spans: [
152
+ tui.text_span(content: @error, style: tui.style(fg: :red)),
153
+ ])
154
+ end
155
+
156
+ tui.block(
157
+ title: "Color Input",
158
+ borders: [:all],
159
+ children: [
160
+ tui.paragraph(text: input_lines),
161
+ ]
162
+ )
163
+ end
164
+
165
+ # Calculates cursor position within the input area.
166
+ #
167
+ # Accounts for block border (1 cell) and current text length.
168
+ private def cursor_position_in(area)
169
+ # Border takes 1 cell on left, cursor goes after last character
170
+ x = area.x + 1 + @value.length
171
+ y = area.y + 1 # First line inside border
172
+ [x, y]
173
+ end
174
+ end
@@ -0,0 +1,178 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "input"
7
+ require_relative "palette"
8
+ require_relative "export_pane"
9
+ require_relative "controls"
10
+ require_relative "clipboard"
11
+ require_relative "copy_dialog"
12
+
13
+ # The root container that owns all child components and orchestrates the UI.
14
+ #
15
+ # Building a complete color picker UI involves layout calculation, widget
16
+ # composition, event routing, and cross-component communication. The Container
17
+ # pattern centralizes this orchestration while keeping components decoupled.
18
+ #
19
+ # This container:
20
+ # - **Layout Phase**: Calculates Rects using tui.layout_split
21
+ # - **Delegation Phase**: Calls child.render(tui, frame, area) for each component
22
+ # - **Event Routing (Chain of Responsibility)**: Delegates events front-to-back
23
+ # - **Mediator Pattern**: Manages cross-component communication via symbolic signals
24
+ #
25
+ # === Component Contract
26
+ #
27
+ # - `render(tui, frame, area)`: Lays out and renders all children
28
+ # - `handle_event(event) -> Symbol | nil`: Routes events to children
29
+ # - `tick`: Delegates lifecycle updates (clipboard timer)
30
+ #
31
+ # === Example
32
+ #
33
+ # container = MainContainer.new(tui)
34
+ # container.render(tui, frame, frame.area)
35
+ # result = container.handle_event(event)
36
+ # container.tick
37
+ class MainContainer
38
+ def initialize(tui)
39
+ @tui = tui
40
+ @input = Input.new
41
+ @palette = Palette.new(@input.parsed_color)
42
+ @export_pane = ExportPane.new
43
+ @controls = Controls.new
44
+ @clipboard = Clipboard.new
45
+ @dialog = CopyDialog.new(@clipboard)
46
+
47
+ # Parse initial color
48
+ initial_result = simulate_initial_parse
49
+ @palette.update_color(initial_result) if initial_result
50
+ end
51
+
52
+ # Renders all child components into the given area.
53
+ #
54
+ # Calculates layout once per frame. Delegates rendering to each component.
55
+ # Renders the dialog overlay last for z-ordering.
56
+ #
57
+ # [tui] Session or TUI factory object
58
+ # [frame] Frame object from RatatuiRuby.draw block
59
+ # [area] Rect area to draw into
60
+ #
61
+ # === Example
62
+ #
63
+ # tui.draw { |frame| container.render(tui, frame, frame.area) }
64
+ def render(tui, frame, area)
65
+ # Layout Phase: calculate all areas
66
+ input_area, rest = tui.layout_split(
67
+ area,
68
+ direction: :vertical,
69
+ constraints: [
70
+ tui.constraint_length(3),
71
+ tui.constraint_fill(1),
72
+ ]
73
+ )
74
+
75
+ color_area, control_area = tui.layout_split(
76
+ rest,
77
+ direction: :vertical,
78
+ constraints: [
79
+ tui.constraint_length(14),
80
+ tui.constraint_fill(1),
81
+ ]
82
+ )
83
+
84
+ harmony_area, export_area = tui.layout_split(
85
+ color_area,
86
+ direction: :vertical,
87
+ constraints: [
88
+ tui.constraint_length(7),
89
+ tui.constraint_fill(1),
90
+ ]
91
+ )
92
+
93
+ # Delegation Phase: render each component
94
+ @input.render(tui, frame, input_area)
95
+ @palette.render(tui, frame, harmony_area)
96
+ @export_pane.render(tui, frame, export_area, palette: @palette)
97
+ @controls.render(tui, frame, control_area, clipboard: @clipboard)
98
+
99
+ # Overlay Logic: dialog rendered last for z-ordering
100
+ if @dialog.active?
101
+ dialog_area = calculate_center_area(area, 40, 8)
102
+ frame.render_widget(tui.clear, area)
103
+ @dialog.render(tui, frame, dialog_area)
104
+ end
105
+ end
106
+
107
+ # Routes events to child components in visual order (front-to-back).
108
+ #
109
+ # Implements Chain of Responsibility:
110
+ # 1. If dialog is active, offer it the event first
111
+ # 2. Then Input, ExportPane (which may trigger dialog)
112
+ # 3. Mediator pattern: interprets symbolic signals for cross-component effects
113
+ #
114
+ # Returns:
115
+ # - `:consumed` when any component handled the event
116
+ # - `nil` when no component handled the event
117
+ #
118
+ # [event] Event from RatatuiRuby.poll_event
119
+ #
120
+ # === Example
121
+ #
122
+ # result = container.handle_event(event)
123
+ def handle_event(event)
124
+ # Clear input error when not in dialog mode
125
+ @input.clear_error unless @dialog.active?
126
+
127
+ # Front-to-back: dialog has priority when active
128
+ if @dialog.active?
129
+ result = @dialog.handle_event(event)
130
+ return :consumed if result == :consumed
131
+ end
132
+
133
+ # Input component
134
+ result = @input.handle_event(event)
135
+ case result
136
+ when :submitted
137
+ # Mediator: sync Input -> Palette
138
+ @palette.update_color(@input.parsed_color)
139
+ return :consumed
140
+ when :consumed
141
+ return :consumed
142
+ end
143
+
144
+ # ExportPane: may request copy dialog
145
+ result = @export_pane.handle_event(event)
146
+ if result == :copy_requested && @palette.main
147
+ @dialog.open(@palette.main.hex)
148
+ return :consumed
149
+ end
150
+
151
+ # Palette and Controls are display-only
152
+ nil
153
+ end
154
+
155
+ # Delegates lifecycle tick to time-sensitive components.
156
+ #
157
+ # Currently handles clipboard feedback timer.
158
+ #
159
+ # === Example
160
+ #
161
+ # container.tick
162
+ def tick
163
+ @controls.tick(@clipboard)
164
+ end
165
+
166
+ private def calculate_center_area(parent_area, width, height)
167
+ x = (parent_area.width - width) / 2
168
+ y = (parent_area.height - height) / 2
169
+ @tui.rect(x:, y:, width:, height:)
170
+ end
171
+
172
+ # Simulates the initial parse that happens when the app starts.
173
+ # Input is initialized with a default color, so we need to parse it.
174
+ private def simulate_initial_parse
175
+ require_relative "color"
176
+ Color.parse(@input.value)
177
+ end
178
+ end
@@ -0,0 +1,109 @@
1
+ # frozen_string_literal: true
2
+
3
+ # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
+ # SPDX-License-Identifier: AGPL-3.0-or-later
5
+
6
+ require_relative "color"
7
+
8
+ # A self-contained component displaying a color palette with harmonies.
9
+ #
10
+ # Color pickers need to show related colors: shades, tints, complements. This
11
+ # component owns a primary color and renders its harmonies.
12
+ #
13
+ # === Component Contract
14
+ #
15
+ # - `render(tui, frame, area)`: Draws the harmony blocks; stores `area`
16
+ # - `handle_event(event) -> nil`: Display-only, always returns nil
17
+ # - `update_color(color)`: Updates the primary color (called by MainContainer)
18
+ #
19
+ # === Example
20
+ #
21
+ # palette = Palette.new
22
+ # palette.update_color(Color.parse("#FF0000"))
23
+ # palette.render(tui, frame, palette_area)
24
+ class Palette
25
+ def initialize(primary_color = nil)
26
+ @primary = primary_color
27
+ @area = nil
28
+ end
29
+
30
+ # The cached render area.
31
+ attr_reader :area
32
+
33
+ # The primary (main) color, or nil if no color is set.
34
+ #
35
+ # === Example
36
+ #
37
+ # palette.main.hex # => "#FF0000"
38
+ def main
39
+ @primary
40
+ end
41
+
42
+ # Updates the primary color.
43
+ #
44
+ # Called by the MainContainer when Input submits a new color.
45
+ #
46
+ # [color] Color object or nil
47
+ def update_color(color)
48
+ @primary = color
49
+ end
50
+
51
+ # All harmonies: main, shade, tint, complement, split 1, split 2, split-complement.
52
+ #
53
+ # Returns an empty array if no primary color is set.
54
+ def all
55
+ return [] if @primary.nil?
56
+
57
+ @primary.harmonies
58
+ end
59
+
60
+ # Renders the palette into the given area.
61
+ #
62
+ # Shows all harmony blocks in a horizontal layout. If no color is set,
63
+ # displays a placeholder message.
64
+ #
65
+ # [tui] Session or TUI factory object
66
+ # [frame] Frame object from RatatuiRuby.draw block
67
+ # [area] Rect area to draw into
68
+ #
69
+ # === Example
70
+ #
71
+ # palette.render(tui, frame, palette_area)
72
+ def render(tui, frame, area)
73
+ @area = area
74
+ widget = build_widget(tui)
75
+ frame.render_widget(widget, area)
76
+ end
77
+
78
+ # Display-only component; always returns nil.
79
+ def handle_event(_event)
80
+ nil
81
+ end
82
+
83
+ private def build_widget(tui)
84
+ if @primary.nil?
85
+ tui.paragraph(text: "No color selected")
86
+ else
87
+ blocks = as_blocks(tui)
88
+ tui.layout(
89
+ direction: :horizontal,
90
+ constraints: Array.new(blocks.size) { tui.constraint_fill(1) },
91
+ children: blocks
92
+ )
93
+ end
94
+ end
95
+
96
+ private def as_blocks(tui)
97
+ return [] if @primary.nil?
98
+
99
+ all.map do |harmony|
100
+ tui.block(
101
+ title: harmony.label,
102
+ borders: [:all],
103
+ children: [
104
+ tui.paragraph(text: harmony.color_swatch_lines(tui)),
105
+ ]
106
+ )
107
+ end
108
+ end
109
+ end
@@ -0,0 +1,47 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Login Form Example
7
+
8
+ Demonstrates how to create a modal overlay for user input.
9
+
10
+ Many applications need to block interaction with the main UI while collecting specific information, like a login prompt or confirmation dialog. Managing the z-index and input focus for these overlays can be tricky.
11
+
12
+ This example solves this by using the `Overlay` widget to stack a centered popup on top of a base layer, conditionally rendering the popup based on state.
13
+
14
+ ## Features Demonstrated
15
+
16
+ - **Overlays:** Stacking widgets on top of each other using `tui.overlay`.
17
+ - **Centering:** Positioning a widget in the center of the screen using `tui.center`.
18
+ - **State Management:** Switching between "Base" and "Popup" views.
19
+ - **Input Handling:** Capturing text input and handling specific keys (Enter, Esc) to trigger state changes.
20
+ - **Cursor Positioning:** Manually calculating cursor position within a `Paragraph`.
21
+
22
+ ## Hotkeys
23
+
24
+ ### Form Mode
25
+ - **Text Input**: Type to enter username (supports all characters including 'q').
26
+ - **Backspace**: Deletes the last character.
27
+ - **Enter**: Submits the form and opens the success popup.
28
+ - **Esc**: Quits the application.
29
+ - **Ctrl+C**: Quits the application.
30
+
31
+ ### Popup Mode
32
+ - **q**: Closes the popup and quits the application.
33
+ - **Ctrl+C**: Quits the application.
34
+
35
+ ## Usage
36
+
37
+ ```bash
38
+ ruby examples/app_login_form/app.rb
39
+ ```
40
+
41
+ ## Learning Outcomes
42
+
43
+ Use this example if you need to...
44
+ - Create a modal dialog or popup.
45
+ - Center a widget on the screen (vertically and horizontally).
46
+ - Implement a simple text input field with cursor management.
47
+ - layer widgets using the `Overlay` widget.
@@ -6,7 +6,7 @@
6
6
  $LOAD_PATH.unshift File.expand_path("../../lib", __dir__)
7
7
  require "ratatui_ruby"
8
8
 
9
- class LoginFormApp
9
+ class AppLoginForm
10
10
  PREFIX = "Enter Username: [ "
11
11
  SUFFIX = " ]"
12
12
 
@@ -16,7 +16,8 @@ class LoginFormApp
16
16
  end
17
17
 
18
18
  def run
19
- RatatuiRuby.run do
19
+ RatatuiRuby.run do |tui|
20
+ @tui = tui
20
21
  loop do
21
22
  render
22
23
  break if handle_input == :quit
@@ -24,9 +25,7 @@ class LoginFormApp
24
25
  end
25
26
  end
26
27
 
27
- private
28
-
29
- def render
28
+ private def render
30
29
  # 1. Base Layer Construction
31
30
  # We want a cursor relative to the paragraph.
32
31
  # So we wrap Paragraph and Cursor in an Overlay, and put that Overlay in a Center.
@@ -39,17 +38,17 @@ class LoginFormApp
39
38
  cursor_y = 1
40
39
 
41
40
  # The content of the base form
42
- form_content = RatatuiRuby::Overlay.new(layers: [
43
- RatatuiRuby::Paragraph.new(
41
+ form_content = @tui.overlay(layers: [
42
+ @tui.paragraph(
44
43
  text: "#{PREFIX}#{@username}#{SUFFIX}",
45
- block: RatatuiRuby::Block.new(borders: :all, title: "Login Form"),
44
+ block: @tui.block(borders: :all, title: "Login Form"),
46
45
  alignment: :left
47
46
  ),
48
- RatatuiRuby::Cursor.new(x: cursor_x, y: cursor_y),
47
+ @tui.cursor(x: cursor_x, y: cursor_y),
49
48
  ])
50
49
 
51
50
  # Center the form on screen
52
- base_layer = RatatuiRuby::Center.new(
51
+ base_layer = @tui.center(
53
52
  child: form_content,
54
53
  width_percent: 50,
55
54
  height_percent: 20
@@ -57,11 +56,11 @@ class LoginFormApp
57
56
 
58
57
  # 2. Popup Layer Construction
59
58
  final_view = if @show_popup
60
- popup_message = RatatuiRuby::Center.new(
61
- child: RatatuiRuby::Paragraph.new(
59
+ popup_message = @tui.center(
60
+ child: @tui.paragraph(
62
61
  text: "Login Successful!\nPress 'q' to quit.",
63
- style: RatatuiRuby::Style.new(fg: :green, bg: :black),
64
- block: RatatuiRuby::Block.new(borders: :all),
62
+ style: @tui.style(fg: :green, bg: :black),
63
+ block: @tui.block(borders: :all),
65
64
  alignment: :center,
66
65
  wrap: true
67
66
  ),
@@ -70,42 +69,39 @@ class LoginFormApp
70
69
  )
71
70
 
72
71
  # Render Base Layer (background) THEN Popup Layer
73
- RatatuiRuby::Overlay.new(layers: [base_layer, popup_message])
72
+ @tui.overlay(layers: [base_layer, popup_message])
74
73
  else
75
74
  base_layer
76
75
  end
77
76
 
78
77
  # 3. Draw
79
- RatatuiRuby.draw(final_view)
78
+ @tui.draw do |frame|
79
+ frame.render_widget(final_view, frame.area)
80
+ end
80
81
  end
81
82
 
82
- def handle_input
83
- # 4. Event Handling
84
- event = RatatuiRuby.poll_event
85
- return unless event
86
-
87
- if event.key?
88
- if @show_popup
89
- return :quit if event == "q" || event == :ctrl_c
90
- else
91
- # Login Form Input
92
- return :quit if event == :ctrl_c
93
- case event.code
94
- when "enter"
95
- @show_popup = true
96
- when "backspace"
97
- @username.chop!
98
- when "esc"
99
- :quit
100
- else
101
- # Simple text input
102
- if event.text? && !event.ctrl? && !event.alt?
103
- @username += event.code
104
- end
105
- end
106
- end
83
+ private def handle_input
84
+ case @tui.poll_event
85
+ in { type: :key, code: "c", modifiers: ["ctrl"] }
86
+ :quit
87
+ in { type: :key, code: "q" } if @show_popup
88
+ :quit
89
+ in { type: :key, code: "enter" }
90
+ @show_popup ||= true
91
+ nil
92
+ in { type: :key, code: "backspace" }
93
+ @username.chop! unless @show_popup
94
+ nil
95
+ in { type: :key, code: "esc" }
96
+ :quit unless @show_popup
97
+ in { type: :key, code:, modifiers: [] }
98
+ # Simple text input (single character, no modifiers)
99
+ @username += code if !@show_popup && code.length == 1
100
+ nil
101
+ else
102
+ nil
107
103
  end
108
104
  end
109
105
  end
110
106
 
111
- LoginFormApp.new.run if __FILE__ == $0
107
+ AppLoginForm.new.run if __FILE__ == $0
@@ -0,0 +1,31 @@
1
+ <!--
2
+ SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
3
+ SPDX-License-Identifier: CC-BY-SA-4.0
4
+ -->
5
+
6
+ # Stateful Interaction Example
7
+
8
+ This example demonstrates High-Fidelity Interaction using **Stateful Widget Rendering**.
9
+
10
+ It showcases a "Database Viewer" layout where:
11
+ 1. **Selection Persistence:** `ListState` and `TableState` objects persist across frames, maintaining selection without manual index tracking variables.
12
+ 2. **Offset Read-back:** The application reads `state.offset` *after* rendering to know exactly which items were visible on screen.
13
+ 3. **Mouse Interaction:** Using the read-back offset, we can calculate exactly which row was clicked, even when the specific item wasn't drawn at that absolute Y position due to scrolling.
14
+
15
+ ## Key Concept: The "Read-back" Loop
16
+
17
+ Standard immediate-mode interaction often requires you to re-calculate layout logic to determine what was clicked.
18
+
19
+ In `ratatui_ruby`'s Stateful Rendering:
20
+ 1. **Update**: You modify `state` (e.g., `state.select(1)`).
21
+ 2. **Render**: You pass `state` to `render_stateful_widget`. Ratatui's Rust backend calculates layout and **updates** `state.offset` in-place if scrolling happened.
22
+ 3. **Interact**: On the next event loop, you use `state.offset` to correctly map mouse coordinates to data indices.
23
+
24
+ ## Hotkeys
25
+
26
+ | Key | Action |
27
+ | --- | --- |
28
+ | `↑` / `↓` | Scroll the active pane |
29
+ | `Tab` / `←` / `→` | Switch active pane (List vs Table) |
30
+ | `Mouse Click` | Select the clicked row (Works with scrolling!) |
31
+ | `q` | Quit |