ratatui_ruby 0.5.0 → 0.7.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 (311) 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 +10 -4
  7. data/CHANGELOG.md +79 -7
  8. data/README.md +37 -5
  9. data/REUSE.toml +2 -7
  10. data/doc/application_architecture.md +96 -22
  11. data/doc/application_testing.md +76 -30
  12. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  13. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  14. data/doc/contributors/architectural_overhaul/task.md +37 -0
  15. data/doc/contributors/design/ruby_frontend.md +288 -56
  16. data/doc/contributors/design/rust_backend.md +349 -54
  17. data/doc/contributors/developing_examples.md +134 -49
  18. data/doc/contributors/index.md +7 -5
  19. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  20. data/doc/event_handling.md +11 -3
  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_canvas_demo.png +0 -0
  32. data/doc/images/widget_cell_demo.png +0 -0
  33. data/doc/images/widget_center_demo.png +0 -0
  34. data/doc/images/widget_chart_demo.png +0 -0
  35. data/doc/images/widget_list_demo.png +0 -0
  36. data/doc/images/widget_overlay_demo.png +0 -0
  37. data/doc/images/widget_render.png +0 -0
  38. data/doc/images/widget_rich_text.png +0 -0
  39. data/doc/images/widget_scroll_text.png +0 -0
  40. data/doc/images/widget_sparkline_demo.png +0 -0
  41. data/doc/images/widget_table_demo.png +0 -0
  42. data/doc/images/widget_tabs_demo.png +0 -0
  43. data/doc/images/widget_text_width.png +0 -0
  44. data/doc/index.md +11 -6
  45. data/doc/interactive_design.md +2 -2
  46. data/doc/quickstart.md +127 -165
  47. data/doc/terminal_limitations.md +92 -0
  48. data/doc/v0.7.0_migration.md +236 -0
  49. data/doc/why.md +93 -0
  50. data/examples/app_all_events/README.md +47 -27
  51. data/examples/app_all_events/app.rb +38 -35
  52. data/examples/app_all_events/model/app_model.rb +157 -0
  53. data/examples/app_all_events/model/event_entry.rb +17 -0
  54. data/examples/app_all_events/model/msg.rb +37 -0
  55. data/examples/app_all_events/update.rb +73 -0
  56. data/examples/app_all_events/view/app_view.rb +9 -9
  57. data/examples/app_all_events/view/controls_view.rb +9 -7
  58. data/examples/app_all_events/view/counts_view.rb +13 -9
  59. data/examples/app_all_events/view/live_view.rb +9 -8
  60. data/examples/app_all_events/view/log_view.rb +11 -16
  61. data/examples/app_color_picker/README.md +84 -42
  62. data/examples/app_color_picker/app.rb +24 -62
  63. data/examples/app_color_picker/controls.rb +90 -0
  64. data/examples/app_color_picker/copy_dialog.rb +45 -49
  65. data/examples/app_color_picker/export_pane.rb +126 -0
  66. data/examples/app_color_picker/input.rb +99 -67
  67. data/examples/app_color_picker/main_container.rb +178 -0
  68. data/examples/app_color_picker/palette.rb +55 -26
  69. data/examples/app_login_form/README.md +49 -0
  70. data/examples/app_login_form/app.rb +2 -3
  71. data/examples/app_stateful_interaction/README.md +33 -0
  72. data/examples/app_stateful_interaction/app.rb +272 -0
  73. data/examples/timeout_demo.rb +43 -0
  74. data/examples/verify_quickstart_dsl/README.md +49 -0
  75. data/examples/verify_quickstart_dsl/app.rb +2 -0
  76. data/examples/verify_quickstart_layout/README.md +71 -0
  77. data/examples/verify_quickstart_layout/app.rb +2 -0
  78. data/examples/verify_quickstart_lifecycle/README.md +56 -0
  79. data/examples/verify_quickstart_lifecycle/app.rb +10 -4
  80. data/examples/verify_readme_usage/README.md +43 -0
  81. data/examples/verify_readme_usage/app.rb +8 -2
  82. data/examples/widget_barchart_demo/README.md +50 -0
  83. data/examples/widget_barchart_demo/app.rb +5 -5
  84. data/examples/widget_block_demo/README.md +36 -0
  85. data/examples/widget_block_demo/app.rb +256 -0
  86. data/examples/widget_box_demo/README.md +45 -0
  87. data/examples/widget_calendar_demo/README.md +39 -0
  88. data/examples/widget_calendar_demo/app.rb +5 -1
  89. data/examples/widget_canvas_demo/README.md +27 -0
  90. data/examples/widget_canvas_demo/app.rb +123 -0
  91. data/examples/widget_cell_demo/README.md +36 -0
  92. data/examples/widget_cell_demo/app.rb +31 -24
  93. data/examples/widget_center_demo/README.md +29 -0
  94. data/examples/widget_center_demo/app.rb +116 -0
  95. data/examples/widget_chart_demo/README.md +41 -0
  96. data/examples/widget_chart_demo/app.rb +7 -2
  97. data/examples/widget_gauge_demo/README.md +41 -0
  98. data/examples/widget_layout_split/README.md +44 -0
  99. data/examples/widget_line_gauge_demo/README.md +41 -0
  100. data/examples/widget_list_demo/README.md +49 -0
  101. data/examples/widget_list_demo/app.rb +91 -107
  102. data/examples/widget_map_demo/README.md +39 -0
  103. data/examples/{app_map_demo → widget_map_demo}/app.rb +4 -4
  104. data/examples/widget_overlay_demo/README.md +36 -0
  105. data/examples/widget_overlay_demo/app.rb +248 -0
  106. data/examples/widget_popup_demo/README.md +36 -0
  107. data/examples/widget_ratatui_logo_demo/README.md +34 -0
  108. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  109. data/examples/widget_ratatui_mascot_demo/README.md +34 -0
  110. data/examples/widget_rect/README.md +38 -0
  111. data/examples/widget_render/README.md +37 -0
  112. data/examples/widget_render/app.rb +3 -3
  113. data/examples/widget_rich_text/README.md +35 -0
  114. data/examples/widget_rich_text/app.rb +62 -33
  115. data/examples/widget_scroll_text/README.md +37 -0
  116. data/examples/widget_scroll_text/app.rb +0 -1
  117. data/examples/widget_scrollbar_demo/README.md +37 -0
  118. data/examples/widget_sparkline_demo/README.md +42 -0
  119. data/examples/widget_sparkline_demo/app.rb +4 -3
  120. data/examples/widget_style_colors/README.md +34 -0
  121. data/examples/widget_table_demo/README.md +48 -0
  122. data/examples/{app_table_select → widget_table_demo}/app.rb +65 -12
  123. data/examples/widget_tabs_demo/README.md +41 -0
  124. data/examples/widget_tabs_demo/app.rb +15 -1
  125. data/examples/widget_text_width/README.md +35 -0
  126. data/examples/widget_text_width/app.rb +113 -0
  127. data/exe/.gitkeep +0 -0
  128. data/ext/ratatui_ruby/Cargo.lock +11 -4
  129. data/ext/ratatui_ruby/Cargo.toml +2 -1
  130. data/ext/ratatui_ruby/src/events.rs +238 -26
  131. data/ext/ratatui_ruby/src/frame.rs +116 -3
  132. data/ext/ratatui_ruby/src/lib.rs +37 -6
  133. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  134. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  135. data/ext/ratatui_ruby/src/terminal.rs +39 -15
  136. data/ext/ratatui_ruby/src/text.rs +13 -4
  137. data/ext/ratatui_ruby/src/widgets/barchart.rs +24 -6
  138. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  139. data/ext/ratatui_ruby/src/widgets/gauge.rs +9 -2
  140. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +9 -2
  141. data/ext/ratatui_ruby/src/widgets/list.rs +179 -3
  142. data/ext/ratatui_ruby/src/widgets/list_state.rs +137 -0
  143. data/ext/ratatui_ruby/src/widgets/mod.rs +3 -0
  144. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +93 -1
  145. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  146. data/ext/ratatui_ruby/src/widgets/table.rs +191 -34
  147. data/ext/ratatui_ruby/src/widgets/table_state.rs +121 -0
  148. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  149. data/lib/ratatui_ruby/buffer.rb +15 -0
  150. data/lib/ratatui_ruby/cell.rb +4 -4
  151. data/lib/ratatui_ruby/event/key/character.rb +35 -0
  152. data/lib/ratatui_ruby/event/key/media.rb +44 -0
  153. data/lib/ratatui_ruby/event/key/modifier.rb +95 -0
  154. data/lib/ratatui_ruby/event/key/navigation.rb +55 -0
  155. data/lib/ratatui_ruby/event/key/system.rb +45 -0
  156. data/lib/ratatui_ruby/event/key.rb +111 -51
  157. data/lib/ratatui_ruby/event/mouse.rb +3 -3
  158. data/lib/ratatui_ruby/event/paste.rb +1 -1
  159. data/lib/ratatui_ruby/frame.rb +100 -4
  160. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  161. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  162. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  163. data/lib/ratatui_ruby/layout.rb +19 -0
  164. data/lib/ratatui_ruby/list_state.rb +88 -0
  165. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +2 -2
  166. data/lib/ratatui_ruby/schema/cursor.rb +5 -0
  167. data/lib/ratatui_ruby/schema/gauge.rb +3 -1
  168. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  169. data/lib/ratatui_ruby/schema/line_gauge.rb +2 -2
  170. data/lib/ratatui_ruby/schema/list.rb +25 -4
  171. data/lib/ratatui_ruby/schema/list_item.rb +41 -0
  172. data/lib/ratatui_ruby/schema/rect.rb +43 -0
  173. data/lib/ratatui_ruby/schema/row.rb +66 -0
  174. data/lib/ratatui_ruby/schema/style.rb +24 -4
  175. data/lib/ratatui_ruby/schema/table.rb +29 -11
  176. data/lib/ratatui_ruby/schema/text.rb +96 -3
  177. data/lib/ratatui_ruby/scrollbar_state.rb +112 -0
  178. data/lib/ratatui_ruby/style/style.rb +81 -0
  179. data/lib/ratatui_ruby/style.rb +15 -0
  180. data/lib/ratatui_ruby/table_state.rb +90 -0
  181. data/lib/ratatui_ruby/test_helper/event_injection.rb +169 -0
  182. data/lib/ratatui_ruby/test_helper/snapshot.rb +414 -0
  183. data/lib/ratatui_ruby/test_helper/style_assertions.rb +351 -0
  184. data/lib/ratatui_ruby/test_helper/terminal.rb +127 -0
  185. data/lib/ratatui_ruby/test_helper/test_doubles.rb +68 -0
  186. data/lib/ratatui_ruby/test_helper.rb +65 -358
  187. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  188. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  189. data/lib/ratatui_ruby/tui/core.rb +38 -0
  190. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  191. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  192. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  193. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  194. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  195. data/lib/ratatui_ruby/tui.rb +75 -0
  196. data/lib/ratatui_ruby/version.rb +1 -1
  197. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  198. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  199. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  200. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  201. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  202. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  203. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  204. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  205. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  206. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  207. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  208. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  209. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  210. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  211. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  212. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  213. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  214. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  215. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  216. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  217. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  218. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  219. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  220. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  221. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  222. data/lib/ratatui_ruby/widgets.rb +40 -0
  223. data/lib/ratatui_ruby.rb +64 -57
  224. data/sig/examples/app_all_events/view.rbs +1 -1
  225. data/sig/examples/app_all_events/view_state.rbs +1 -1
  226. data/sig/examples/app_stateful_interaction/app.rbs +33 -0
  227. data/sig/examples/widget_block_demo/app.rbs +32 -0
  228. data/sig/examples/{app_map_demo → widget_map_demo}/app.rbs +2 -2
  229. data/sig/examples/{app_table_select → widget_table_demo}/app.rbs +2 -2
  230. data/sig/examples/{widget_table_flex → widget_text_width}/app.rbs +2 -3
  231. data/sig/ratatui_ruby/event.rbs +11 -1
  232. data/sig/ratatui_ruby/frame.rbs +2 -0
  233. data/sig/ratatui_ruby/list_state.rbs +13 -0
  234. data/sig/ratatui_ruby/ratatui_ruby.rbs +2 -2
  235. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +3 -3
  236. data/sig/ratatui_ruby/schema/gauge.rbs +2 -2
  237. data/sig/ratatui_ruby/schema/line_gauge.rbs +2 -2
  238. data/sig/ratatui_ruby/schema/list.rbs +4 -2
  239. data/sig/ratatui_ruby/schema/list_item.rbs +10 -0
  240. data/sig/ratatui_ruby/schema/rect.rbs +3 -0
  241. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  242. data/sig/ratatui_ruby/schema/style.rbs +3 -3
  243. data/sig/ratatui_ruby/schema/table.rbs +3 -1
  244. data/sig/ratatui_ruby/schema/text.rbs +9 -6
  245. data/sig/ratatui_ruby/scrollbar_state.rbs +18 -0
  246. data/sig/ratatui_ruby/session.rbs +41 -48
  247. data/sig/ratatui_ruby/table_state.rbs +15 -0
  248. data/sig/ratatui_ruby/test_helper/event_injection.rbs +16 -0
  249. data/sig/ratatui_ruby/test_helper/snapshot.rbs +12 -0
  250. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +64 -0
  251. data/sig/ratatui_ruby/test_helper/terminal.rbs +14 -0
  252. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +22 -0
  253. data/sig/ratatui_ruby/test_helper.rbs +5 -4
  254. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  255. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  256. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  257. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  258. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  259. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  260. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  261. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  262. data/sig/ratatui_ruby/tui.rbs +19 -0
  263. data/tasks/autodoc/examples.rb +79 -0
  264. data/tasks/autodoc.rake +7 -35
  265. data/tasks/bump/changelog.rb +3 -3
  266. data/tasks/bump/links.rb +67 -0
  267. data/tasks/sourcehut.rake +64 -21
  268. data/tasks/terminal_preview/app_screenshot.rb +13 -3
  269. data/tasks/terminal_preview/saved_screenshot.rb +4 -3
  270. metadata +169 -48
  271. data/doc/contributors/dwim_dx.md +0 -366
  272. data/doc/images/app_analytics.png +0 -0
  273. data/doc/images/app_custom_widget.png +0 -0
  274. data/doc/images/app_mouse_events.png +0 -0
  275. data/doc/images/app_table_select.png +0 -0
  276. data/doc/images/widget_block_padding.png +0 -0
  277. data/doc/images/widget_block_titles.png +0 -0
  278. data/doc/images/widget_list_styles.png +0 -0
  279. data/doc/images/widget_table_flex.png +0 -0
  280. data/examples/app_all_events/model/events.rb +0 -180
  281. data/examples/app_all_events/model/highlight.rb +0 -57
  282. data/examples/app_all_events/test/snapshots/after_focus_lost.txt +0 -24
  283. data/examples/app_all_events/test/snapshots/after_focus_regained.txt +0 -24
  284. data/examples/app_all_events/test/snapshots/after_horizontal_resize.txt +0 -24
  285. data/examples/app_all_events/test/snapshots/after_key_a.txt +0 -24
  286. data/examples/app_all_events/test/snapshots/after_key_ctrl_x.txt +0 -24
  287. data/examples/app_all_events/test/snapshots/after_mouse_click.txt +0 -24
  288. data/examples/app_all_events/test/snapshots/after_mouse_drag.txt +0 -24
  289. data/examples/app_all_events/test/snapshots/after_multiple_events.txt +0 -24
  290. data/examples/app_all_events/test/snapshots/after_paste.txt +0 -24
  291. data/examples/app_all_events/test/snapshots/after_resize.txt +0 -24
  292. data/examples/app_all_events/test/snapshots/after_right_click.txt +0 -24
  293. data/examples/app_all_events/test/snapshots/after_vertical_resize.txt +0 -24
  294. data/examples/app_all_events/test/snapshots/initial_state.txt +0 -24
  295. data/examples/app_all_events/view_state.rb +0 -42
  296. data/examples/app_color_picker/scene.rb +0 -201
  297. data/examples/widget_block_padding/app.rb +0 -67
  298. data/examples/widget_block_titles/app.rb +0 -69
  299. data/examples/widget_list_styles/app.rb +0 -141
  300. data/examples/widget_table_flex/app.rb +0 -95
  301. data/lib/ratatui_ruby/session/autodoc.rb +0 -417
  302. data/lib/ratatui_ruby/session.rb +0 -163
  303. data/sig/examples/widget_block_padding/app.rbs +0 -11
  304. data/sig/examples/widget_block_titles/app.rbs +0 -11
  305. data/sig/examples/widget_list_styles/app.rbs +0 -11
  306. data/tasks/autodoc/inventory.rb +0 -61
  307. data/tasks/autodoc/notice.rb +0 -26
  308. data/tasks/autodoc/rbs.rb +0 -38
  309. data/tasks/autodoc/rdoc.rb +0 -45
  310. data/tasks/bump/comparison_links.rb +0 -41
  311. /data/doc/images/{app_map_demo.png → widget_map_demo.png} +0 -0
@@ -0,0 +1,95 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for modifier keys.
10
+ module Modifier
11
+ # Returns true if CTRL is held OR if this is a left_control/right_control key event.
12
+ def ctrl?
13
+ @modifiers.include?("ctrl") || @code == "left_control" || @code == "right_control"
14
+ end
15
+
16
+ # Alias for {#ctrl?}.
17
+ alias control? ctrl?
18
+
19
+ # Returns true if ALT is held OR if this is a left_alt/right_alt key event.
20
+ def alt?
21
+ @modifiers.include?("alt") || @code == "left_alt" || @code == "right_alt"
22
+ end
23
+
24
+ # Alias for {#alt?}.
25
+ alias option? alt?
26
+
27
+ # Returns true if SHIFT is held OR if this is a left_shift/right_shift key event.
28
+ def shift?
29
+ @modifiers.include?("shift") || @code == "left_shift" || @code == "right_shift"
30
+ end
31
+
32
+ # Returns true if SUPER is held OR if this is a left_super/right_super key event.
33
+ # Also responds to platform aliases: win?, command?, cmd?, tux?
34
+ def super?
35
+ @modifiers.include?("super") || @code == "left_super" || @code == "right_super"
36
+ end
37
+
38
+ # Alias for {#super?}.
39
+ alias win? super?
40
+ # Alias for {#super?}.
41
+ alias command? super?
42
+ # Alias for {#super?}.
43
+ alias cmd? super?
44
+ # Alias for {#super?}.
45
+ alias tux? super?
46
+
47
+ # Returns true if HYPER is held OR if this is a left_hyper/right_hyper key event.
48
+ def hyper?
49
+ @modifiers.include?("hyper") || @code == "left_hyper" || @code == "right_hyper"
50
+ end
51
+
52
+ # Returns true if META is held OR if this is a left_meta/right_meta key event.
53
+ def meta?
54
+ @modifiers.include?("meta") || @code == "left_meta" || @code == "right_meta"
55
+ end
56
+
57
+ # Returns true if this is a modifier key event.
58
+ #
59
+ # Some applications need to know if an event represents a generic key
60
+ # press or a specific modifier key (like CTRL or ALT) being pressed on
61
+ # its own.
62
+ #
63
+ # This method identifies if the key event itself is a modifier key.
64
+ #
65
+ # === Example
66
+ #
67
+ # if event.modifier?
68
+ # # Handle solo modifier key press
69
+ # end
70
+ def modifier?
71
+ @kind == :modifier
72
+ end
73
+
74
+ # Handles modifier-specific DWIM logic for method_missing.
75
+ private def match_modifier_dwim?(key_name, key_sym)
76
+ # Platform modifier aliases
77
+ modifier_aliases = {
78
+ win: "super",
79
+ command: "super",
80
+ cmd: "super",
81
+ tux: "super",
82
+ }.freeze
83
+
84
+ target_modifier = modifier_aliases[key_sym]
85
+ if target_modifier
86
+ return true if @modifiers.include?(target_modifier)
87
+ return true if @code == "left_#{target_modifier}" || @code == "right_#{target_modifier}"
88
+ end
89
+
90
+ false
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,55 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for navigation keys.
10
+ module Navigation
11
+ # Returns true if this is a standard key.
12
+ #
13
+ # Standard keys include: characters, Enter, Tab, arrow keys, navigation keys.
14
+ #
15
+ # event.standard? # => true for "a", "enter", "up", etc.
16
+ def standard?
17
+ @kind == :standard
18
+ end
19
+
20
+ # Alias for {#standard?}.
21
+ #
22
+ # Provided for semantic clarity when checking if a key has no special category.
23
+ #
24
+ # event.unmodified? # => true for standard keys like "a", "enter", "up"
25
+ alias unmodified? standard?
26
+
27
+ # Handles navigation-specific DWIM logic for method_missing.
28
+ private def match_navigation_dwim?(key_name, key_sym)
29
+ # DWIM: reverse_tab? matches both BackTab key and Shift+Tab combo
30
+ if key_name == "reverse_tab"
31
+ return true if @code == "back_tab"
32
+ return true if @code == "tab" && @modifiers.include?("shift")
33
+ end
34
+
35
+ # DWIM: Check explicit aliases
36
+ navigation_aliases = {
37
+ return: "enter",
38
+ back: "backspace",
39
+ del: "delete",
40
+ ins: "insert",
41
+ pgup: "page_up",
42
+ pageup: "page_up",
43
+ pgdn: "page_down",
44
+ pagedown: "page_down",
45
+ }.freeze
46
+
47
+ target_code = navigation_aliases[key_sym]
48
+ return true if target_code && @code == target_code && @modifiers.empty?
49
+
50
+ false
51
+ end
52
+ end
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,45 @@
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
+ module RatatuiRuby
7
+ class Event
8
+ class Key < Event
9
+ # Methods and logic for system and function keys.
10
+ module System
11
+ # Returns true if this is a system key.
12
+ #
13
+ # System keys include: Esc, CapsLock, ScrollLock, NumLock, PrintScreen, Pause, Menu, KeypadBegin.
14
+ #
15
+ # event.system? # => true for pause, esc, caps_lock, etc.
16
+ def system?
17
+ @kind == :system
18
+ end
19
+
20
+ # Returns true if this is a function key (F1-F24).
21
+ #
22
+ # event.function? # => true for f1, f2, ..., f24
23
+ def function?
24
+ @kind == :function
25
+ end
26
+
27
+ # Handles system-specific DWIM logic for method_missing.
28
+ private def match_system_dwim?(key_name, key_sym)
29
+ system_aliases = {
30
+ scrlk: "scroll_lock",
31
+ scroll: "scroll_lock",
32
+ prtsc: "print_screen",
33
+ print: "print_screen",
34
+ escape: "esc",
35
+ }.freeze
36
+
37
+ target_code = system_aliases[key_sym]
38
+ return true if target_code && @code == target_code && @modifiers.empty?
39
+
40
+ false
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -3,6 +3,12 @@
3
3
  # SPDX-FileCopyrightText: 2025 Kerrick Long <me@kerricklong.com>
4
4
  # SPDX-License-Identifier: AGPL-3.0-or-later
5
5
 
6
+ require_relative "key/character"
7
+ require_relative "key/media"
8
+ require_relative "key/modifier"
9
+ require_relative "key/navigation"
10
+ require_relative "key/system"
11
+
6
12
  module RatatuiRuby
7
13
  class Event
8
14
  # Captures a keyboard interaction.
@@ -33,7 +39,34 @@ module RatatuiRuby
33
39
  # in type: :key, code: "c", modifiers: ["ctrl"]
34
40
  # exit
35
41
  # end
42
+ #
43
+ # === Terminal Compatibility
44
+ #
45
+ # Some key combinations never reach your application. Terminal emulators intercept them for
46
+ # built-in features like tab switching. Common culprits:
47
+ #
48
+ # * Ctrl+PageUp/PageDown (tab switching in Terminal.app, iTerm2)
49
+ # * Ctrl+Tab (tab switching)
50
+ # * Cmd+key combinations (macOS system shortcuts)
51
+ #
52
+ # If modifiers appear missing, test with a different terminal. Kitty, WezTerm, and Alacritty
53
+ # pass more keys through. See <tt>doc/terminal_limitations.md</tt> for details.
54
+ #
55
+ # === Enhanced Keys (Kitty Protocol)
56
+ #
57
+ # Terminals supporting the Kitty keyboard protocol report additional keys:
58
+ #
59
+ # * Media keys: <tt>:play</tt>, <tt>:play_pause</tt>, <tt>:track_next</tt>, <tt>:mute_volume</tt>
60
+ # * Individual modifiers: <tt>:left_shift</tt>, <tt>:right_control</tt>, <tt>:left_super</tt>
61
+ #
62
+ # These keys will not work in Terminal.app, iTerm2, or GNOME Terminal.
36
63
  class Key < Event
64
+ include Character
65
+ include Media
66
+ include Modifier
67
+ include Navigation
68
+ include System
69
+
37
70
  # The key code (e.g., <tt>"a"</tt>, <tt>"enter"</tt>, <tt>"up"</tt>).
38
71
  #
39
72
  # puts event.code # => "enter"
@@ -44,6 +77,15 @@ module RatatuiRuby
44
77
  # puts event.modifiers # => ["ctrl", "shift"]
45
78
  attr_reader :modifiers
46
79
 
80
+ # The category of the key.
81
+ #
82
+ # One of: <tt>:standard</tt>, <tt>:function</tt>, <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>.
83
+ #
84
+ # This allows grouping keys by their logical type without parsing the code string.
85
+ #
86
+ # event.kind # => :media
87
+ attr_reader :kind
88
+
47
89
  # Returns true for Key events.
48
90
  #
49
91
  # event.key? # => true
@@ -59,17 +101,21 @@ module RatatuiRuby
59
101
  # The key code (String).
60
102
  # [modifiers]
61
103
  # List of modifiers (Array<String>).
62
- def initialize(code:, modifiers: [])
63
- @code = code
64
- @modifiers = modifiers.sort
104
+ # [kind]
105
+ # The key category (Symbol). One of: <tt>:standard</tt>, <tt>:function</tt>,
106
+ # <tt>:media</tt>, <tt>:modifier</tt>, <tt>:system</tt>. Defaults to <tt>:standard</tt>.
107
+ def initialize(code:, modifiers: [], kind: :standard)
108
+ @code = code.freeze
109
+ @modifiers = modifiers.map(&:freeze).sort.freeze
110
+ @kind = kind
65
111
  end
66
112
 
67
113
  # Compares the event with another object.
68
114
  #
69
115
  # - If +other+ is a +Symbol+, compares against #to_sym.
70
116
  # - If +other+ is a +String+, compares against #to_s.
71
- # - Otherwise, performs standard equality check.
72
- # - Otherwise, compares internal state (code + modifiers).
117
+ # - If +other+ is a +Key+, compares as a value object.
118
+ # - Otherwise, compares using standard equality.
73
119
  def ==(other)
74
120
  case other
75
121
  when Symbol
@@ -91,9 +137,25 @@ module RatatuiRuby
91
137
  # === Supported Keys
92
138
  #
93
139
  # [Standard]
94
- # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:esc</tt>, <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:home</tt>, <tt>:end</tt>, <tt>:delete</tt>, <tt>:insert</tt>, <tt>:f1</tt>..<tt>:f12</tt>
140
+ # <tt>:enter</tt>, <tt>:backspace</tt>, <tt>:tab</tt>, <tt>:back_tab</tt>, <tt>:esc</tt>, <tt>:null</tt>
95
141
  # [Navigation]
96
- # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>
142
+ # <tt>:up</tt>, <tt>:down</tt>, <tt>:left</tt>, <tt>:right</tt>, <tt>:home</tt>, <tt>:end</tt>,
143
+ # <tt>:page_up</tt>, <tt>:page_down</tt>, <tt>:insert</tt>, <tt>:delete</tt>
144
+ # [Function Keys]
145
+ # <tt>:f1</tt> through <tt>:f12</tt> (and beyond, e.g. <tt>:f24</tt>)
146
+ # [Lock Keys]
147
+ # <tt>:caps_lock</tt>, <tt>:scroll_lock</tt>, <tt>:num_lock</tt>
148
+ # [System Keys]
149
+ # <tt>:print_screen</tt>, <tt>:pause</tt>, <tt>:menu</tt>, <tt>:keypad_begin</tt>
150
+ # [Media Keys]
151
+ # <tt>:play</tt>, <tt>:media_pause</tt>, <tt>:play_pause</tt>, <tt>:reverse</tt>, <tt>:stop</tt>,
152
+ # <tt>:fast_forward</tt>, <tt>:rewind</tt>, <tt>:track_next</tt>, <tt>:track_previous</tt>,
153
+ # <tt>:record</tt>, <tt>:lower_volume</tt>, <tt>:raise_volume</tt>, <tt>:mute_volume</tt>
154
+ # [Modifier Keys]
155
+ # <tt>:left_shift</tt>, <tt>:left_control</tt>, <tt>:left_alt</tt>, <tt>:left_super</tt>,
156
+ # <tt>:left_hyper</tt>, <tt>:left_meta</tt>, <tt>:right_shift</tt>, <tt>:right_control</tt>,
157
+ # <tt>:right_alt</tt>, <tt>:right_super</tt>, <tt>:right_hyper</tt>, <tt>:right_meta</tt>,
158
+ # <tt>:iso_level3_shift</tt>, <tt>:iso_level5_shift</tt>
97
159
  # [Characters]
98
160
  # <tt>:a</tt>, <tt>:b</tt>, <tt>:1</tt>, <tt>:space</tt>, etc.
99
161
  #
@@ -131,46 +193,7 @@ module RatatuiRuby
131
193
 
132
194
  # Returns inspection string.
133
195
  def inspect
134
- "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect}>"
135
- end
136
-
137
- # Returns true if CTRL is held.
138
- def ctrl?
139
- @modifiers.include?("ctrl")
140
- end
141
-
142
- # Returns true if ALT is held.
143
- def alt?
144
- @modifiers.include?("alt")
145
- end
146
-
147
- # Returns true if SHIFT is held.
148
- def shift?
149
- @modifiers.include?("shift")
150
- end
151
-
152
- # Returns true if the key represents a single printable character.
153
- #
154
- # RatatuiRuby::Event::Key.new(code: "a").text? # => true
155
- # RatatuiRuby::Event::Key.new(code: "enter").text? # => false
156
- # RatatuiRuby::Event::Key.new(code: "space").text? # => false ("space" is not 1 char, " " is)
157
- def text?
158
- @code.length == 1
159
- end
160
-
161
- # Returns the key as a printable character (if applicable).
162
- #
163
- # [Printable Characters]
164
- # Returns the character itself (e.g., <tt>"a"</tt>, <tt>"1"</tt>, <tt>" "</tt>).
165
- # [Special Keys]
166
- # Returns an empty string (e.g., <tt>"enter"</tt>, <tt>"up"</tt>, <tt>"f1"</tt>).
167
- #
168
- # This is equivalent to +to_s+.
169
- #
170
- # RatatuiRuby::Event::Key.new(code: "a").char # => "a"
171
- # RatatuiRuby::Event::Key.new(code: "enter").char # => ""
172
- def char
173
- to_s
196
+ "#<#{self.class} code=#{@code.inspect} modifiers=#{@modifiers.inspect} kind=#{@kind.inspect}>"
174
197
  end
175
198
 
176
199
  # Supports dynamic key predicate methods via method_missing.
@@ -184,12 +207,47 @@ module RatatuiRuby
184
207
  #
185
208
  # The method name is converted to a symbol and compared against the event.
186
209
  # This works for any key code or modifier+key combination.
210
+ #
211
+ # === Smart Predicates (DWIM)
212
+ #
213
+ # For convenience, generic predicates match both system and media variants:
214
+ #
215
+ # event.pause? # => true for BOTH system "pause" AND "media_pause"
216
+ # event.play? # => true for "media_play"
217
+ # event.stop? # => true for "media_stop"
218
+ #
219
+ # This "Do What I Mean" behavior reduces boilerplate when you just want to
220
+ # respond to a conceptual action (e.g., "pause the playback") regardless of
221
+ # whether the user pressed a keyboard key or a media button.
222
+ #
223
+ # For strict matching, use the full predicate or compare the code directly:
224
+ #
225
+ # event.media_pause? # => true ONLY for media pause
226
+ # event.code == "pause" # => true ONLY for system pause
187
227
  def method_missing(name, *args, &block)
188
228
  if name.to_s.end_with?("?")
189
- key_sym = name.to_s[0...-1].to_sym
190
- return self == key_sym
229
+ key_name = name.to_s[0...-1]
230
+ key_sym = key_name.to_sym
231
+
232
+ # Fast path: Exact match (e.g., media_pause? for media_pause)
233
+ return true if self == key_sym
234
+
235
+ # Delegate category-specific DWIM logic to mixins
236
+ return true if match_media_dwim?(key_name)
237
+ return true if match_modifier_dwim?(key_name, key_sym)
238
+ return true if match_navigation_dwim?(key_name, key_sym)
239
+ return true if match_system_dwim?(key_name, key_sym)
240
+
241
+ # DWIM: Universal underscore-insensitivity
242
+ # Normalize both predicate and code by stripping underscores
243
+ normalized_predicate = key_name.delete("_")
244
+ normalized_code = @code.delete("_")
245
+ return true if normalized_predicate == normalized_code && @modifiers.empty?
246
+
247
+ false
248
+ else
249
+ super
191
250
  end
192
- super
193
251
  end
194
252
 
195
253
  # Declares that this class responds to dynamic predicate methods.
@@ -202,9 +260,11 @@ module RatatuiRuby
202
260
  # case event
203
261
  # in type: :key, code: "c", modifiers: ["ctrl"]
204
262
  # puts "Ctrl+C pressed"
263
+ # in type: :key, kind: :media
264
+ # puts "Media key pressed"
205
265
  # end
206
266
  def deconstruct_keys(keys)
207
- { type: :key, code: @code, modifiers: @modifiers }
267
+ { type: :key, code: @code, modifiers: @modifiers, kind: @kind }
208
268
  end
209
269
  end
210
270
  end
@@ -67,11 +67,11 @@ module RatatuiRuby
67
67
  # [modifiers]
68
68
  # List of modifiers (Array<String>).
69
69
  def initialize(kind:, x:, y:, button:, modifiers: [])
70
- @kind = kind
70
+ @kind = kind.freeze
71
71
  @x = x
72
72
  @y = y
73
- @button = button || "none"
74
- @modifiers = modifiers.sort
73
+ @button = (button || "none").freeze
74
+ @modifiers = modifiers.map(&:freeze).sort.freeze
75
75
  end
76
76
 
77
77
  # Returns true if mouse button was pressed down.
@@ -47,7 +47,7 @@ module RatatuiRuby
47
47
  # [content]
48
48
  # Pasted text (String).
49
49
  def initialize(content:)
50
- @content = content
50
+ @content = content.freeze
51
51
  end
52
52
 
53
53
  # Deconstructs the event for pattern matching.
@@ -16,12 +16,20 @@ module RatatuiRuby
16
16
  # Use it inside a <tt>RatatuiRuby.draw</tt> block to render widgets with full
17
17
  # control over placement.
18
18
  #
19
+ # == Thread/Ractor Safety
20
+ #
21
+ # Frame is an *I/O handle*, not a data object. It has side effects
22
+ # (render_widget, set_cursor_position) and is intentionally *not*
23
+ # Ractor-shareable. Passing it to helper methods during the draw block is
24
+ # fine. However, do not include it in immutable Models/Messages or pass
25
+ # it to other Ractors. Frame is only valid during the draw block's execution.
26
+ #
19
27
  # === Examples
20
28
  #
21
29
  # Basic usage with a single widget:
22
30
  #
23
31
  # RatatuiRuby.draw do |frame|
24
- # paragraph = RatatuiRuby::Paragraph.new(text: "Hello, world!")
32
+ # paragraph = RatatuiRuby::Widgets::Paragraph.new(text: "Hello, world!")
25
33
  # frame.render_widget(paragraph, frame.area)
26
34
  # end
27
35
  #
@@ -32,8 +40,8 @@ module RatatuiRuby
32
40
  # frame.area,
33
41
  # direction: :horizontal,
34
42
  # constraints: [
35
- # RatatuiRuby::Constraint.length(20),
36
- # RatatuiRuby::Constraint.fill(1)
43
+ # RatatuiRuby::Layout::Constraint.length(20),
44
+ # RatatuiRuby::Layout::Constraint.fill(1)
37
45
  # ]
38
46
  # )
39
47
  #
@@ -78,10 +86,98 @@ module RatatuiRuby
78
86
  # === Example
79
87
  #
80
88
  # RatatuiRuby.draw do |frame|
81
- # para = RatatuiRuby::Paragraph.new(text: "Content")
89
+ # para = RatatuiRuby::Widgets::Paragraph.new(text: "Content")
82
90
  # frame.render_widget(para, frame.area)
83
91
  # end
84
92
  #
85
93
  # (Native method implemented in Rust)
94
+
95
+ ##
96
+ # :method: render_stateful_widget
97
+ # :call-seq: render_stateful_widget(widget, area, state) -> nil
98
+ #
99
+ # Renders a widget with persistent state.
100
+ #
101
+ # Some UI components (like List or Table) have **runtime status** (Status) that
102
+ # changes during rendering, such as the current scroll offset.
103
+ #
104
+ # Since Widget definitions (Configuration Definition) are immutable inputs,
105
+ # you must pass a separate mutable State object (Output Status) to capture
106
+ # these changes.
107
+ #
108
+ # Note: The Widget configuration is *always* required. The State object is
109
+ # only used for specific widgets that need to persist runtime status.
110
+ #
111
+ #
112
+ # [widget]
113
+ # The immutable widget configuration (Input) (e.g., RatatuiRuby::List).
114
+ # [area]
115
+ # The Rect area to render into.
116
+ # [state]
117
+ # The mutable state object (Output) (e.g., RatatuiRuby::ListState).
118
+ #
119
+ # === Example
120
+ #
121
+ # # Initialize state once (outside the loop)
122
+ # @list_state = RatatuiRuby::ListState.new
123
+ #
124
+ # RatatuiRuby.draw do |frame|
125
+ # list = RatatuiRuby::Widgets::List.new(items: ["A", "B"])
126
+ # frame.render_stateful_widget(list, frame.area, @list_state)
127
+ # end
128
+ #
129
+ # # Read back the offset calculated by Ratatui
130
+ # puts @list_state.offset
131
+ #
132
+ # (Native method implemented in Rust)
133
+
134
+ ##
135
+ # :method: set_cursor_position
136
+ # :call-seq: set_cursor_position(x, y) -> nil
137
+ #
138
+ # Positions the blinking cursor at the given coordinates.
139
+ #
140
+ # Text input fields show users where typed characters will appear. Without
141
+ # a visible cursor, users cannot tell if the input is focused or where text
142
+ # will insert.
143
+ #
144
+ # This method moves the terminal cursor to a specific cell. Coordinates are
145
+ # 0-indexed from the terminal's top-left corner.
146
+ #
147
+ # Use it when building login forms, search bars, or command palettes.
148
+ #
149
+ # [x]
150
+ # Column position (<tt>0</tt> = leftmost column).
151
+ # [y]
152
+ # Row position (<tt>0</tt> = topmost row).
153
+ #
154
+ # === Example
155
+ #
156
+ # Position the cursor at the end of typed text in a login form:
157
+ #
158
+ # PREFIX = "Username: [ "
159
+ # username = "alice"
160
+ #
161
+ # RatatuiRuby.draw do |frame|
162
+ # # Render the input field
163
+ # prompt = RatatuiRuby::Widgets::Paragraph.new(
164
+ # text: "#{PREFIX}#{username} ]",
165
+ # block: RatatuiRuby::Widgets::Block.new(borders: :all)
166
+ # )
167
+ # frame.render_widget(prompt, frame.area)
168
+ #
169
+ # # Position cursor after the typed text
170
+ # # Account for border (1) + prefix length + username length
171
+ # cursor_x = 1 + PREFIX.length + username.length
172
+ # cursor_y = 1 # First line inside border
173
+ # frame.set_cursor_position(cursor_x, cursor_y)
174
+ # end
175
+ #
176
+ # See also:
177
+ # - {Component-based implementation using Frame API}[link:/examples/app_color_picker/app_rb.html]
178
+ # - {Declarative implementation using Tree API}[link:/examples/app_login_form/app_rb.html]
179
+ # - RatatuiRuby::Cursor (Tree API alternative)
180
+ #
181
+ # (Native method implemented in Rust)
86
182
  end
87
183
  end
@@ -0,0 +1,95 @@
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
+ module RatatuiRuby
7
+ module Layout
8
+ # Defines the sizing rule for a layout section.
9
+ #
10
+ # Flexible layouts need rules. You can't just place widgets at absolute coordinates; they must adapt to changing terminal sizes.
11
+ #
12
+ # This class defines the rules of engagement. It tells the layout engine exactly how much space a section requires relative to others.
13
+ #
14
+ # Mix and match fixed lengths, percentages, ratios, and minimums. Build layouts that breathe.
15
+ #
16
+ # === Examples
17
+ #
18
+ # Layout::Constraint.length(5) # Exactly 5 cells
19
+ # Layout::Constraint.percentage(50) # Half the available space
20
+ # Layout::Constraint.min(10) # At least 10 cells, maybe more
21
+ # Layout::Constraint.fill(1) # Fill remaining space (weight 1)
22
+ class Constraint < Data.define(:type, :value)
23
+ ##
24
+ # :attr_reader: type
25
+ # The type of constraint.
26
+ #
27
+ # <tt>:length</tt>, <tt>:percentage</tt>, <tt>:min</tt>, <tt>:max</tt>, <tt>:fill</tt>, or <tt>:ratio</tt>.
28
+
29
+ ##
30
+ # :attr_reader: value
31
+ # The numeric value (or array for ratio) associated with the rule.
32
+
33
+ # Requests a fixed size.
34
+ #
35
+ # Layout::Constraint.length(10) # 10 characters wide/high
36
+ #
37
+ # [v] Number of cells (Integer).
38
+ def self.length(v)
39
+ new(type: :length, value: Integer(v))
40
+ end
41
+
42
+ # Requests a percentage of available space.
43
+ #
44
+ # Layout::Constraint.percentage(25) # 25% of the area
45
+ #
46
+ # [v] Percentage 0-100 (Integer).
47
+ def self.percentage(v)
48
+ new(type: :percentage, value: Integer(v))
49
+ end
50
+
51
+ # Enforces a minimum size.
52
+ #
53
+ # Layout::Constraint.min(5) # At least 5 cells
54
+ #
55
+ # This section will grow if space permits, but never shrink below +v+.
56
+ #
57
+ # [v] Minimum cells (Integer).
58
+ def self.min(v)
59
+ new(type: :min, value: Integer(v))
60
+ end
61
+
62
+ # Enforces a maximum size.
63
+ #
64
+ # Layout::Constraint.max(10) # At most 10 cells
65
+ #
66
+ # [v] Maximum cells (Integer).
67
+ def self.max(v)
68
+ new(type: :max, value: Integer(v))
69
+ end
70
+
71
+ # Fills remaining space proportionally.
72
+ #
73
+ # Layout::Constraint.fill(1) # Equal share
74
+ # Layout::Constraint.fill(2) # Double share
75
+ #
76
+ # Fill constraints distribute any space left after satisfying strict rules.
77
+ # They behave like flex-grow. A fill(2) takes twice as much space as a fill(1).
78
+ #
79
+ # [v] Proportional weight (Integer, default: 1).
80
+ def self.fill(v = 1)
81
+ new(type: :fill, value: Integer(v))
82
+ end
83
+
84
+ # Requests a specific ratio of the total space.
85
+ #
86
+ # Layout::Constraint.ratio(1, 3) # 1/3rd of the area
87
+ #
88
+ # [numerator] Top part of fraction (Integer).
89
+ # [denominator] Bottom part of fraction (Integer).
90
+ def self.ratio(numerator, denominator)
91
+ new(type: :ratio, value: [Integer(numerator), Integer(denominator)])
92
+ end
93
+ end
94
+ end
95
+ end