ratatui_ruby 0.9.1 → 0.10.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 (267) 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 +2 -1
  7. data/CHANGELOG.md +98 -0
  8. data/REUSE.toml +5 -0
  9. data/Rakefile +1 -1
  10. data/Steepfile +49 -0
  11. data/doc/concepts/debugging.md +401 -0
  12. data/doc/getting_started/quickstart.md +8 -3
  13. data/doc/images/app_all_events.png +0 -0
  14. data/doc/images/app_color_picker.png +0 -0
  15. data/doc/images/app_debugging_showcase.gif +0 -0
  16. data/doc/images/app_debugging_showcase.png +0 -0
  17. data/doc/images/app_login_form.png +0 -0
  18. data/doc/images/app_stateful_interaction.png +0 -0
  19. data/doc/images/verify_quickstart_dsl.png +0 -0
  20. data/doc/images/verify_quickstart_layout.png +0 -0
  21. data/doc/images/verify_quickstart_lifecycle.png +0 -0
  22. data/doc/images/verify_readme_usage.png +0 -0
  23. data/doc/images/widget_barchart.png +0 -0
  24. data/doc/images/widget_block.png +0 -0
  25. data/doc/images/widget_box.png +0 -0
  26. data/doc/images/widget_calendar.png +0 -0
  27. data/doc/images/widget_canvas.png +0 -0
  28. data/doc/images/widget_cell.png +0 -0
  29. data/doc/images/widget_center.png +0 -0
  30. data/doc/images/widget_chart.png +0 -0
  31. data/doc/images/widget_gauge.png +0 -0
  32. data/doc/images/widget_layout_split.png +0 -0
  33. data/doc/images/widget_line_gauge.png +0 -0
  34. data/doc/images/widget_list.png +0 -0
  35. data/doc/images/widget_map.png +0 -0
  36. data/doc/images/widget_overlay.png +0 -0
  37. data/doc/images/widget_popup.png +0 -0
  38. data/doc/images/widget_ratatui_logo.png +0 -0
  39. data/doc/images/widget_ratatui_mascot.png +0 -0
  40. data/doc/images/widget_rect.png +0 -0
  41. data/doc/images/widget_render.png +0 -0
  42. data/doc/images/widget_rich_text.png +0 -0
  43. data/doc/images/widget_scroll_text.png +0 -0
  44. data/doc/images/widget_scrollbar.png +0 -0
  45. data/doc/images/widget_sparkline.png +0 -0
  46. data/doc/images/widget_style_colors.png +0 -0
  47. data/doc/images/widget_table.png +0 -0
  48. data/doc/images/widget_tabs.png +0 -0
  49. data/doc/images/widget_text_width.png +0 -0
  50. data/doc/troubleshooting/async.md +4 -0
  51. data/examples/app_debugging_showcase/README.md +119 -0
  52. data/examples/app_debugging_showcase/app.rb +318 -0
  53. data/examples/widget_canvas/app.rb +19 -14
  54. data/examples/widget_gauge/app.rb +18 -3
  55. data/examples/widget_layout_split/app.rb +10 -4
  56. data/examples/widget_list/app.rb +22 -6
  57. data/examples/widget_rect/app.rb +7 -6
  58. data/examples/widget_rich_text/app.rb +62 -37
  59. data/examples/widget_style_colors/app.rb +26 -47
  60. data/examples/widget_table/app.rb +28 -5
  61. data/examples/widget_text_width/app.rb +6 -4
  62. data/ext/ratatui_ruby/Cargo.lock +48 -1
  63. data/ext/ratatui_ruby/Cargo.toml +6 -2
  64. data/ext/ratatui_ruby/src/color.rs +82 -0
  65. data/ext/ratatui_ruby/src/errors.rs +28 -0
  66. data/ext/ratatui_ruby/src/events.rs +15 -14
  67. data/ext/ratatui_ruby/src/lib.rs +56 -0
  68. data/ext/ratatui_ruby/src/rendering.rs +3 -1
  69. data/ext/ratatui_ruby/src/style.rs +48 -21
  70. data/ext/ratatui_ruby/src/terminal.rs +40 -9
  71. data/ext/ratatui_ruby/src/text.rs +21 -9
  72. data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
  73. data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
  74. data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
  75. data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
  76. data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
  77. data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
  78. data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
  79. data/lib/ratatui_ruby/buffer/cell.rb +25 -15
  80. data/lib/ratatui_ruby/buffer.rb +134 -2
  81. data/lib/ratatui_ruby/cell.rb +13 -5
  82. data/lib/ratatui_ruby/debug.rb +215 -0
  83. data/lib/ratatui_ruby/event/key.rb +3 -2
  84. data/lib/ratatui_ruby/event.rb +1 -1
  85. data/lib/ratatui_ruby/layout/constraint.rb +49 -0
  86. data/lib/ratatui_ruby/layout/layout.rb +119 -13
  87. data/lib/ratatui_ruby/layout/position.rb +55 -0
  88. data/lib/ratatui_ruby/layout/rect.rb +188 -0
  89. data/lib/ratatui_ruby/layout/size.rb +55 -0
  90. data/lib/ratatui_ruby/layout.rb +4 -0
  91. data/lib/ratatui_ruby/style/color.rb +149 -0
  92. data/lib/ratatui_ruby/style/style.rb +51 -4
  93. data/lib/ratatui_ruby/style.rb +2 -0
  94. data/lib/ratatui_ruby/symbols.rb +435 -0
  95. data/lib/ratatui_ruby/synthetic_events.rb +1 -1
  96. data/lib/ratatui_ruby/table_state.rb +51 -0
  97. data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
  98. data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
  99. data/lib/ratatui_ruby/test_helper.rb +9 -0
  100. data/lib/ratatui_ruby/text/line.rb +245 -0
  101. data/lib/ratatui_ruby/text/span.rb +158 -0
  102. data/lib/ratatui_ruby/text.rb +99 -0
  103. data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
  104. data/lib/ratatui_ruby/tui/core.rb +13 -2
  105. data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
  106. data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
  107. data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
  108. data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
  109. data/lib/ratatui_ruby/tui.rb +22 -1
  110. data/lib/ratatui_ruby/version.rb +1 -1
  111. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
  112. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
  113. data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
  114. data/lib/ratatui_ruby/widgets/block.rb +14 -6
  115. data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
  116. data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
  117. data/lib/ratatui_ruby/widgets/cell.rb +2 -0
  118. data/lib/ratatui_ruby/widgets/center.rb +2 -0
  119. data/lib/ratatui_ruby/widgets/chart.rb +6 -0
  120. data/lib/ratatui_ruby/widgets/clear.rb +2 -0
  121. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  122. data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
  123. data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
  124. data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
  125. data/lib/ratatui_ruby/widgets/list.rb +87 -3
  126. data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
  127. data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
  128. data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
  129. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
  131. data/lib/ratatui_ruby/widgets/row.rb +45 -0
  132. data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
  133. data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
  134. data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
  135. data/lib/ratatui_ruby/widgets/table.rb +13 -3
  136. data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
  137. data/lib/ratatui_ruby/widgets.rb +1 -0
  138. data/lib/ratatui_ruby.rb +40 -9
  139. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  140. data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
  141. data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
  142. data/sig/examples/app_all_events/view.rbs +1 -1
  143. data/sig/examples/app_stateful_interaction/app.rbs +5 -5
  144. data/sig/examples/widget_block_demo/app.rbs +6 -6
  145. data/sig/manifest.yaml +5 -0
  146. data/sig/patches/data.rbs +26 -0
  147. data/sig/patches/debugger__.rbs +8 -0
  148. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  149. data/sig/ratatui_ruby/buffer.rbs +18 -0
  150. data/sig/ratatui_ruby/cell.rbs +44 -0
  151. data/sig/ratatui_ruby/clear.rbs +18 -0
  152. data/sig/ratatui_ruby/constraint.rbs +26 -0
  153. data/sig/ratatui_ruby/debug.rbs +45 -0
  154. data/sig/ratatui_ruby/draw.rbs +30 -0
  155. data/sig/ratatui_ruby/event.rbs +68 -8
  156. data/sig/ratatui_ruby/frame.rbs +4 -4
  157. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  158. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  159. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  160. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  161. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  162. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  163. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  164. data/sig/ratatui_ruby/ratatui_ruby.rbs +83 -4
  165. data/sig/ratatui_ruby/rect.rbs +17 -0
  166. data/sig/ratatui_ruby/style/color.rbs +22 -0
  167. data/sig/ratatui_ruby/style/style.rbs +29 -0
  168. data/sig/ratatui_ruby/symbols.rbs +141 -0
  169. data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
  170. data/sig/ratatui_ruby/table_state.rbs +6 -0
  171. data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
  172. data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
  173. data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
  174. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
  175. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
  176. data/sig/ratatui_ruby/text/line.rbs +27 -0
  177. data/sig/ratatui_ruby/text/span.rbs +23 -0
  178. data/sig/ratatui_ruby/text.rbs +12 -0
  179. data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
  180. data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
  181. data/sig/ratatui_ruby/tui/core.rbs +2 -2
  182. data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
  183. data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
  184. data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
  185. data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
  186. data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
  187. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  188. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  189. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  190. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  191. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  192. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  193. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  194. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  195. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  196. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  197. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  198. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  199. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  200. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  201. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  202. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  203. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  204. data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
  205. data/tasks/steep.rake +11 -0
  206. metadata +80 -63
  207. data/doc/contributors/v1.0.0_blockers.md +0 -870
  208. data/doc/troubleshooting/debugging.md +0 -101
  209. data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
  210. data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
  211. data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
  212. data/lib/ratatui_ruby/schema/block.rb +0 -198
  213. data/lib/ratatui_ruby/schema/calendar.rb +0 -84
  214. data/lib/ratatui_ruby/schema/canvas.rb +0 -239
  215. data/lib/ratatui_ruby/schema/center.rb +0 -67
  216. data/lib/ratatui_ruby/schema/chart.rb +0 -159
  217. data/lib/ratatui_ruby/schema/clear.rb +0 -62
  218. data/lib/ratatui_ruby/schema/constraint.rb +0 -151
  219. data/lib/ratatui_ruby/schema/cursor.rb +0 -50
  220. data/lib/ratatui_ruby/schema/gauge.rb +0 -72
  221. data/lib/ratatui_ruby/schema/layout.rb +0 -122
  222. data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
  223. data/lib/ratatui_ruby/schema/list.rb +0 -135
  224. data/lib/ratatui_ruby/schema/list_item.rb +0 -51
  225. data/lib/ratatui_ruby/schema/overlay.rb +0 -51
  226. data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
  227. data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
  228. data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
  229. data/lib/ratatui_ruby/schema/rect.rb +0 -174
  230. data/lib/ratatui_ruby/schema/row.rb +0 -76
  231. data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
  232. data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
  233. data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
  234. data/lib/ratatui_ruby/schema/style.rb +0 -97
  235. data/lib/ratatui_ruby/schema/table.rb +0 -141
  236. data/lib/ratatui_ruby/schema/tabs.rb +0 -85
  237. data/lib/ratatui_ruby/schema/text.rb +0 -217
  238. data/sig/examples/app_all_events/model/events.rbs +0 -15
  239. data/sig/examples/app_all_events/view_state.rbs +0 -21
  240. data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
  241. data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
  242. data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
  243. data/sig/ratatui_ruby/schema/block.rbs +0 -18
  244. data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
  245. data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
  246. data/sig/ratatui_ruby/schema/center.rbs +0 -17
  247. data/sig/ratatui_ruby/schema/chart.rbs +0 -39
  248. data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
  249. data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
  250. data/sig/ratatui_ruby/schema/draw.rbs +0 -33
  251. data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
  252. data/sig/ratatui_ruby/schema/layout.rbs +0 -27
  253. data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
  254. data/sig/ratatui_ruby/schema/list.rbs +0 -28
  255. data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
  256. data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
  257. data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
  258. data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
  259. data/sig/ratatui_ruby/schema/rect.rbs +0 -48
  260. data/sig/ratatui_ruby/schema/row.rbs +0 -28
  261. data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
  262. data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
  263. data/sig/ratatui_ruby/schema/style.rbs +0 -19
  264. data/sig/ratatui_ruby/schema/table.rbs +0 -32
  265. data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
  266. data/sig/ratatui_ruby/schema/text.rbs +0 -31
  267. /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
@@ -379,6 +379,37 @@ module RatatuiRuby
379
379
  )
380
380
  end
381
381
 
382
+ # Expands the rect by a uniform margin on all sides.
383
+ #
384
+ # Containers wrap content with decorations. Adding margin to all four edges inline is verbose.
385
+ # Off-by-one errors happen when you forget to double the margin.
386
+ #
387
+ # This method computes the outer area. Saturates x/y at 0 when margin exceeds position.
388
+ # Use Rect#clamp to constrain the result if it may exceed screen bounds.
389
+ #
390
+ # [margin] Integer expansion on all sides.
391
+ #
392
+ # === Example
393
+ #
394
+ #--
395
+ # SPDX-SnippetBegin
396
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
397
+ # SPDX-License-Identifier: MIT-0
398
+ #++
399
+ # rect = Layout::Rect.new(x: 10, y: 10, width: 20, height: 10)
400
+ # rect.outer(5) # => Rect(x: 5, y: 5, width: 30, height: 20)
401
+ #--
402
+ # SPDX-SnippetEnd
403
+ #++
404
+ def outer(margin)
405
+ new_x = [x - margin, 0].max
406
+ new_y = [y - margin, 0].max
407
+ new_width = right + margin - new_x
408
+ new_height = bottom + margin - new_y
409
+
410
+ Rect.new(x: new_x, y: new_y, width: new_width, height: new_height)
411
+ end
412
+
382
413
  # Moves the rect without changing size.
383
414
  #
384
415
  # Animations and drag-and-drop shift widgets.
@@ -405,6 +436,32 @@ module RatatuiRuby
405
436
  Rect.new(x: x + dx, y: y + dy, width:, height:)
406
437
  end
407
438
 
439
+ # Changes dimensions while preserving position.
440
+ #
441
+ # Window resizing and responsive layouts adjust size mid-session.
442
+ # Creating a new rect with the same position but different size is common.
443
+ #
444
+ # This method returns a resized copy. Position unchanged.
445
+ #
446
+ # [new_size] Size object with new dimensions.
447
+ #
448
+ # === Example
449
+ #
450
+ #--
451
+ # SPDX-SnippetBegin
452
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
453
+ # SPDX-License-Identifier: MIT-0
454
+ #++
455
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 20, height: 10)
456
+ # rect.resize(Size.new(width: 40, height: 20))
457
+ # # => Rect(x: 10, y: 5, width: 40, height: 20)
458
+ #--
459
+ # SPDX-SnippetEnd
460
+ #++
461
+ def resize(new_size)
462
+ Rect.new(x:, y:, width: new_size.width, height: new_size.height)
463
+ end
464
+
408
465
  # Constrains the rect to fit inside bounds.
409
466
  #
410
467
  # Popups and tooltips may extend beyond screen edges.
@@ -516,6 +573,137 @@ module RatatuiRuby
516
573
  end
517
574
  end
518
575
  end
576
+
577
+ # Extracts the position (x, y) from this rect.
578
+ #
579
+ # Layout code sometimes separates position from size.
580
+ # Extracting x and y into multiple variables is verbose.
581
+ #
582
+ # This method returns a Position object containing just the coordinates.
583
+ #
584
+ # === Example
585
+ #
586
+ #--
587
+ # SPDX-SnippetBegin
588
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
589
+ # SPDX-License-Identifier: MIT-0
590
+ #++
591
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
592
+ # rect.as_position # => Position(x: 10, y: 5)
593
+ #--
594
+ # SPDX-SnippetEnd
595
+ #++
596
+ def as_position
597
+ Position.new(x:, y:)
598
+ end
599
+
600
+ # Extracts the size (width, height) from this rect.
601
+ #
602
+ # Layout code sometimes separates size from position.
603
+ # Extracting width and height into multiple variables is verbose.
604
+ #
605
+ # This method returns a Size object containing just the dimensions.
606
+ #
607
+ # === Example
608
+ #
609
+ #--
610
+ # SPDX-SnippetBegin
611
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
612
+ # SPDX-License-Identifier: MIT-0
613
+ #++
614
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
615
+ # rect.as_size # => Size(width: 80, height: 24)
616
+ #--
617
+ # SPDX-SnippetEnd
618
+ #++
619
+ def as_size
620
+ Size.new(width:, height:)
621
+ end
622
+
623
+ # Returns a new Rect, centered horizontally within this rect based on the constraint.
624
+ #
625
+ # Modal dialogs and centered content need horizontal centering.
626
+ # Computing the left offset manually is error-prone.
627
+ #
628
+ # This method uses Layout to compute the centered position.
629
+ #
630
+ # [constraint] Constraint defining the width of the centered area.
631
+ #
632
+ # === Example
633
+ #
634
+ #--
635
+ # SPDX-SnippetBegin
636
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
637
+ # SPDX-License-Identifier: MIT-0
638
+ #++
639
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 100, height: 24)
640
+ # rect.centered_horizontally(Constraint.length(40))
641
+ # # => Rect(x: 30, y: 0, width: 40, height: 24)
642
+ #--
643
+ # SPDX-SnippetEnd
644
+ #++
645
+ def centered_horizontally(constraint)
646
+ areas = Layout.split(self, direction: :horizontal, constraints: [constraint], flex: :center)
647
+ areas.first
648
+ end
649
+
650
+ # Returns a new Rect, centered vertically within this rect based on the constraint.
651
+ #
652
+ # Modal dialogs and centered content need vertical centering.
653
+ # Computing the top offset manually is error-prone.
654
+ #
655
+ # This method uses Layout to compute the centered position.
656
+ #
657
+ # [constraint] Constraint defining the height of the centered area.
658
+ #
659
+ # === Example
660
+ #
661
+ #--
662
+ # SPDX-SnippetBegin
663
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
664
+ # SPDX-License-Identifier: MIT-0
665
+ #++
666
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 80, height: 100)
667
+ # rect.centered_vertically(Constraint.length(20))
668
+ # # => Rect(x: 0, y: 40, width: 80, height: 20)
669
+ #--
670
+ # SPDX-SnippetEnd
671
+ #++
672
+ def centered_vertically(constraint)
673
+ areas = Layout.split(self, direction: :vertical, constraints: [constraint], flex: :center)
674
+ areas.first
675
+ end
676
+
677
+ # Returns a new Rect, centered both horizontally and vertically within this rect.
678
+ #
679
+ # Modal dialogs often need exact centering on both axes.
680
+ # Computing both offsets manually is tedious.
681
+ #
682
+ # This method chains centered_horizontally and centered_vertically.
683
+ #
684
+ # [horizontal_constraint] Constraint defining the width of the centered area.
685
+ # [vertical_constraint] Constraint defining the height of the centered area.
686
+ #
687
+ # === Example
688
+ #
689
+ #--
690
+ # SPDX-SnippetBegin
691
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
692
+ # SPDX-License-Identifier: MIT-0
693
+ #++
694
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 100, height: 100)
695
+ # rect.centered(Constraint.length(40), Constraint.length(20))
696
+ # # => Rect(x: 30, y: 40, width: 40, height: 20)
697
+ #--
698
+ # SPDX-SnippetEnd
699
+ #++
700
+ def centered(horizontal_constraint, vertical_constraint)
701
+ centered_horizontally(horizontal_constraint).centered_vertically(vertical_constraint)
702
+ end
703
+
704
+ # Ruby-idiomatic aliases (TIMTOWTDI)
705
+ alias position as_position
706
+ alias size as_size
519
707
  end
520
708
  end
521
709
  end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module Layout
10
+ # Terminal dimensions as width and height.
11
+ #
12
+ # Layout calculations need sizes. Passing width and height
13
+ # as separate arguments is verbose and easy to swap by mistake.
14
+ #
15
+ # This class bundles dimensions into a single immutable object.
16
+ # Extract it from a Rect or create it directly for sizing operations.
17
+ #
18
+ # Use it for terminal dimensions, widget sizing constraints,
19
+ # or anywhere you need width/height without position.
20
+ #
21
+ # === Example
22
+ #
23
+ #--
24
+ # SPDX-SnippetBegin
25
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
26
+ # SPDX-License-Identifier: MIT-0
27
+ #++
28
+ # size = Layout::Size.new(width: 80, height: 24)
29
+ # puts "Terminal is #{size.width} columns by #{size.height} rows"
30
+ #
31
+ # # Extract from a Rect
32
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
33
+ # size = rect.as_size # => Size(width: 80, height: 24)
34
+ #--
35
+ # SPDX-SnippetEnd
36
+ #++
37
+ class Size < Data.define(:width, :height)
38
+ ##
39
+ # :attr_reader: width
40
+ # Width in terminal columns.
41
+
42
+ ##
43
+ # :attr_reader: height
44
+ # Height in terminal rows.
45
+
46
+ # Creates a new Size.
47
+ #
48
+ # [width] Width in columns (Integer, coerced via +Integer()+).
49
+ # [height] Height in rows (Integer, coerced via +Integer()+).
50
+ def initialize(width: 0, height: 0)
51
+ super(width: Integer(width), height: Integer(height))
52
+ end
53
+ end
54
+ end
55
+ end
@@ -10,6 +10,8 @@ module RatatuiRuby
10
10
  #
11
11
  # This module mirrors +ratatui::layout+ and contains:
12
12
  # - {Rect} — Rectangle geometry
13
+ # - {Position} — Terminal coordinates
14
+ # - {Size} — Terminal dimensions
13
15
  # - {Constraint} — Sizing rules
14
16
  # - {Layout} — Space distribution
15
17
  module Layout
@@ -17,5 +19,7 @@ module RatatuiRuby
17
19
  end
18
20
 
19
21
  require_relative "layout/rect"
22
+ require_relative "layout/position"
23
+ require_relative "layout/size"
20
24
  require_relative "layout/constraint"
21
25
  require_relative "layout/layout"
@@ -0,0 +1,149 @@
1
+ # frozen_string_literal: true
2
+
3
+ #--
4
+ # SPDX-FileCopyrightText: 2026 Kerrick Long <me@kerricklong.com>
5
+ # SPDX-License-Identifier: LGPL-3.0-or-later
6
+ #++
7
+
8
+ module RatatuiRuby
9
+ module Style
10
+ # Color constructors for creating RGB color values.
11
+ #
12
+ # Styles accept colors in multiple formats: symbols (<tt>:red</tt>),
13
+ # indexed integers (0-255), or hex strings (<tt>"#FF0000"</tt>).
14
+ # Converting from other color representations manually is tedious.
15
+ #
16
+ # This module provides factory methods matching Ratatui's Color API.
17
+ # Convert from hex integers, HSL, or other formats in a single call.
18
+ #
19
+ # Use these constructors when you have color data in non-standard formats.
20
+ #
21
+ # === Example
22
+ #
23
+ #--
24
+ # SPDX-SnippetBegin
25
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
26
+ # SPDX-License-Identifier: MIT-0
27
+ #++
28
+ # # From a hex integer (common in design tools)
29
+ # red = Style::Color.from_u32(0xFF0000)
30
+ # style = Style::Style.new(fg: red)
31
+ #
32
+ # # From HSL (common in color pickers)
33
+ # blue = Style::Color.from_hsl(240, 100, 50)
34
+ # style = Style::Style.new(bg: blue)
35
+ #--
36
+ # SPDX-SnippetEnd
37
+ #++
38
+ module Color
39
+ class << self
40
+ # Creates a color from a 32-bit unsigned integer.
41
+ #
42
+ # Design tools and APIs often represent colors as hex integers.
43
+ # Manually extracting RGB components and formatting is error-prone.
44
+ #
45
+ # This method parses the integer and returns a hex string
46
+ # ready for use with Style.
47
+ #
48
+ # === Example
49
+ #
50
+ #--
51
+ # SPDX-SnippetBegin
52
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
53
+ # SPDX-License-Identifier: MIT-0
54
+ #++
55
+ # Color.from_u32(0xFF0000) # => "#ff0000" (red)
56
+ # Color.from_u32(0x00FF00) # => "#00ff00" (green)
57
+ # Color.from_u32(0x0000FF) # => "#0000ff" (blue)
58
+ #--
59
+ # SPDX-SnippetEnd
60
+ #++
61
+ #
62
+ # [value] Integer in <tt>0xRRGGBB</tt> format.
63
+ #
64
+ # Returns a hex string like <tt>"#rrggbb"</tt>.
65
+ def from_u32(value)
66
+ RatatuiRuby._color_from_u32(value)
67
+ end
68
+
69
+ # Creates a color from HSL (Hue, Saturation, Lightness) values.
70
+ #
71
+ # Color pickers often use HSL because it matches human perception
72
+ # better than RGB. Converting HSL to RGB manually requires math.
73
+ #
74
+ # This method handles the conversion by delegating to Ratatui's
75
+ # palette integration.
76
+ #
77
+ # Note: This implementation uses degrees (0-360) for hue and
78
+ # percentages (0-100) for saturation and lightness, matching
79
+ # common UI conventions.
80
+ #
81
+ # === Example
82
+ #
83
+ #--
84
+ # SPDX-SnippetBegin
85
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
86
+ # SPDX-License-Identifier: MIT-0
87
+ #++
88
+ # Color.from_hsl(0, 100, 50) # => "#ff0000" (red)
89
+ # Color.from_hsl(120, 100, 50) # => "#00ff00" (green)
90
+ # Color.from_hsl(240, 100, 50) # => "#0000ff" (blue)
91
+ # Color.from_hsl(0, 0, 50) # => "#808080" (gray)
92
+ #--
93
+ # SPDX-SnippetEnd
94
+ #++
95
+ #
96
+ # [h] Hue in degrees (0-360). Values wrap automatically.
97
+ # [s] Saturation as percentage (0-100).
98
+ # [l] Lightness as percentage (0-100).
99
+ #
100
+ # Returns a hex string like <tt>"#rrggbb"</tt>.
101
+ def from_hsl(h, s, l)
102
+ RatatuiRuby._color_from_hsl(h.to_f, s.to_f, l.to_f)
103
+ end
104
+
105
+ # Ruby-idiomatic aliases (TIMTOWTDI)
106
+ alias hex from_u32
107
+ alias hsl from_hsl
108
+
109
+ # Creates a color from HSLuv (Human-friendly Hue, Saturation, Lightness) values.
110
+ #
111
+ # HSLuv is a perceptually uniform color space. Unlike standard HSL,
112
+ # colors at the same lightness appear equally bright regardless of hue.
113
+ # This makes it ideal for generating color palettes with consistent
114
+ # perceived brightness.
115
+ #
116
+ # This method delegates to Ratatui's palette integration for the
117
+ # complex HSLuv to RGB conversion.
118
+ #
119
+ # Note: Ratatui uses the range [-180, 180] for hue and [0, 100] for
120
+ # saturation and lightness. This implementation matches those conventions.
121
+ #
122
+ # === Example
123
+ #
124
+ #--
125
+ # SPDX-SnippetBegin
126
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
127
+ # SPDX-License-Identifier: MIT-0
128
+ #++
129
+ # Color.from_hsluv(12.18, 100, 53.2) # => "#ff0000" (bright red)
130
+ # Color.from_hsluv(-94.13, 100, 32.3) # => "#0000ff" (bright blue)
131
+ # Color.from_hsluv(0, 0, 50) # => gray
132
+ #--
133
+ # SPDX-SnippetEnd
134
+ #++
135
+ #
136
+ # [h] Hue in degrees (-180 to 360). Values wrap automatically.
137
+ # [s] Saturation as percentage (0-100). Values are clamped.
138
+ # [l] Lightness as percentage (0-100). Values are clamped.
139
+ #
140
+ # Returns a hex string like <tt>"#rrggbb"</tt>.
141
+ def from_hsluv(h, s, l)
142
+ RatatuiRuby._color_from_hsluv(h.to_f, s.to_f, l.to_f)
143
+ end
144
+
145
+ alias hsluv from_hsluv
146
+ end
147
+ end
148
+ end
149
+ end
@@ -60,7 +60,7 @@ module RatatuiRuby
60
60
  # ==== String
61
61
  # Represents a specific RGB color using a Hex code (<tt>"#RRGGBB"</tt>).
62
62
  # Requires a terminal emulator with "True Color" (24-bit color) support.
63
- class Style < Data.define(:fg, :bg, :modifiers)
63
+ class Style < Data.define(:fg, :bg, :underline_color, :modifiers, :remove_modifiers)
64
64
  ##
65
65
  # :attr_reader: fg
66
66
  # Foreground color.
@@ -73,19 +73,36 @@ module RatatuiRuby
73
73
  #
74
74
  # Symbol (<tt>:black</tt>), Hex String (<tt>"#000000"</tt>), or Integer (0-255).
75
75
 
76
+ ##
77
+ # :attr_reader: underline_color
78
+ # Color of the underline.
79
+ #
80
+ # Symbol (<tt>:red</tt>), Hex String (<tt>"#ff0000"</tt>), or Integer (0-255).
81
+ # Distinct from foreground color. Terminals supporting this feature render
82
+ # the underline in this color while text remains in the foreground color.
83
+
76
84
  ##
77
85
  # :attr_reader: modifiers
78
- # Text effects.
86
+ # Text effects to add.
79
87
  #
80
88
  # Array of symbols: <tt>:bold</tt>, <tt>:dim</tt>, <tt>:italic</tt>, <tt>:underlined</tt>,
81
89
  # <tt>:slow_blink</tt>, <tt>:rapid_blink</tt>, <tt>:reversed</tt>, <tt>:hidden</tt>, <tt>:crossed_out</tt>.
82
90
 
91
+ ##
92
+ # :attr_reader: remove_modifiers
93
+ # Text effects to remove.
94
+ #
95
+ # Array of symbols. When this style is applied, these modifiers are removed
96
+ # from any inherited/patched styles. Corresponds to Ratatui's sub_modifier.
97
+
83
98
  # Creates a new Style.
84
99
  #
85
100
  # [fg] Color (Symbol/String/Integer).
86
101
  # [bg] Color (Symbol/String/Integer).
87
- # [modifiers] Array of Symbols.
88
- def initialize(fg: nil, bg: nil, modifiers: [])
102
+ # [underline_color] Color for underline (Symbol/String/Integer).
103
+ # [modifiers] Array of Symbols to add.
104
+ # [remove_modifiers] Array of Symbols to remove (Ratatui: sub_modifier).
105
+ def initialize(fg: nil, bg: nil, underline_color: nil, modifiers: [], remove_modifiers: [])
89
106
  super
90
107
  end
91
108
 
@@ -95,6 +112,36 @@ module RatatuiRuby
95
112
  def self.default
96
113
  new
97
114
  end
115
+
116
+ # Creates a new Style (convenience alias for {#initialize}).
117
+ #
118
+ # Constructor keyword arguments require typing out the full <tt>Style.new</tt> form.
119
+ # This gets verbose in tight layout code or one-liners.
120
+ #
121
+ # <tt>Style.with</tt> reads more naturally and enables method chaining.
122
+ # It shows intent: "use this style with these properties."
123
+ #
124
+ # Use it for inline styling where conciseness matters.
125
+ #
126
+ # === Examples
127
+ #
128
+ #--
129
+ # SPDX-SnippetBegin
130
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
131
+ # SPDX-License-Identifier: MIT-0
132
+ #++
133
+ # Style.with(fg: :red, bg: :black, modifiers: [:bold])
134
+ # Style.with(fg: :white, modifiers: [:underlined], underline_color: :red)
135
+ # Style.with(modifiers: [:bold], remove_modifiers: [:italic]) # Add bold, remove italic
136
+ # paragraph = Paragraph.new(text: "Alert!", style: Style.with(fg: :red))
137
+ #--
138
+ # SPDX-SnippetEnd
139
+ #++
140
+ #
141
+ # @return [Style]
142
+ def self.with(fg: nil, bg: nil, underline_color: nil, modifiers: [], remove_modifiers: [])
143
+ new(fg:, bg:, underline_color:, modifiers:, remove_modifiers:)
144
+ end
98
145
  end
99
146
  end
100
147
  end
@@ -10,8 +10,10 @@ module RatatuiRuby
10
10
  #
11
11
  # This module mirrors +ratatui::style+ and contains:
12
12
  # - {Style} — Colors and modifiers
13
+ # - {Color} — Color constructors
13
14
  module Style
14
15
  end
15
16
  end
16
17
 
17
18
  require_relative "style/style"
19
+ require_relative "style/color"