tuile 0.14.0 → 0.15.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +69 -0
  3. data/DECISIONS.md +3181 -41
  4. data/README.md +25 -5
  5. data/TERMINOLOGY.md +17 -3
  6. data/book/05-focus.md +63 -2
  7. data/book/06-theming.md +55 -7
  8. data/book/07-components.md +474 -48
  9. data/book/08-testing.md +78 -0
  10. data/book/10-locale.md +216 -0
  11. data/book/README.md +14 -5
  12. data/examples/sampler.rb +265 -25
  13. data/ideas/binder.md +177 -0
  14. data/ideas/composite-field.md +77 -0
  15. data/ideas/focus-accent.md +116 -0
  16. data/ideas/form-layout.md +151 -0
  17. data/ideas/hover/probe.rb +241 -0
  18. data/ideas/hover/probe_spec.rb +82 -0
  19. data/ideas/hover.md +909 -0
  20. data/ideas/new-components.md +26 -6
  21. data/lib/tuile/component/abstract_string_field.rb +106 -58
  22. data/lib/tuile/component/abstract_wrapping_field.rb +243 -0
  23. data/lib/tuile/component/big_decimal_field.rb +52 -79
  24. data/lib/tuile/component/checkbox_group.rb +36 -20
  25. data/lib/tuile/component/combo_box.rb +59 -31
  26. data/lib/tuile/component/date_field.rb +322 -0
  27. data/lib/tuile/component/float_field.rb +57 -82
  28. data/lib/tuile/component/has_bad_input.rb +88 -0
  29. data/lib/tuile/component/has_caption.rb +8 -0
  30. data/lib/tuile/component/has_content.rb +29 -10
  31. data/lib/tuile/component/has_placeholder.rb +62 -0
  32. data/lib/tuile/component/has_validation.rb +115 -0
  33. data/lib/tuile/component/has_value.rb +27 -0
  34. data/lib/tuile/component/integer_field.rb +51 -78
  35. data/lib/tuile/component/label.rb +6 -38
  36. data/lib/tuile/component/layout/box.rb +87 -19
  37. data/lib/tuile/component/layout.rb +13 -3
  38. data/lib/tuile/component/list.rb +11 -6
  39. data/lib/tuile/component/list_dropdown.rb +4 -0
  40. data/lib/tuile/component/overlay.rb +17 -0
  41. data/lib/tuile/component/radio_group.rb +39 -22
  42. data/lib/tuile/component/select.rb +12 -4
  43. data/lib/tuile/component/text_area.rb +14 -8
  44. data/lib/tuile/component/text_field.rb +42 -15
  45. data/lib/tuile/component/text_view.rb +25 -8
  46. data/lib/tuile/component/time_field.rb +454 -0
  47. data/lib/tuile/component/window.rb +26 -13
  48. data/lib/tuile/component.rb +469 -73
  49. data/lib/tuile/fake_screen.rb +11 -1
  50. data/lib/tuile/final.rb +75 -0
  51. data/lib/tuile/locale.rb +851 -0
  52. data/lib/tuile/screen.rb +131 -17
  53. data/lib/tuile/screen_pane.rb +13 -9
  54. data/lib/tuile/testing.rb +198 -0
  55. data/lib/tuile/theme.rb +100 -10
  56. data/lib/tuile/version.rb +1 -1
  57. data/lib/tuile/vertical_scroll_bar.rb +88 -12
  58. data/lib/tuile.rb +1 -0
  59. data/sig/tuile.rbs +3398 -412
  60. metadata +18 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 18944d23fd95ada3802f613eb6ff6847e845a082e1b155f2ed9d72e324181179
4
- data.tar.gz: 33c476d0d194ae365a95e0ec65f9652fb8da44ba566e34e4a0c99a043747080a
3
+ metadata.gz: b22fdf0be0fec94a9e4fdfd3b7875549bdae21c05268633c698584506d867c42
4
+ data.tar.gz: 58ceb7bb8ef81351d5f1f0a7c63413111c32fd21974b591446712444ea61ab3e
5
5
  SHA512:
6
- metadata.gz: 5e7a9b67f3530b86123933ccfaab6116b0aa0c4031f03e81c6b60094f6196aaa8f8adbc86634f4cc052048a1b15078ed5230983a3fff0ba7186d7d8b3d1e2d71
7
- data.tar.gz: ae43fd49b5217606f88d3b7d631c89c8f3c86b82882c0850b6c6980ce100cdbaefd9237da9ea148680775d1c3a980b708934fad0ab71616de3560ea35804602f
6
+ metadata.gz: 0a565f876fd6918d9303145a9aa545e3892b346bbc43a3213ccec579346a9f71e61e2646e1af080d5ab6906d72b0817f85ca116604e67c057243813c05902b80
7
+ data.tar.gz: 553527746d69b5e2f078d7edfb16941288252f850a5fa4b326bec0f0ae814400ad568c509a7080e1d1c5e75e3b7479650204be809a39926ef7f9ce2e9fd27984
data/CHANGELOG.md CHANGED
@@ -1,5 +1,74 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.15.0] - 2026-09-05
4
+
5
+ 0.15.0 is about the form. A field can now report input its type cannot
6
+ represent (`HasBadInput`), hold an outside validator's verdict
7
+ (`HasValidation`), paint either as a red well, hint at what it wants
8
+ (`HasPlaceholder`), and spell dates and times the way the session's new
9
+ `Tuile::Locale` says — `DateField` and `TimeField` are the two new ones, over a
10
+ shared `AbstractWrappingField`. Under them, two seams the widgets had been
11
+ improvising become framework: a background that resolves down the tree through
12
+ `default_bg_color` and `BG_INHERIT`, and `Component#visible=`, which hides a
13
+ component as if it were detached while it stays in the tree. And `Tuile::Testing`
14
+ lets a spec drive that UI by looking components up instead of reading the
15
+ painted buffer.
16
+
17
+ - Add `Component#visible=` / `#visible?` — hides a component as if it were detached while it stays in the tree: it paints nothing, takes no space in a `Layout::Box`, and is unreachable by focus, Tab, keys, the mouse and `Testing.find`, while keeping its parent, rect, state and any running resource. See `DECISIONS.md` `D_visibility` and book ch7.
18
+ - Add `Component#on_shown_tree` — `on_tree` with hidden subtrees pruned; the walk every "can the user reach it" question takes, and the one a new focus traversal must use.
19
+ - Add `Component#on_child_visibility_changed` — the protected notice a container with layout arithmetic overrides to re-divide space when a direct child is hidden or shown; `Layout::Box` relayouts from it.
20
+ - Add a visibility rule to `Testing.find` / `.get`: they simulate a user, so a hidden component is never returned, and a failed lookup counts the hidden matches it excluded. `Screen#focused=` likewise raises on a hidden target, as it already did on a detached one.
21
+ - Add `Tuile::Testing` — `Testing.get` / `.find`, which locate a component in the tree by class, mixin, `id`, caption or a block, so a spec can drive the UI instead of only reading the painted buffer. A failed lookup dumps the tree it searched. See `DECISIONS.md` `D_component_lookup`.
22
+ - Add `Component#id` — a `Symbol` tag for finding a component again; nothing paints it and nothing enforces uniqueness, since `Testing.get` raising on two matches is the enforcement.
23
+ - Add `Component#inspect` — one line naming the class, `id`, rect and each mixin's contribution via the protected `inspect_details` hook, replacing an `Object#inspect` that walked `parent`, `children` and the `Screen`.
24
+ - Add `Tuile::Final` — the `final` keyword Ruby lacks: a class `extend`s it, marks the methods a subclass may not redefine, and calls `verify_final!` from its own `initialize`. See `DECISIONS.md` `D_final_tree`.
25
+ - Add `Component#default_bg_color` — a protected hook where a widget declares the background it paints itself; the level the resolution chain was missing, sitting under `bg_color` and over the parent's. See `DECISIONS.md` `D_bg_surface`.
26
+ - Add `Component::BG_INHERIT` — assign it to `bg_color` to contribute no background of your own and take what surrounds you, skipping this component's `default_bg_color`; it is how a widget owned by a bigger one drops its well. See `DECISIONS.md` `D_bg_surface`.
27
+ - Add a state-keyed form of `Component#bg_color`: `{ normal: …, active: … }` over `Component::BG_STATES`, so an app can keep a focus shade of its own rather than flattening one. An absent key falls through to the next level of the chain.
28
+ - Add `Component::HasBadInput` — `bad_input?` / `bad_input_message`, the report a field owes when its input is something its value cannot represent (a lone `-` in an `IntegerField` reads `nil`, exactly like an empty one). Included by the three numeric fields; ask it before `empty?`. See `DECISIONS.md` `D_bad_input` and book ch7.
29
+ - Add `Component::HasValidation` — `error_message` plus `on_error_message_change`, the verdict slot a validator writes and the field shows as a red well; included by `HasValue`, so every field has it, and `bad_input?` reddens the well too. See `DECISIONS.md` `D_has_validation` and book ch7.
30
+ - Add `Component#error_bg_color` — a protected hook for the background a component paints while signalling an error, resolving *above* `bg_color` so tinting a panel cannot switch the signal off on the fields inside it.
31
+ - Add `VerticalScrollBar.handle_char=` / `.track_char=` — the two bar glyphs as an app-global look-and-feel knob (`▐` over `│` for a lazygit-style bar); a glyph that is not exactly one cluster one column wide is rejected at assignment.
32
+ - Add `Component::HasPlaceholder` — `placeholder=` paints a hint into a field's empty well (`dd.mm.yyyy`) in the new barely-visible `Theme#placeholder_color`, never touching the value; carried by `TextField`, forwarded by the four composed fields. See `DECISIONS.md` `D_placeholder` and book ch7.
33
+ - Add `Component::AbstractWrappingField` — the base for a field that carries a typed value but paints nothing itself, owning and hiding one inner editor, committing on both commit gestures and offering subclasses an `on_editor_change` hook; `IntegerField`, `FloatField` and `BigDecimalField` are built on it. See `DECISIONS.md` `D_wrapping_field`.
34
+ - Add `Component::DateField` — a typed `Date`/`nil` field over a list of strftime `formats` taken from `Screen#locale`: parsing tries them in order, leaving the field rewrites the buffer in the first, Up/Down step a day, and unparseable input is reported through `bad_input?` rather than filtered. See `DECISIONS.md` `D_date_field` and book ch7.
35
+ - Add `Component::TimeField` — a typed `Time`/`nil` time of day on a fixed epoch date, spelled the way `Screen#locale` says: `step` is both the Up/Down stride and the precision, PageUp/PageDown step an hour, and unparseable input is reported through `bad_input?`. See `DECISIONS.md` `D_time_field` and book ch7.
36
+ - Add `Locale#time_formats` — the strftime patterns a time field accepts, detected from glibc's `t_fmt` and carrying its full precision, since dropping the seconds is a form field's policy and a clock display wants them. See `DECISIONS.md` `D_time_field` and book ch10.
37
+ - Add `Component::HasBadInput#bad_input_settled?` — the protected gate deciding whether bad input may paint the invalid well *yet*; `true` by default (the numeric fields redden as you type), latched to the commit gestures by `DateField`, whose every prefix is bad input. The `bad_input?` pull is unaffected. See `DECISIONS.md` `D_has_validation`.
38
+ - Add `Tuile::Locale` — a frozen value type of formatting conventions (date formats, calendar, month/weekday names, decimal separator), detected from `locale -k` by `Locale.system` and never holding prose. `Locale::ISO` is the only preset and the fallback. See `DECISIONS.md` `D_locale` and book ch10.
39
+ - Add `Screen#locale` / `#locale=` — the session's conventions, seeded at construction; assigning fires `Component#on_locale_changed` across the tree and repaints it, so a `DateField` respells the dates it holds. See `DECISIONS.md` `D_locale`.
40
+ - Add `Component#on_locale_changed` — the protected hook (plus an assignable listener) for state derived from the old conventions and *pushed* somewhere; anything read at paint or parse time needs nothing, since the locale change repaints everything.
41
+ - Add `Screen.instance?` — whether `Screen.instance` would answer rather than raise, for code that must work with no screen in the process, as a detached component tree legitimately is.
42
+ - Add `Component::DateField#calendar_start` / `#calendar_start=` — the calendar the field parses and formats against, inheriting `Screen#locale` until you set one and again when set back to `nil`; proleptic Gregorian by default, not Ruby's `Date::ITALY`. See `DECISIONS.md` `D_date_field`.
43
+ - Add a `DateField` pane to `examples/sampler.rb` under Input → Typed, where typing `4.9.2026` and Tabbing away shows the buffer rewrite.
44
+ - Add readline's kill keys to both text inputs (`AbstractStringField`): `Ctrl+W` deletes the word behind the caret, `Ctrl+U` everything before it — back to index 0 in a `TextField`, to the caret's row start in a `TextArea`. See `DECISIONS.md` `D_kill_keys` and book ch7.
45
+ - Add `Component#invalidate_children` — the protected repaint cascade as a named call, so a container that paints its own rect can skip the default's blanket clear without silently dropping the one line it may not.
46
+ - Add an `at:` keyword to `Component::Layout::Box#add`, placing the child at an index instead of appending, so a pane hidden with `#remove` goes back where it was. See `DECISIONS.md` `D_empty_ancestor`.
47
+ - Add `Component::Layout::Box#constrain` — re-constrains a child already added, a `nil` axis keeping whatever it had, so a pane can be collapsed with `Fixed[0]` without removing and re-adding it.
48
+ - Add `Component#on_blur` — the protected mirror of `on_focus`, fired on the component that just lost focus, so a field can commit or canonicalize what the user typed even when they Tab away. See `DECISIONS.md` `D_on_blur` and book ch5.
49
+ - Fix `Component::Window` and its subclasses re-emitting their whole border on every unchanged repaint — 925 bytes for an 80x25 window, paid on each focus change — by no longer blanking the rect it is about to repaint. See `DECISIONS.md` `D_component_contract`.
50
+ - Fix a `Component::Window` with `scrollbar = true` painting its right border under the content's scrollbar, dirtying that column into every frame; the border now leaves the column it gave away alone.
51
+ - Fix `Component::Layout::Box` stranding its children at their old rects when its own rect went empty, so the next full repaint — which any popup close runs — painted the hidden subtree back at stale coordinates. See `DECISIONS.md` `D_empty_ancestor`.
52
+ - Fix `Screen#repaint` painting a component that sits under an empty-rect ancestor: the drain filter now drops it, as it already drops a detached one, so a container that forgets to zero its children is inert rather than wrong.
53
+ - Fix `Screen`'s pane carrying no rect until the first `#layout`, which runs from the event loop; it is sized at construction from `#size`, so the tree is not filtered out of the repaint as sitting under an empty ancestor.
54
+ - Fix `Component::IntegerField`, `FloatField` and `BigDecimalField` accepting *pasted* text their own filter forbids — `"abc"` pasted into an integer field left it showing `abc42` while `value` silently read `nil`. Each now filters in its inner field's `insert_text`, which typing and pasting both pass through. See `DECISIONS.md` `D_input_filters`.
55
+ - Fix the numeric fields occupying their inner field's `on_key` slot for Up/Down stepping, so an app that set `field.content.on_key` silently killed the spinner. They use `on_key_up` / `on_key_down` now, and `on_key` is free.
56
+ - Fix `Component#bg_color` being inert on `Component::TextField`, `TextArea`, `Select` and `ComboBox`, which reached past it to the theme — contradicting what `bg_color=` and `effective_bg_color` documented. Setting one on a field now wins over its well.
57
+ - Fix `Component::TextView` wrapping text right up against a visible scrollbar (`…to show the█`): the bar now reserves a blank column beside it, as `List` rows always have. See `DECISIONS.md` `D_scrollbar_reserve`.
58
+ - Fix the scrollbar painting a solid full-height `█` when the content fits: with nothing to scroll the bar now shows only its track, keeping its column and its content width. See `DECISIONS.md` `D_scrollbar_ink`.
59
+ - **Breaking:** `Locale::DateFormats::DIRECTIVE`, `::LOCALE_LOOKALIKES` and `.each_directive` moved to the new `Locale::Formats`, the strftime lexer `DateFormats` and `TimeFormats` now share. Reference them under `Locale::Formats`; `REF`, `HINTS`, `.validate`, `.humanize` and `.widen` are unmoved.
60
+ - **Breaking:** `Component::AbstractStringField#on_key` is gone — the pre-dispatch interceptor that could claim any key. Subclass and override `handle_text_input_key` instead, calling `super` for keys you don't claim; a key you decline still bubbles to an ancestor. See `DECISIONS.md` `D_no_key_interceptor` and book ch7.
61
+ - **Breaking:** A paste is delivered to `Screen#focused` alone and no longer bubbles up the focus chain; `ScreenPane#handle_paste` returns false when the focused component declines. A container that relied on catching a paste its child refused must move that `handle_paste` onto the focusable component. See `DECISIONS.md` `D_bracketed_paste`.
62
+ - **Breaking:** `Component::TextField#preprocess_paste` keeps the paste's first line instead of flattening every newline to a space, so a copied whole line no longer arrives with an invisible trailing space. Override it to restore the old behavior. See `DECISIONS.md` `D_paste_newlines`.
63
+ - **Breaking:** `Theme` carries a further token, `placeholder_color`, the ink `Component::HasPlaceholder` paints an empty field's hint in. A hand-rolled `Theme.new(...)` must now pass it; `Theme::DARK.with(...)` is unaffected. See `DECISIONS.md` `D_placeholder`.
64
+ - **Breaking:** `Theme` carries four further tokens — `scrollbar_color`, plus `error_color` / `error_bg_color` / `error_active_bg_color` — so a scrollbar is themed rather than painted in the terminal's default foreground, and an invalid field shows a well that still reads as focused. A hand-rolled `Theme.new(...)` must now pass all four; `Theme::DARK.with(...)` is unaffected. See `DECISIONS.md` `D_scrollbar_ink` and `D_has_validation`.
65
+ - **Breaking:** `Component::Label#bg` / `#bg=` are gone. Use `bg_color`, which now covers the text, the trailing padding and the blank rows alike; to override a span's own background as `#bg` did, restyle the text with `label.text = text.with_bg(c)`.
66
+ - **Breaking:** `Component#effective_bg_color` is protected and final. A widget declares its own background by overriding `default_bg_color`; nothing else needs to read the resolved value, since `draw_text` / `draw_char` / `clear_background` apply it.
67
+ - **Breaking:** `Component::AbstractStringField#background` is gone. A subclass painting its own row calls `draw_text`, which applies the well; to change the well, override `default_bg_color`.
68
+ - **Breaking:** `Component::FINAL_METHODS` is gone, replaced by the `Tuile::Final` declaration it became. Read `Component.final_methods` instead; `Component.verify_final!` is unchanged.
69
+ - **Breaking:** `content` / `content=` are gone from `Component::IntegerField`, `FloatField` and `BigDecimalField`, whose inner editor is private machinery. Use the field's own `placeholder` / `on_enter` / `cursor_position` / `clear`, write a forwarder for anything else, and reach the editor from a *spec* with `Testing.get(Component::TextField, in: field)`. See `DECISIONS.md` `D_wrapping_field`.
70
+ - **Breaking:** `content` is now `list` on `Component::CheckboxGroup` and `RadioGroup`, and read-only — an app tunes the composed `List` but must not swap it, since the group's renderer and selection are wired into that one. Rename `group.content` to `group.list`; there is no `content=` replacement. See `DECISIONS.md` `D_wrapping_field`.
71
+
3
72
  ## [0.14.0] - 2026-08-31
4
73
 
5
74
  Tuile names the foundations it had been improvising: the floating layer becomes