ratatui_ruby 1.4.0-x86_64-linux

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 (292) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +15 -0
  3. data/LICENSES/AGPL-3.0-or-later.txt +661 -0
  4. data/LICENSES/CC-BY-SA-4.0.txt +427 -0
  5. data/LICENSES/CC0-1.0.txt +121 -0
  6. data/LICENSES/LGPL-3.0-or-later.txt +304 -0
  7. data/LICENSES/MIT-0.txt +16 -0
  8. data/LICENSES/MIT.txt +21 -0
  9. data/REUSE.toml +42 -0
  10. data/exe/.gitkeep +0 -0
  11. data/ext/ratatui_ruby/.cargo/config.toml +13 -0
  12. data/ext/ratatui_ruby/.gitignore +4 -0
  13. data/ext/ratatui_ruby/Cargo.lock +1737 -0
  14. data/ext/ratatui_ruby/Cargo.toml +24 -0
  15. data/ext/ratatui_ruby/clippy.toml +7 -0
  16. data/ext/ratatui_ruby/extconf.rb +21 -0
  17. data/ext/ratatui_ruby/src/color.rs +82 -0
  18. data/ext/ratatui_ruby/src/errors.rs +28 -0
  19. data/ext/ratatui_ruby/src/events.rs +700 -0
  20. data/ext/ratatui_ruby/src/frame.rs +241 -0
  21. data/ext/ratatui_ruby/src/lib.rs +343 -0
  22. data/ext/ratatui_ruby/src/lib_header.rs +11 -0
  23. data/ext/ratatui_ruby/src/rendering.rs +158 -0
  24. data/ext/ratatui_ruby/src/string_width.rs +101 -0
  25. data/ext/ratatui_ruby/src/style.rs +469 -0
  26. data/ext/ratatui_ruby/src/terminal/capabilities.rs +46 -0
  27. data/ext/ratatui_ruby/src/terminal/init.rs +233 -0
  28. data/ext/ratatui_ruby/src/terminal/mod.rs +42 -0
  29. data/ext/ratatui_ruby/src/terminal/mutations.rs +158 -0
  30. data/ext/ratatui_ruby/src/terminal/queries.rs +231 -0
  31. data/ext/ratatui_ruby/src/terminal/query.rs +400 -0
  32. data/ext/ratatui_ruby/src/terminal/storage.rs +109 -0
  33. data/ext/ratatui_ruby/src/terminal/wrapper.rs +16 -0
  34. data/ext/ratatui_ruby/src/text.rs +225 -0
  35. data/ext/ratatui_ruby/src/widgets/barchart.rs +169 -0
  36. data/ext/ratatui_ruby/src/widgets/block.rs +41 -0
  37. data/ext/ratatui_ruby/src/widgets/calendar.rs +84 -0
  38. data/ext/ratatui_ruby/src/widgets/canvas.rs +183 -0
  39. data/ext/ratatui_ruby/src/widgets/center.rs +79 -0
  40. data/ext/ratatui_ruby/src/widgets/chart.rs +222 -0
  41. data/ext/ratatui_ruby/src/widgets/clear.rs +39 -0
  42. data/ext/ratatui_ruby/src/widgets/cursor.rs +32 -0
  43. data/ext/ratatui_ruby/src/widgets/gauge.rs +65 -0
  44. data/ext/ratatui_ruby/src/widgets/layout.rs +379 -0
  45. data/ext/ratatui_ruby/src/widgets/line_gauge.rs +100 -0
  46. data/ext/ratatui_ruby/src/widgets/list.rs +378 -0
  47. data/ext/ratatui_ruby/src/widgets/list_state.rs +173 -0
  48. data/ext/ratatui_ruby/src/widgets/mod.rs +26 -0
  49. data/ext/ratatui_ruby/src/widgets/overlay.rs +24 -0
  50. data/ext/ratatui_ruby/src/widgets/paragraph.rs +87 -0
  51. data/ext/ratatui_ruby/src/widgets/ratatui_logo.rs +40 -0
  52. data/ext/ratatui_ruby/src/widgets/ratatui_mascot.rs +55 -0
  53. data/ext/ratatui_ruby/src/widgets/scrollbar.rs +214 -0
  54. data/ext/ratatui_ruby/src/widgets/scrollbar_state.rs +169 -0
  55. data/ext/ratatui_ruby/src/widgets/sparkline.rs +127 -0
  56. data/ext/ratatui_ruby/src/widgets/table.rs +415 -0
  57. data/ext/ratatui_ruby/src/widgets/table_state.rs +203 -0
  58. data/ext/ratatui_ruby/src/widgets/tabs.rs +194 -0
  59. data/lib/ratatui_ruby/backend/window_size.rb +50 -0
  60. data/lib/ratatui_ruby/backend.rb +59 -0
  61. data/lib/ratatui_ruby/buffer/cell.rb +212 -0
  62. data/lib/ratatui_ruby/buffer.rb +149 -0
  63. data/lib/ratatui_ruby/cell.rb +208 -0
  64. data/lib/ratatui_ruby/debug.rb +215 -0
  65. data/lib/ratatui_ruby/draw.rb +63 -0
  66. data/lib/ratatui_ruby/event/focus_gained.rb +125 -0
  67. data/lib/ratatui_ruby/event/focus_lost.rb +127 -0
  68. data/lib/ratatui_ruby/event/key/character.rb +53 -0
  69. data/lib/ratatui_ruby/event/key/dwim.rb +301 -0
  70. data/lib/ratatui_ruby/event/key/media.rb +46 -0
  71. data/lib/ratatui_ruby/event/key/modifier.rb +107 -0
  72. data/lib/ratatui_ruby/event/key/navigation.rb +72 -0
  73. data/lib/ratatui_ruby/event/key/system.rb +47 -0
  74. data/lib/ratatui_ruby/event/key.rb +479 -0
  75. data/lib/ratatui_ruby/event/mouse.rb +291 -0
  76. data/lib/ratatui_ruby/event/none.rb +53 -0
  77. data/lib/ratatui_ruby/event/paste.rb +130 -0
  78. data/lib/ratatui_ruby/event/resize.rb +221 -0
  79. data/lib/ratatui_ruby/event/sync.rb +52 -0
  80. data/lib/ratatui_ruby/event.rb +163 -0
  81. data/lib/ratatui_ruby/frame.rb +257 -0
  82. data/lib/ratatui_ruby/labs/a11y.rb +182 -0
  83. data/lib/ratatui_ruby/labs/frame_a11y_capture.rb +50 -0
  84. data/lib/ratatui_ruby/labs.rb +47 -0
  85. data/lib/ratatui_ruby/layout/alignment.rb +91 -0
  86. data/lib/ratatui_ruby/layout/constraint.rb +337 -0
  87. data/lib/ratatui_ruby/layout/layout.rb +258 -0
  88. data/lib/ratatui_ruby/layout/position.rb +81 -0
  89. data/lib/ratatui_ruby/layout/rect.rb +733 -0
  90. data/lib/ratatui_ruby/layout/size.rb +62 -0
  91. data/lib/ratatui_ruby/layout.rb +29 -0
  92. data/lib/ratatui_ruby/list_state.rb +201 -0
  93. data/lib/ratatui_ruby/output_guard.rb +171 -0
  94. data/lib/ratatui_ruby/ratatui_ruby.so +0 -0
  95. data/lib/ratatui_ruby/scrollbar_state.rb +122 -0
  96. data/lib/ratatui_ruby/style/color.rb +149 -0
  97. data/lib/ratatui_ruby/style/style.rb +147 -0
  98. data/lib/ratatui_ruby/style.rb +19 -0
  99. data/lib/ratatui_ruby/symbols.rb +435 -0
  100. data/lib/ratatui_ruby/synthetic_events.rb +106 -0
  101. data/lib/ratatui_ruby/table_state.rb +251 -0
  102. data/lib/ratatui_ruby/terminal/capabilities.rb +316 -0
  103. data/lib/ratatui_ruby/terminal/viewport.rb +80 -0
  104. data/lib/ratatui_ruby/terminal.rb +66 -0
  105. data/lib/ratatui_ruby/terminal_lifecycle.rb +303 -0
  106. data/lib/ratatui_ruby/terminal_lifecycle.rb.bak +197 -0
  107. data/lib/ratatui_ruby/test_helper/event_injection.rb +241 -0
  108. data/lib/ratatui_ruby/test_helper/global_state.rb +111 -0
  109. data/lib/ratatui_ruby/test_helper/snapshot.rb +568 -0
  110. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.ansi +24 -0
  111. data/lib/ratatui_ruby/test_helper/snapshots/axis_labels_alignment.txt +24 -0
  112. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.ansi +5 -0
  113. data/lib/ratatui_ruby/test_helper/snapshots/barchart_styled_label.txt +5 -0
  114. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.ansi +24 -0
  115. data/lib/ratatui_ruby/test_helper/snapshots/chart_rendering.txt +24 -0
  116. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.ansi +12 -0
  117. data/lib/ratatui_ruby/test_helper/snapshots/half_block_marker.txt +12 -0
  118. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.ansi +12 -0
  119. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_bottom.txt +12 -0
  120. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.ansi +12 -0
  121. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_left.txt +12 -0
  122. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.ansi +12 -0
  123. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_right.txt +12 -0
  124. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.ansi +12 -0
  125. data/lib/ratatui_ruby/test_helper/snapshots/legend_position_top.txt +12 -0
  126. data/lib/ratatui_ruby/test_helper/snapshots/my_snapshot.txt +1 -0
  127. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.ansi +10 -0
  128. data/lib/ratatui_ruby/test_helper/snapshots/styled_axis_title.txt +10 -0
  129. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.ansi +10 -0
  130. data/lib/ratatui_ruby/test_helper/snapshots/styled_dataset_name.txt +10 -0
  131. data/lib/ratatui_ruby/test_helper/style_assertions.rb +449 -0
  132. data/lib/ratatui_ruby/test_helper/subprocess_timeout.rb +35 -0
  133. data/lib/ratatui_ruby/test_helper/terminal.rb +187 -0
  134. data/lib/ratatui_ruby/test_helper/test_doubles.rb +86 -0
  135. data/lib/ratatui_ruby/test_helper.rb +115 -0
  136. data/lib/ratatui_ruby/text/line.rb +245 -0
  137. data/lib/ratatui_ruby/text/span.rb +158 -0
  138. data/lib/ratatui_ruby/text.rb +99 -0
  139. data/lib/ratatui_ruby/tui/buffer_factories.rb +22 -0
  140. data/lib/ratatui_ruby/tui/canvas_factories.rb +149 -0
  141. data/lib/ratatui_ruby/tui/core.rb +67 -0
  142. data/lib/ratatui_ruby/tui/layout_factories.rb +153 -0
  143. data/lib/ratatui_ruby/tui/state_factories.rb +77 -0
  144. data/lib/ratatui_ruby/tui/style_factories.rb +22 -0
  145. data/lib/ratatui_ruby/tui/text_factories.rb +86 -0
  146. data/lib/ratatui_ruby/tui/widget_factories.rb +272 -0
  147. data/lib/ratatui_ruby/tui.rb +106 -0
  148. data/lib/ratatui_ruby/version.rb +12 -0
  149. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +51 -0
  150. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +29 -0
  151. data/lib/ratatui_ruby/widgets/bar_chart.rb +308 -0
  152. data/lib/ratatui_ruby/widgets/block.rb +266 -0
  153. data/lib/ratatui_ruby/widgets/calendar.rb +88 -0
  154. data/lib/ratatui_ruby/widgets/canvas.rb +297 -0
  155. data/lib/ratatui_ruby/widgets/cell.rb +59 -0
  156. data/lib/ratatui_ruby/widgets/center.rb +71 -0
  157. data/lib/ratatui_ruby/widgets/chart.rb +172 -0
  158. data/lib/ratatui_ruby/widgets/clear.rb +66 -0
  159. data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
  160. data/lib/ratatui_ruby/widgets/cursor.rb +54 -0
  161. data/lib/ratatui_ruby/widgets/gauge.rb +146 -0
  162. data/lib/ratatui_ruby/widgets/line_gauge.rb +158 -0
  163. data/lib/ratatui_ruby/widgets/list.rb +252 -0
  164. data/lib/ratatui_ruby/widgets/list_item.rb +55 -0
  165. data/lib/ratatui_ruby/widgets/overlay.rb +55 -0
  166. data/lib/ratatui_ruby/widgets/paragraph.rb +113 -0
  167. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +35 -0
  168. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +40 -0
  169. data/lib/ratatui_ruby/widgets/row.rb +123 -0
  170. data/lib/ratatui_ruby/widgets/scrollbar.rb +147 -0
  171. data/lib/ratatui_ruby/widgets/shape/label.rb +80 -0
  172. data/lib/ratatui_ruby/widgets/sparkline.rb +153 -0
  173. data/lib/ratatui_ruby/widgets/table.rb +213 -0
  174. data/lib/ratatui_ruby/widgets/tabs.rb +91 -0
  175. data/lib/ratatui_ruby/widgets.rb +43 -0
  176. data/lib/ratatui_ruby.rb +555 -0
  177. data/sig/examples/app_all_events/app.rbs +11 -0
  178. data/sig/examples/app_all_events/model/app_model.rbs +23 -0
  179. data/sig/examples/app_all_events/model/event_entry.rbs +23 -0
  180. data/sig/examples/app_all_events/model/timestamp.rbs +11 -0
  181. data/sig/examples/app_all_events/view/app_view.rbs +8 -0
  182. data/sig/examples/app_all_events/view/controls_view.rbs +6 -0
  183. data/sig/examples/app_all_events/view/counts_view.rbs +6 -0
  184. data/sig/examples/app_all_events/view/live_view.rbs +6 -0
  185. data/sig/examples/app_all_events/view/log_view.rbs +6 -0
  186. data/sig/examples/app_all_events/view.rbs +14 -0
  187. data/sig/examples/app_cli_rich_moments/app.rbs +12 -0
  188. data/sig/examples/app_color_picker/app.rbs +17 -0
  189. data/sig/examples/app_external_editor/app.rbs +12 -0
  190. data/sig/examples/app_login_form/app.rbs +11 -0
  191. data/sig/examples/app_stateful_interaction/app.rbs +39 -0
  192. data/sig/examples/verify_quickstart_dsl/app.rbs +17 -0
  193. data/sig/examples/verify_quickstart_lifecycle/app.rbs +17 -0
  194. data/sig/examples/verify_readme_usage/app.rbs +17 -0
  195. data/sig/examples/widget_block_demo/app.rbs +38 -0
  196. data/sig/examples/widget_box_demo/app.rbs +17 -0
  197. data/sig/examples/widget_calendar_demo/app.rbs +17 -0
  198. data/sig/examples/widget_cell_demo/app.rbs +17 -0
  199. data/sig/examples/widget_chart_demo/app.rbs +17 -0
  200. data/sig/examples/widget_gauge_demo/app.rbs +17 -0
  201. data/sig/examples/widget_layout_split/app.rbs +16 -0
  202. data/sig/examples/widget_line_gauge_demo/app.rbs +17 -0
  203. data/sig/examples/widget_list_demo/app.rbs +17 -0
  204. data/sig/examples/widget_map_demo/app.rbs +17 -0
  205. data/sig/examples/widget_popup_demo/app.rbs +17 -0
  206. data/sig/examples/widget_ratatui_logo_demo/app.rbs +17 -0
  207. data/sig/examples/widget_ratatui_mascot_demo/app.rbs +17 -0
  208. data/sig/examples/widget_rect/app.rbs +18 -0
  209. data/sig/examples/widget_render/app.rbs +16 -0
  210. data/sig/examples/widget_rich_text/app.rbs +17 -0
  211. data/sig/examples/widget_scroll_text/app.rbs +17 -0
  212. data/sig/examples/widget_scrollbar_demo/app.rbs +17 -0
  213. data/sig/examples/widget_sparkline_demo/app.rbs +16 -0
  214. data/sig/examples/widget_style_colors/app.rbs +20 -0
  215. data/sig/examples/widget_table_demo/app.rbs +17 -0
  216. data/sig/examples/widget_text_width/app.rbs +16 -0
  217. data/sig/generated/event_key_predicates.rbs +1348 -0
  218. data/sig/manifest.yaml +5 -0
  219. data/sig/patches/data.rbs +26 -0
  220. data/sig/patches/debugger__.rbs +8 -0
  221. data/sig/ratatui_ruby/backend/window_size.rbs +17 -0
  222. data/sig/ratatui_ruby/backend.rbs +12 -0
  223. data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
  224. data/sig/ratatui_ruby/buffer.rbs +18 -0
  225. data/sig/ratatui_ruby/cell.rbs +44 -0
  226. data/sig/ratatui_ruby/clear.rbs +18 -0
  227. data/sig/ratatui_ruby/constraint.rbs +26 -0
  228. data/sig/ratatui_ruby/debug.rbs +45 -0
  229. data/sig/ratatui_ruby/draw.rbs +30 -0
  230. data/sig/ratatui_ruby/event.rbs +249 -0
  231. data/sig/ratatui_ruby/frame.rbs +23 -0
  232. data/sig/ratatui_ruby/interfaces.rbs +25 -0
  233. data/sig/ratatui_ruby/labs.rbs +90 -0
  234. data/sig/ratatui_ruby/layout/alignment.rbs +26 -0
  235. data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
  236. data/sig/ratatui_ruby/layout/layout.rbs +45 -0
  237. data/sig/ratatui_ruby/layout/position.rbs +18 -0
  238. data/sig/ratatui_ruby/layout/rect.rbs +64 -0
  239. data/sig/ratatui_ruby/layout/size.rbs +18 -0
  240. data/sig/ratatui_ruby/list_state.rbs +23 -0
  241. data/sig/ratatui_ruby/output_guard.rbs +23 -0
  242. data/sig/ratatui_ruby/ratatui_ruby.rbs +113 -0
  243. data/sig/ratatui_ruby/rect.rbs +17 -0
  244. data/sig/ratatui_ruby/scrollbar_state.rbs +24 -0
  245. data/sig/ratatui_ruby/session.rbs +93 -0
  246. data/sig/ratatui_ruby/style/color.rbs +22 -0
  247. data/sig/ratatui_ruby/style/style.rbs +29 -0
  248. data/sig/ratatui_ruby/symbols.rbs +141 -0
  249. data/sig/ratatui_ruby/synthetic_events.rbs +24 -0
  250. data/sig/ratatui_ruby/table_state.rbs +27 -0
  251. data/sig/ratatui_ruby/terminal/capabilities.rbs +38 -0
  252. data/sig/ratatui_ruby/terminal/viewport.rbs +33 -0
  253. data/sig/ratatui_ruby/terminal_lifecycle.rbs +39 -0
  254. data/sig/ratatui_ruby/test_helper/event_injection.rbs +22 -0
  255. data/sig/ratatui_ruby/test_helper/snapshot.rbs +37 -0
  256. data/sig/ratatui_ruby/test_helper/style_assertions.rbs +77 -0
  257. data/sig/ratatui_ruby/test_helper/terminal.rbs +20 -0
  258. data/sig/ratatui_ruby/test_helper/test_doubles.rbs +32 -0
  259. data/sig/ratatui_ruby/test_helper.rbs +18 -0
  260. data/sig/ratatui_ruby/text/line.rbs +27 -0
  261. data/sig/ratatui_ruby/text/span.rbs +23 -0
  262. data/sig/ratatui_ruby/text.rbs +12 -0
  263. data/sig/ratatui_ruby/tui/buffer_factories.rbs +16 -0
  264. data/sig/ratatui_ruby/tui/canvas_factories.rbs +38 -0
  265. data/sig/ratatui_ruby/tui/core.rbs +23 -0
  266. data/sig/ratatui_ruby/tui/layout_factories.rbs +39 -0
  267. data/sig/ratatui_ruby/tui/state_factories.rbs +23 -0
  268. data/sig/ratatui_ruby/tui/style_factories.rbs +18 -0
  269. data/sig/ratatui_ruby/tui/text_factories.rbs +23 -0
  270. data/sig/ratatui_ruby/tui/widget_factories.rbs +138 -0
  271. data/sig/ratatui_ruby/tui.rbs +25 -0
  272. data/sig/ratatui_ruby/version.rbs +12 -0
  273. data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
  274. data/sig/ratatui_ruby/widgets/block.rbs +51 -0
  275. data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
  276. data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
  277. data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
  278. data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
  279. data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
  280. data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
  281. data/sig/ratatui_ruby/widgets/list.rbs +63 -0
  282. data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
  283. data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
  284. data/sig/ratatui_ruby/widgets/row.rbs +43 -0
  285. data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
  286. data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
  287. data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
  288. data/sig/ratatui_ruby/widgets/table.rbs +78 -0
  289. data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
  290. data/sig/ratatui_ruby/widgets.rbs +16 -0
  291. data/vendor/goodcop/base.yml +1047 -0
  292. metadata +729 -0
@@ -0,0 +1,733 @@
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
+ # Defines a rectangular area in the terminal grid.
11
+ #
12
+ # Geometry management involves passing groups of four integers (`x, y, width, height`) repeatedly.
13
+ # This is verbose and prone to parameter mismatch errors.
14
+ #
15
+ # This class encapsulates the geometry. It provides a standard primitive for passing area definitions
16
+ # between layout engines and rendering functions.
17
+ #
18
+ # Use it when manual positioning is required or when querying layout results.
19
+ #
20
+ # === Examples
21
+ #
22
+ #--
23
+ # SPDX-SnippetBegin
24
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
25
+ # SPDX-License-Identifier: MIT-0
26
+ #++
27
+ # area = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
28
+ # puts area.width # => 80
29
+ #--
30
+ # SPDX-SnippetEnd
31
+ #++
32
+ class Rect < Data.define(:x, :y, :width, :height)
33
+ ##
34
+ # :attr_reader: x
35
+ # X coordinate (column) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
36
+
37
+ ##
38
+ # :attr_reader: y
39
+ # Y coordinate (row) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
40
+
41
+ ##
42
+ # :attr_reader: width
43
+ # Width in characters (Integer, coerced via +to_int+ or +to_i+).
44
+
45
+ ##
46
+ # :attr_reader: height
47
+ # Height in characters (Integer, coerced via +to_int+ or +to_i+).
48
+
49
+ # Creates a new Rect.
50
+ #
51
+ # All parameters accept any object responding to +to_int+ or +to_i+ (duck-typed).
52
+ #
53
+ # [x] Column index (Numeric).
54
+ # [y] Row index (Numeric).
55
+ # [width] Width in columns (Numeric).
56
+ # [height] Height in rows (Numeric).
57
+ def initialize(x: 0, y: 0, width: 0, height: 0)
58
+ super(
59
+ x: Integer(x),
60
+ y: Integer(y),
61
+ width: Integer(width),
62
+ height: Integer(height)
63
+ )
64
+ end
65
+
66
+ # Tests whether a point is inside this rectangle.
67
+ #
68
+ # Essential for hit testing mouse clicks against layout regions.
69
+ #
70
+ #--
71
+ # SPDX-SnippetBegin
72
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
73
+ # SPDX-License-Identifier: MIT-0
74
+ #++
75
+ # area = Layout::Rect.new(x: 10, y: 5, width: 20, height: 10)
76
+ # area.contains?(15, 8) # => true
77
+ # area.contains?(5, 8) # => false
78
+ #
79
+ #--
80
+ # SPDX-SnippetEnd
81
+ #++
82
+ # [px]
83
+ # X coordinate to test (column).
84
+ # [py]
85
+ #--
86
+ # SPDX-SnippetBegin
87
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
88
+ # SPDX-License-Identifier: MIT-0
89
+ #++
90
+ # Y coordinate to test (row).
91
+ #
92
+ #--
93
+ # SPDX-SnippetEnd
94
+ #++
95
+ # Returns true if the point (px, py) is within the rectangle bounds.
96
+ def contains?(px, py)
97
+ px >= x && px < x + width && py >= y && py < y + height
98
+ end
99
+
100
+ # Tests whether this rectangle overlaps with another.
101
+ #
102
+ # Essential for determining if a widget is visible within a viewport or clipping area.
103
+ #
104
+ #--
105
+ # SPDX-SnippetBegin
106
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
107
+ # SPDX-License-Identifier: MIT-0
108
+ #++
109
+ # viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
110
+ # widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
111
+ # viewport.intersects?(widget) # => true (partial overlap)
112
+ #
113
+ #--
114
+ # SPDX-SnippetEnd
115
+ #++
116
+ # [other]
117
+ #--
118
+ # SPDX-SnippetBegin
119
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
120
+ # SPDX-License-Identifier: MIT-0
121
+ #++
122
+ # Another Rect to test against.
123
+ #
124
+ #--
125
+ # SPDX-SnippetEnd
126
+ #++
127
+ # Returns true if the rectangles overlap.
128
+ def intersects?(other)
129
+ x < other.x + other.width &&
130
+ x + width > other.x &&
131
+ y < other.y + other.height &&
132
+ y + height > other.y
133
+ end
134
+
135
+ # Returns the overlapping area between this rectangle and another.
136
+ #
137
+ # Essential for calculating visible portions of widgets inside scroll views.
138
+ #
139
+ #--
140
+ # SPDX-SnippetBegin
141
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
142
+ # SPDX-License-Identifier: MIT-0
143
+ #++
144
+ # viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
145
+ # widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
146
+ # visible = viewport.intersection(widget)
147
+ # # => Rect(x: 70, y: 20, width: 10, height: 4)
148
+ #
149
+ #--
150
+ # SPDX-SnippetEnd
151
+ #++
152
+ # [other]
153
+ #--
154
+ # SPDX-SnippetBegin
155
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
156
+ # SPDX-License-Identifier: MIT-0
157
+ #++
158
+ # Another Rect to intersect with.
159
+ #
160
+ #--
161
+ # SPDX-SnippetEnd
162
+ #++
163
+ # Returns a new Rect representing the intersection, or +nil+ if no overlap.
164
+ def intersection(other)
165
+ return nil unless intersects?(other)
166
+
167
+ new_x = [x, other.x].max
168
+ new_y = [y, other.y].max
169
+ new_right = [x + width, other.x + other.width].min
170
+ new_bottom = [y + height, other.y + other.height].min
171
+
172
+ Rect.new(x: new_x, y: new_y, width: new_right - new_x, height: new_bottom - new_y)
173
+ end
174
+
175
+ # Left edge coordinate.
176
+ #
177
+ # Layout algorithms compute bounding boxes and check overlaps.
178
+ # Reading <tt>rect.x</tt> forces you to remember that x means "left."
179
+ #
180
+ # Call <tt>left</tt> instead. Your code reads like prose.
181
+ #
182
+ # === Example
183
+ #
184
+ #--
185
+ # SPDX-SnippetBegin
186
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
187
+ # SPDX-License-Identifier: MIT-0
188
+ #++
189
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
190
+ # rect.left # => 10
191
+ #--
192
+ # SPDX-SnippetEnd
193
+ #++
194
+ def left
195
+ x
196
+ end
197
+
198
+ # Right edge coordinate.
199
+ #
200
+ # Bounds checks compare edges. Writing <tt>x + width</tt> inline clutters conditions.
201
+ # Errors creep in when you forget the addition.
202
+ #
203
+ # This method computes and names the boundary. Returns the first column outside the rect.
204
+ #
205
+ # === Example
206
+ #
207
+ #--
208
+ # SPDX-SnippetBegin
209
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
210
+ # SPDX-License-Identifier: MIT-0
211
+ #++
212
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
213
+ # rect.right # => 90
214
+ #--
215
+ # SPDX-SnippetEnd
216
+ #++
217
+ def right
218
+ x + width
219
+ end
220
+
221
+ # Top edge coordinate.
222
+ #
223
+ # Layout algorithms compute bounding boxes and check overlaps.
224
+ # Reading <tt>rect.y</tt> forces you to remember that y means "top."
225
+ #
226
+ # Call <tt>top</tt> instead. Your code reads like prose.
227
+ #
228
+ # === Example
229
+ #
230
+ #--
231
+ # SPDX-SnippetBegin
232
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
233
+ # SPDX-License-Identifier: MIT-0
234
+ #++
235
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
236
+ # rect.top # => 5
237
+ #--
238
+ # SPDX-SnippetEnd
239
+ #++
240
+ def top
241
+ y
242
+ end
243
+
244
+ # Bottom edge coordinate.
245
+ #
246
+ # Bounds checks compare edges. Writing <tt>y + height</tt> inline clutters conditions.
247
+ # Errors creep in when you forget the addition.
248
+ #
249
+ # This method computes and names the boundary. Returns the first row outside the rect.
250
+ #
251
+ # === Example
252
+ #
253
+ #--
254
+ # SPDX-SnippetBegin
255
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
256
+ # SPDX-License-Identifier: MIT-0
257
+ #++
258
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
259
+ # rect.bottom # => 29
260
+ #--
261
+ # SPDX-SnippetEnd
262
+ #++
263
+ def bottom
264
+ y + height
265
+ end
266
+
267
+ # Total area in cells.
268
+ #
269
+ # Size comparisons and allocation calculations need area.
270
+ # Computing <tt>width * height</tt> inline is noisy and error-prone.
271
+ #
272
+ # This method does the multiplication once.
273
+ #
274
+ # === Example
275
+ #
276
+ #--
277
+ # SPDX-SnippetBegin
278
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
279
+ # SPDX-License-Identifier: MIT-0
280
+ #++
281
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 10, height: 5)
282
+ # rect.area # => 50
283
+ #--
284
+ # SPDX-SnippetEnd
285
+ #++
286
+ def area
287
+ width * height
288
+ end
289
+
290
+ # True when the rect has zero area.
291
+ #
292
+ # Zero-width or zero-height rects break layout math.
293
+ # Checking <tt>width == 0 || height == 0</tt> inline is tedious and easy to forget.
294
+ #
295
+ # Guard clauses call <tt>empty?</tt> to skip degenerate rects.
296
+ #
297
+ # === Example
298
+ #
299
+ #--
300
+ # SPDX-SnippetBegin
301
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
302
+ # SPDX-License-Identifier: MIT-0
303
+ #++
304
+ # Layout::Rect.new(width: 0, height: 10).empty? # => true
305
+ # Layout::Rect.new(width: 10, height: 5).empty? # => false
306
+ #--
307
+ # SPDX-SnippetEnd
308
+ #++
309
+ def empty?
310
+ width.zero? || height.zero?
311
+ end
312
+
313
+ # Bounding box containing both rectangles.
314
+ #
315
+ # Damage tracking and hit testing combine rects.
316
+ # Computing min/max of all four edges inline is tedious and error-prone.
317
+ #
318
+ # This method returns the smallest rect that encloses both.
319
+ #
320
+ # [other] Rect to merge.
321
+ #
322
+ # === Example
323
+ #
324
+ #--
325
+ # SPDX-SnippetBegin
326
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
327
+ # SPDX-License-Identifier: MIT-0
328
+ #++
329
+ # r1 = Layout::Rect.new(x: 0, y: 0, width: 10, height: 10)
330
+ # r2 = Layout::Rect.new(x: 5, y: 5, width: 10, height: 10)
331
+ # r1.union(r2) # => Rect(x: 0, y: 0, width: 15, height: 15)
332
+ #--
333
+ # SPDX-SnippetEnd
334
+ #++
335
+ def union(other)
336
+ new_x = [left, other.left].min
337
+ new_y = [top, other.top].min
338
+ new_right = [right, other.right].max
339
+ new_bottom = [bottom, other.bottom].max
340
+
341
+ Rect.new(
342
+ x: new_x,
343
+ y: new_y,
344
+ width: new_right - new_x,
345
+ height: new_bottom - new_y
346
+ )
347
+ end
348
+
349
+ # Shrinks the rect by a uniform margin on all sides.
350
+ #
351
+ # Widgets render text inside borders. Subtracting margin from all four edges inline is verbose.
352
+ # Off-by-one errors happen when you forget to double the margin.
353
+ #
354
+ # This method computes the content area. Returns a zero-area rect if margin exceeds dimensions.
355
+ #
356
+ # [margin] Integer padding on all sides.
357
+ #
358
+ # === Example
359
+ #
360
+ #--
361
+ # SPDX-SnippetBegin
362
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
363
+ # SPDX-License-Identifier: MIT-0
364
+ #++
365
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 20, height: 10)
366
+ # rect.inner(2) # => Rect(x: 2, y: 2, width: 16, height: 6)
367
+ #--
368
+ # SPDX-SnippetEnd
369
+ #++
370
+ def inner(margin)
371
+ doubled = margin * 2
372
+ return Rect.new(x: 0, y: 0, width: 0, height: 0) if width < doubled || height < doubled
373
+
374
+ Rect.new(
375
+ x: x + margin,
376
+ y: y + margin,
377
+ width: width - doubled,
378
+ height: height - doubled
379
+ )
380
+ end
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
+
413
+ # Moves the rect without changing size.
414
+ #
415
+ # Animations and drag-and-drop shift widgets.
416
+ # Adding offsets to x and y inline clutters the code.
417
+ #
418
+ # This method returns a translated copy.
419
+ #
420
+ # [dx] Horizontal shift (positive moves right).
421
+ # [dy] Vertical shift (positive moves down).
422
+ #
423
+ # === Example
424
+ #
425
+ #--
426
+ # SPDX-SnippetBegin
427
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
428
+ # SPDX-License-Identifier: MIT-0
429
+ #++
430
+ # rect = Layout::Rect.new(x: 10, y: 5, width: 20, height: 10)
431
+ # rect.offset(5, 3) # => Rect(x: 15, y: 8, width: 20, height: 10)
432
+ #--
433
+ # SPDX-SnippetEnd
434
+ #++
435
+ def offset(dx, dy)
436
+ Rect.new(x: x + dx, y: y + dy, width:, height:)
437
+ end
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
+
465
+ # Constrains the rect to fit inside bounds.
466
+ #
467
+ # Popups and tooltips may extend beyond screen edges.
468
+ # Manually clamping x, y, width, and height is verbose and error-prone.
469
+ #
470
+ # This method repositions and shrinks the rect to stay within bounds.
471
+ #
472
+ # [other] Bounding Rect.
473
+ #
474
+ # === Example
475
+ #
476
+ #--
477
+ # SPDX-SnippetBegin
478
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
479
+ # SPDX-License-Identifier: MIT-0
480
+ #++
481
+ # screen = Layout::Rect.new(x: 0, y: 0, width: 100, height: 100)
482
+ # popup = Layout::Rect.new(x: 80, y: 80, width: 30, height: 30)
483
+ # popup.clamp(screen) # => Rect(x: 70, y: 70, width: 30, height: 30)
484
+ #--
485
+ # SPDX-SnippetEnd
486
+ #++
487
+ def clamp(other)
488
+ clamped_width = [width, other.width].min
489
+ clamped_height = [height, other.height].min
490
+ clamped_x = x.clamp(other.left, other.right - clamped_width)
491
+ clamped_y = y.clamp(other.top, other.bottom - clamped_height)
492
+
493
+ Rect.new(x: clamped_x, y: clamped_y, width: clamped_width, height: clamped_height)
494
+ end
495
+
496
+ # Iterates over horizontal slices.
497
+ #
498
+ # Lists render line by line. Looping <tt>height.times</tt> and constructing rects inline is noisy.
499
+ #
500
+ # This method yields each row as a Rect with height 1. Returns an Enumerator if no block given.
501
+ #
502
+ # === Example
503
+ #
504
+ #--
505
+ # SPDX-SnippetBegin
506
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
507
+ # SPDX-License-Identifier: MIT-0
508
+ #++
509
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 5, height: 3)
510
+ # rect.rows.map { |r| r.y } # => [0, 1, 2]
511
+ #--
512
+ # SPDX-SnippetEnd
513
+ #++
514
+ def rows
515
+ return to_enum(:rows) unless block_given?
516
+
517
+ height.times do |i|
518
+ yield Rect.new(x:, y: y + i, width:, height: 1)
519
+ end
520
+ end
521
+
522
+ # Iterates over vertical slices.
523
+ #
524
+ # Grids render column by column. Looping <tt>width.times</tt> and constructing rects inline is noisy.
525
+ #
526
+ # This method yields each column as a Rect with width 1. Returns an Enumerator if no block given.
527
+ #
528
+ # === Example
529
+ #
530
+ #--
531
+ # SPDX-SnippetBegin
532
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
533
+ # SPDX-License-Identifier: MIT-0
534
+ #++
535
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 5, height: 3)
536
+ # rect.columns.map { |c| c.x } # => [0, 1, 2, 3, 4]
537
+ #--
538
+ # SPDX-SnippetEnd
539
+ #++
540
+ def columns
541
+ return to_enum(:columns) unless block_given?
542
+
543
+ width.times do |i|
544
+ yield Rect.new(x: x + i, y:, width: 1, height:)
545
+ end
546
+ end
547
+
548
+ # Iterates over every cell in row-major order.
549
+ #
550
+ # Hit testing and pixel rendering touch every position.
551
+ # Nested loops with manual coordinate math are verbose.
552
+ #
553
+ # This method yields <tt>[x, y]</tt> pairs. Returns an Enumerator if no block given.
554
+ #
555
+ # === Example
556
+ #
557
+ #--
558
+ # SPDX-SnippetBegin
559
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
560
+ # SPDX-License-Identifier: MIT-0
561
+ #++
562
+ # rect = Layout::Rect.new(x: 0, y: 0, width: 2, height: 2)
563
+ # rect.positions.to_a # => [[0, 0], [1, 0], [0, 1], [1, 1]]
564
+ #--
565
+ # SPDX-SnippetEnd
566
+ #++
567
+ def positions
568
+ return to_enum(:positions) unless block_given?
569
+
570
+ height.times do |row|
571
+ width.times do |col|
572
+ yield [x + col, y + row]
573
+ end
574
+ end
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
+ # Enables array destructuring of the rectangle.
705
+ #
706
+ # Inline viewports and layout code often need position and size together.
707
+ # Accessing x, y, width, height individually is verbose.
708
+ #
709
+ # This method allows convenient array destructuring.
710
+ #
711
+ # === Example
712
+ #
713
+ #--
714
+ # SPDX-SnippetBegin
715
+ # SPDX-FileCopyrightText: 2026 Kerrick Long
716
+ # SPDX-License-Identifier: MIT-0
717
+ #++
718
+ # area = tui.viewport_area
719
+ # x, y, width, height = area
720
+ # # Now you can use x, y, width, height directly
721
+ #--
722
+ # SPDX-SnippetEnd
723
+ #++
724
+ def to_ary
725
+ [x, y, width, height]
726
+ end
727
+
728
+ # Ruby-idiomatic aliases (TIMTOWTDI)
729
+ alias position as_position
730
+ alias size as_size
731
+ end
732
+ end
733
+ end