ratatui_ruby 0.6.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 (171) 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 +4 -4
  7. data/CHANGELOG.md +35 -0
  8. data/README.md +26 -1
  9. data/doc/application_architecture.md +16 -16
  10. data/doc/application_testing.md +1 -1
  11. data/doc/contributors/architectural_overhaul/chat_conversations.md +4952 -0
  12. data/doc/contributors/architectural_overhaul/implementation_plan.md +60 -0
  13. data/doc/contributors/architectural_overhaul/task.md +37 -0
  14. data/doc/contributors/design/ruby_frontend.md +277 -81
  15. data/doc/contributors/design/rust_backend.md +349 -55
  16. data/doc/contributors/developing_examples.md +5 -5
  17. data/doc/contributors/index.md +7 -5
  18. data/doc/contributors/v1.0.0_blockers.md +1729 -0
  19. data/doc/index.md +11 -6
  20. data/doc/interactive_design.md +2 -2
  21. data/doc/quickstart.md +66 -97
  22. data/doc/v0.7.0_migration.md +236 -0
  23. data/doc/why.md +93 -0
  24. data/examples/app_all_events/README.md +6 -4
  25. data/examples/app_all_events/app.rb +1 -1
  26. data/examples/app_all_events/model/app_model.rb +1 -1
  27. data/examples/app_all_events/model/msg.rb +1 -1
  28. data/examples/app_all_events/update.rb +1 -1
  29. data/examples/app_all_events/view/app_view.rb +1 -1
  30. data/examples/app_all_events/view/controls_view.rb +1 -1
  31. data/examples/app_all_events/view/counts_view.rb +1 -1
  32. data/examples/app_all_events/view/live_view.rb +1 -1
  33. data/examples/app_all_events/view/log_view.rb +1 -1
  34. data/examples/app_color_picker/README.md +7 -5
  35. data/examples/app_color_picker/app.rb +1 -1
  36. data/examples/app_login_form/README.md +2 -0
  37. data/examples/app_stateful_interaction/README.md +2 -0
  38. data/examples/app_stateful_interaction/app.rb +1 -1
  39. data/examples/verify_quickstart_dsl/README.md +4 -3
  40. data/examples/verify_quickstart_dsl/app.rb +1 -1
  41. data/examples/verify_quickstart_layout/README.md +1 -1
  42. data/examples/verify_quickstart_lifecycle/README.md +3 -3
  43. data/examples/verify_quickstart_lifecycle/app.rb +2 -2
  44. data/examples/verify_readme_usage/README.md +1 -1
  45. data/examples/widget_barchart_demo/README.md +2 -1
  46. data/examples/widget_block_demo/README.md +2 -0
  47. data/examples/widget_box_demo/README.md +3 -3
  48. data/examples/widget_calendar_demo/README.md +3 -3
  49. data/examples/widget_calendar_demo/app.rb +5 -1
  50. data/examples/widget_canvas_demo/README.md +3 -3
  51. data/examples/widget_cell_demo/README.md +3 -3
  52. data/examples/widget_center_demo/README.md +3 -3
  53. data/examples/widget_chart_demo/README.md +3 -3
  54. data/examples/widget_gauge_demo/README.md +3 -3
  55. data/examples/widget_layout_split/README.md +3 -3
  56. data/examples/widget_line_gauge_demo/README.md +3 -3
  57. data/examples/widget_list_demo/README.md +3 -3
  58. data/examples/widget_map_demo/README.md +3 -3
  59. data/examples/widget_map_demo/app.rb +2 -2
  60. data/examples/widget_overlay_demo/README.md +36 -0
  61. data/examples/widget_popup_demo/README.md +3 -3
  62. data/examples/widget_ratatui_logo_demo/README.md +3 -3
  63. data/examples/widget_ratatui_logo_demo/app.rb +1 -1
  64. data/examples/widget_ratatui_mascot_demo/README.md +3 -3
  65. data/examples/widget_rect/README.md +3 -3
  66. data/examples/widget_render/README.md +3 -3
  67. data/examples/widget_render/app.rb +3 -3
  68. data/examples/widget_rich_text/README.md +3 -3
  69. data/examples/widget_scroll_text/README.md +3 -3
  70. data/examples/widget_scrollbar_demo/README.md +3 -3
  71. data/examples/widget_sparkline_demo/README.md +3 -3
  72. data/examples/widget_style_colors/README.md +3 -3
  73. data/examples/widget_table_demo/README.md +3 -3
  74. data/examples/widget_table_demo/app.rb +19 -4
  75. data/examples/widget_tabs_demo/README.md +3 -3
  76. data/examples/widget_text_width/README.md +3 -3
  77. data/examples/widget_text_width/app.rb +8 -1
  78. data/ext/ratatui_ruby/Cargo.lock +1 -1
  79. data/ext/ratatui_ruby/Cargo.toml +1 -1
  80. data/ext/ratatui_ruby/src/frame.rs +6 -5
  81. data/ext/ratatui_ruby/src/lib.rs +3 -2
  82. data/ext/ratatui_ruby/src/rendering.rs +22 -21
  83. data/ext/ratatui_ruby/src/text.rs +12 -3
  84. data/ext/ratatui_ruby/src/widgets/canvas.rs +5 -5
  85. data/ext/ratatui_ruby/src/widgets/table.rs +81 -36
  86. data/lib/ratatui_ruby/buffer/cell.rb +168 -0
  87. data/lib/ratatui_ruby/buffer.rb +15 -0
  88. data/lib/ratatui_ruby/frame.rb +8 -8
  89. data/lib/ratatui_ruby/layout/constraint.rb +95 -0
  90. data/lib/ratatui_ruby/layout/layout.rb +106 -0
  91. data/lib/ratatui_ruby/layout/rect.rb +118 -0
  92. data/lib/ratatui_ruby/layout.rb +19 -0
  93. data/lib/ratatui_ruby/list_state.rb +2 -2
  94. data/lib/ratatui_ruby/schema/layout.rb +1 -1
  95. data/lib/ratatui_ruby/schema/row.rb +66 -0
  96. data/lib/ratatui_ruby/schema/table.rb +10 -10
  97. data/lib/ratatui_ruby/schema/text.rb +27 -2
  98. data/lib/ratatui_ruby/style/style.rb +81 -0
  99. data/lib/ratatui_ruby/style.rb +15 -0
  100. data/lib/ratatui_ruby/table_state.rb +1 -1
  101. data/lib/ratatui_ruby/test_helper/snapshot.rb +24 -0
  102. data/lib/ratatui_ruby/test_helper/style_assertions.rb +1 -1
  103. data/lib/ratatui_ruby/tui/buffer_factories.rb +20 -0
  104. data/lib/ratatui_ruby/tui/canvas_factories.rb +44 -0
  105. data/lib/ratatui_ruby/tui/core.rb +38 -0
  106. data/lib/ratatui_ruby/tui/layout_factories.rb +74 -0
  107. data/lib/ratatui_ruby/tui/state_factories.rb +33 -0
  108. data/lib/ratatui_ruby/tui/style_factories.rb +20 -0
  109. data/lib/ratatui_ruby/tui/text_factories.rb +44 -0
  110. data/lib/ratatui_ruby/tui/widget_factories.rb +195 -0
  111. data/lib/ratatui_ruby/tui.rb +75 -0
  112. data/lib/ratatui_ruby/version.rb +1 -1
  113. data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +47 -0
  114. data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +25 -0
  115. data/lib/ratatui_ruby/widgets/bar_chart.rb +239 -0
  116. data/lib/ratatui_ruby/widgets/block.rb +192 -0
  117. data/lib/ratatui_ruby/widgets/calendar.rb +84 -0
  118. data/lib/ratatui_ruby/widgets/canvas.rb +231 -0
  119. data/lib/ratatui_ruby/widgets/cell.rb +47 -0
  120. data/lib/ratatui_ruby/widgets/center.rb +59 -0
  121. data/lib/ratatui_ruby/widgets/chart.rb +185 -0
  122. data/lib/ratatui_ruby/widgets/clear.rb +54 -0
  123. data/lib/ratatui_ruby/widgets/cursor.rb +42 -0
  124. data/lib/ratatui_ruby/widgets/gauge.rb +72 -0
  125. data/lib/ratatui_ruby/widgets/line_gauge.rb +80 -0
  126. data/lib/ratatui_ruby/widgets/list.rb +127 -0
  127. data/lib/ratatui_ruby/widgets/list_item.rb +43 -0
  128. data/lib/ratatui_ruby/widgets/overlay.rb +43 -0
  129. data/lib/ratatui_ruby/widgets/paragraph.rb +99 -0
  130. data/lib/ratatui_ruby/widgets/ratatui_logo.rb +31 -0
  131. data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +36 -0
  132. data/lib/ratatui_ruby/widgets/row.rb +68 -0
  133. data/lib/ratatui_ruby/widgets/scrollbar.rb +143 -0
  134. data/lib/ratatui_ruby/widgets/shape/label.rb +68 -0
  135. data/lib/ratatui_ruby/widgets/sparkline.rb +134 -0
  136. data/lib/ratatui_ruby/widgets/table.rb +141 -0
  137. data/lib/ratatui_ruby/widgets/tabs.rb +85 -0
  138. data/lib/ratatui_ruby/widgets.rb +40 -0
  139. data/lib/ratatui_ruby.rb +23 -39
  140. data/sig/examples/app_all_events/view.rbs +1 -1
  141. data/sig/examples/app_all_events/view_state.rbs +1 -1
  142. data/sig/ratatui_ruby/schema/row.rbs +22 -0
  143. data/sig/ratatui_ruby/schema/table.rbs +1 -1
  144. data/sig/ratatui_ruby/schema/text.rbs +1 -0
  145. data/sig/ratatui_ruby/session.rbs +29 -49
  146. data/sig/ratatui_ruby/tui/buffer_factories.rbs +10 -0
  147. data/sig/ratatui_ruby/tui/canvas_factories.rbs +14 -0
  148. data/sig/ratatui_ruby/tui/core.rbs +14 -0
  149. data/sig/ratatui_ruby/tui/layout_factories.rbs +19 -0
  150. data/sig/ratatui_ruby/tui/state_factories.rbs +12 -0
  151. data/sig/ratatui_ruby/tui/style_factories.rbs +10 -0
  152. data/sig/ratatui_ruby/tui/text_factories.rbs +14 -0
  153. data/sig/ratatui_ruby/tui/widget_factories.rbs +39 -0
  154. data/sig/ratatui_ruby/tui.rbs +19 -0
  155. data/tasks/autodoc.rake +1 -35
  156. data/tasks/sourcehut.rake +4 -1
  157. metadata +62 -15
  158. data/doc/contributors/dwim_dx.md +0 -366
  159. data/doc/contributors/examples_audit/p1_high.md +0 -21
  160. data/doc/contributors/examples_audit/p2_moderate.md +0 -81
  161. data/doc/contributors/examples_audit.md +0 -41
  162. data/doc/images/app_analytics.png +0 -0
  163. data/doc/images/app_custom_widget.png +0 -0
  164. data/doc/images/app_mouse_events.png +0 -0
  165. data/doc/images/widget_table_flex.png +0 -0
  166. data/lib/ratatui_ruby/session/autodoc.rb +0 -482
  167. data/lib/ratatui_ruby/session.rb +0 -178
  168. data/tasks/autodoc/inventory.rb +0 -63
  169. data/tasks/autodoc/notice.rb +0 -26
  170. data/tasks/autodoc/rbs.rb +0 -38
  171. data/tasks/autodoc/rdoc.rb +0 -45
@@ -0,0 +1,168 @@
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 Buffer
8
+ # Represents a single cell in the terminal buffer.
9
+ #
10
+ # A terminal grid is made of cells. Each cell contains a character (symbol) and styling (colors, modifiers).
11
+ # When testing, you often need to verify that a specific cell renders correctly.
12
+ #
13
+ # This object encapsulates that state. It provides predicate methods for modifiers, making assertions readable.
14
+ #
15
+ # Use it to inspect the visual state of your application in tests.
16
+ #
17
+ # === Examples
18
+ #
19
+ # cell = RatatuiRuby.get_cell_at(0, 0)
20
+ # cell.char # => "H"
21
+ # cell.fg # => :red
22
+ # cell.bold? # => true
23
+ #
24
+ class Cell
25
+ # The character displayed in the cell.
26
+ #
27
+ # Named to match Ratatui's Cell::symbol() method.
28
+ attr_reader :symbol
29
+
30
+ # Alias for Rubyists who prefer a shorter name.
31
+ alias char symbol
32
+
33
+ # The foreground color of the cell (e.g., :red, :blue, "#ff0000").
34
+ attr_reader :fg
35
+
36
+ # The background color of the cell (e.g., :black, nil).
37
+ attr_reader :bg
38
+
39
+ # The list of active modifiers (e.g., ["bold", "italic"]).
40
+ attr_reader :modifiers
41
+
42
+ # Returns an empty cell (space character, no styles).
43
+ #
44
+ # === Example
45
+ #
46
+ # Buffer::Cell.empty # => #<RatatuiRuby::Buffer::Cell char=" ">
47
+ #
48
+ def self.empty
49
+ new(symbol: " ", fg: nil, bg: nil, modifiers: [])
50
+ end
51
+
52
+ # Returns a default cell (alias for empty).
53
+ #
54
+ # === Example
55
+ #
56
+ # Buffer::Cell.default # => #<RatatuiRuby::Buffer::Cell char=" ">
57
+ #
58
+ def self.default
59
+ empty
60
+ end
61
+
62
+ # Returns a cell with a specific character and no styles.
63
+ #
64
+ # [symbol] String (single character).
65
+ #
66
+ # === Example
67
+ #
68
+ # Buffer::Cell.symbol("X") # => #<RatatuiRuby::Buffer::Cell symbol="X">
69
+ #
70
+ def self.symbol(symbol)
71
+ new(symbol:, fg: nil, bg: nil, modifiers: [])
72
+ end
73
+
74
+ # Alias for Rubyists who prefer a shorter name.
75
+ def self.char(char)
76
+ symbol(char)
77
+ end
78
+
79
+ # Creates a new Cell.
80
+ #
81
+ # [symbol] String (single character). Aliased as <tt>char:</tt>.
82
+ # [fg] Symbol or String (nullable).
83
+ # [bg] Symbol or String (nullable).
84
+ # [modifiers] Array of Strings.
85
+ def initialize(symbol: nil, char: nil, fg: nil, bg: nil, modifiers: [])
86
+ @symbol = (symbol || char || " ").freeze
87
+ @fg = fg&.freeze
88
+ @bg = bg&.freeze
89
+ @modifiers = modifiers.map(&:freeze).freeze
90
+ freeze
91
+ end
92
+
93
+ # Returns true if the cell has the bold modifier.
94
+ def bold?
95
+ modifiers.include?("bold")
96
+ end
97
+
98
+ # Returns true if the cell has the dim modifier.
99
+ def dim?
100
+ modifiers.include?("dim")
101
+ end
102
+
103
+ # Returns true if the cell has the italic modifier.
104
+ def italic?
105
+ modifiers.include?("italic")
106
+ end
107
+
108
+ # Returns true if the cell has the underlined modifier.
109
+ def underlined?
110
+ modifiers.include?("underlined")
111
+ end
112
+
113
+ # Returns true if the cell has the slow_blink modifier.
114
+ def slow_blink?
115
+ modifiers.include?("slow_blink")
116
+ end
117
+
118
+ # Returns true if the cell has the rapid_blink modifier.
119
+ def rapid_blink?
120
+ modifiers.include?("rapid_blink")
121
+ end
122
+
123
+ # Returns true if the cell has the reversed modifier.
124
+ def reversed?
125
+ modifiers.include?("reversed")
126
+ end
127
+
128
+ # Returns true if the cell has the hidden modifier.
129
+ def hidden?
130
+ modifiers.include?("hidden")
131
+ end
132
+
133
+ # Returns true if the cell has the crossed_out modifier.
134
+ def crossed_out?
135
+ modifiers.include?("crossed_out")
136
+ end
137
+
138
+ # Checks equality with another Cell.
139
+ def ==(other)
140
+ other.is_a?(Cell) &&
141
+ char == other.char &&
142
+ fg == other.fg &&
143
+ bg == other.bg &&
144
+ modifiers == other.modifiers
145
+ end
146
+
147
+ # Returns a string representation of the cell.
148
+ def inspect
149
+ parts = ["symbol=#{symbol.inspect}"]
150
+ parts << "fg=#{fg.inspect}" if fg
151
+ parts << "bg=#{bg.inspect}" if bg
152
+ parts << "modifiers=#{modifiers.inspect}" unless modifiers.empty?
153
+ "#<#{self.class} #{parts.join(' ')}>"
154
+ end
155
+
156
+ # Returns the cell's character.
157
+ def to_s
158
+ symbol
159
+ end
160
+
161
+ # Support for pattern matching.
162
+ # Supports both <tt>:symbol</tt> and <tt>:char</tt> keys.
163
+ def deconstruct_keys(keys)
164
+ { symbol:, char: symbol, fg:, bg:, modifiers: }
165
+ end
166
+ end
167
+ end
168
+ end
@@ -0,0 +1,15 @@
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
+ # Buffer primitives for terminal cell inspection.
8
+ #
9
+ # This module mirrors +ratatui::buffer+ and contains:
10
+ # - {Cell} — Single terminal cell (for inspection)
11
+ module Buffer
12
+ end
13
+ end
14
+
15
+ require_relative "buffer/cell"
@@ -21,7 +21,7 @@ module RatatuiRuby
21
21
  # Frame is an *I/O handle*, not a data object. It has side effects
22
22
  # (render_widget, set_cursor_position) and is intentionally *not*
23
23
  # Ractor-shareable. Passing it to helper methods during the draw block is
24
- # fine. However, do not include it in immutable TEA Models/Messages or pass
24
+ # fine. However, do not include it in immutable Models/Messages or pass
25
25
  # it to other Ractors. Frame is only valid during the draw block's execution.
26
26
  #
27
27
  # === Examples
@@ -29,7 +29,7 @@ module RatatuiRuby
29
29
  # Basic usage with a single widget:
30
30
  #
31
31
  # RatatuiRuby.draw do |frame|
32
- # paragraph = RatatuiRuby::Paragraph.new(text: "Hello, world!")
32
+ # paragraph = RatatuiRuby::Widgets::Paragraph.new(text: "Hello, world!")
33
33
  # frame.render_widget(paragraph, frame.area)
34
34
  # end
35
35
  #
@@ -40,8 +40,8 @@ module RatatuiRuby
40
40
  # frame.area,
41
41
  # direction: :horizontal,
42
42
  # constraints: [
43
- # RatatuiRuby::Constraint.length(20),
44
- # RatatuiRuby::Constraint.fill(1)
43
+ # RatatuiRuby::Layout::Constraint.length(20),
44
+ # RatatuiRuby::Layout::Constraint.fill(1)
45
45
  # ]
46
46
  # )
47
47
  #
@@ -86,7 +86,7 @@ module RatatuiRuby
86
86
  # === Example
87
87
  #
88
88
  # RatatuiRuby.draw do |frame|
89
- # para = RatatuiRuby::Paragraph.new(text: "Content")
89
+ # para = RatatuiRuby::Widgets::Paragraph.new(text: "Content")
90
90
  # frame.render_widget(para, frame.area)
91
91
  # end
92
92
  #
@@ -122,7 +122,7 @@ module RatatuiRuby
122
122
  # @list_state = RatatuiRuby::ListState.new
123
123
  #
124
124
  # RatatuiRuby.draw do |frame|
125
- # list = RatatuiRuby::List.new(items: ["A", "B"])
125
+ # list = RatatuiRuby::Widgets::List.new(items: ["A", "B"])
126
126
  # frame.render_stateful_widget(list, frame.area, @list_state)
127
127
  # end
128
128
  #
@@ -160,9 +160,9 @@ module RatatuiRuby
160
160
  #
161
161
  # RatatuiRuby.draw do |frame|
162
162
  # # Render the input field
163
- # prompt = RatatuiRuby::Paragraph.new(
163
+ # prompt = RatatuiRuby::Widgets::Paragraph.new(
164
164
  # text: "#{PREFIX}#{username} ]",
165
- # block: RatatuiRuby::Block.new(borders: :all)
165
+ # block: RatatuiRuby::Widgets::Block.new(borders: :all)
166
166
  # )
167
167
  # frame.render_widget(prompt, frame.area)
168
168
  #
@@ -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
@@ -0,0 +1,106 @@
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
+ # Divides an area into smaller chunks.
9
+ #
10
+ # Terminal screens vary in size. Hardcoded positions break when the window resizes. You need a way to organize space dynamically.
11
+ #
12
+ # This class manages geometry. It splits a given area into multiple sections based on a list of constraints.
13
+ #
14
+ # Use layouts to build responsive grids. Stack sections vertically for a sidebar-main structure. Partition them horizontally for headers and footers. Let the layout engine do the math.
15
+ #
16
+ # {rdoc-image:/doc/images/widget_layout_split.png}[link:/examples/widget_layout_split/app_rb.html]
17
+ #
18
+ # === Example
19
+ #
20
+ # Run the interactive demo from the terminal:
21
+ #
22
+ # ruby examples/widget_layout_split/app.rb
23
+ class Layout < Data.define(:direction, :constraints, :children, :flex)
24
+ ##
25
+ # :attr_reader: direction
26
+ # Direction of the split.
27
+ #
28
+ # Either <tt>:vertical</tt> (top to bottom) or <tt>:horizontal</tt> (left to right).
29
+ #
30
+ # layout.direction # => :vertical
31
+
32
+ ##
33
+ # :attr_reader: constraints
34
+ # Array of rules defining section sizes.
35
+ #
36
+ # See RatatuiRuby::Layout::Constraint.
37
+
38
+ ##
39
+ # :attr_reader: children
40
+ # Widgets to render in each section (optional).
41
+ #
42
+ # If provided, `children[i]` is rendered into the area defined by `constraints[i]`.
43
+
44
+ ##
45
+ # :attr_reader: flex
46
+ # Strategy for distributing extra space.
47
+ #
48
+ # One of <tt>:legacy</tt>, <tt>:start</tt>, <tt>:center</tt>, <tt>:end</tt>, <tt>:space_between</tt>, <tt>:space_around</tt>.
49
+
50
+ # :nodoc:
51
+ FLEX_MODES = %i[legacy start center end space_between space_around space_evenly].freeze
52
+
53
+ # Creates a new Layout.
54
+ #
55
+ # [direction]
56
+ # <tt>:vertical</tt> or <tt>:horizontal</tt> (default: <tt>:vertical</tt>).
57
+ # [constraints]
58
+ # list of Constraint objects.
59
+ # [children]
60
+ # List of widgets to render (optional).
61
+ # [flex]
62
+ # Flex mode for spacing (default: <tt>:legacy</tt>).
63
+ def initialize(direction: :vertical, constraints: [], children: [], flex: :legacy)
64
+ super
65
+ end
66
+
67
+ # Splits an area into multiple rectangles.
68
+ #
69
+ # This is a pure calculation helper for hit testing. It computes where
70
+ # widgets *would* be placed without actually rendering them.
71
+ #
72
+ # rects = Layout::Layout.split(
73
+ # area,
74
+ # direction: :horizontal,
75
+ # constraints: [Layout::Constraint.percentage(50), Layout::Constraint.percentage(50)]
76
+ # )
77
+ # left, right = rects
78
+ #
79
+ # [area]
80
+ # The area to split. Can be a <tt>Rect</tt> or a <tt>Hash</tt> containing <tt>:x</tt>, <tt>:y</tt>, <tt>:width</tt>, and <tt>:height</tt>.
81
+ # [direction]
82
+ # <tt>:vertical</tt> or <tt>:horizontal</tt> (default: <tt>:vertical</tt>).
83
+ # [constraints]
84
+ # Array of <tt>Constraint</tt> objects defining section sizes.
85
+ # [flex]
86
+ # Flex mode for spacing (default: <tt>:legacy</tt>).
87
+ #
88
+ # Returns an Array of <tt>Rect</tt> objects.
89
+ def self.split(area, direction: :vertical, constraints:, flex: :legacy)
90
+ # Duck-typing: If it lacks geometry methods but can be a Hash, convert it.
91
+ if !area.respond_to?(:x) && area.respond_to?(:to_h)
92
+ # Assume it's a Hash-like object with :x, :y, etc.
93
+ hash = area.to_h
94
+ area = Rect.new(
95
+ x: hash.fetch(:x, 0),
96
+ y: hash.fetch(:y, 0),
97
+ width: hash.fetch(:width, 0),
98
+ height: hash.fetch(:height, 0)
99
+ )
100
+ end
101
+ raw_rects = _split(area, direction, constraints, flex)
102
+ raw_rects.map { |r| Rect.new(x: r[:x], y: r[:y], width: r[:width], height: r[:height]) }
103
+ end
104
+ end
105
+ end
106
+ end
@@ -0,0 +1,118 @@
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 a rectangular area in the terminal grid.
9
+ #
10
+ # Geometry management involves passing groups of four integers (`x, y, width, height`) repeatedly.
11
+ # This is verbose and prone to parameter mismatch errors.
12
+ #
13
+ # This class encapsulates the geometry. It provides a standard primitive for passing area definitions
14
+ # between layout engines and rendering functions.
15
+ #
16
+ # Use it when manual positioning is required or when querying layout results.
17
+ #
18
+ # === Examples
19
+ #
20
+ # area = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
21
+ # puts area.width # => 80
22
+ class Rect < Data.define(:x, :y, :width, :height)
23
+ ##
24
+ # :attr_reader: x
25
+ # X coordinate (column) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
26
+
27
+ ##
28
+ # :attr_reader: y
29
+ # Y coordinate (row) of the top-left corner (Integer, coerced via +to_int+ or +to_i+).
30
+
31
+ ##
32
+ # :attr_reader: width
33
+ # Width in characters (Integer, coerced via +to_int+ or +to_i+).
34
+
35
+ ##
36
+ # :attr_reader: height
37
+ # Height in characters (Integer, coerced via +to_int+ or +to_i+).
38
+
39
+ # Creates a new Rect.
40
+ #
41
+ # All parameters accept any object responding to +to_int+ or +to_i+ (duck-typed).
42
+ #
43
+ # [x] Column index (Numeric).
44
+ # [y] Row index (Numeric).
45
+ # [width] Width in columns (Numeric).
46
+ # [height] Height in rows (Numeric).
47
+ def initialize(x: 0, y: 0, width: 0, height: 0)
48
+ super(
49
+ x: Integer(x),
50
+ y: Integer(y),
51
+ width: Integer(width),
52
+ height: Integer(height)
53
+ )
54
+ end
55
+
56
+ # Tests whether a point is inside this rectangle.
57
+ #
58
+ # Essential for hit testing mouse clicks against layout regions.
59
+ #
60
+ # area = Layout::Rect.new(x: 10, y: 5, width: 20, height: 10)
61
+ # area.contains?(15, 8) # => true
62
+ # area.contains?(5, 8) # => false
63
+ #
64
+ # [px]
65
+ # X coordinate to test (column).
66
+ # [py]
67
+ # Y coordinate to test (row).
68
+ #
69
+ # Returns true if the point (px, py) is within the rectangle bounds.
70
+ def contains?(px, py)
71
+ px >= x && px < x + width && py >= y && py < y + height
72
+ end
73
+
74
+ # Tests whether this rectangle overlaps with another.
75
+ #
76
+ # Essential for determining if a widget is visible within a viewport or clipping area.
77
+ #
78
+ # viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
79
+ # widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
80
+ # viewport.intersects?(widget) # => true (partial overlap)
81
+ #
82
+ # [other]
83
+ # Another Rect to test against.
84
+ #
85
+ # Returns true if the rectangles overlap.
86
+ def intersects?(other)
87
+ x < other.x + other.width &&
88
+ x + width > other.x &&
89
+ y < other.y + other.height &&
90
+ y + height > other.y
91
+ end
92
+
93
+ # Returns the overlapping area between this rectangle and another.
94
+ #
95
+ # Essential for calculating visible portions of widgets inside scroll views.
96
+ #
97
+ # viewport = Layout::Rect.new(x: 0, y: 0, width: 80, height: 24)
98
+ # widget = Layout::Rect.new(x: 70, y: 20, width: 20, height: 10)
99
+ # visible = viewport.intersection(widget)
100
+ # # => Rect(x: 70, y: 20, width: 10, height: 4)
101
+ #
102
+ # [other]
103
+ # Another Rect to intersect with.
104
+ #
105
+ # Returns a new Rect representing the intersection, or +nil+ if no overlap.
106
+ def intersection(other)
107
+ return nil unless intersects?(other)
108
+
109
+ new_x = [x, other.x].max
110
+ new_y = [y, other.y].max
111
+ new_right = [x + width, other.x + other.width].min
112
+ new_bottom = [y + height, other.y + other.height].min
113
+
114
+ Rect.new(x: new_x, y: new_y, width: new_right - new_x, height: new_bottom - new_y)
115
+ end
116
+ end
117
+ end
118
+ end
@@ -0,0 +1,19 @@
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
+ # Layout primitives for geometry and space distribution.
8
+ #
9
+ # This module mirrors +ratatui::layout+ and contains:
10
+ # - {Rect} — Rectangle geometry
11
+ # - {Constraint} — Sizing rules
12
+ # - {Layout} — Space distribution
13
+ module Layout
14
+ end
15
+ end
16
+
17
+ require_relative "layout/rect"
18
+ require_relative "layout/constraint"
19
+ require_relative "layout/layout"
@@ -18,7 +18,7 @@ module RatatuiRuby
18
18
  # == Thread/Ractor Safety
19
19
  #
20
20
  # ListState is *not* Ractor-shareable. It contains mutable internal state.
21
- # Store it in instance variables, not in immutable TEA Models.
21
+ # Store it in instance variables, not in immutable Models.
22
22
  #
23
23
  # == Example
24
24
  #
@@ -26,7 +26,7 @@ module RatatuiRuby
26
26
  # @list_state.select(2) # Select third item
27
27
  #
28
28
  # RatatuiRuby.draw do |frame|
29
- # list = RatatuiRuby::List.new(items: ["A", "B", "C", "D", "E"])
29
+ # list = RatatuiRuby::Widgets::List.new(items: ["A", "B", "C", "D", "E"])
30
30
  # frame.render_stateful_widget(list, frame.area, @list_state)
31
31
  # end
32
32
  #
@@ -32,7 +32,7 @@ module RatatuiRuby
32
32
  # :attr_reader: constraints
33
33
  # Array of rules defining section sizes.
34
34
  #
35
- # See RatatuiRuby::Constraint.
35
+ # See RatatuiRuby::Layout::Constraint.
36
36
 
37
37
  ##
38
38
  # :attr_reader: children
@@ -0,0 +1,66 @@
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
+ # A styled table row combining cells with optional row-level styling.
8
+ #
9
+ # By default, Table rows are arrays of cell content. For more control over styling
10
+ # individual rows, wrap the cells in a Row object to apply row-level style.
11
+ #
12
+ # The cells can be Strings, Text::Spans, Text::Lines, Paragraphs, or Cells.
13
+ # The style applies to the entire row background.
14
+ #
15
+ # === Examples
16
+ #
17
+ # # Row with red background
18
+ # Row.new(cells: ["Error", "Something went wrong"], style: Style.new(bg: :red))
19
+ #
20
+ # # Row with styled cells and custom height
21
+ # Row.new(
22
+ # cells: [
23
+ # Text::Span.new(content: "Status", style: Style.new(modifiers: [:bold])),
24
+ # Text::Span.new(content: "OK", style: Style.new(fg: :green))
25
+ # ],
26
+ # height: 2
27
+ # )
28
+ class Row < Data.define(:cells, :style, :height, :top_margin, :bottom_margin)
29
+ ##
30
+ # :attr_reader: cells
31
+ # The cells to display (Array of Strings, Text::Spans, Text::Lines, Paragraphs, or Cells).
32
+
33
+ ##
34
+ # :attr_reader: style
35
+ # The style to apply to the row (optional Style).
36
+
37
+ ##
38
+ # :attr_reader: height
39
+ # Fixed row height in lines (optional Integer).
40
+
41
+ ##
42
+ # :attr_reader: top_margin
43
+ # Margin above the row in lines (optional Integer).
44
+
45
+ ##
46
+ # :attr_reader: bottom_margin
47
+ # Margin below the row in lines (optional Integer).
48
+
49
+ # Creates a new Row.
50
+ #
51
+ # [cells] Array of Strings, Text::Spans, Text::Lines, Paragraphs, or Cells.
52
+ # [style] Style object (optional).
53
+ # [height] Integer for fixed height (optional).
54
+ # [top_margin] Integer for top margin (optional).
55
+ # [bottom_margin] Integer for bottom margin (optional).
56
+ def initialize(cells:, style: nil, height: nil, top_margin: nil, bottom_margin: nil)
57
+ super(
58
+ cells:,
59
+ style:,
60
+ height: height.nil? ? nil : Integer(height),
61
+ top_margin: top_margin.nil? ? nil : Integer(top_margin),
62
+ bottom_margin: bottom_margin.nil? ? nil : Integer(bottom_margin)
63
+ )
64
+ end
65
+ end
66
+ end