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
@@ -2,12 +2,14 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::{parse_block, parse_style};
5
+ use crate::text::{parse_line, parse_span};
6
+ use crate::widgets::list_state::RubyListState;
5
7
  use bumpalo::Bump;
6
8
  use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
7
9
  use ratatui::{
8
10
  layout::Rect,
9
11
  text::Line,
10
- widgets::{HighlightSpacing, List, ListState},
12
+ widgets::{HighlightSpacing, List, ListItem, ListState},
11
13
  Frame,
12
14
  };
13
15
 
@@ -27,10 +29,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
27
29
  let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
28
30
  let block_val: Value = node.funcall("block", ())?;
29
31
 
30
- let mut items: Vec<String> = Vec::new();
32
+ let mut items: Vec<ListItem> = Vec::new();
31
33
  for i in 0..items_array.len() {
32
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
33
- let item: String = items_array.entry(index)?;
34
+ let index = isize::try_from(i)
35
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
36
+ let item_val: Value = items_array.entry(index)?;
37
+ let item = parse_list_item(item_val)?;
34
38
  items.push(item);
35
39
  }
36
40
 
@@ -46,6 +50,12 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
46
50
  state.select(Some(index));
47
51
  }
48
52
 
53
+ let offset_val: Value = node.funcall("offset", ())?;
54
+ if !offset_val.is_nil() {
55
+ let offset: usize = offset_val.funcall("to_int", ())?;
56
+ *state.offset_mut() = offset;
57
+ }
58
+
49
59
  let mut list = List::new(items);
50
60
 
51
61
  let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
@@ -100,6 +110,173 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
100
110
  Ok(())
101
111
  }
102
112
 
113
+ /// Renders a List with an external state object.
114
+ ///
115
+ /// This function ignores `selected_index` and `offset` from the widget.
116
+ /// The State object is the single source of truth for selection and scroll position.
117
+ pub fn render_stateful(
118
+ frame: &mut Frame,
119
+ area: Rect,
120
+ node: Value,
121
+ state_wrapper: Value,
122
+ ) -> Result<(), Error> {
123
+ let bump = Bump::new();
124
+ let ruby = magnus::Ruby::get().unwrap();
125
+
126
+ // Extract the RubyListState wrapper
127
+ let state: &RubyListState = TryConvert::try_convert(state_wrapper)?;
128
+
129
+ // Build items
130
+ let items_val: Value = node.funcall("items", ())?;
131
+ let items_array = magnus::RArray::from_value(items_val)
132
+ .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array"))?;
133
+
134
+ let mut items: Vec<ListItem> = Vec::new();
135
+ for i in 0..items_array.len() {
136
+ let index = isize::try_from(i)
137
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
138
+ let item_val: Value = items_array.entry(index)?;
139
+ let item = parse_list_item(item_val)?;
140
+ items.push(item);
141
+ }
142
+
143
+ // Build widget (ignoring selected_index and offset — State is truth)
144
+ let style_val: Value = node.funcall("style", ())?;
145
+ let highlight_style_val: Value = node.funcall("highlight_style", ())?;
146
+ let highlight_symbol_val: Value = node.funcall("highlight_symbol", ())?;
147
+ let repeat_highlight_symbol_val: Value = node.funcall("repeat_highlight_symbol", ())?;
148
+ let highlight_spacing_sym: Symbol = node.funcall("highlight_spacing", ())?;
149
+ let direction_val: Value = node.funcall("direction", ())?;
150
+ let scroll_padding_val: Value = node.funcall("scroll_padding", ())?;
151
+ let block_val: Value = node.funcall("block", ())?;
152
+
153
+ let symbol: String = if highlight_symbol_val.is_nil() {
154
+ String::new()
155
+ } else {
156
+ String::try_convert(highlight_symbol_val)?
157
+ };
158
+
159
+ let mut list = List::new(items);
160
+
161
+ let highlight_spacing = match highlight_spacing_sym.to_string().as_str() {
162
+ "always" => HighlightSpacing::Always,
163
+ "never" => HighlightSpacing::Never,
164
+ _ => HighlightSpacing::WhenSelected,
165
+ };
166
+ list = list.highlight_spacing(highlight_spacing);
167
+
168
+ if !highlight_symbol_val.is_nil() {
169
+ list = list.highlight_symbol(Line::from(symbol));
170
+ }
171
+
172
+ if !repeat_highlight_symbol_val.is_nil() {
173
+ let repeat: bool = TryConvert::try_convert(repeat_highlight_symbol_val)?;
174
+ list = list.repeat_highlight_symbol(repeat);
175
+ }
176
+
177
+ if !direction_val.is_nil() {
178
+ let direction_sym: magnus::Symbol = TryConvert::try_convert(direction_val)?;
179
+ let direction_str = direction_sym.name().unwrap();
180
+ match direction_str.as_ref() {
181
+ "top_to_bottom" => list = list.direction(ratatui::widgets::ListDirection::TopToBottom),
182
+ "bottom_to_top" => list = list.direction(ratatui::widgets::ListDirection::BottomToTop),
183
+ _ => {
184
+ return Err(Error::new(
185
+ ruby.exception_arg_error(),
186
+ "direction must be :top_to_bottom or :bottom_to_top",
187
+ ))
188
+ }
189
+ }
190
+ }
191
+
192
+ if !scroll_padding_val.is_nil() {
193
+ let padding: usize = TryConvert::try_convert(scroll_padding_val)?;
194
+ list = list.scroll_padding(padding);
195
+ }
196
+
197
+ if !style_val.is_nil() {
198
+ list = list.style(parse_style(style_val)?);
199
+ }
200
+
201
+ if !highlight_style_val.is_nil() {
202
+ list = list.highlight_style(parse_style(highlight_style_val)?);
203
+ }
204
+
205
+ if !block_val.is_nil() {
206
+ list = list.block(parse_block(block_val, &bump)?);
207
+ }
208
+
209
+ // Borrow the inner ListState, render, and release the borrow immediately
210
+ {
211
+ let mut inner_state = state.borrow_mut();
212
+ frame.render_stateful_widget(list, area, &mut inner_state);
213
+ }
214
+ // Borrow is now released
215
+
216
+ Ok(())
217
+ }
218
+
219
+ /// Parses a Ruby list item into a ratatui `ListItem`.
220
+ ///
221
+ /// Accepts:
222
+ /// - `String`: Plain text item
223
+ /// - `Text::Span`: A single styled fragment
224
+ /// - `Text::Line`: A line composed of multiple spans
225
+ /// - `RatatuiRuby::ListItem`: A `ListItem` object with content and optional style
226
+ fn parse_list_item(value: Value) -> Result<ListItem<'static>, Error> {
227
+ let ruby = magnus::Ruby::get().unwrap();
228
+
229
+ // Check if it's a RatatuiRuby::ListItem
230
+ if let Ok(class_obj) = value.funcall::<_, _, Value>("class", ()) {
231
+ if let Ok(class_name) = class_obj.funcall::<_, _, String>("name", ()) {
232
+ if class_name.contains("ListItem") {
233
+ // Extract content and style from the ListItem
234
+ let content_val: Value = value.funcall("content", ())?;
235
+ let style_val: Value = value.funcall("style", ())?;
236
+
237
+ // Parse content as a Line
238
+ let line = if let Ok(s) = String::try_convert(content_val) {
239
+ Line::from(s)
240
+ } else if let Ok(line) = parse_line(content_val) {
241
+ line
242
+ } else if let Ok(span) = parse_span(content_val) {
243
+ Line::from(vec![span])
244
+ } else {
245
+ Line::from("")
246
+ };
247
+
248
+ // Parse and apply style if present
249
+ let mut item = ListItem::new(line);
250
+ if !style_val.is_nil() {
251
+ item = item.style(parse_style(style_val)?);
252
+ }
253
+ return Ok(item);
254
+ }
255
+ }
256
+ }
257
+
258
+ // Try as String
259
+ if let Ok(s) = String::try_convert(value) {
260
+ return Ok(ListItem::new(Line::from(s)));
261
+ }
262
+
263
+ // Try as Line
264
+ if let Ok(line) = parse_line(value) {
265
+ return Ok(ListItem::new(line));
266
+ }
267
+
268
+ // Try as Span
269
+ if let Ok(span) = parse_span(value) {
270
+ return Ok(ListItem::new(Line::from(vec![span])));
271
+ }
272
+
273
+ // Fallback
274
+ Err(Error::new(
275
+ ruby.exception_type_error(),
276
+ "expected String, Text::Span, Text::Line, or ListItem",
277
+ ))
278
+ }
279
+
103
280
  #[cfg(test)]
104
281
  mod tests {
105
282
  use super::*;
@@ -142,21 +319,41 @@ mod tests {
142
319
  fn test_repeat_highlight_symbol() {
143
320
  let items = vec!["Item 1", "Item 2"];
144
321
  let list_without_repeat = List::new(items.clone()).highlight_symbol(Line::from(">> "));
145
- let list_with_repeat = List::new(items).highlight_symbol(Line::from(">> ")).repeat_highlight_symbol(true);
146
-
322
+ let list_with_repeat = List::new(items)
323
+ .highlight_symbol(Line::from(">> "))
324
+ .repeat_highlight_symbol(true);
325
+
147
326
  let mut state = ListState::default();
148
327
  state.select(Some(0));
149
328
 
150
329
  let mut buf1 = Buffer::empty(Rect::new(0, 0, 10, 2));
151
330
  use ratatui::widgets::StatefulWidget;
152
- StatefulWidget::render(list_without_repeat, Rect::new(0, 0, 10, 2), &mut buf1, &mut state);
331
+ StatefulWidget::render(
332
+ list_without_repeat,
333
+ Rect::new(0, 0, 10, 2),
334
+ &mut buf1,
335
+ &mut state,
336
+ );
153
337
 
154
338
  let mut buf2 = Buffer::empty(Rect::new(0, 0, 10, 2));
155
- StatefulWidget::render(list_with_repeat, Rect::new(0, 0, 10, 2), &mut buf2, &mut state);
339
+ StatefulWidget::render(
340
+ list_with_repeat,
341
+ Rect::new(0, 0, 10, 2),
342
+ &mut buf2,
343
+ &mut state,
344
+ );
156
345
 
157
346
  // Both should render, but the behavior might differ based on content width
158
- let content1 = buf1.content().iter().map(|c| c.symbol()).collect::<String>();
159
- let content2 = buf2.content().iter().map(|c| c.symbol()).collect::<String>();
347
+ let content1 = buf1
348
+ .content()
349
+ .iter()
350
+ .map(|c| c.symbol())
351
+ .collect::<String>();
352
+ let content2 = buf2
353
+ .content()
354
+ .iter()
355
+ .map(|c| c.symbol())
356
+ .collect::<String>();
160
357
  assert!(!content1.is_empty());
161
358
  assert!(!content2.is_empty());
162
359
  }
@@ -164,8 +361,10 @@ mod tests {
164
361
  #[test]
165
362
  fn test_scroll_padding() {
166
363
  let items = vec!["Item 1", "Item 2", "Item 3", "Item 4"];
167
- let list = List::new(items).scroll_padding(1).highlight_symbol(Line::from(">> "));
168
-
364
+ let list = List::new(items)
365
+ .scroll_padding(1)
366
+ .highlight_symbol(Line::from(">> "));
367
+
169
368
  let mut state = ListState::default();
170
369
  state.select(Some(1));
171
370
 
@@ -0,0 +1,137 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
+
4
+ //! `ListState` wrapper for exposing Ratatui's `ListState` to Ruby.
5
+ //!
6
+ //! This module provides `RubyListState`, a Magnus-wrapped struct that holds
7
+ //! a `RefCell<ListState>` for interior mutability during stateful rendering.
8
+ //!
9
+ //! # Design
10
+ //!
11
+ //! When using `render_stateful_widget`, the State object is the single source
12
+ //! of truth for selection and offset. Widget properties (`selected_index`, `offset`)
13
+ //! are ignored in stateful mode.
14
+ //!
15
+ //! # Safety
16
+ //!
17
+ //! The `RefCell` is borrowed only during the `render_stateful_widget` call.
18
+ //! The borrow is released immediately after to avoid double-borrow panics
19
+ //! if a user inspects state inside a custom widget's render method.
20
+
21
+ use magnus::{function, method, prelude::*, Error, Module, Ruby};
22
+ use ratatui::widgets::ListState;
23
+ use std::cell::RefCell;
24
+
25
+ /// A wrapper around Ratatui's `ListState` exposed to Ruby.
26
+ ///
27
+ /// This struct uses `RefCell` for interior mutability, allowing the state
28
+ /// to be updated during rendering while remaining accessible from Ruby.
29
+ #[magnus::wrap(class = "RatatuiRuby::ListState")]
30
+ pub struct RubyListState {
31
+ inner: RefCell<ListState>,
32
+ }
33
+
34
+ impl RubyListState {
35
+ /// Creates a new `RubyListState` with optional initial selection.
36
+ ///
37
+ /// # Arguments
38
+ ///
39
+ /// * `selected` - Optional initial selection index
40
+ pub fn new(selected: Option<usize>) -> Self {
41
+ let mut state = ListState::default();
42
+ if let Some(idx) = selected {
43
+ state.select(Some(idx));
44
+ }
45
+ Self {
46
+ inner: RefCell::new(state),
47
+ }
48
+ }
49
+
50
+ /// Sets the selected index.
51
+ ///
52
+ /// Pass `nil` to deselect.
53
+ pub fn select(&self, index: Option<usize>) {
54
+ self.inner.borrow_mut().select(index);
55
+ }
56
+
57
+ /// Returns the currently selected index, or `nil` if nothing is selected.
58
+ pub fn selected(&self) -> Option<usize> {
59
+ self.inner.borrow().selected()
60
+ }
61
+
62
+ /// Returns the current scroll offset.
63
+ ///
64
+ /// This is the critical read-back method. After `render_stateful_widget`,
65
+ /// this returns the scroll position calculated by Ratatui to keep the
66
+ /// selection visible.
67
+ pub fn offset(&self) -> usize {
68
+ self.inner.borrow().offset()
69
+ }
70
+
71
+ /// Scrolls down by the given number of items.
72
+ pub fn scroll_down_by(&self, amount: u16) {
73
+ self.inner.borrow_mut().scroll_down_by(amount);
74
+ }
75
+
76
+ /// Scrolls up by the given number of items.
77
+ pub fn scroll_up_by(&self, amount: u16) {
78
+ self.inner.borrow_mut().scroll_up_by(amount);
79
+ }
80
+
81
+ /// Borrows the inner `ListState` mutably for rendering.
82
+ ///
83
+ /// # Safety
84
+ ///
85
+ /// The caller must ensure the borrow is released before returning
86
+ /// control to Ruby to avoid double-borrow panics.
87
+ pub fn borrow_mut(&self) -> std::cell::RefMut<'_, ListState> {
88
+ self.inner.borrow_mut()
89
+ }
90
+ }
91
+
92
+ /// Registers the `ListState` class with Ruby.
93
+ pub fn register(ruby: &Ruby, module: magnus::RModule) -> Result<(), Error> {
94
+ let class = module.define_class("ListState", ruby.class_object())?;
95
+ class.define_singleton_method("new", function!(RubyListState::new, 1))?;
96
+ class.define_method("select", method!(RubyListState::select, 1))?;
97
+ class.define_method("selected", method!(RubyListState::selected, 0))?;
98
+ class.define_method("offset", method!(RubyListState::offset, 0))?;
99
+ class.define_method("scroll_down_by", method!(RubyListState::scroll_down_by, 1))?;
100
+ class.define_method("scroll_up_by", method!(RubyListState::scroll_up_by, 1))?;
101
+ Ok(())
102
+ }
103
+
104
+ #[cfg(test)]
105
+ mod tests {
106
+ use super::*;
107
+
108
+ #[test]
109
+ fn test_new_with_no_selection() {
110
+ let state = RubyListState::new(None);
111
+ assert_eq!(state.selected(), None);
112
+ assert_eq!(state.offset(), 0);
113
+ }
114
+
115
+ #[test]
116
+ fn test_new_with_selection() {
117
+ let state = RubyListState::new(Some(5));
118
+ assert_eq!(state.selected(), Some(5));
119
+ }
120
+
121
+ #[test]
122
+ fn test_select_and_deselect() {
123
+ let state = RubyListState::new(None);
124
+ state.select(Some(3));
125
+ assert_eq!(state.selected(), Some(3));
126
+ state.select(None);
127
+ assert_eq!(state.selected(), None);
128
+ }
129
+
130
+ #[test]
131
+ fn test_scroll_operations() {
132
+ let state = RubyListState::new(None);
133
+ state.scroll_down_by(5);
134
+ // Note: scroll operations affect offset, but the exact behavior
135
+ // depends on the list size which is determined during rendering
136
+ }
137
+ }
@@ -13,11 +13,14 @@ pub mod gauge;
13
13
  pub mod layout;
14
14
  pub mod line_gauge;
15
15
  pub mod list;
16
+ pub mod list_state;
16
17
  pub mod overlay;
17
18
  pub mod paragraph;
18
19
  pub mod ratatui_logo;
19
20
  pub mod ratatui_mascot;
20
21
  pub mod scrollbar;
22
+ pub mod scrollbar_state;
21
23
  pub mod sparkline;
22
24
  pub mod table;
25
+ pub mod table_state;
23
26
  pub mod tabs;
@@ -12,7 +12,8 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
12
12
  .ok_or_else(|| Error::new(ruby.exception_type_error(), "expected array for layers"))?;
13
13
 
14
14
  for i in 0..layers_array.len() {
15
- let index = isize::try_from(i).map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
15
+ let index = isize::try_from(i)
16
+ .map_err(|e| Error::new(ruby.exception_range_error(), e.to_string()))?;
16
17
  let layer: Value = layers_array.entry(index)?;
17
18
  if let Err(e) = render_node(frame, area, layer) {
18
19
  eprintln!("Error rendering overlay layer {i}: {e:?}");
@@ -12,7 +12,7 @@ use ratatui::{
12
12
 
13
13
  use crate::text::parse_text;
14
14
 
15
- fn create_paragraph<'a>(node: Value, bump: &'a Bump) -> Result<Paragraph<'a>, Error> {
15
+ fn create_paragraph(node: Value, bump: &Bump) -> Result<Paragraph<'_>, Error> {
16
16
  let text_val: Value = node.funcall("text", ())?;
17
17
  let style_val: Value = node.funcall("style", ())?;
18
18
  let block_val: Value = node.funcall("block", ())?;
@@ -1,27 +1,38 @@
1
- use magnus::{Error, Value};
2
- use ratatui::{layout::Rect, widgets::{RatatuiLogo, RatatuiLogoSize}, Frame};
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ //
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
3
4
 
4
- pub fn render(frame: &mut Frame, area: Rect, _node: Value) -> Result<(), Error> {
5
+ use magnus::Value;
6
+ use ratatui::{
7
+ layout::Rect,
8
+ widgets::{RatatuiLogo, RatatuiLogoSize},
9
+ Frame,
10
+ };
11
+
12
+ pub fn render(frame: &mut Frame, area: Rect, _node: Value) {
5
13
  // RatatuiLogo does not support custom styling (it has fixed colors).
6
14
  // It requires a size argument.
7
15
  let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
8
16
  frame.render_widget(widget, area);
9
- Ok(())
10
17
  }
11
18
 
12
19
  #[cfg(test)]
13
20
  mod tests {
14
- use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
15
21
  use super::*;
22
+ use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
16
23
 
17
24
  #[test]
18
25
  fn test_render() {
19
26
  let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
20
27
  let widget = RatatuiLogo::new(RatatuiLogoSize::Small);
21
28
  widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
22
-
23
- let content = buffer.content().iter().map(|c| c.symbol()).collect::<String>();
24
-
29
+
30
+ let content = buffer
31
+ .content()
32
+ .iter()
33
+ .map(|c| c.symbol())
34
+ .collect::<String>();
35
+
25
36
  // The logo uses block characters for rendering
26
37
  assert!(content.contains('█'));
27
38
  assert!(!content.trim().is_empty());
@@ -1,11 +1,11 @@
1
+ // SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
2
+ //
3
+ // SPDX-License-Identifier: AGPL-3.0-or-later
4
+
1
5
  use crate::style::parse_block;
2
6
  use bumpalo::Bump;
3
7
  use magnus::{prelude::*, Error, Value};
4
- use ratatui::{
5
- layout::Rect,
6
- widgets::RatatuiMascot,
7
- Frame,
8
- };
8
+ use ratatui::{layout::Rect, widgets::RatatuiMascot, Frame};
9
9
 
10
10
  pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
11
11
  let block_val: Value = node.funcall("block", ())?;
@@ -26,19 +26,26 @@ pub fn render_ratatui_mascot(frame: &mut Frame, area: Rect, node: Value) -> Resu
26
26
 
27
27
  #[cfg(test)]
28
28
  mod tests {
29
- use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
30
29
  use super::*;
30
+ use ratatui::{buffer::Buffer, layout::Rect, widgets::Widget};
31
31
 
32
32
  #[test]
33
33
  fn test_render() {
34
34
  let mut buffer = Buffer::empty(Rect::new(0, 0, 50, 20));
35
35
  let widget = RatatuiMascot::new();
36
36
  widget.render(Rect::new(0, 0, 50, 20), &mut buffer);
37
-
38
- let content = buffer.content().iter().map(|c| c.symbol()).collect::<String>();
39
-
37
+
38
+ let content = buffer
39
+ .content()
40
+ .iter()
41
+ .map(|c| c.symbol())
42
+ .collect::<String>();
43
+
40
44
  // The mascot uses block drawing characters
41
- assert!(content.contains("█"), "Mascot rendering should contain block characters");
45
+ assert!(
46
+ content.contains("█"),
47
+ "Mascot rendering should contain block characters"
48
+ );
42
49
  assert!(!content.trim().is_empty());
43
50
  }
44
51
  }
@@ -2,8 +2,9 @@
2
2
  // SPDX-License-Identifier: AGPL-3.0-or-later
3
3
 
4
4
  use crate::style::parse_block;
5
+ use crate::widgets::scrollbar_state::RubyScrollbarState;
5
6
  use bumpalo::Bump;
6
- use magnus::{prelude::*, Error, Symbol, Value};
7
+ use magnus::{prelude::*, Error, Symbol, TryConvert, Value};
7
8
  use ratatui::{
8
9
  layout::Rect,
9
10
  widgets::{Scrollbar, ScrollbarOrientation, ScrollbarState},
@@ -14,7 +15,7 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
14
15
  let content_length: usize = node.funcall("content_length", ())?;
15
16
  let position: usize = node.funcall("position", ())?;
16
17
  let orientation_sym: Symbol = node.funcall("orientation", ())?;
17
-
18
+
18
19
  let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
19
20
  let thumb_style_val: Value = node.funcall("thumb_style", ())?;
20
21
  let track_symbol_val: Value = node.funcall("track_symbol", ())?;
@@ -32,7 +33,9 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
32
33
 
33
34
  scrollbar = match orientation_sym.to_string().as_str() {
34
35
  "vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
35
- "horizontal_bottom" | "horizontal" => scrollbar.orientation(ScrollbarOrientation::HorizontalBottom),
36
+ "horizontal_bottom" | "horizontal" => {
37
+ scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
38
+ }
36
39
  "horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
37
40
  _ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
38
41
  };
@@ -87,6 +90,97 @@ pub fn render(frame: &mut Frame, area: Rect, node: Value) -> Result<(), Error> {
87
90
  Ok(())
88
91
  }
89
92
 
93
+ /// Renders a Scrollbar with an external state object.
94
+ ///
95
+ /// The State object is the single source of truth for position and `content_length`.
96
+ /// Widget properties (`position`, `content_length`) are ignored.
97
+ pub fn render_stateful(
98
+ frame: &mut Frame,
99
+ area: Rect,
100
+ node: Value,
101
+ state_wrapper: Value,
102
+ ) -> Result<(), Error> {
103
+ // Extract the RubyScrollbarState wrapper
104
+ let state: &RubyScrollbarState = TryConvert::try_convert(state_wrapper)?;
105
+
106
+ let orientation_sym: Symbol = node.funcall("orientation", ())?;
107
+ let thumb_symbol_val: Value = node.funcall("thumb_symbol", ())?;
108
+ let thumb_style_val: Value = node.funcall("thumb_style", ())?;
109
+ let track_symbol_val: Value = node.funcall("track_symbol", ())?;
110
+ let track_style_val: Value = node.funcall("track_style", ())?;
111
+ let begin_symbol_val: Value = node.funcall("begin_symbol", ())?;
112
+ let begin_style_val: Value = node.funcall("begin_style", ())?;
113
+ let end_symbol_val: Value = node.funcall("end_symbol", ())?;
114
+ let end_style_val: Value = node.funcall("end_style", ())?;
115
+ let style_val: Value = node.funcall("style", ())?;
116
+ let block_val: Value = node.funcall("block", ())?;
117
+
118
+ let mut scrollbar = Scrollbar::default();
119
+
120
+ scrollbar = match orientation_sym.to_string().as_str() {
121
+ "vertical_left" => scrollbar.orientation(ScrollbarOrientation::VerticalLeft),
122
+ "horizontal_bottom" | "horizontal" => {
123
+ scrollbar.orientation(ScrollbarOrientation::HorizontalBottom)
124
+ }
125
+ "horizontal_top" => scrollbar.orientation(ScrollbarOrientation::HorizontalTop),
126
+ _ => scrollbar.orientation(ScrollbarOrientation::VerticalRight),
127
+ };
128
+
129
+ // Hoisted strings to extend lifetime
130
+ let thumb_str: String;
131
+ let track_str: String;
132
+ let begin_str: String;
133
+ let end_str: String;
134
+
135
+ if !thumb_symbol_val.is_nil() {
136
+ thumb_str = thumb_symbol_val.funcall("to_s", ())?;
137
+ scrollbar = scrollbar.thumb_symbol(&thumb_str);
138
+ }
139
+ if !thumb_style_val.is_nil() {
140
+ scrollbar = scrollbar.thumb_style(crate::style::parse_style(thumb_style_val)?);
141
+ }
142
+ if !track_symbol_val.is_nil() {
143
+ track_str = track_symbol_val.funcall("to_s", ())?;
144
+ scrollbar = scrollbar.track_symbol(Some(&track_str));
145
+ }
146
+ if !track_style_val.is_nil() {
147
+ scrollbar = scrollbar.track_style(crate::style::parse_style(track_style_val)?);
148
+ }
149
+ if !begin_symbol_val.is_nil() {
150
+ begin_str = begin_symbol_val.funcall("to_s", ())?;
151
+ scrollbar = scrollbar.begin_symbol(Some(&begin_str));
152
+ }
153
+ if !begin_style_val.is_nil() {
154
+ scrollbar = scrollbar.begin_style(crate::style::parse_style(begin_style_val)?);
155
+ }
156
+ if !end_symbol_val.is_nil() {
157
+ end_str = end_symbol_val.funcall("to_s", ())?;
158
+ scrollbar = scrollbar.end_symbol(Some(&end_str));
159
+ }
160
+ if !end_style_val.is_nil() {
161
+ scrollbar = scrollbar.end_style(crate::style::parse_style(end_style_val)?);
162
+ }
163
+ if !style_val.is_nil() {
164
+ scrollbar = scrollbar.style(crate::style::parse_style(style_val)?);
165
+ }
166
+
167
+ // Borrow the inner ScrollbarState, render, and release the borrow immediately
168
+ {
169
+ let mut inner_state = state.borrow_mut();
170
+ if block_val.is_nil() {
171
+ frame.render_stateful_widget(scrollbar, area, &mut inner_state);
172
+ } else {
173
+ let bump = Bump::new();
174
+ let block = parse_block(block_val, &bump)?;
175
+ let inner_area = block.inner(area);
176
+ frame.render_widget(block, area);
177
+ frame.render_stateful_widget(scrollbar, inner_area, &mut inner_state);
178
+ }
179
+ }
180
+
181
+ Ok(())
182
+ }
183
+
90
184
  #[cfg(test)]
91
185
  mod tests {
92
186
  use super::*;