tuile 0.10.0 → 0.11.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 +77 -64
- data/DECISIONS.md +619 -14
- data/README.md +6 -0
- data/book/03-layout.md +153 -8
- data/book/05-focus.md +2 -0
- data/book/07-components.md +105 -10
- data/book/README.md +3 -1
- data/examples/sampler.rb +282 -132
- data/ideas/arrow-key-navigation.md +205 -0
- data/ideas/new-components.md +17 -8
- data/lib/tuile/component/big_decimal_field.rb +199 -0
- data/lib/tuile/component/checkbox.rb +10 -9
- data/lib/tuile/component/combo_box.rb +8 -26
- data/lib/tuile/component/float_field.rb +161 -0
- 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_dropdown.rb +69 -18
- data/lib/tuile/component/select.rb +251 -0
- data/lib/tuile/styled_string.rb +13 -3
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +4 -0
- data/sig/tuile.rbs +882 -29
- metadata +8 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 7560121c75afad2579449ec75052597e780749ecd56b9aca7169288bd5ffbbca
|
|
4
|
+
data.tar.gz: 2032440b0a0831027997ce8a193457ae64eb96c425866c2f40a123933b80f20f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: a9fc19844524d5c3762a80ec43315bc8e98408cf3b62adb427ae3b93361a4e7a7d415a92eeddb068d12ba28050b0c5dce7c1716a8d5245ac47ee6d86d2869507
|
|
7
|
+
data.tar.gz: d92742ed5181d75d1ae80c8552cf7c0112cd485bf2b613206227e907fb660d49a2a1e041b1b97f3c0ca76b6bd06a87b8259e3a118381f4d9b6b5cb854a053d13
|
data/CHANGELOG.md
CHANGED
|
@@ -1,95 +1,108 @@
|
|
|
1
1
|
## [Unreleased]
|
|
2
2
|
|
|
3
|
+
## [0.11.0] - 2026-08-12
|
|
4
|
+
|
|
5
|
+
- Add `Component::Select` — a one-row enum field that drops open a `ListDropdown` of its typed `items`; `value` is the selected item, and Enter, Space or Down opens it. It claims no printable key but Space, so a form's own letter bindings keep working while it has focus. See `DECISIONS.md` `D-select` and book ch7.
|
|
6
|
+
- Add `Component::Layout::Vertical` and `Component::Layout::Horizontal` (on the abstract `Component::Layout::Box`) — declarative 1-D layouts where a caller *declares* each child's extent (`Fixed` / `Percent` / `Expand`, plus box-global `spacing`, `padding` and a per-child `align:`) instead of computing it. See `DECISIONS.md` `D-box-layouts` and book ch3.
|
|
7
|
+
- Add `Component::BigDecimalField` — the money field: the shape of `IntegerField`/`FloatField` with an exact `BigDecimal` (or `nil`) value, where assigning a `Float` raises rather than converting. See `DECISIONS.md` `D-bigdecimal-field`.
|
|
8
|
+
- Add `Component::FloatField` — the `IntegerField` twin whose `value` is a `Float` (or `nil`), accepting a single `.` and stepping by `1.0` on Up/Down; `value=` raises on a NaN or infinity. See `DECISIONS.md` `D-float-field`.
|
|
9
|
+
- Add `Component::ListDropdown#anchor_to(anchor, rows:, width:, max_rows:)` — placement now lives on the dropdown: below the driver, flipped above when the rows won't fit beneath, and slid left to stay on screen. Width stays a caller decision.
|
|
10
|
+
- `bigdecimal` is Tuile's first optional dependency and is deliberately not in the gemspec: only an app naming `Component::BigDecimalField` loads it, and doing so without the gem raises `LoadError` with the fix in the message. A Bundler app on Ruby 3.4+ must name `bigdecimal` in its own `Gemfile`.
|
|
11
|
+
- `examples/sampler.rb` gains `Select`, `FloatField` and `BigDecimalField` panes, and its demo panes are ported to the box layouts.
|
|
12
|
+
- **Fix (behavior):** `StyledString#wrap` no longer eats a **first-line** indent, so indented text is displayable at all (every `Component::TextView` line goes through `wrap`). `plain(" ").wrap(5)` is now `[" "]` rather than `[""]`; continuations still start at column 0. See `DECISIONS.md` `D-wrap-leading-space`.
|
|
13
|
+
- **Fix (behavior):** a `Component::ListDropdown` that scrolls now shows a scrollbar — `List`'s `scrollbar_visibility` defaults to `:gone` and the dropdown never changed it, so an 11-match `ComboBox` looked identical to a 10-match one.
|
|
14
|
+
- **Fix:** `Component::ComboBox` no longer hands its inner `TextField` a one-row rect when its own rect is zero-height — a child painting outside its parent, which a box layout can provoke by starving an over-subscribed child.
|
|
15
|
+
- **Breaking:** `Component::ComboBox::MAX_VISIBLE_ROWS` moved to `Component::ListDropdown::MAX_VISIBLE_ROWS`. Update the constant reference.
|
|
16
|
+
- **Breaking (behavior):** `Component::Checkbox` now toggles on **Enter** as well as Space, matching a checkable row inside a `List`. A focused checkbox therefore consumes Enter, so an ancestor's Enter-to-submit must move to a key no focused field claims. See `DECISIONS.md` `D-boolean-fields`.
|
|
17
|
+
|
|
3
18
|
## [0.10.0] - 2026-08-02
|
|
4
19
|
|
|
5
|
-
-
|
|
6
|
-
- Add `Component::
|
|
7
|
-
- Add `Component::
|
|
8
|
-
- Add `Component::
|
|
9
|
-
- Add `Component
|
|
10
|
-
- `
|
|
11
|
-
-
|
|
12
|
-
-
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
- For both group components — and for `ComboBox` before them — `items=` is chrome and never touches `value`: an absent value renders as nothing selected and survives intact, so a form saved without edits changes nothing silently. There is no reconcile step, clamp or silent drop.
|
|
20
|
+
- Add `Component::Checkbox` — a one-row boolean input (`[x] Enable syslog forwarding`) toggled by Space or a left-click, whose `value` is always `true`/`false`, with `checked?`/`checked=`/`toggle` as the domain-word face over it. See `DECISIONS.md` `D-boolean-fields`.
|
|
21
|
+
- Add `Component::CheckboxGroup` — multi-select over typed `items` whose `value` is a **frozen** `Set` of the selected items, iterating in toggle order (use `items & value.to_a` when order matters). See `DECISIONS.md` `D-checkbox-group`.
|
|
22
|
+
- Add `Component::RadioGroup` — single-select over typed `items` whose `value` is the selected item; it composes a `List`, so the cursor roams without selecting and Space, Enter or a click commits the row under it. See `DECISIONS.md` `D-radio-group`.
|
|
23
|
+
- Add `Component::IntegerField` — a single-line input whose `value` is an `Integer` (or `nil`), accepting only digits and a single leading `-`, with Up/Down stepping by one; it composes a `TextField` rather than subclassing one.
|
|
24
|
+
- Add `Component::PasswordField` — a `TextField` painting one mask glyph per character (`mask_char=`, default `*`) with a `revealed=`/`revealed?` toggle; while masked, word-jumps collapse to the ends of the buffer so the mask can't leak word boundaries.
|
|
25
|
+
- Add `Component::TextField#display_text` — the protected seam a subclass overrides when it paints something other than `text`, since the caret, the scroll window and click-to-position all measure it. The contract is one display character per `text` character, in order.
|
|
26
|
+
- Add `Component::ProgressBar` — a display-only fill over a `Range` with `fraction`/`percent` readers and an `indeterminate` mode owning a 5 fps ticker; it stays deliberately outside `Component::HasValue`. See `DECISIONS.md` `D-progress-bar` and `D-color-slots`.
|
|
27
|
+
- Add `Component#on_attached` / `on_detached` — lifecycle hooks fired once per component per transition, letting a component own a mounted-lifetime resource. They are hooks, not destructors: a process exiting without `Screen#close` fires nothing. See `DECISIONS.md` `D-attach-hooks`.
|
|
28
|
+
- Add `Component::HasCaption` — the caption seam included by `Button` and `Window`, coining the naming split (**caption** is app-authored chrome, **text** is the user-editable value) and making `is_a?`-plus-caption tree lookups possible.
|
|
29
|
+
- `Component::HasValue` now carries `focusable? = true` (overridable). `tab_stop?` is deliberately not folded in: it stays `true` on the leaf `AbstractStringField` and `false` on the composing wrappers, whose inner field carries the stop.
|
|
30
|
+
- `Component::ComboBox` and `Component::IntegerField` compose their inner field via `Component::HasContent` instead of hand-rolling `children`/`rect=`/`on_focus`, so their `content`/`content=` are consequently public.
|
|
31
|
+
- `Component::Window#caption` and `Component::Button#caption` accept a `String | StyledString | nil` and are built, clipped and ellipsized by **display** width, so a CJK/emoji caption no longer overruns the frame or the rect.
|
|
32
|
+
- `Screen#close` now unmounts the component tree (via `ScreenPane#detach_all`), so teardown fires `on_detached` across it.
|
|
33
|
+
- `items=` is chrome on `ComboBox` and on both group components: it never touches `value`, so an absent value renders as nothing selected and survives intact, with no reconcile step, clamp or silent drop.
|
|
20
34
|
- `examples/sampler.rb` gains panes for `ProgressBar`, `RadioGroup` and `CheckboxGroup`.
|
|
21
|
-
-
|
|
22
|
-
-
|
|
23
|
-
|
|
24
|
-
- **Breaking:** `Component#
|
|
25
|
-
- **Breaking:**
|
|
26
|
-
-
|
|
27
|
-
- `
|
|
28
|
-
-
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
- `Component::
|
|
32
|
-
- **Breaking:** `Component::Button#
|
|
33
|
-
- **Breaking:** `Component::TextInput` is renamed `Component::AbstractStringField` (file `text_input.rb` → `abstract_string_field.rb`), and its doc now scopes it as the *String-valued* base of `TextField`/`TextArea` — a field whose value isn't a `String` composes one rather than subclassing it. `TextField`/`TextArea` are unaffected; only code that referenced `TextInput` directly must update the constant.
|
|
35
|
+
- Fix the caret stepping by *character* rather than by grapheme cluster: LEFT/RIGHT, BACKSPACE and DELETE now move over and delete exactly one cluster, and the caret is boundary-locked so a mid-cluster position is unrepresentable. See `DECISIONS.md` `D-cluster-caret`.
|
|
36
|
+
- Fix `Component::TextArea` hanging the UI thread on text containing `\r`, `\v` or `\f` — the word-scan measured zero and the wrap loop never advanced, so `area.text = File.read(crlf_file)` was enough to lock up an app. See `DECISIONS.md` `D-text-area-columns`.
|
|
37
|
+
- **Breaking:** `Component#children` is final, and reparenting goes through the protected `add_child(child, at:)` / `remove_child(child)` / `detach_child(child)`. A custom container must stop overriding `children` or hand-wiring `child.parent = …`; named slots become readers over the array. See `DECISIONS.md` `D-tree-api`.
|
|
38
|
+
- **Breaking:** `Component#attached?` is now the one-axis type test `root.is_a?(ScreenPane)` — it consults no `Screen`, so it never raises and a tree can be assembled with no screen in the process. See `DECISIONS.md` `D-tree-first`.
|
|
39
|
+
- **Breaking:** the UI-thread guard is rewritten — `EventQueue#locked?` becomes `#running?` plus `#on_loop_thread?`, the internal `@pretend_ui_lock` and `FakeScreen#check_locked`'s bypass are gone, and `Screen#state` is added. A spec mutating UI from a *spawned* thread now raises exactly as an app would. See `DECISIONS.md` `D-screen-lifecycle`.
|
|
40
|
+
- **Breaking:** `Component#key_shortcut` and `#find_shortcut_component` are removed, along with `ScreenPane#handle_key`'s capture phase and `Window`'s `[k]-Caption` border prefix. Migration: `w.key_shortcut = "1"` becomes a `case` in the containing layout's own `handle_key`. See `DECISIONS.md` `D-key-dispatch`.
|
|
41
|
+
- **Breaking:** `Screen#register_global_shortcut` now also rejects `Screen::EDITING_KEYS` — `ENTER`, `BACKSPACE`, `DELETE` and the arrows — which silently broke `TextArea` newlines app-wide. Bind those on an ancestor's `handle_key` instead; `HOME`/`END`/`PAGE_UP`/`PAGE_DOWN` stay legal.
|
|
42
|
+
- **Breaking:** `Component::TextInput` is renamed `Component::AbstractStringField` (file `text_input.rb` → `abstract_string_field.rb`). Only code referencing the constant directly must update.
|
|
43
|
+
- **Breaking:** `Component::Button#caption` and `Component::Window#caption` return a `StyledString`, not a `String`. Measure with `caption.display_width` and recover the plain text with `caption.to_s`.
|
|
44
|
+
- **Breaking (behavior):** all glyph measurement is now per grapheme cluster under one emoji policy (`:rgi`), so `"👍🏽"` measures 2 columns rather than 4 and no longer overruns its cell. Custom components measuring with `String#length` or iterating `each_char` must move to `StyledString#display_width` / `slice` / `ellipsize`. See `DECISIONS.md` `D-cluster-width`.
|
|
45
|
+
- **Breaking (behavior):** `Component::TextField` separates the caret's character index from its terminal column and **scrolls horizontally** instead of being capped by its own width. `max_text_length` is now an explicit, settable cap defaulting to `nil` (unbounded) rather than the derived `rect.width - 1`. See `DECISIONS.md` `D-text-field-axes`.
|
|
46
|
+
- **Breaking (behavior):** `Component::Button#handle_mouse` fires `on_click` only within the button's painted **extent**, not anywhere in its `rect`, and `Component::Checkbox` follows the identical rule. A click on the blank tail still focuses the button but no longer activates it.
|
|
34
47
|
|
|
35
48
|
## [0.9.0] - 2026-07-05
|
|
36
49
|
|
|
37
50
|
Layout is now strictly top-down: a parent assigns each child's `rect`, and components no longer advertise how big they want to be. The book's chapter 3 is the long-form rationale.
|
|
38
51
|
|
|
39
52
|
- Add `Tuile::Fraction` (`HALF` / `FULL`, int-coercing, floor-at-1 `resolve`) — the one relational sizing primitive, scoped to `Popup#size=`.
|
|
40
|
-
- `Component::
|
|
41
|
-
- `Component::Window`: the single footer slot is split into two purpose-fit members — `footer_text=` (a `StyledString` embedded into the bottom border line at its own width with dashes filling the remainder, mirroring `caption` on top; not a component, not focusable) and `footer=` (a focusable component always spanning the full inner width of the bottom row). A `footer=` component present hides `footer_text`.
|
|
42
|
-
- Add optional `Component::Label.new(text = nil)` — constructor symmetry with `Window.new(caption)`, reusing the `text=` coercion path (`String | StyledString | nil`). Purely additive; `Label.new` still works.
|
|
53
|
+
- Add optional `Component::Label.new(text = nil)` — constructor symmetry with `Window.new(caption)`, reusing the `text=` coercion path. Purely additive; `Label.new` still works.
|
|
43
54
|
- Add `EventQueue#tick_fps(fps)` = `tick(1.0 / fps)` for the frames-per-second animation idiom; lands on both the real and fake queues.
|
|
44
|
-
-
|
|
45
|
-
-
|
|
46
|
-
-
|
|
55
|
+
- `Component::Popup#size=` accepts a `Size` (clamped to the screen) or a `Fraction` (re-resolved on every layout pass, so it tracks resize), default `Fraction::HALF`; popups no longer auto-size to their content.
|
|
56
|
+
- `Component::Window`: the single footer slot splits into `footer_text=` (a `StyledString` embedded into the bottom border line, mirroring `caption` on top) and `footer=` (a focusable component spanning the full inner width of the bottom row), the latter hiding the former.
|
|
57
|
+
- Fix `Buffer#flush` corrupting the left neighbour of a wide glyph: a dirty continuation cell no longer opens a flush run, so an adjacent change no longer blanks the glyph (an emoji vanishing, persisting across tmux window switches).
|
|
58
|
+
- Memoize display-width measurement in `Buffer` and measure each grapheme once per paint: a full-screen 160×50 repaint drops from ~29.5ms to ~5.8ms (~5×). Adds `rake benchmark`.
|
|
59
|
+
- **Breaking:** the eager bottom-up sizing channel is deleted — `Component#content_size` / `content_size=` / `on_child_content_size_changed`, plus 0.8.0's `popup_min_height` / `popup_max_height`. A custom component that advertised a size must move that logic into its parent's `rect=`.
|
|
47
60
|
- **Breaking:** `Tuile::Sizing` (`FILL` / `WRAP_CONTENT` / `Sizing.fixed`) and `Window#footer_sizing`, both added in 0.6.0, are removed — a bottom-row `footer=` widget is always FILL by construction.
|
|
48
|
-
- **Breaking:** `EventQueue#tick(seconds)` now takes an interval in **seconds**, not frames-per-second. `tick(4)` used to mean "4 times a second"
|
|
61
|
+
- **Breaking:** `EventQueue#tick(seconds)` now takes an interval in **seconds**, not frames-per-second. `tick(4)` used to mean "4 times a second" and now means "every 4 seconds"; use the new `tick_fps(fps)` for the animation mental model.
|
|
49
62
|
|
|
50
63
|
## [0.8.0] - 2026-06-11
|
|
51
64
|
|
|
52
|
-
- Render through a back buffer: `Screen#buffer` is now a `Tuile::Buffer` cell grid
|
|
53
|
-
- Add non-modal popups
|
|
54
|
-
- Add `Component::TextInput#on_key` — an interceptor consulted before the input's own key handling (a truthy return consumes the key)
|
|
55
|
-
- Add `Component#popup_min_height` / `popup_max_height` — content components can advise a wrapping `Popup` of preferred height bounds
|
|
65
|
+
- Render through a back buffer: `Screen#buffer` is now a `Tuile::Buffer` cell grid, and `Screen#repaint` flushes only the cells that changed since the last frame in one synchronized-output batch — flicker-free on **any** terminal, since an unchanged cell is never rewritten.
|
|
66
|
+
- Add non-modal popups (`Component::Popup.new(modal: false)`) — they paint on top but don't center, grab focus, capture keys or block clicks, which is the building block for autocomplete menus anchored to a caret. `ScreenPane#modal_popup` is the topmost *modal* popup, through which all modal-owner reads now route.
|
|
67
|
+
- Add `Component::TextInput#on_key` — an interceptor consulted before the input's own key handling (a truthy return consumes the key), letting app code layer Up/Down/Enter/ESC onto a field without subclassing.
|
|
68
|
+
- Add `Component#popup_min_height` / `popup_max_height` — content components can advise a wrapping `Popup` of preferred height bounds, which `Component::LogWindow` uses to grow from half-screen to full-screen as the log fills.
|
|
69
|
+
- Add an `examples/sampler.rb` "Slash menu" demo: a `TextArea` whose `on_change` refills a non-modal `Popup`-wrapped `List` anchored to the caret, with focus and caret staying in the field throughout.
|
|
56
70
|
- `Component::LogWindow` now renders through a `TextView`, so long log lines wrap instead of being ellipsized.
|
|
57
|
-
-
|
|
58
|
-
- **Breaking:**
|
|
59
|
-
- **Breaking:** key dispatch is centralized into a capture + bubble model in `ScreenPane#handle_key` (a `key_shortcut` match anywhere in scope focuses that component; the key is then delivered to `Screen#focused` and bubbles up its ancestor chain). A component's `handle_key` now acts on the key alone and never gates on its own `active?` state. `Layout#handle_key`, `Window#handle_key`, and `HasContent#handle_key` are removed (routing is the dispatcher's job), and the `active?` guards in `TextInput` / `List` / `Button` are dropped.
|
|
71
|
+
- **Breaking:** components paint into `Screen#buffer` via `set_line` / `fill` / `set_char` instead of writing escape sequences through `screen.print`. Custom components that drew via `screen.print(move_to, ansi)` must migrate to the buffer API.
|
|
72
|
+
- **Breaking:** key dispatch is centralized into a capture + bubble model in `ScreenPane#handle_key`, and a component's `handle_key` now acts on the key alone rather than gating on its own `active?` state. `Layout`/`Window`/`HasContent#handle_key` are removed and the `active?` guards in `TextInput`/`List`/`Button` are dropped.
|
|
60
73
|
|
|
61
74
|
## [0.7.0] - 2026-06-09
|
|
62
75
|
|
|
63
76
|
- Lower the Ruby floor to 3.3 (was 3.4): replaced the `it` implicit block parameter (3.4+) with `_1` throughout, and added 3.3 to the CI matrix.
|
|
64
|
-
- Fix `Component::Popup#close` raising `Tuile::Error` when the popup was not open — it is now the documented no-op (also
|
|
77
|
+
- Fix `Component::Popup#close` raising `Tuile::Error` when the popup was not open — it is now the documented no-op (also covering a double `close`). `Screen#remove_popup` guards on `has_popup?`; `ScreenPane#remove_popup` keeps its strict internal assertion.
|
|
65
78
|
|
|
66
79
|
## [0.6.0] - 2026-06-07
|
|
67
80
|
|
|
68
|
-
- Add `Tuile::Theme` — semantic color tokens for the accents built-in components paint (
|
|
69
|
-
-
|
|
81
|
+
- Add `Tuile::Theme` — semantic color tokens for the accents built-in components paint (`active_bg_color`, `input_bg_color`, `active_border_color`, `hint_color`) with `DARK`/`LIGHT` presets, living at `Screen#theme`; everything that isn't an accent keeps inheriting the terminal's own default fg/bg.
|
|
82
|
+
- Add `Tuile::ThemeDef` — an app's dark/light `Theme` pair. Assigning `Screen#theme_def=` is the durable way to theme an app, where a bare `theme=` is transient; construction validates that both members declare the same custom key set.
|
|
83
|
+
- Add `ThemeDef.default` — the definition newly-constructed screens start from. Reassign it once in `spec_helper.rb` and every `Screen.fake` carries the app's custom tokens, instead of repeating `theme_def=` in each `before` block.
|
|
84
|
+
- Add app-specific theme tokens: `Theme#custom` (`Hash{Symbol => Color}`), looked up fail-fast via `Theme#[]` and rendered via the generic `#fg`/`#bg` helpers. Subclass `Theme` to add one coloring function per custom token — `Data#with` preserves the subclass.
|
|
85
|
+
- Add `Component#on_theme_changed` — fired pre-order across the attached tree on every theme change, so apps can rebuild styled content whose colors came from the old theme. Override it (calling `super`) or assign the `on_theme_changed=` proc.
|
|
86
|
+
- Add `Color.hex` — a 24-bit RGB color from a CSS-style hex string (leading `#` optional, case-insensitive, 3-digit shorthand expands as in CSS). Alpha forms are rejected: SGR has no alpha channel.
|
|
87
|
+
- Add `Tuile::Sizing` (`FILL` / `WRAP_CONTENT` / `Sizing.fixed(n)`) and `Window#footer_sizing` — the footer slot is sized per policy against the inner width, and is excluded from `Window#content_size` since decoration must not drive window size.
|
|
88
|
+
- Name the 256-color palette: a constant per standard xterm chart name for indices 16..255 (`Color::CADET_BLUE`, `Color::GREY37`, …), listed in `Color::PALETTE_NAMES`; indices 0..15 keep the symbolic constants, which respect the terminal's own scheme.
|
|
89
|
+
- Auto-detect the light/dark terminal background at startup: `Screen.new` queries the terminal via OSC 11 (`COLORFGBG` fallback, dark when inconclusive) and picks the matching preset.
|
|
70
90
|
- Follow OS light/dark appearance flips live via mode 2031 (kitty, foot, contour, ghostty, …): the screen re-picks the matching theme and repaints everything.
|
|
71
|
-
-
|
|
72
|
-
-
|
|
73
|
-
- Add `ThemeDef.default` — the definition newly-constructed screens start from (initially `ThemeDef::DEFAULT`). Reassign it once in `spec_helper.rb` and every `Screen.fake` carries the app's custom tokens, instead of repeating `theme_def=` in each `before` block.
|
|
74
|
-
- Name the 256-color palette: a constant per standard xterm chart name for palette indices 16..255 (`Color::CADET_BLUE` is `Color.palette(72)`; `Color::DODGER_BLUE1`, `Color::GREY37`, …) — exact palette cells, no quantization, listed in `Color::PALETTE_NAMES`. Where the chart names several cells identically, the first cell wins the constant; indices 0..15 keep the symbolic `Color::RED`/`Color::BRIGHT_BLUE`/… constants, which respect the terminal's own scheme.
|
|
75
|
-
- Add `Color.hex` — a 24-bit RGB color from a CSS-style hex string (`Color.hex("#333333") == Color.rgb(51, 51, 51)`; leading `#` optional, case-insensitive, 3-digit shorthand expands as in CSS). Alpha forms (`#rgba`/`#rrggbbaa`) are rejected — SGR has no alpha channel. `Color.coerce` stays string-free; `.hex` is the explicit entry point.
|
|
76
|
-
- Add `Component#on_theme_changed` — fired pre-order across the attached tree on every theme change, so apps can rebuild styled content whose colors were derived from the old theme. Override it (calling `super`) or assign the `on_theme_changed=` proc.
|
|
77
|
-
- Add `Tuile::Sizing` (`FILL` / `WRAP_CONTENT` / `Sizing.fixed(n)`) and `Window#footer_sizing` — the footer slot is sized per policy against the inner width; a `WRAP_CONTENT` footer re-lays-out live as its content changes. The footer is excluded from `Window#content_size`: it is decoration overlaying the border and must not drive window size.
|
|
78
|
-
- `Component#content_size` is now maintained eagerly: content mutators assign via the protected `content_size=` setter, which fires `parent.on_child_content_size_changed(self)` only when the value actually changed. Fixes a `Popup` staleness — an open popup now re-sizes and recenters when its content grows.
|
|
79
|
-
- **Breaking:** `rainbow` is no longer a runtime dependency (nothing under `lib/` uses it — `Theme`/`StyledString`/`Color` produce all SGR output). Apps that style text with Rainbow must add it to their own Gemfile.
|
|
91
|
+
- `Component#content_size` is now maintained eagerly via the protected `content_size=` setter, which fires `parent.on_child_content_size_changed(self)` only on a real change; this fixes an open `Popup` not re-sizing when its content grew.
|
|
92
|
+
- **Breaking:** `rainbow` is no longer a runtime dependency (nothing under `lib/` uses it). Apps that style text with Rainbow must add it to their own Gemfile.
|
|
80
93
|
|
|
81
94
|
## [0.5.0] - 2026-05-21
|
|
82
95
|
|
|
83
|
-
- Add `Tuile::Color` — a value type wrapping the four color forms ANSI understands (named Symbol, 256-color Integer, RGB Array, or `nil`)
|
|
84
|
-
- `Component::
|
|
85
|
-
- Add `Component::TextView
|
|
86
|
-
- Add `
|
|
87
|
-
-
|
|
88
|
-
-
|
|
89
|
-
-
|
|
96
|
+
- Add `Tuile::Color` — a value type wrapping the four color forms ANSI understands (named Symbol, 256-color Integer, RGB Array, or `nil`), with constants for the 16 named ANSI colors and a `Color.coerce` that accepts raw forms transparently.
|
|
97
|
+
- Add `Component::TextView::Region` — an opaque handle to a contiguous run of hard lines, so apps can stream into logical sections without tracking line indices across sibling mutations; create with `view.create_region`, and detached handles raise on every reader and mutator.
|
|
98
|
+
- Add `Component::TextView#replace(range, str)` and `#insert(at, str)` for mid-buffer hard-line splices (Integer or Range, empty range == insertion, `begin == hard-line count` valid for end-insertion).
|
|
99
|
+
- Add `EventQueue#tick(fps) { |n| ... }` returning a `Ticker` backed by `Concurrent::TimerTask`; it fires on the event-loop thread with a 0-based monotonic counter and auto-cancels on raise.
|
|
100
|
+
- Add `FakeEventQueue#tick` and `FakeTicker` — a synchronous test double that drives ticks deterministically.
|
|
101
|
+
- `Component::Label`: add a `bg` accessor applying a background color uniformly across every painted row — text, trailing pad, and blank rows past the last line.
|
|
102
|
+
- `Component::TextView`: incremental wrap via a per-hard-line row-count cache — a mid-buffer mutation now re-wraps only the affected slice instead of the whole buffer, speeding up the LLM streaming path.
|
|
90
103
|
- **Breaking:** `StyledString::Style#fg` and `#bg` now return `Color` (or `nil`) instead of the raw `Symbol`/`Integer`/`Array`. `Style.new` and `#merge` continue to accept the raw forms via `Color.coerce`.
|
|
91
|
-
- **Breaking:**
|
|
92
|
-
- **Breaking:** `EventQueue#run_loop` now yields submitted `Proc` events to its consumer block instead of dispatching them inline, so a raise from a `submit{}` block
|
|
104
|
+
- **Breaking:** `StyledString::Style::COLOR_SYMBOLS` is removed — it moved to `Color::COLOR_SYMBOLS`.
|
|
105
|
+
- **Breaking:** `EventQueue#run_loop` now yields submitted `Proc` events to its consumer block instead of dispatching them inline, so a raise from a `submit{}` block routes through `Screen#on_error`. Custom `run_loop` consumers must `call` Procs in their case statement.
|
|
93
106
|
|
|
94
107
|
## [0.4.0] - 2026-05-20
|
|
95
108
|
|
|
@@ -105,7 +118,7 @@ Layout is now strictly top-down: a parent assigns each child's `rect`, and compo
|
|
|
105
118
|
- `Component::List`: skip `auto_scroll` when rect is empty; re-snap on width change; snap cursor to last line on `auto_scroll`.
|
|
106
119
|
- Document `Component#repaint`'s attached-only call contract.
|
|
107
120
|
- Document keyboard input dispatch order and testing (`FakeScreen`, PTY system tests) in the README.
|
|
108
|
-
- **Breaking:** `Component::TextView#append` is now verbatim — chunks
|
|
121
|
+
- **Breaking:** `Component::TextView#append` is now verbatim — chunks concatenate onto the current last hard line, embedded `\n` becomes hard breaks, and no implicit newline is inserted (aliased as `<<`). The old "add a new entry" behavior is now `Component::TextView#add_line`.
|
|
109
122
|
- **Breaking:** `MouseEvent.parse` raises on malformed input instead of silently truncating.
|
|
110
123
|
- Fix: `Component` gates `invalidate` and `repaint` on `attached?`, dropping the negative-rect relic.
|
|
111
124
|
- Fix: `Popup` recomputes size from content on every `#open`.
|