ratatui_ruby 0.9.1 → 0.10.1
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.
- checksums.yaml +4 -4
- data/.builds/ruby-3.2.yml +1 -1
- data/.builds/ruby-3.3.yml +1 -1
- data/.builds/ruby-3.4.yml +1 -1
- data/.builds/ruby-4.0.0.yml +1 -1
- data/AGENTS.md +2 -1
- data/CHANGELOG.md +113 -0
- data/README.md +17 -0
- data/REUSE.toml +5 -0
- data/Rakefile +1 -1
- data/Steepfile +49 -0
- data/doc/concepts/debugging.md +401 -0
- data/doc/getting_started/quickstart.md +8 -3
- data/doc/images/app_all_events.png +0 -0
- data/doc/images/app_color_picker.png +0 -0
- data/doc/images/app_debugging_showcase.gif +0 -0
- data/doc/images/app_debugging_showcase.png +0 -0
- data/doc/images/app_login_form.png +0 -0
- data/doc/images/app_stateful_interaction.png +0 -0
- data/doc/images/verify_quickstart_dsl.png +0 -0
- data/doc/images/verify_quickstart_layout.png +0 -0
- data/doc/images/verify_quickstart_lifecycle.png +0 -0
- data/doc/images/verify_readme_usage.png +0 -0
- data/doc/images/widget_barchart.png +0 -0
- data/doc/images/widget_block.png +0 -0
- data/doc/images/widget_box.png +0 -0
- data/doc/images/widget_calendar.png +0 -0
- data/doc/images/widget_canvas.png +0 -0
- data/doc/images/widget_cell.png +0 -0
- data/doc/images/widget_center.png +0 -0
- data/doc/images/widget_chart.png +0 -0
- data/doc/images/widget_gauge.png +0 -0
- data/doc/images/widget_layout_split.png +0 -0
- data/doc/images/widget_line_gauge.png +0 -0
- data/doc/images/widget_list.png +0 -0
- data/doc/images/widget_map.png +0 -0
- data/doc/images/widget_overlay.png +0 -0
- data/doc/images/widget_popup.png +0 -0
- data/doc/images/widget_ratatui_logo.png +0 -0
- data/doc/images/widget_ratatui_mascot.png +0 -0
- data/doc/images/widget_rect.png +0 -0
- data/doc/images/widget_render.png +0 -0
- data/doc/images/widget_rich_text.png +0 -0
- data/doc/images/widget_scroll_text.png +0 -0
- data/doc/images/widget_scrollbar.png +0 -0
- data/doc/images/widget_sparkline.png +0 -0
- data/doc/images/widget_style_colors.png +0 -0
- data/doc/images/widget_table.png +0 -0
- data/doc/images/widget_tabs.png +0 -0
- data/doc/images/widget_text_width.png +0 -0
- data/doc/troubleshooting/async.md +4 -0
- data/examples/app_debugging_showcase/README.md +119 -0
- data/examples/app_debugging_showcase/app.rb +318 -0
- data/examples/widget_canvas/app.rb +19 -14
- data/examples/widget_gauge/app.rb +18 -3
- data/examples/widget_layout_split/app.rb +10 -4
- data/examples/widget_list/app.rb +22 -6
- data/examples/widget_rect/app.rb +7 -6
- data/examples/widget_rich_text/app.rb +62 -37
- data/examples/widget_style_colors/app.rb +26 -47
- data/examples/widget_table/app.rb +28 -5
- data/examples/widget_text_width/app.rb +6 -4
- data/ext/ratatui_ruby/Cargo.lock +48 -1
- data/ext/ratatui_ruby/Cargo.toml +6 -2
- data/ext/ratatui_ruby/src/color.rs +82 -0
- data/ext/ratatui_ruby/src/errors.rs +28 -0
- data/ext/ratatui_ruby/src/events.rs +15 -14
- data/ext/ratatui_ruby/src/lib.rs +56 -0
- data/ext/ratatui_ruby/src/rendering.rs +3 -1
- data/ext/ratatui_ruby/src/style.rs +48 -21
- data/ext/ratatui_ruby/src/terminal.rs +40 -9
- data/ext/ratatui_ruby/src/text.rs +21 -9
- data/ext/ratatui_ruby/src/widgets/chart.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/layout.rs +90 -2
- data/ext/ratatui_ruby/src/widgets/list.rs +6 -5
- data/ext/ratatui_ruby/src/widgets/overlay.rs +2 -1
- data/ext/ratatui_ruby/src/widgets/table.rs +7 -6
- data/ext/ratatui_ruby/src/widgets/table_state.rs +55 -0
- data/ext/ratatui_ruby/src/widgets/tabs.rs +3 -2
- data/lib/ratatui_ruby/buffer/cell.rb +25 -15
- data/lib/ratatui_ruby/buffer.rb +134 -2
- data/lib/ratatui_ruby/cell.rb +13 -5
- data/lib/ratatui_ruby/debug.rb +215 -0
- data/lib/ratatui_ruby/event/key.rb +3 -2
- data/lib/ratatui_ruby/event.rb +1 -1
- data/lib/ratatui_ruby/layout/constraint.rb +49 -0
- data/lib/ratatui_ruby/layout/layout.rb +119 -13
- data/lib/ratatui_ruby/layout/position.rb +55 -0
- data/lib/ratatui_ruby/layout/rect.rb +188 -0
- data/lib/ratatui_ruby/layout/size.rb +55 -0
- data/lib/ratatui_ruby/layout.rb +4 -0
- data/lib/ratatui_ruby/style/color.rb +149 -0
- data/lib/ratatui_ruby/style/style.rb +51 -4
- data/lib/ratatui_ruby/style.rb +2 -0
- data/lib/ratatui_ruby/symbols.rb +435 -0
- data/lib/ratatui_ruby/synthetic_events.rb +1 -1
- data/lib/ratatui_ruby/table_state.rb +51 -0
- data/lib/ratatui_ruby/terminal_lifecycle.rb +2 -1
- data/lib/ratatui_ruby/test_helper/event_injection.rb +6 -1
- data/lib/ratatui_ruby/test_helper.rb +9 -0
- data/lib/ratatui_ruby/text/line.rb +245 -0
- data/lib/ratatui_ruby/text/span.rb +158 -0
- data/lib/ratatui_ruby/text.rb +99 -0
- data/lib/ratatui_ruby/tui/canvas_factories.rb +103 -0
- data/lib/ratatui_ruby/tui/core.rb +13 -2
- data/lib/ratatui_ruby/tui/layout_factories.rb +50 -3
- data/lib/ratatui_ruby/tui/state_factories.rb +42 -0
- data/lib/ratatui_ruby/tui/text_factories.rb +40 -0
- data/lib/ratatui_ruby/tui/widget_factories.rb +135 -60
- data/lib/ratatui_ruby/tui.rb +22 -1
- data/lib/ratatui_ruby/version.rb +1 -1
- data/lib/ratatui_ruby/widgets/bar_chart/bar.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart/bar_group.rb +2 -0
- data/lib/ratatui_ruby/widgets/bar_chart.rb +30 -20
- data/lib/ratatui_ruby/widgets/block.rb +14 -6
- data/lib/ratatui_ruby/widgets/calendar.rb +2 -0
- data/lib/ratatui_ruby/widgets/canvas.rb +56 -0
- data/lib/ratatui_ruby/widgets/cell.rb +2 -0
- data/lib/ratatui_ruby/widgets/center.rb +2 -0
- data/lib/ratatui_ruby/widgets/chart.rb +6 -0
- data/lib/ratatui_ruby/widgets/clear.rb +2 -0
- data/lib/ratatui_ruby/widgets/coerceable_widget.rb +77 -0
- data/lib/ratatui_ruby/widgets/cursor.rb +2 -0
- data/lib/ratatui_ruby/widgets/gauge.rb +61 -3
- data/lib/ratatui_ruby/widgets/line_gauge.rb +66 -4
- data/lib/ratatui_ruby/widgets/list.rb +87 -3
- data/lib/ratatui_ruby/widgets/list_item.rb +2 -0
- data/lib/ratatui_ruby/widgets/overlay.rb +2 -0
- data/lib/ratatui_ruby/widgets/paragraph.rb +4 -0
- data/lib/ratatui_ruby/widgets/ratatui_logo.rb +2 -0
- data/lib/ratatui_ruby/widgets/ratatui_mascot.rb +2 -0
- data/lib/ratatui_ruby/widgets/row.rb +45 -0
- data/lib/ratatui_ruby/widgets/scrollbar.rb +2 -0
- data/lib/ratatui_ruby/widgets/shape/label.rb +2 -0
- data/lib/ratatui_ruby/widgets/sparkline.rb +21 -13
- data/lib/ratatui_ruby/widgets/table.rb +13 -3
- data/lib/ratatui_ruby/widgets/tabs.rb +6 -4
- data/lib/ratatui_ruby/widgets.rb +1 -0
- data/lib/ratatui_ruby.rb +42 -11
- data/sig/examples/app_all_events/model/app_model.rbs +23 -0
- data/sig/examples/app_all_events/model/event_entry.rbs +15 -8
- data/sig/examples/app_all_events/model/timestamp.rbs +1 -1
- data/sig/examples/app_all_events/view.rbs +1 -1
- data/sig/examples/app_stateful_interaction/app.rbs +5 -5
- data/sig/examples/widget_block_demo/app.rbs +6 -6
- data/sig/manifest.yaml +5 -0
- data/sig/patches/data.rbs +26 -0
- data/sig/patches/debugger__.rbs +8 -0
- data/sig/ratatui_ruby/buffer/cell.rbs +46 -0
- data/sig/ratatui_ruby/buffer.rbs +18 -0
- data/sig/ratatui_ruby/cell.rbs +44 -0
- data/sig/ratatui_ruby/clear.rbs +18 -0
- data/sig/ratatui_ruby/constraint.rbs +26 -0
- data/sig/ratatui_ruby/debug.rbs +45 -0
- data/sig/ratatui_ruby/draw.rbs +30 -0
- data/sig/ratatui_ruby/event.rbs +68 -8
- data/sig/ratatui_ruby/frame.rbs +4 -4
- data/sig/ratatui_ruby/interfaces.rbs +25 -0
- data/sig/ratatui_ruby/layout/constraint.rbs +39 -0
- data/sig/ratatui_ruby/layout/layout.rbs +45 -0
- data/sig/ratatui_ruby/layout/position.rbs +18 -0
- data/sig/ratatui_ruby/layout/rect.rbs +64 -0
- data/sig/ratatui_ruby/layout/size.rbs +18 -0
- data/sig/ratatui_ruby/output_guard.rbs +23 -0
- data/sig/ratatui_ruby/ratatui_ruby.rbs +84 -5
- data/sig/ratatui_ruby/rect.rbs +17 -0
- data/sig/ratatui_ruby/style/color.rbs +22 -0
- data/sig/ratatui_ruby/style/style.rbs +29 -0
- data/sig/ratatui_ruby/symbols.rbs +141 -0
- data/sig/ratatui_ruby/synthetic_events.rbs +21 -0
- data/sig/ratatui_ruby/table_state.rbs +6 -0
- data/sig/ratatui_ruby/terminal_lifecycle.rbs +31 -0
- data/sig/ratatui_ruby/test_helper/event_injection.rbs +2 -2
- data/sig/ratatui_ruby/test_helper/snapshot.rbs +22 -3
- data/sig/ratatui_ruby/test_helper/style_assertions.rbs +8 -1
- data/sig/ratatui_ruby/test_helper/test_doubles.rbs +7 -3
- data/sig/ratatui_ruby/text/line.rbs +27 -0
- data/sig/ratatui_ruby/text/span.rbs +23 -0
- data/sig/ratatui_ruby/text.rbs +12 -0
- data/sig/ratatui_ruby/tui/buffer_factories.rbs +1 -1
- data/sig/ratatui_ruby/tui/canvas_factories.rbs +23 -5
- data/sig/ratatui_ruby/tui/core.rbs +2 -2
- data/sig/ratatui_ruby/tui/layout_factories.rbs +16 -2
- data/sig/ratatui_ruby/tui/state_factories.rbs +8 -3
- data/sig/ratatui_ruby/tui/style_factories.rbs +3 -1
- data/sig/ratatui_ruby/tui/text_factories.rbs +7 -4
- data/sig/ratatui_ruby/tui/widget_factories.rbs +123 -30
- data/sig/ratatui_ruby/widgets/bar_chart.rbs +95 -0
- data/sig/ratatui_ruby/widgets/block.rbs +51 -0
- data/sig/ratatui_ruby/widgets/calendar.rbs +45 -0
- data/sig/ratatui_ruby/widgets/canvas.rbs +95 -0
- data/sig/ratatui_ruby/widgets/chart.rbs +91 -0
- data/sig/ratatui_ruby/widgets/coerceable_widget.rbs +26 -0
- data/sig/ratatui_ruby/widgets/gauge.rbs +44 -0
- data/sig/ratatui_ruby/widgets/line_gauge.rbs +48 -0
- data/sig/ratatui_ruby/widgets/list.rbs +63 -0
- data/sig/ratatui_ruby/widgets/misc.rbs +158 -0
- data/sig/ratatui_ruby/widgets/paragraph.rbs +45 -0
- data/sig/ratatui_ruby/widgets/row.rbs +43 -0
- data/sig/ratatui_ruby/widgets/scrollbar.rbs +53 -0
- data/sig/ratatui_ruby/widgets/shape/label.rbs +37 -0
- data/sig/ratatui_ruby/widgets/sparkline.rbs +45 -0
- data/sig/ratatui_ruby/widgets/table.rbs +78 -0
- data/sig/ratatui_ruby/widgets/tabs.rbs +44 -0
- data/sig/ratatui_ruby/{schema/list_item.rbs → widgets.rbs} +4 -4
- data/tasks/steep.rake +11 -0
- metadata +80 -63
- data/doc/contributors/v1.0.0_blockers.md +0 -870
- data/doc/troubleshooting/debugging.md +0 -101
- data/lib/ratatui_ruby/schema/bar_chart/bar.rb +0 -47
- data/lib/ratatui_ruby/schema/bar_chart/bar_group.rb +0 -25
- data/lib/ratatui_ruby/schema/bar_chart.rb +0 -287
- data/lib/ratatui_ruby/schema/block.rb +0 -198
- data/lib/ratatui_ruby/schema/calendar.rb +0 -84
- data/lib/ratatui_ruby/schema/canvas.rb +0 -239
- data/lib/ratatui_ruby/schema/center.rb +0 -67
- data/lib/ratatui_ruby/schema/chart.rb +0 -159
- data/lib/ratatui_ruby/schema/clear.rb +0 -62
- data/lib/ratatui_ruby/schema/constraint.rb +0 -151
- data/lib/ratatui_ruby/schema/cursor.rb +0 -50
- data/lib/ratatui_ruby/schema/gauge.rb +0 -72
- data/lib/ratatui_ruby/schema/layout.rb +0 -122
- data/lib/ratatui_ruby/schema/line_gauge.rb +0 -80
- data/lib/ratatui_ruby/schema/list.rb +0 -135
- data/lib/ratatui_ruby/schema/list_item.rb +0 -51
- data/lib/ratatui_ruby/schema/overlay.rb +0 -51
- data/lib/ratatui_ruby/schema/paragraph.rb +0 -107
- data/lib/ratatui_ruby/schema/ratatui_logo.rb +0 -31
- data/lib/ratatui_ruby/schema/ratatui_mascot.rb +0 -36
- data/lib/ratatui_ruby/schema/rect.rb +0 -174
- data/lib/ratatui_ruby/schema/row.rb +0 -76
- data/lib/ratatui_ruby/schema/scrollbar.rb +0 -143
- data/lib/ratatui_ruby/schema/shape/label.rb +0 -76
- data/lib/ratatui_ruby/schema/sparkline.rb +0 -142
- data/lib/ratatui_ruby/schema/style.rb +0 -97
- data/lib/ratatui_ruby/schema/table.rb +0 -141
- data/lib/ratatui_ruby/schema/tabs.rb +0 -85
- data/lib/ratatui_ruby/schema/text.rb +0 -217
- data/sig/examples/app_all_events/model/events.rbs +0 -15
- data/sig/examples/app_all_events/view_state.rbs +0 -21
- data/sig/ratatui_ruby/schema/bar_chart/bar.rbs +0 -22
- data/sig/ratatui_ruby/schema/bar_chart/bar_group.rbs +0 -19
- data/sig/ratatui_ruby/schema/bar_chart.rbs +0 -38
- data/sig/ratatui_ruby/schema/block.rbs +0 -18
- data/sig/ratatui_ruby/schema/calendar.rbs +0 -23
- data/sig/ratatui_ruby/schema/canvas.rbs +0 -81
- data/sig/ratatui_ruby/schema/center.rbs +0 -17
- data/sig/ratatui_ruby/schema/chart.rbs +0 -39
- data/sig/ratatui_ruby/schema/constraint.rbs +0 -30
- data/sig/ratatui_ruby/schema/cursor.rbs +0 -16
- data/sig/ratatui_ruby/schema/draw.rbs +0 -33
- data/sig/ratatui_ruby/schema/gauge.rbs +0 -23
- data/sig/ratatui_ruby/schema/layout.rbs +0 -27
- data/sig/ratatui_ruby/schema/line_gauge.rbs +0 -24
- data/sig/ratatui_ruby/schema/list.rbs +0 -28
- data/sig/ratatui_ruby/schema/overlay.rbs +0 -15
- data/sig/ratatui_ruby/schema/paragraph.rbs +0 -20
- data/sig/ratatui_ruby/schema/ratatui_logo.rbs +0 -14
- data/sig/ratatui_ruby/schema/ratatui_mascot.rbs +0 -17
- data/sig/ratatui_ruby/schema/rect.rbs +0 -48
- data/sig/ratatui_ruby/schema/row.rbs +0 -28
- data/sig/ratatui_ruby/schema/scrollbar.rbs +0 -42
- data/sig/ratatui_ruby/schema/sparkline.rbs +0 -22
- data/sig/ratatui_ruby/schema/style.rbs +0 -19
- data/sig/ratatui_ruby/schema/table.rbs +0 -32
- data/sig/ratatui_ruby/schema/tabs.rbs +0 -21
- data/sig/ratatui_ruby/schema/text.rbs +0 -31
- /data/lib/ratatui_ruby/{schema/draw.rb → draw.rb} +0 -0
|
@@ -283,6 +283,55 @@ module RatatuiRuby
|
|
|
283
283
|
def self.from_ratios(pairs)
|
|
284
284
|
pairs.map { |n, d| ratio(n, d) }
|
|
285
285
|
end
|
|
286
|
+
|
|
287
|
+
# Computes the size this constraint would produce given available space.
|
|
288
|
+
#
|
|
289
|
+
# Layout engines use constraints to compute actual dimensions.
|
|
290
|
+
# Calling apply lets you preview the result without rendering.
|
|
291
|
+
#
|
|
292
|
+
# === Example
|
|
293
|
+
#
|
|
294
|
+
#--
|
|
295
|
+
# SPDX-SnippetBegin
|
|
296
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
297
|
+
# SPDX-License-Identifier: MIT-0
|
|
298
|
+
#++
|
|
299
|
+
# Constraint.percentage(50).apply(100) # => 50
|
|
300
|
+
# Constraint.length(10).apply(100) # => 10
|
|
301
|
+
# Constraint.min(10).apply(5) # => 10
|
|
302
|
+
# Constraint.max(10).apply(15) # => 10
|
|
303
|
+
# Constraint.ratio(1, 4).apply(100) # => 25
|
|
304
|
+
#--
|
|
305
|
+
# SPDX-SnippetEnd
|
|
306
|
+
#++
|
|
307
|
+
#
|
|
308
|
+
# [length] Available space (Integer).
|
|
309
|
+
#
|
|
310
|
+
# Returns the computed size (Integer).
|
|
311
|
+
def apply(length)
|
|
312
|
+
length = Integer(length)
|
|
313
|
+
case type
|
|
314
|
+
when :length
|
|
315
|
+
value
|
|
316
|
+
when :percentage
|
|
317
|
+
(length * value) / 100
|
|
318
|
+
when :min
|
|
319
|
+
[value, length].max
|
|
320
|
+
when :max
|
|
321
|
+
[value, length].min
|
|
322
|
+
when :fill
|
|
323
|
+
length
|
|
324
|
+
when :ratio
|
|
325
|
+
numerator, denominator = value
|
|
326
|
+
denominator.zero? ? 0 : (length * numerator) / denominator
|
|
327
|
+
else
|
|
328
|
+
length
|
|
329
|
+
end
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
# Ruby-idiomatic alias (TIMTOWTDI)
|
|
333
|
+
# Allows proc-like invocation: constraint.(100)
|
|
334
|
+
alias call apply
|
|
286
335
|
end
|
|
287
336
|
end
|
|
288
337
|
end
|
|
@@ -22,7 +22,7 @@ module RatatuiRuby
|
|
|
22
22
|
# Run the interactive demo from the terminal:
|
|
23
23
|
#
|
|
24
24
|
# ruby examples/widget_layout_split/app.rb
|
|
25
|
-
class Layout < Data.define(:direction, :constraints, :children, :flex)
|
|
25
|
+
class Layout < Data.define(:direction, :constraints, :children, :flex, :margin, :spacing)
|
|
26
26
|
##
|
|
27
27
|
# :attr_reader: direction
|
|
28
28
|
# Direction of the split.
|
|
@@ -81,6 +81,23 @@ module RatatuiRuby
|
|
|
81
81
|
# Flex: space evenly between elements.
|
|
82
82
|
FLEX_SPACE_EVENLY = :space_evenly
|
|
83
83
|
|
|
84
|
+
##
|
|
85
|
+
# :attr_reader: margin
|
|
86
|
+
# Margin around the layout area.
|
|
87
|
+
#
|
|
88
|
+
# Either a single <tt>Integer</tt> for uniform margin on all sides, or a
|
|
89
|
+
# <tt>Hash</tt> with <tt>:horizontal</tt> and <tt>:vertical</tt> keys.
|
|
90
|
+
#
|
|
91
|
+
# layout.margin # => 2
|
|
92
|
+
|
|
93
|
+
##
|
|
94
|
+
# :attr_reader: spacing
|
|
95
|
+
# Gap between segments (in cells).
|
|
96
|
+
#
|
|
97
|
+
# A positive integer that specifies the number of cells between each segment.
|
|
98
|
+
#
|
|
99
|
+
# layout.spacing # => 1
|
|
100
|
+
|
|
84
101
|
# Creates a new Layout.
|
|
85
102
|
#
|
|
86
103
|
# [direction]
|
|
@@ -91,7 +108,11 @@ module RatatuiRuby
|
|
|
91
108
|
# List of widgets to render (optional).
|
|
92
109
|
# [flex]
|
|
93
110
|
# Flex mode for spacing (default: <tt>:legacy</tt>).
|
|
94
|
-
|
|
111
|
+
# [margin]
|
|
112
|
+
# Edge margin in cells (default: <tt>0</tt>).
|
|
113
|
+
# [spacing]
|
|
114
|
+
# Gap between segments in cells (default: <tt>0</tt>).
|
|
115
|
+
def initialize(direction: :vertical, constraints: [], children: [], flex: :legacy, margin: 0, spacing: 0)
|
|
95
116
|
super
|
|
96
117
|
end
|
|
97
118
|
|
|
@@ -134,20 +155,105 @@ module RatatuiRuby
|
|
|
134
155
|
#++
|
|
135
156
|
# Returns an Array of <tt>Rect</tt> objects.
|
|
136
157
|
def self.split(area, direction: :vertical, constraints:, flex: :legacy)
|
|
137
|
-
#
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
158
|
+
# Coerce area to Rect for type safety (supports duck typing via _RectLike interface)
|
|
159
|
+
rect = case area
|
|
160
|
+
when Rect
|
|
161
|
+
area
|
|
162
|
+
when Hash
|
|
163
|
+
Rect.new(
|
|
164
|
+
x: Integer(area.fetch(:x, 0)),
|
|
165
|
+
y: Integer(area.fetch(:y, 0)),
|
|
166
|
+
width: Integer(area.fetch(:width, 0)),
|
|
167
|
+
height: Integer(area.fetch(:height, 0))
|
|
168
|
+
)
|
|
169
|
+
else
|
|
170
|
+
# Duck typing: accept any object responding to x, y, width, height
|
|
171
|
+
if area.respond_to?(:x) && area.respond_to?(:y) && area.respond_to?(:width) && area.respond_to?(:height)
|
|
172
|
+
# @type var rect_like: _RectLike
|
|
173
|
+
rect_like = area
|
|
174
|
+
Rect.new(x: rect_like.x, y: rect_like.y, width: rect_like.width, height: rect_like.height)
|
|
175
|
+
else
|
|
176
|
+
raise ArgumentError, "area must be a Rect, Hash, or respond to x/y/width/height, got #{area.class}"
|
|
177
|
+
end
|
|
147
178
|
end
|
|
148
|
-
raw_rects = _split(
|
|
179
|
+
raw_rects = _split(rect, direction, constraints, flex)
|
|
149
180
|
raw_rects.map { |r| Rect.new(x: r[:x], y: r[:y], width: r[:width], height: r[:height]) }
|
|
150
181
|
end
|
|
182
|
+
|
|
183
|
+
# Splits an area into multiple rectangles, returning both segments and spacers.
|
|
184
|
+
#
|
|
185
|
+
# Layout splitting returns only the content areas. But some designs need to
|
|
186
|
+
# render content in the gaps (dividers, separators, decorations).
|
|
187
|
+
#
|
|
188
|
+
# This method returns both the segments (content areas) and the spacers
|
|
189
|
+
# (gaps between segments) as separate arrays. The spacers are the Rects
|
|
190
|
+
# that represent the spacing between each segment.
|
|
191
|
+
#
|
|
192
|
+
# Use it to render custom separators or to calculate layout with spacing.
|
|
193
|
+
#
|
|
194
|
+
# [area]
|
|
195
|
+
# 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>.
|
|
196
|
+
# [direction]
|
|
197
|
+
# <tt>:vertical</tt> or <tt>:horizontal</tt> (default: <tt>:vertical</tt>).
|
|
198
|
+
# [constraints]
|
|
199
|
+
# Array of <tt>Constraint</tt> objects defining section sizes.
|
|
200
|
+
# [flex]
|
|
201
|
+
#--
|
|
202
|
+
# SPDX-SnippetBegin
|
|
203
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
204
|
+
# SPDX-License-Identifier: MIT-0
|
|
205
|
+
#++
|
|
206
|
+
# Flex mode for spacing (default: <tt>:legacy</tt>).
|
|
207
|
+
#
|
|
208
|
+
#--
|
|
209
|
+
# SPDX-SnippetEnd
|
|
210
|
+
#++
|
|
211
|
+
# Returns an Array of two Arrays: <tt>[segments, spacers]</tt>, each containing <tt>Rect</tt> objects.
|
|
212
|
+
#
|
|
213
|
+
# === Example
|
|
214
|
+
#
|
|
215
|
+
#--
|
|
216
|
+
# SPDX-SnippetBegin
|
|
217
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
218
|
+
# SPDX-License-Identifier: MIT-0
|
|
219
|
+
#++
|
|
220
|
+
# area = Rect.new(x: 0, y: 0, width: 100, height: 10)
|
|
221
|
+
# segments, spacers = Layout.split_with_spacers(
|
|
222
|
+
# area,
|
|
223
|
+
# direction: :horizontal,
|
|
224
|
+
# constraints: [Constraint.length(40), Constraint.length(40)],
|
|
225
|
+
# flex: :space_around
|
|
226
|
+
# )
|
|
227
|
+
# # segments: 2 Rects for content
|
|
228
|
+
# # spacers: Rects for gaps between/around segments
|
|
229
|
+
#--
|
|
230
|
+
# SPDX-SnippetEnd
|
|
231
|
+
#++
|
|
232
|
+
def self.split_with_spacers(area, direction: :vertical, constraints:, flex: :legacy)
|
|
233
|
+
# Coerce area to Rect for type safety
|
|
234
|
+
rect = case area
|
|
235
|
+
when Rect
|
|
236
|
+
area
|
|
237
|
+
when Hash
|
|
238
|
+
Rect.new(
|
|
239
|
+
x: Integer(area.fetch(:x, 0)),
|
|
240
|
+
y: Integer(area.fetch(:y, 0)),
|
|
241
|
+
width: Integer(area.fetch(:width, 0)),
|
|
242
|
+
height: Integer(area.fetch(:height, 0))
|
|
243
|
+
)
|
|
244
|
+
else
|
|
245
|
+
if area.respond_to?(:x) && area.respond_to?(:y) && area.respond_to?(:width) && area.respond_to?(:height)
|
|
246
|
+
rect_like = area
|
|
247
|
+
Rect.new(x: rect_like.x, y: rect_like.y, width: rect_like.width, height: rect_like.height)
|
|
248
|
+
else
|
|
249
|
+
raise ArgumentError, "area must be a Rect, Hash, or respond to x/y/width/height, got #{area.class}"
|
|
250
|
+
end
|
|
251
|
+
end
|
|
252
|
+
raw_segments, raw_spacers = _split_with_spacers(rect, direction, constraints, flex)
|
|
253
|
+
segments = raw_segments.map { |r| Rect.new(x: r[:x], y: r[:y], width: r[:width], height: r[:height]) }
|
|
254
|
+
spacers = raw_spacers.map { |r| Rect.new(x: r[:x], y: r[:y], width: r[:width], height: r[:height]) }
|
|
255
|
+
[segments, spacers]
|
|
256
|
+
end
|
|
151
257
|
end
|
|
152
258
|
end
|
|
153
259
|
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
|
+
# A position in terminal coordinates.
|
|
11
|
+
#
|
|
12
|
+
# Layout code passes x/y pairs between functions. Bundling them
|
|
13
|
+
# into separate variables is verbose and prone to ordering mistakes.
|
|
14
|
+
#
|
|
15
|
+
# This class wraps column and row into a single immutable object.
|
|
16
|
+
# Pass it around, destructure it, or convert from a Rect.
|
|
17
|
+
#
|
|
18
|
+
# Use it for cursor positioning, mouse coordinates, or anywhere
|
|
19
|
+
# you need to represent a single point on the terminal grid.
|
|
20
|
+
#
|
|
21
|
+
# === Example
|
|
22
|
+
#
|
|
23
|
+
#--
|
|
24
|
+
# SPDX-SnippetBegin
|
|
25
|
+
# SPDX-FileCopyrightText: 2026 Kerrick Long
|
|
26
|
+
# SPDX-License-Identifier: MIT-0
|
|
27
|
+
#++
|
|
28
|
+
# pos = Layout::Position.new(x: 10, y: 5)
|
|
29
|
+
# puts "Cursor at column #{pos.x}, row #{pos.y}"
|
|
30
|
+
#
|
|
31
|
+
# # Extract from a Rect
|
|
32
|
+
# rect = Layout::Rect.new(x: 10, y: 5, width: 80, height: 24)
|
|
33
|
+
# pos = rect.as_position # => Position(x: 10, y: 5)
|
|
34
|
+
#--
|
|
35
|
+
# SPDX-SnippetEnd
|
|
36
|
+
#++
|
|
37
|
+
class Position < Data.define(:x, :y)
|
|
38
|
+
##
|
|
39
|
+
# :attr_reader: x
|
|
40
|
+
# Column index (0-indexed from left edge).
|
|
41
|
+
|
|
42
|
+
##
|
|
43
|
+
# :attr_reader: y
|
|
44
|
+
# Row index (0-indexed from top edge).
|
|
45
|
+
|
|
46
|
+
# Creates a new Position.
|
|
47
|
+
#
|
|
48
|
+
# [x] Column index (Integer, coerced via +Integer()+).
|
|
49
|
+
# [y] Row index (Integer, coerced via +Integer()+).
|
|
50
|
+
def initialize(x: 0, y: 0)
|
|
51
|
+
super(x: Integer(x), y: Integer(y))
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -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
|
data/lib/ratatui_ruby/layout.rb
CHANGED
|
@@ -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"
|