charming 0.1.2 → 0.1.3

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 (68) hide show
  1. checksums.yaml +4 -4
  2. data/lib/charming/application.rb +3 -3
  3. data/lib/charming/controller/class_methods.rb +2 -2
  4. data/lib/charming/controller/command_palette.rb +2 -2
  5. data/lib/charming/controller/rendering.rb +2 -2
  6. data/lib/charming/controller/session_state.rb +1 -1
  7. data/lib/charming/generators/component_generator.rb +1 -1
  8. data/lib/charming/generators/templates/app/application.template +1 -1
  9. data/lib/charming/generators/templates/app/layout.template +3 -6
  10. data/lib/charming/generators/templates/app/view.template +1 -1
  11. data/lib/charming/generators/templates/component/component.rb.template +1 -1
  12. data/lib/charming/generators/templates/screen/view.rb.template +1 -1
  13. data/lib/charming/generators/templates/view/view.rb.template +1 -1
  14. data/lib/charming/internal/renderer/differential.rb +13 -5
  15. data/lib/charming/internal/terminal/tty_backend.rb +22 -2
  16. data/lib/charming/presentation/component.rb +3 -5
  17. data/lib/charming/presentation/components/activity_indicator.rb +173 -134
  18. data/lib/charming/presentation/components/command_palette.rb +94 -96
  19. data/lib/charming/presentation/components/command_palette_modal.rb +33 -0
  20. data/lib/charming/presentation/components/empty_state.rb +47 -49
  21. data/lib/charming/presentation/components/form/builder.rb +52 -54
  22. data/lib/charming/presentation/components/form/confirm.rb +49 -51
  23. data/lib/charming/presentation/components/form/field.rb +94 -96
  24. data/lib/charming/presentation/components/form/input.rb +53 -55
  25. data/lib/charming/presentation/components/form/note.rb +27 -29
  26. data/lib/charming/presentation/components/form/select.rb +84 -86
  27. data/lib/charming/presentation/components/form/textarea.rb +67 -69
  28. data/lib/charming/presentation/components/form.rb +120 -122
  29. data/lib/charming/presentation/components/keyboard_handler.rb +41 -43
  30. data/lib/charming/presentation/components/list.rb +123 -125
  31. data/lib/charming/presentation/components/markdown.rb +21 -23
  32. data/lib/charming/presentation/components/modal.rb +46 -48
  33. data/lib/charming/presentation/components/progressbar.rb +51 -53
  34. data/lib/charming/presentation/components/spinner.rb +40 -42
  35. data/lib/charming/presentation/components/table.rb +109 -111
  36. data/lib/charming/presentation/components/text_area.rb +219 -221
  37. data/lib/charming/presentation/components/text_input.rb +120 -122
  38. data/lib/charming/presentation/components/viewport.rb +218 -220
  39. data/lib/charming/presentation/layout/builder.rb +64 -66
  40. data/lib/charming/presentation/layout/overlay.rb +48 -50
  41. data/lib/charming/presentation/layout/pane.rb +122 -118
  42. data/lib/charming/presentation/layout/rect.rb +14 -16
  43. data/lib/charming/presentation/layout/screen_layout.rb +40 -42
  44. data/lib/charming/presentation/layout/split.rb +101 -103
  45. data/lib/charming/presentation/layout.rb +28 -30
  46. data/lib/charming/presentation/markdown/block_renderers.rb +94 -96
  47. data/lib/charming/presentation/markdown/inline_renderers.rb +52 -54
  48. data/lib/charming/presentation/markdown/render_context.rb +12 -14
  49. data/lib/charming/presentation/markdown/renderer.rb +84 -86
  50. data/lib/charming/presentation/markdown/syntax_highlighter.rb +57 -59
  51. data/lib/charming/presentation/markdown.rb +4 -6
  52. data/lib/charming/presentation/template_view.rb +22 -24
  53. data/lib/charming/presentation/templates/erb_handler.rb +4 -6
  54. data/lib/charming/presentation/templates.rb +47 -49
  55. data/lib/charming/presentation/ui/ansi_codes.rb +66 -68
  56. data/lib/charming/presentation/ui/ansi_slicer.rb +67 -69
  57. data/lib/charming/presentation/ui/border.rb +24 -26
  58. data/lib/charming/presentation/ui/border_painter.rb +37 -39
  59. data/lib/charming/presentation/ui/canvas.rb +59 -61
  60. data/lib/charming/presentation/ui/style.rb +173 -175
  61. data/lib/charming/presentation/ui/theme.rb +133 -135
  62. data/lib/charming/presentation/ui/width.rb +12 -14
  63. data/lib/charming/presentation/ui.rb +69 -71
  64. data/lib/charming/presentation/view.rb +103 -105
  65. data/lib/charming/runtime.rb +23 -10
  66. data/lib/charming/version.rb +1 -1
  67. data/lib/charming.rb +3 -2
  68. metadata +2 -1
@@ -3,269 +3,267 @@
3
3
  require "unicode/display_width"
4
4
 
5
5
  module Charming
6
- module Presentation
7
- module Components
8
- # Viewport is a scrollable region over multi-line content. Supports keyboard scrolling
9
- # (up/down/left/right, page up/down, home/end) and mouse interactions (scroll wheel and
10
- # click-to-position). Lines are clipped with ANSI awareness via `UI::ANSISlicer` so styled
11
- # text is preserved across horizontal scrolls. When `wrap:` is true, long lines are wrapped
12
- # to the configured *width* before scrolling.
13
- class Viewport < Component
14
- include KeyboardHandler
15
-
16
- # Matches an ANSI SGR escape sequence (e.g., "\e[31m" for red foreground).
17
- ANSI_PATTERN = /\e\[[0-9;]*m/
18
-
19
- # Maps scroll keys to the instance methods that perform them via KeyboardHandler.
20
- KEY_ACTIONS = {
21
- up: :scroll_up,
22
- down: :scroll_down,
23
- page_up: :page_up,
24
- page_down: :page_down,
25
- home: :scroll_home,
26
- end: :scroll_end,
27
- left: :scroll_left,
28
- right: :scroll_right
29
- }.freeze
30
-
31
- # The current top-visible row and left-visible column, respectively.
32
- attr_reader :offset, :column
33
-
34
- # *content* may be a string, an array of lines, or any object responding to `render`.
35
- # *width* and *height* constrain the visible window; *offset* is the top-visible row
36
- # and *column* is the left-visible column. *wrap* enables soft-wrapping of long lines.
37
- def initialize(content:, width: nil, height: nil, offset: 0, column: 0, wrap: false, keymap: :vim)
38
- super()
39
- @content = content
40
- @width = width
41
- @height = height
42
- @offset = offset
43
- @column = column
44
- @wrap = wrap
45
- @keymap = keymap
46
- clamp_position
47
- end
6
+ module Components
7
+ # Viewport is a scrollable region over multi-line content. Supports keyboard scrolling
8
+ # (up/down/left/right, page up/down, home/end) and mouse interactions (scroll wheel and
9
+ # click-to-position). Lines are clipped with ANSI awareness via `UI::ANSISlicer` so styled
10
+ # text is preserved across horizontal scrolls. When `wrap:` is true, long lines are wrapped
11
+ # to the configured *width* before scrolling.
12
+ class Viewport < Component
13
+ include KeyboardHandler
14
+
15
+ # Matches an ANSI SGR escape sequence (e.g., "\e[31m" for red foreground).
16
+ ANSI_PATTERN = /\e\[[0-9;]*m/
17
+
18
+ # Maps scroll keys to the instance methods that perform them via KeyboardHandler.
19
+ KEY_ACTIONS = {
20
+ up: :scroll_up,
21
+ down: :scroll_down,
22
+ page_up: :page_up,
23
+ page_down: :page_down,
24
+ home: :scroll_home,
25
+ end: :scroll_end,
26
+ left: :scroll_left,
27
+ right: :scroll_right
28
+ }.freeze
29
+
30
+ # The current top-visible row and left-visible column, respectively.
31
+ attr_reader :offset, :column
32
+
33
+ # *content* may be a string, an array of lines, or any object responding to `render`.
34
+ # *width* and *height* constrain the visible window; *offset* is the top-visible row
35
+ # and *column* is the left-visible column. *wrap* enables soft-wrapping of long lines.
36
+ def initialize(content:, width: nil, height: nil, offset: 0, column: 0, wrap: false, keymap: :vim)
37
+ super()
38
+ @content = content
39
+ @width = width
40
+ @height = height
41
+ @offset = offset
42
+ @column = column
43
+ @wrap = wrap
44
+ @keymap = keymap
45
+ clamp_position
46
+ end
48
47
 
49
- # Renders the visible window of content as a multi-line string.
50
- def render
51
- visible_lines.map { |line| render_line(line) }.join("\n")
52
- end
48
+ # Renders the visible window of content as a multi-line string.
49
+ def render
50
+ visible_lines.map { |line| render_line(line) }.join("\n")
51
+ end
53
52
 
54
- # Handles mouse events: scroll wheel adjusts the row offset, click moves the top
55
- # visible row to the clicked position. Returns :handled on success.
56
- def handle_mouse(event)
57
- return nil unless height
53
+ # Handles mouse events: scroll wheel adjusts the row offset, click moves the top
54
+ # visible row to the clicked position. Returns :handled on success.
55
+ def handle_mouse(event)
56
+ return nil unless height
58
57
 
59
- if event.scroll?
60
- scroll_delta = (event.button_name == :scroll_up) ? -1 : 1
61
- @offset += scroll_delta
62
- clamp_position
63
- return :handled
64
- end
58
+ if event.scroll?
59
+ scroll_delta = (event.button_name == :scroll_up) ? -1 : 1
60
+ @offset += scroll_delta
61
+ clamp_position
62
+ return :handled
63
+ end
65
64
 
66
- return nil unless event.click?
65
+ return nil unless event.click?
67
66
 
68
- clicked_row = event.y
69
- return nil if clicked_row < offset || clicked_row >= offset + viewport_height
67
+ clicked_row = event.y
68
+ return nil if clicked_row < offset || clicked_row >= offset + viewport_height
70
69
 
71
- @offset = clicked_row
72
- clamp_position
73
- :handled
74
- end
70
+ @offset = clicked_row
71
+ clamp_position
72
+ :handled
73
+ end
75
74
 
76
- private
75
+ private
77
76
 
78
- attr_reader :content, :width, :height
77
+ attr_reader :content, :width, :height
79
78
 
80
- # Scrolls the viewport up by one row.
81
- def scroll_up
82
- @offset -= 1
83
- clamp_position
84
- end
79
+ # Scrolls the viewport up by one row.
80
+ def scroll_up
81
+ @offset -= 1
82
+ clamp_position
83
+ end
85
84
 
86
- # Scrolls the viewport down by one row.
87
- def scroll_down
88
- @offset += 1
89
- clamp_position
90
- end
85
+ # Scrolls the viewport down by one row.
86
+ def scroll_down
87
+ @offset += 1
88
+ clamp_position
89
+ end
91
90
 
92
- # Scrolls up by one viewport page.
93
- def page_up
94
- @offset -= page_size
95
- clamp_position
96
- end
91
+ # Scrolls up by one viewport page.
92
+ def page_up
93
+ @offset -= page_size
94
+ clamp_position
95
+ end
97
96
 
98
- # Scrolls down by one viewport page.
99
- def page_down
100
- @offset += page_size
101
- clamp_position
102
- end
97
+ # Scrolls down by one viewport page.
98
+ def page_down
99
+ @offset += page_size
100
+ clamp_position
101
+ end
103
102
 
104
- # Scrolls to the top-left of the content.
105
- def scroll_home
106
- @offset = 0
107
- @column = 0
108
- end
103
+ # Scrolls to the top-left of the content.
104
+ def scroll_home
105
+ @offset = 0
106
+ @column = 0
107
+ end
109
108
 
110
- # Scrolls to the bottom-right of the content.
111
- def scroll_end
112
- @offset = max_offset
113
- @column = max_column
114
- end
109
+ # Scrolls to the bottom-right of the content.
110
+ def scroll_end
111
+ @offset = max_offset
112
+ @column = max_column
113
+ end
115
114
 
116
- # Scrolls one column left.
117
- def scroll_left
118
- @column -= 1
119
- clamp_position
120
- end
115
+ # Scrolls one column left.
116
+ def scroll_left
117
+ @column -= 1
118
+ clamp_position
119
+ end
121
120
 
122
- # Scrolls one column right.
123
- def scroll_right
124
- @column += 1
125
- clamp_position
126
- end
121
+ # Scrolls one column right.
122
+ def scroll_right
123
+ @column += 1
124
+ clamp_position
125
+ end
127
126
 
128
- # Clamps both the row offset and the column to their valid ranges.
129
- def clamp_position
130
- @offset = offset.clamp(0, max_offset)
131
- @column = column.clamp(0, max_column)
132
- end
127
+ # Clamps both the row offset and the column to their valid ranges.
128
+ def clamp_position
129
+ @offset = offset.clamp(0, max_offset)
130
+ @column = column.clamp(0, max_column)
131
+ end
133
132
 
134
- # Returns the slice of content lines visible in the current viewport, padded to *height*.
135
- def visible_lines
136
- lines = content_lines.slice(offset, viewport_height) || []
137
- return lines unless height
133
+ # Returns the slice of content lines visible in the current viewport, padded to *height*.
134
+ def visible_lines
135
+ lines = content_lines.slice(offset, viewport_height) || []
136
+ return lines unless height
138
137
 
139
- lines + Array.new([height - lines.length, 0].max, "")
140
- end
138
+ lines + Array.new([height - lines.length, 0].max, "")
139
+ end
141
140
 
142
- # Renders a single line according to the configured width and wrap mode: clips to the
143
- # visible column window when not wrapping, otherwise wraps the line to the width.
144
- def render_line(line)
145
- return line unless width
146
- return pad_line(line, width) if wrap?
141
+ # Renders a single line according to the configured width and wrap mode: clips to the
142
+ # visible column window when not wrapping, otherwise wraps the line to the width.
143
+ def render_line(line)
144
+ return line unless width
145
+ return pad_line(line, width) if wrap?
147
146
 
148
- pad_line(clip_line(line), width)
149
- end
147
+ pad_line(clip_line(line), width)
148
+ end
150
149
 
151
- # Clips *line* to the visible column window while preserving active ANSI styling.
152
- def clip_line(line)
153
- clipped = clip_tokens(line.to_s)
154
- needs_reset?(clipped) ? "#{clipped}\e[0m" : clipped
155
- end
150
+ # Clips *line* to the visible column window while preserving active ANSI styling.
151
+ def clip_line(line)
152
+ clipped = clip_tokens(line.to_s)
153
+ needs_reset?(clipped) ? "#{clipped}\e[0m" : clipped
154
+ end
156
155
 
157
- # Walks *line* token-by-token, copying ANSI escapes through and emitting only the
158
- # characters that fall inside the visible column window.
159
- def clip_tokens(line)
160
- state = {cursor: 0, output: +""}
161
- line.scan(/#{ANSI_PATTERN}|./mo) do |token|
162
- ansi?(token) ? append_ansi(state, token) : append_character(state, token)
163
- end
164
- state.fetch(:output)
156
+ # Walks *line* token-by-token, copying ANSI escapes through and emitting only the
157
+ # characters that fall inside the visible column window.
158
+ def clip_tokens(line)
159
+ state = {cursor: 0, output: +""}
160
+ line.scan(/#{ANSI_PATTERN}|./mo) do |token|
161
+ ansi?(token) ? append_ansi(state, token) : append_character(state, token)
165
162
  end
163
+ state.fetch(:output)
164
+ end
166
165
 
167
- # Appends an ANSI escape token to the output buffer unchanged.
168
- def append_ansi(state, token)
169
- state.fetch(:output) << token
170
- end
166
+ # Appends an ANSI escape token to the output buffer unchanged.
167
+ def append_ansi(state, token)
168
+ state.fetch(:output) << token
169
+ end
171
170
 
172
- # Appends a single character token to the output buffer when it falls inside the
173
- # visible column window, advancing the visual cursor.
174
- def append_character(state, char)
175
- char_width = Unicode::DisplayWidth.of(char)
176
- cursor = state.fetch(:cursor)
177
- state.fetch(:output) << char if visible?(cursor, char_width)
178
- state[:cursor] = cursor + char_width
179
- end
171
+ # Appends a single character token to the output buffer when it falls inside the
172
+ # visible column window, advancing the visual cursor.
173
+ def append_character(state, char)
174
+ char_width = Unicode::DisplayWidth.of(char)
175
+ cursor = state.fetch(:cursor)
176
+ state.fetch(:output) << char if visible?(cursor, char_width)
177
+ state[:cursor] = cursor + char_width
178
+ end
180
179
 
181
- # True when the character at *cursor* (with the given display *char_width*) is within
182
- # the visible column window.
183
- def visible?(cursor, char_width)
184
- cursor >= column && cursor + char_width <= column + width
185
- end
180
+ # True when the character at *cursor* (with the given display *char_width*) is within
181
+ # the visible column window.
182
+ def visible?(cursor, char_width)
183
+ cursor >= column && cursor + char_width <= column + width
184
+ end
186
185
 
187
- # True when *value* contains ANSI codes but does not end with a reset — needed because
188
- # the clip may truncate styling in the middle of a styled run.
189
- def needs_reset?(value)
190
- value.match?(ANSI_PATTERN) && !value.end_with?("\e[0m")
191
- end
186
+ # True when *value* contains ANSI codes but does not end with a reset — needed because
187
+ # the clip may truncate styling in the middle of a styled run.
188
+ def needs_reset?(value)
189
+ value.match?(ANSI_PATTERN) && !value.end_with?("\e[0m")
190
+ end
192
191
 
193
- # Pads *line* to *target_width* with trailing spaces, leaving the line itself unchanged.
194
- def pad_line(line, target_width)
195
- line + (" " * [target_width - UI::Width.measure(line), 0].max)
196
- end
192
+ # Pads *line* to *target_width* with trailing spaces, leaving the line itself unchanged.
193
+ def pad_line(line, target_width)
194
+ line + (" " * [target_width - UI::Width.measure(line), 0].max)
195
+ end
197
196
 
198
- # Returns the content lines, wrapped to *width* when wrap is enabled.
199
- def content_lines
200
- return wrapped_content_lines if wrap?
197
+ # Returns the content lines, wrapped to *width* when wrap is enabled.
198
+ def content_lines
199
+ return wrapped_content_lines if wrap?
201
200
 
202
- rendered_content.lines(chomp: true)
203
- end
201
+ rendered_content.lines(chomp: true)
202
+ end
204
203
 
205
- # Wraps the content to *width* via UI::visible_slice, returning an array of wrapped lines.
206
- def wrapped_content_lines
207
- rendered_content.lines(chomp: true).flat_map { |line| wrap_line(line) }
208
- end
204
+ # Wraps the content to *width* via UI::visible_slice, returning an array of wrapped lines.
205
+ def wrapped_content_lines
206
+ rendered_content.lines(chomp: true).flat_map { |line| wrap_line(line) }
207
+ end
209
208
 
210
- # Wraps a single *line* into chunks of *width* display columns.
211
- def wrap_line(line)
212
- line_width = UI::Width.measure(line)
213
- return [""] if line_width.zero?
214
-
215
- start_column = 0
216
- out = []
217
- while start_column < line_width
218
- out << UI.visible_slice(line, start_column, width)
219
- start_column += width
220
- end
221
- out
222
- end
209
+ # Wraps a single *line* into chunks of *width* display columns.
210
+ def wrap_line(line)
211
+ line_width = UI::Width.measure(line)
212
+ return [""] if line_width.zero?
223
213
 
224
- # Returns the rendered content string, calling `render.to_s` on the content object when
225
- # it responds to render.
226
- def rendered_content
227
- content.respond_to?(:render) ? content.render.to_s : content.to_s
214
+ start_column = 0
215
+ out = []
216
+ while start_column < line_width
217
+ out << UI.visible_slice(line, start_column, width)
218
+ start_column += width
228
219
  end
220
+ out
221
+ end
229
222
 
230
- # Returns the visible row count (the configured *height* or the content's line count).
231
- def viewport_height
232
- height || content_lines.length
233
- end
223
+ # Returns the rendered content string, calling `render.to_s` on the content object when
224
+ # it responds to render.
225
+ def rendered_content
226
+ content.respond_to?(:render) ? content.render.to_s : content.to_s
227
+ end
234
228
 
235
- # Returns the number of rows to advance on a page up/down: at least 1, otherwise the
236
- # viewport height.
237
- def page_size
238
- [viewport_height, 1].max
239
- end
229
+ # Returns the visible row count (the configured *height* or the content's line count).
230
+ def viewport_height
231
+ height || content_lines.length
232
+ end
240
233
 
241
- # Returns the maximum allowed row offset (so the bottom of the content is reachable).
242
- def max_offset
243
- [content_lines.length - viewport_height, 0].max
244
- end
234
+ # Returns the number of rows to advance on a page up/down: at least 1, otherwise the
235
+ # viewport height.
236
+ def page_size
237
+ [viewport_height, 1].max
238
+ end
245
239
 
246
- # Returns the maximum allowed column offset. Returns 0 when wrapping is enabled or
247
- # when no width is configured.
248
- def max_column
249
- return 0 if wrap?
250
- return 0 unless width
240
+ # Returns the maximum allowed row offset (so the bottom of the content is reachable).
241
+ def max_offset
242
+ [content_lines.length - viewport_height, 0].max
243
+ end
251
244
 
252
- [content_width - width, 0].max
253
- end
245
+ # Returns the maximum allowed column offset. Returns 0 when wrapping is enabled or
246
+ # when no width is configured.
247
+ def max_column
248
+ return 0 if wrap?
249
+ return 0 unless width
254
250
 
255
- # Returns the maximum display width across all content lines.
256
- def content_width
257
- content_lines.map { |line| UI::Width.measure(line) }.max || 0
258
- end
251
+ [content_width - width, 0].max
252
+ end
259
253
 
260
- # True when *token* is an ANSI escape sequence.
261
- def ansi?(token)
262
- token.match?(ANSI_PATTERN)
263
- end
254
+ # Returns the maximum display width across all content lines.
255
+ def content_width
256
+ content_lines.map { |line| UI::Width.measure(line) }.max || 0
257
+ end
264
258
 
265
- # True when soft-wrapping is enabled and a positive width is configured.
266
- def wrap?
267
- @wrap && width&.positive?
268
- end
259
+ # True when *token* is an ANSI escape sequence.
260
+ def ansi?(token)
261
+ token.match?(ANSI_PATTERN)
262
+ end
263
+
264
+ # True when soft-wrapping is enabled and a positive width is configured.
265
+ def wrap?
266
+ @wrap && width&.positive?
269
267
  end
270
268
  end
271
269
  end
@@ -1,85 +1,83 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Charming
4
- module Presentation
5
- module Layout
6
- # Builder turns a declarative `screen_layout { ... }` block into a layout tree of
7
- # ScreenLayout Split Pane nodes. The block DSL is `split(direction) { ... }`,
8
- # `pane(name) { ... }`, and `overlay { ... }`. Unknown method calls in the block are
9
- # forwarded to the underlying view so view helpers (e.g., `text`) work inside layout blocks.
10
- class Builder
11
- # Builds the layout tree by evaluating the *block* in the builder's context.
12
- # Returns the root ScreenLayout node.
13
- def self.build(screen:, view:, background: nil, &)
14
- new(screen: screen, view: view, background: background).build(&)
15
- end
4
+ module Layout
5
+ # Builder turns a declarative `screen_layout { ... }` block into a layout tree of
6
+ # ScreenLayout Split Pane nodes. The block DSL is `split(direction) { ... }`,
7
+ # `pane(name) { ... }`, and `overlay { ... }`. Unknown method calls in the block are
8
+ # forwarded to the underlying view so view helpers (e.g., `text`) work inside layout blocks.
9
+ class Builder
10
+ # Builds the layout tree by evaluating the *block* in the builder's context.
11
+ # Returns the root ScreenLayout node.
12
+ def self.build(screen:, view:, background: nil, &)
13
+ new(screen: screen, view: view, background: background).build(&)
14
+ end
16
15
 
17
- def initialize(screen:, view:, background: nil)
18
- @view = view
19
- @root = ScreenLayout.new(screen: screen, background: background)
20
- @stack = [@root]
21
- end
16
+ def initialize(screen:, view:, background: nil)
17
+ @view = view
18
+ @root = ScreenLayout.new(screen: screen, background: background)
19
+ @stack = [@root]
20
+ end
22
21
 
23
- # Evaluates *block* in the builder's context, then returns the root ScreenLayout node.
24
- def build(&)
25
- instance_eval(&) if block_given?
26
- root
27
- end
22
+ # Evaluates *block* in the builder's context, then returns the root ScreenLayout node.
23
+ def build(&)
24
+ instance_eval(&) if block_given?
25
+ root
26
+ end
28
27
 
29
- # Adds a Split node to the current scope. *direction* is `:horizontal` or `:vertical`.
30
- # *gap* (in cells) is inserted between children. Additional *options* are forwarded
31
- # to Split. The block, if given, is evaluated in the split's scope (for nested children).
32
- def split(direction, gap: 0, **options, &)
33
- node = Split.new(direction: direction, gap: gap, **options)
34
- append(node)
35
- within(node, &)
36
- node
37
- end
28
+ # Adds a Split node to the current scope. *direction* is `:horizontal` or `:vertical`.
29
+ # *gap* (in cells) is inserted between children. Additional *options* are forwarded
30
+ # to Split. The block, if given, is evaluated in the split's scope (for nested children).
31
+ def split(direction, gap: 0, **options, &)
32
+ node = Split.new(direction: direction, gap: gap, **options)
33
+ append(node)
34
+ within(node, &)
35
+ node
36
+ end
38
37
 
39
- # Adds a Pane leaf node to the current scope. *name* (optional) is the focus slot name;
40
- # *content* (or a *block*) is the body. *options* are forwarded to Pane.
41
- def pane(name = nil, content = nil, **options, &block)
42
- node = Pane.new(name: name, content: content, block: block, view: view, **options)
43
- append(node)
44
- node
45
- end
38
+ # Adds a Pane leaf node to the current scope. *name* (optional) is the focus slot name;
39
+ # *content* (or a *block*) is the body. *options* are forwarded to Pane.
40
+ def pane(name = nil, content = nil, **options, &block)
41
+ node = Pane.new(name: name, content: content, block: block, view: view, **options)
42
+ append(node)
43
+ node
44
+ end
46
45
 
47
- # Adds an Overlay node to the root ScreenLayout. *top* and *left* default to :center.
48
- # The block, if given, is evaluated in the view's context.
49
- def overlay(content = nil, top: :center, left: :center, **options, &block)
50
- root.add_overlay(Overlay.new(content: content, block: block, view: view, top: top, left: left, **options))
51
- end
46
+ # Adds an Overlay node to the root ScreenLayout. *top* and *left* default to :center.
47
+ # The block, if given, is evaluated in the view's context.
48
+ def overlay(content = nil, top: :center, left: :center, **options, &block)
49
+ root.add_overlay(Overlay.new(content: content, block: block, view: view, top: top, left: left, **options))
50
+ end
52
51
 
53
- # Forwards unknown method calls to the underlying view so helpers like `text` work
54
- # inside layout blocks.
55
- def respond_to_missing?(name, include_private = false)
56
- view.respond_to?(name, include_private) || super
57
- end
52
+ # Forwards unknown method calls to the underlying view so helpers like `text` work
53
+ # inside layout blocks.
54
+ def respond_to_missing?(name, include_private = false)
55
+ view.respond_to?(name, include_private) || super
56
+ end
58
57
 
59
- def method_missing(name, ...)
60
- return view.__send__(name, ...) if view.respond_to?(name, true)
58
+ def method_missing(name, ...)
59
+ return view.__send__(name, ...) if view.respond_to?(name, true)
61
60
 
62
- super
63
- end
61
+ super
62
+ end
64
63
 
65
- private
64
+ private
66
65
 
67
- attr_reader :root, :stack, :view
66
+ attr_reader :root, :stack, :view
68
67
 
69
- # Appends *node* to the topmost scope on the stack.
70
- def append(node)
71
- stack.last.add_child(node)
72
- end
68
+ # Appends *node* to the topmost scope on the stack.
69
+ def append(node)
70
+ stack.last.add_child(node)
71
+ end
73
72
 
74
- # Pushes *node* onto the stack, evaluates *block* in the builder's context, then pops it.
75
- def within(node, &)
76
- return unless block_given?
73
+ # Pushes *node* onto the stack, evaluates *block* in the builder's context, then pops it.
74
+ def within(node, &)
75
+ return unless block_given?
77
76
 
78
- stack.push(node)
79
- instance_eval(&)
80
- ensure
81
- stack.pop
82
- end
77
+ stack.push(node)
78
+ instance_eval(&)
79
+ ensure
80
+ stack.pop
83
81
  end
84
82
  end
85
83
  end