tuile 0.10.0 → 0.12.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +109 -64
- data/DECISIONS.md +1299 -22
- data/README.md +18 -13
- data/TERMINOLOGY.md +61 -0
- data/book/02-repaint.md +1 -1
- data/book/03-layout.md +154 -9
- data/book/05-focus.md +2 -0
- data/book/06-theming.md +1 -1
- data/book/07-components.md +202 -37
- data/book/README.md +3 -1
- data/examples/file_commander.rb +5 -4
- data/examples/sampler.rb +320 -133
- data/ideas/arrow-key-navigation.md +205 -0
- data/ideas/new-components.md +26 -12
- data/lib/tuile/buffer.rb +7 -7
- data/lib/tuile/component/big_decimal_field.rb +199 -0
- data/lib/tuile/component/button.rb +1 -1
- data/lib/tuile/component/checkbox.rb +11 -10
- data/lib/tuile/component/checkbox_group.rb +31 -26
- data/lib/tuile/component/combo_box.rb +18 -33
- data/lib/tuile/component/float_field.rb +161 -0
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/layout/box.rb +316 -0
- data/lib/tuile/component/layout/horizontal.rb +40 -0
- data/lib/tuile/component/layout/vertical.rb +41 -0
- data/lib/tuile/component/layout.rb +149 -1
- data/lib/tuile/component/list.rb +291 -216
- data/lib/tuile/component/list_dropdown.rb +82 -24
- data/lib/tuile/component/notification.rb +317 -0
- data/lib/tuile/component/picker_window.rb +3 -3
- data/lib/tuile/component/popup.rb +8 -10
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +251 -0
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +79 -273
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_view.rb +191 -177
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +5 -5
- data/lib/tuile/screen.rb +1 -1
- data/lib/tuile/styled_string.rb +25 -15
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/lib/tuile.rb +4 -0
- data/sig/tuile.rbs +1670 -406
- metadata +11 -1
data/README.md
CHANGED
|
@@ -49,6 +49,12 @@ gem "tuile", git: "https://github.com/mvysny/tuile.git"
|
|
|
49
49
|
|
|
50
50
|
Tuile requires Ruby 3.3+.
|
|
51
51
|
|
|
52
|
+
One component — `Component::BigDecimalField` — additionally needs the
|
|
53
|
+
`bigdecimal` gem, which Tuile deliberately does *not* depend on (it has been a
|
|
54
|
+
bundled gem since Ruby 3.4, so Bundler no longer puts it on the load path for
|
|
55
|
+
free). Add `gem "bigdecimal"` to your Gemfile if you use that field; nothing
|
|
56
|
+
else in Tuile loads it.
|
|
57
|
+
|
|
52
58
|
## Documentation
|
|
53
59
|
|
|
54
60
|
- **[The Tuile guide](book/README.md)** teaches Tuile cover to cover — the
|
|
@@ -126,7 +132,7 @@ bytes sent to the terminal. There is no clipping in between.
|
|
|
126
132
|
`cursor_position` (e.g. into a focused text field).
|
|
127
133
|
|
|
128
134
|
Components never write escape sequences to the terminal. They paint styled
|
|
129
|
-
cells into a back buffer (`Tuile::Buffer`) via `
|
|
135
|
+
cells into a back buffer (`Tuile::Buffer`) via `set_text` / `fill` /
|
|
130
136
|
`set_char`. When the pass finishes, `Buffer#flush` emits the **minimal diff**
|
|
131
137
|
— only the cells that actually changed since the last flush — wrapped in one
|
|
132
138
|
synchronized-output batch. That is what keeps repaint flicker-free on any
|
|
@@ -145,7 +151,7 @@ of its own and positions its children within its rect.
|
|
|
145
151
|
|
|
146
152
|
`Tuile::Screen#run_event_loop` reads keys and mouse events on a worker thread,
|
|
147
153
|
funnels them through `Tuile::EventQueue`, and processes them on the main
|
|
148
|
-
thread. **All** UI mutations — `rect=`, `content=`, `
|
|
154
|
+
thread. **All** UI mutations — `rect=`, `content=`, `items=`, `invalidate`,
|
|
149
155
|
`screen.focused=` — must run on that thread. Most UI methods will raise
|
|
150
156
|
`"UI lock not held"` if you violate this.
|
|
151
157
|
|
|
@@ -380,7 +386,7 @@ Tuile::ThemeDef.default = APP_THEME # every Screen.fake now carries it
|
|
|
380
386
|
|
|
381
387
|
Built-in components read `screen.theme` at paint time, so their accents
|
|
382
388
|
restyle automatically. Content you rendered yourself does not: a
|
|
383
|
-
`StyledString` stored in `Label#text` / `List#lines
|
|
389
|
+
`StyledString` stored in `Label#text` / `List#lines=` / `TextView#text`
|
|
384
390
|
has its colors baked in at construction, and only your app knows which of
|
|
385
391
|
those were theme-derived (as opposed to inherent to the data — log-level
|
|
386
392
|
colors, say). `Component#on_theme_changed` fires on every attached
|
|
@@ -472,15 +478,17 @@ focusable; focus delegates to content (or footer when active).
|
|
|
472
478
|
|
|
473
479
|
### `Component::List`
|
|
474
480
|
|
|
475
|
-
A scrollable list of
|
|
481
|
+
A scrollable list of items — one row each, rendered by a `renderer` — with
|
|
482
|
+
optional cursor and scrollbar. `lines=` is the shortcut for items that are
|
|
483
|
+
their own rendering.
|
|
476
484
|
|
|
477
485
|
```ruby
|
|
478
486
|
list = Tuile::Component::List.new
|
|
479
487
|
list.lines = ["alpha", "beta", "gamma"]
|
|
480
488
|
list.cursor = Tuile::Component::List::Cursor.new
|
|
481
|
-
list.on_item_chosen = ->(index,
|
|
482
|
-
list.auto_scroll = true # auto-scroll to bottom
|
|
483
|
-
list.
|
|
489
|
+
list.on_item_chosen = ->(index, item) { Tuile.logger.info("picked #{item}") }
|
|
490
|
+
list.auto_scroll = true # auto-scroll to bottom as the list grows
|
|
491
|
+
list.lines = list.items + ["delta"] # no appenders: assign the items whole
|
|
484
492
|
```
|
|
485
493
|
|
|
486
494
|
Cursor variants:
|
|
@@ -493,8 +501,8 @@ Cursor variants:
|
|
|
493
501
|
|
|
494
502
|
Pressing Enter or left-clicking an item fires `on_item_chosen(index, line)`.
|
|
495
503
|
|
|
496
|
-
Key API: `
|
|
497
|
-
`auto_scroll=`, `scrollbar_visibility=`, `on_item_chosen`,
|
|
504
|
+
Key API: `items=`, `renderer=`, `lines=`, `build_lines`, `cursor=`,
|
|
505
|
+
`scroll_top_row=`, `auto_scroll=`, `scrollbar_visibility=`, `on_item_chosen`,
|
|
498
506
|
`select_next` / `select_prev` (search).
|
|
499
507
|
|
|
500
508
|
### `Component::TextField`
|
|
@@ -528,10 +536,7 @@ drawn on top of the tiled content; multiple popups stack.
|
|
|
528
536
|
```ruby
|
|
529
537
|
window = Tuile::Component::Window.new("Help")
|
|
530
538
|
window.content = help_list
|
|
531
|
-
Tuile::Component::Popup.
|
|
532
|
-
# or, equivalently:
|
|
533
|
-
popup = Tuile::Component::Popup.new(content: window)
|
|
534
|
-
popup.open
|
|
539
|
+
popup = Tuile::Component::Popup.new(content: window, size: Tuile::Fraction::HALF).open
|
|
535
540
|
# popup.close, popup.open?
|
|
536
541
|
```
|
|
537
542
|
|
data/TERMINOLOGY.md
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# TERMINOLOGY.md
|
|
2
|
+
|
|
3
|
+
Tuile's house vocabulary — one line per term, looked up by word.
|
|
4
|
+
|
|
5
|
+
This file owns **definitions only**. The *rules that bite* live in AGENTS.md
|
|
6
|
+
("Nomenclature" and the sections each word belongs to); the *why we chose a word
|
|
7
|
+
and not its synonym* lives in DECISIONS.md (`D-scroll-nomenclature` for the
|
|
8
|
+
row/line/item split); the *concepts* live in the book. When a definition here
|
|
9
|
+
needs a paragraph of justification, that paragraph belongs in one of those three.
|
|
10
|
+
|
|
11
|
+
## The grid
|
|
12
|
+
|
|
13
|
+
| term | means |
|
|
14
|
+
|---|---|
|
|
15
|
+
| **row** | one row of the terminal grid — the framework's only word for it. A wrapped unit of text *is* a row; wrapping is what turns text into rows. |
|
|
16
|
+
| **column** | one cell-column of the terminal grid; the unit `display_width` counts. |
|
|
17
|
+
| **cell** | one grid position: a grapheme plus a {Tuile::StyledString::Style}, in {Tuile::Buffer}. |
|
|
18
|
+
| **glyph** | what the terminal draws in one or more cells. Ambiguous-width glyphs count as **one** column (the bet in `D-ambiguous-width`). |
|
|
19
|
+
| **cluster** | a grapheme cluster — the unit measurement, slicing, caret motion and deletion all work in. Never `each_char`. |
|
|
20
|
+
| **row_in_viewport** | a row measured `0...rect.height`, i.e. relative to a component's own rect. |
|
|
21
|
+
| **scroll_top_row** | the content row currently sitting at the top of the viewport. |
|
|
22
|
+
| **viewport_rows** | how many rows of content are visible — always `rect.height`; kept private, since `rect.height` is the public form. |
|
|
23
|
+
| **row_count** | how many rows the wrapped content occupies. Public on `TextArea` (with `caret_row`, its companion); also on the private `WrappedText` and as `VerticalScrollBar.new(row_count:)`. Not on `TextView` / `List`, which have no caller for it. |
|
|
24
|
+
| **caret_row** | the row a text input's caret sits in, counted from the content's first row. `TextArea` only. |
|
|
25
|
+
| **extent** | the sub-rect a one-row caption widget actually occupies (`min(caption width + 4, rect.width)`), used for its highlight and hit test — narrower than its `rect`. |
|
|
26
|
+
|
|
27
|
+
**Space rule 1.** An object with only one row space leaves `row` unqualified:
|
|
28
|
+
{Tuile::Buffer} *is* the grid, so its rows are screen rows;
|
|
29
|
+
`TextArea::WrappedText` is content, so its rows are content rows.
|
|
30
|
+
|
|
31
|
+
**Space rule 2.** A component holding both spaces qualifies the viewport one
|
|
32
|
+
(`row_in_viewport`); its unqualified `row` and its `scroll_top_row` are
|
|
33
|
+
content-space.
|
|
34
|
+
|
|
35
|
+
## Text and content
|
|
36
|
+
|
|
37
|
+
| term | means |
|
|
38
|
+
|---|---|
|
|
39
|
+
| **line** | a `\n`-delimited unit of a String — exactly what `String#lines` returns. **Never a coordinate.** |
|
|
40
|
+
| **line_count** | a count of `\n` units (`TextView::Region#line_count`). Never a row count. |
|
|
41
|
+
| **item** | a domain object a widget holds and renders — `List#items`, and the enum widgets above it. |
|
|
42
|
+
| **renderer** | the `item -> row` proc a generic component uses to render an item it knows nothing about. |
|
|
43
|
+
| **item_count** / **item_index** | how a `List::Cursor` counts and addresses; equal to a row count in a `List`, but the cursor indexes *items*. |
|
|
44
|
+
| **text** | the user-editable **value** of an input ({Tuile::Component::HasValue}, aliased as `text` on `AbstractStringField`). |
|
|
45
|
+
| **caption** | app-authored **chrome** text ({Tuile::Component::HasCaption}) — a `Window` title, a `Button` label. Never a value. |
|
|
46
|
+
| **chrome** | framework- or app-authored decoration around content: captions, borders, footers, the status bar. |
|
|
47
|
+
| **caret** | the index into an input's `text` where editing happens; always on a cluster boundary. Distinct from the *cursor*. |
|
|
48
|
+
|
|
49
|
+
## Tree, paint and theme
|
|
50
|
+
|
|
51
|
+
| term | means |
|
|
52
|
+
|---|---|
|
|
53
|
+
| **component** | a node of the UI tree ({Tuile::Component}); the only thing that paints. |
|
|
54
|
+
| **tile** / **tiled** | the non-popup part of the tree — `ScreenPane#content` and its descendants. Also *to tile*: to cover a rect completely. |
|
|
55
|
+
| **attached** | reachable from a {Tuile::ScreenPane} via the parent chain — the one axis `attached?` consults. |
|
|
56
|
+
| **slot** | a named child a container holds by identity (`content`, `footer`) as well as in `children`. |
|
|
57
|
+
| **invalidate** | record a component as needing repaint; the loop coalesces and repaints once per tick. |
|
|
58
|
+
| **cursor** | *(two senses, both live)* the hardware terminal cursor (`Screen#cursor_position`), and a `List::Cursor` — the selection position within a list. |
|
|
59
|
+
| **well** | the explicit background an input paints over its whole rect (`Theme#input_bg_color` / `#active_bg_color`), which opts it out of `bg_color` inheritance. |
|
|
60
|
+
| **token** | a semantic colour name on {Tuile::Theme} — an accent, never a global fg/bg. |
|
|
61
|
+
| **scheme** | `:dark` or `:light`; a {Tuile::ThemeDef} pairs one {Tuile::Theme} per scheme. |
|
data/book/02-repaint.md
CHANGED
|
@@ -50,7 +50,7 @@ cells into the screen's **back buffer** — a {Tuile::Buffer}, an in-memory
|
|
|
50
50
|
grid of styled cells mirroring the terminal — through three methods:
|
|
51
51
|
|
|
52
52
|
```ruby
|
|
53
|
-
screen.buffer.
|
|
53
|
+
screen.buffer.set_text(x, y, styled_string) # a run of text
|
|
54
54
|
screen.buffer.set_char(x, y, grapheme, style) # one cell
|
|
55
55
|
screen.buffer.fill(rect, style) # a blank region
|
|
56
56
|
```
|
data/book/03-layout.md
CHANGED
|
@@ -125,6 +125,15 @@ complex TUIs — tmux, neovim's splits, k9s, lazygit, htop — are all
|
|
|
125
125
|
them needs flex grow/shrink/wrap/basis or a constraint solve. The
|
|
126
126
|
hardest real terminal UIs already live comfortably inside "simple."
|
|
127
127
|
|
|
128
|
+
Be precise about what that validates, though: it's TUI *app architecture*,
|
|
129
|
+
not TUI *framework feature lists*. Several terminal frameworks do ship a
|
|
130
|
+
full engine — Textual has CSS, Ink embeds Yoga (the flexbox engine React
|
|
131
|
+
Native uses), ratatui runs a real Cassowary solver. The reason isn't that
|
|
132
|
+
terminals need one; it's that those frameworks never hand you a rectangle,
|
|
133
|
+
so an engine is the only way their users can lay anything out. Tuile hands
|
|
134
|
+
you coordinates, which is what makes richer layout *optional* here —
|
|
135
|
+
available where it helps, declinable everywhere else.
|
|
136
|
+
|
|
128
137
|
It's stronger than "simple happens to work," though. Importing a CSS-like
|
|
129
138
|
system would be *actively worse* on a terminal, for three concrete
|
|
130
139
|
reasons:
|
|
@@ -204,6 +213,146 @@ That's the whole "responsive" story: plain Ruby, recomputed on a
|
|
|
204
213
|
discrete resize event. No breakpoint DSL, no media queries — just the
|
|
205
214
|
arithmetic you'd write anyway.
|
|
206
215
|
|
|
216
|
+
## Stacks without the arithmetic: `Vertical` and `Horizontal`
|
|
217
|
+
|
|
218
|
+
`Absolute` is the right tool for genuinely two-dimensional geometry, and
|
|
219
|
+
tedious for the most common shape in any app: a stack. So Tuile ships two
|
|
220
|
+
*box* layouts that do that arithmetic for you. You declare what extent each
|
|
221
|
+
child should get, and the box hands down rectangles through the very same
|
|
222
|
+
`rect=`:
|
|
223
|
+
|
|
224
|
+
```ruby
|
|
225
|
+
form = Tuile::Component::Layout::Vertical.new(spacing: 1)
|
|
226
|
+
form.add(prompt, Tuile::Component::Layout::Fixed[3]) # 3 rows
|
|
227
|
+
form.add(field, Tuile::Component::Layout::Fixed[1]) # 1 row
|
|
228
|
+
form.add(log, Tuile::Component::Layout::Expand[1]) # …all that's left
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
`Horizontal` is the same with the axes swapped — the constraint is a width,
|
|
232
|
+
and `Expand` claims the rest of the row:
|
|
233
|
+
|
|
234
|
+
```ruby
|
|
235
|
+
split = Tuile::Component::Layout::Horizontal.new
|
|
236
|
+
split.add(sidebar, Tuile::Component::Layout::Fixed[30])
|
|
237
|
+
split.add(main, Tuile::Component::Layout::Expand[1])
|
|
238
|
+
```
|
|
239
|
+
|
|
240
|
+
Inside a subclass the constraint names need no prefix, since they live on
|
|
241
|
+
`Layout`, an ancestor:
|
|
242
|
+
|
|
243
|
+
```ruby
|
|
244
|
+
class LoginForm < Tuile::Component::Layout::Vertical
|
|
245
|
+
def initialize
|
|
246
|
+
super(spacing: 1, padding: Insets[top: 1])
|
|
247
|
+
add(@user = Tuile::Component::TextField.new, Fixed[1], cross: Fixed[30])
|
|
248
|
+
add(@log = Tuile::Component::TextView.new, Expand[1])
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
### The three constraints
|
|
254
|
+
|
|
255
|
+
- **`Fixed[n]`** — exactly `n` cells, clamped to what's still unassigned.
|
|
256
|
+
- **`Percent[n]`** — `n`% of the space *available*, measured after padding
|
|
257
|
+
and the gaps between children come off. So two `Percent[50]` children fit
|
|
258
|
+
exactly instead of overflowing by the gap between them.
|
|
259
|
+
- **`Expand[weight]`** — a share of whatever is left once the `Fixed` and
|
|
260
|
+
`Percent` children have taken theirs, split in proportion to the weights.
|
|
261
|
+
|
|
262
|
+
That's the entire vocabulary, and the omission is the point: **there is no
|
|
263
|
+
`Auto`.** Nothing asks a child how big it would like to be. This is the same
|
|
264
|
+
rule as the rest of the chapter, wearing a friendlier face.
|
|
265
|
+
|
|
266
|
+
Two more knobs, both on the box rather than on each child: `spacing:` (blank
|
|
267
|
+
cells between adjacent children) and `padding:` (an inset from the box's own
|
|
268
|
+
rect — `Insets[top: 1, left: 2]`, or a plain integer for all four edges).
|
|
269
|
+
|
|
270
|
+
### The cross axis, and alignment
|
|
271
|
+
|
|
272
|
+
Each child also gets a `cross:` constraint — its width in a `Vertical`, its
|
|
273
|
+
height in a `Horizontal`. It defaults to `Percent[100]`, so children fill the
|
|
274
|
+
box across the axis, which is usually what you want. Narrow one when it isn't:
|
|
275
|
+
|
|
276
|
+
```ruby
|
|
277
|
+
form.add(field, Fixed[1], cross: Fixed[30]) # 30 columns
|
|
278
|
+
form.add(title, Fixed[1], cross: Percent[50], align: :center)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
`align:` is `:start`, `:center` or `:end` — axis-agnostic on purpose, since
|
|
282
|
+
`:start` means the left edge in a `Vertical` and the top edge in a
|
|
283
|
+
`Horizontal`. It does something only when the child is narrower than the
|
|
284
|
+
space available.
|
|
285
|
+
|
|
286
|
+
Alignment might look like it contradicts the top-down rule — surely centering
|
|
287
|
+
needs to know how wide the child is? It doesn't. It needs *a* width, and the
|
|
288
|
+
`cross:` constraint is where that width came from. Nothing gets measured.
|
|
289
|
+
(`Expand` is main-axis only for a related reason: across the axis a child has
|
|
290
|
+
no siblings to compete with, so a weight would have nothing to mean. Passing
|
|
291
|
+
one as `cross:` raises.)
|
|
292
|
+
|
|
293
|
+
### Packing, starving, and remainders
|
|
294
|
+
|
|
295
|
+
Three behaviours worth knowing, because they are what you get *instead of* a
|
|
296
|
+
solver:
|
|
297
|
+
|
|
298
|
+
**Children pack from the start edge.** With no `Expand` among them the slack
|
|
299
|
+
is simply left at the end — there's no invisible filler to add, the way
|
|
300
|
+
Swing's `BoxLayout` needs glue.
|
|
301
|
+
|
|
302
|
+
**Over-subscription starves rather than raising.** If the children ask for
|
|
303
|
+
more than there is, they're satisfied in declaration order and whoever is
|
|
304
|
+
left over gets an empty rect — which, as chapter 2 established, paints
|
|
305
|
+
nothing. A pane too short for its content degrades quietly instead of
|
|
306
|
+
throwing or spilling outside its rect.
|
|
307
|
+
|
|
308
|
+
**A remainder goes to the earliest `Expand` children, one cell each.** Five
|
|
309
|
+
equal `Expand`s in 12 rows get `3, 3, 2, 2, 2` — never `2, 2, 2, 2, 4`, which
|
|
310
|
+
is what "give the leftover to the last one" produces. On a character grid a
|
|
311
|
+
doubled pane is plainly visible, so spare cells are spread rather than dumped.
|
|
312
|
+
One wrinkle, since this chapter showed you the hand-written version first: the
|
|
313
|
+
two-pane `Absolute` example above gives the odd column to the *right* pane,
|
|
314
|
+
while two `Expand[1]` children give it to the *left*. Both are deterministic;
|
|
315
|
+
they're just different code.
|
|
316
|
+
|
|
317
|
+
### Varying the gap: nest, don't configure
|
|
318
|
+
|
|
319
|
+
`spacing` belongs to the box rather than to individual children, deliberately.
|
|
320
|
+
A gap sits *between* two children, so "whose gap is it?" has no good answer —
|
|
321
|
+
and both possible conventions confuse readers.
|
|
322
|
+
|
|
323
|
+
When you want tighter grouping, nest a box. A `spacing: 0` stack inside a
|
|
324
|
+
`spacing: 1` stack keeps two rows flush while the rest of the form breathes:
|
|
325
|
+
|
|
326
|
+
```ruby
|
|
327
|
+
pair = Tuile::Component::Layout::Vertical.new # spacing: 0
|
|
328
|
+
pair.add(bar, Fixed[1])
|
|
329
|
+
pair.add(caption, Fixed[1]) # flush under the bar
|
|
330
|
+
|
|
331
|
+
form = Tuile::Component::Layout::Vertical.new(spacing: 1)
|
|
332
|
+
form.add(prompt, Fixed[4])
|
|
333
|
+
form.add(pair, Fixed[2]) # blank row around the pair
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
That *states* the grouping instead of faking it with a per-child gap — boxes
|
|
337
|
+
within boxes, which is how the rest of Tuile composes anyway.
|
|
338
|
+
|
|
339
|
+
### When to stay with `Absolute`
|
|
340
|
+
|
|
341
|
+
The boxes are sugar, not a replacement, and they can't say everything. A **cap
|
|
342
|
+
on a proportion** is the case to recognise:
|
|
343
|
+
|
|
344
|
+
```ruby
|
|
345
|
+
list_width = (rect.width / 3).clamp(20, 40) # a third, but never <20 or >40
|
|
346
|
+
group_width = [16, rect.width / 3].min # a third, but never more than 16
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
Both of these are in `examples/sampler.rb`, and both keep a `rect=`
|
|
350
|
+
override. That's the intended division of labour rather than a gap to work
|
|
351
|
+
around: use a box for the stack, drop to `Absolute` for the region that
|
|
352
|
+
genuinely needs arithmetic — usually nesting one inside the other, so only the
|
|
353
|
+
awkward part carries any. The sampler does exactly that, and porting it to
|
|
354
|
+
these layouts took it from 59 hand-written rectangles down to 7.
|
|
355
|
+
|
|
207
356
|
## Geometry: `Point`, `Size`, `Rect`
|
|
208
357
|
|
|
209
358
|
The values you compute with are three small frozen types
|
|
@@ -294,7 +443,7 @@ A `Window`'s bottom border can carry one of two things, and they are
|
|
|
294
443
|
genuinely different jobs, so they are two different members.
|
|
295
444
|
|
|
296
445
|
**`footer_text=`** is *border chrome* — a styled string (a `String` is
|
|
297
|
-
coerced) embedded into the bottom border
|
|
446
|
+
coerced) embedded into the bottom border row, mirroring the caption on
|
|
298
447
|
the top line. It draws at its own width with the border's dashes filling
|
|
299
448
|
the remainder, clipped to the inner width. Like the caption, it embeds
|
|
300
449
|
with **no added padding** — it butts straight against the left corner:
|
|
@@ -369,11 +518,7 @@ own code and set the size top-down. Keep measurement opt-in and
|
|
|
369
518
|
caller-side; the moment the framework starts consulting children for
|
|
370
519
|
sizes automatically, it's on the road back to the constraint solver.
|
|
371
520
|
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
feeding results to the very same `rect=` setter you already use, with no
|
|
377
|
-
change to the foundation. Absolute-first is the base; a descriptive
|
|
378
|
-
split layer is an optional convenience on top, added if and when the
|
|
379
|
-
convenience pays for itself.
|
|
521
|
+
Note that the box layouts above are not an exception to any of this. They
|
|
522
|
+
compute rectangles *for* you, but they compute them from constraints you
|
|
523
|
+
supplied, and they hand them down through the same `rect=`. No child is ever
|
|
524
|
+
consulted.
|
data/book/05-focus.md
CHANGED
|
@@ -159,6 +159,8 @@ The same mechanism gives you a form's default button, one form per popup:
|
|
|
159
159
|
| `TextField` with an `on_enter` | consumes it | no double-submit |
|
|
160
160
|
| `TextField` without one | declines | bubbles up → submit |
|
|
161
161
|
| `Button` | consumes it | activates *itself*, not the default |
|
|
162
|
+
| `Checkbox` | consumes it (toggles) | the form never sees it |
|
|
163
|
+
| `Select` | consumes it (opens, then commits) | the form never sees it |
|
|
162
164
|
|
|
163
165
|
Because bubbling stops at the scope root, two forms in two popups each get
|
|
164
166
|
their own Enter — something a global registry structurally cannot do. This
|
data/book/06-theming.md
CHANGED
|
@@ -121,7 +121,7 @@ For plain chrome — a border string, a status-bar hint — the theme's
|
|
|
121
121
|
right channel for the token's role (a `*_bg` token wraps as a background,
|
|
122
122
|
a hint as a foreground) and passes the content through verbatim, so the
|
|
123
123
|
string may already contain other escape sequences — which is how
|
|
124
|
-
{Tuile::Component::Window} feeds its whole border
|
|
124
|
+
{Tuile::Component::Window} feeds its whole border row, cursor moves and
|
|
125
125
|
all, through `active_border`.
|
|
126
126
|
|
|
127
127
|
But chrome text is flat. Content is not. A list row or a label may be a
|