tuile 0.11.0 → 0.13.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 (56) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +77 -0
  3. data/DECISIONS.md +1970 -14
  4. data/README.md +136 -491
  5. data/TERMINOLOGY.md +70 -0
  6. data/book/01-first-app.md +22 -17
  7. data/book/02-repaint.md +19 -6
  8. data/book/03-layout.md +12 -11
  9. data/book/05-focus.md +133 -18
  10. data/book/06-theming.md +6 -3
  11. data/book/07-components.md +498 -38
  12. data/book/08-testing.md +18 -4
  13. data/book/README.md +7 -5
  14. data/examples/file_commander.rb +27 -20
  15. data/examples/hello_world.rb +17 -5
  16. data/examples/sampler.rb +422 -66
  17. data/ideas/arrow-key-navigation.md +16 -0
  18. data/ideas/new-components.md +16 -10
  19. data/lib/tuile/ansi.rb +10 -0
  20. data/lib/tuile/buffer.rb +7 -7
  21. data/lib/tuile/component/abstract_string_field.rb +36 -0
  22. data/lib/tuile/component/button.rb +1 -1
  23. data/lib/tuile/component/checkbox.rb +1 -1
  24. data/lib/tuile/component/checkbox_group.rb +31 -26
  25. data/lib/tuile/component/combo_box.rb +13 -8
  26. data/lib/tuile/component/info_window.rb +1 -1
  27. data/lib/tuile/component/label.rb +14 -14
  28. data/lib/tuile/component/list.rb +313 -216
  29. data/lib/tuile/component/list_dropdown.rb +100 -10
  30. data/lib/tuile/component/menu_bar/cascade.rb +255 -0
  31. data/lib/tuile/component/menu_bar.rb +582 -0
  32. data/lib/tuile/component/notification.rb +320 -0
  33. data/lib/tuile/component/picker_window.rb +3 -8
  34. data/lib/tuile/component/popup.rb +83 -19
  35. data/lib/tuile/component/progress_bar.rb +1 -1
  36. data/lib/tuile/component/radio_group.rb +32 -30
  37. data/lib/tuile/component/select.rb +10 -8
  38. data/lib/tuile/component/tab_sheet.rb +242 -0
  39. data/lib/tuile/component/tabs.rb +528 -0
  40. data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
  41. data/lib/tuile/component/text_area.rb +84 -277
  42. data/lib/tuile/component/text_field.rb +24 -7
  43. data/lib/tuile/component/text_view.rb +197 -180
  44. data/lib/tuile/component/window.rb +8 -8
  45. data/lib/tuile/component.rb +43 -18
  46. data/lib/tuile/event_queue.rb +25 -1
  47. data/lib/tuile/fake_screen.rb +14 -0
  48. data/lib/tuile/keys.rb +65 -0
  49. data/lib/tuile/screen.rb +95 -78
  50. data/lib/tuile/screen_pane.rb +109 -27
  51. data/lib/tuile/styled_string.rb +52 -12
  52. data/lib/tuile/version.rb +1 -1
  53. data/lib/tuile/vertical_scroll_bar.rb +6 -6
  54. data/sig/tuile.rbs +2307 -516
  55. metadata +9 -3
  56. data/mise.toml +0 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7560121c75afad2579449ec75052597e780749ecd56b9aca7169288bd5ffbbca
4
- data.tar.gz: 2032440b0a0831027997ce8a193457ae64eb96c425866c2f40a123933b80f20f
3
+ metadata.gz: 5eb20428f8e05c04c1a1ba4b56313cdd0250cf7b002a3f3e6e7a6e74273950db
4
+ data.tar.gz: dc18582bfb77d159b2181c5f946160c2ffa72232f22e977b37d75288756b4e8b
5
5
  SHA512:
6
- metadata.gz: a9fc19844524d5c3762a80ec43315bc8e98408cf3b62adb427ae3b93361a4e7a7d415a92eeddb068d12ba28050b0c5dce7c1716a8d5245ac47ee6d86d2869507
7
- data.tar.gz: d92742ed5181d75d1ae80c8552cf7c0112cd485bf2b613206227e907fb660d49a2a1e041b1b97f3c0ca76b6bd06a87b8259e3a118381f4d9b6b5cb854a053d13
6
+ metadata.gz: 27d48291bf7cb57d7cfea72da131cbf5b44ffd85ce3b49d772b62b3944898b741038ce39da2d9999cc56b483b1eecd5c444f86c6fe19def878f8098957706031
7
+ data.tar.gz: 473586d2424cd5f2d398b450aa10ee38e1b23560b427f99edbf7cef462c31fc32bf52e3cb450ff356e4c185ab3594060ace0bc743a5da95b06c4ae29fc3f928c
data/CHANGELOG.md CHANGED
@@ -1,5 +1,82 @@
1
1
  ## [Unreleased]
2
2
 
3
+ ## [0.13.0] - 2026-08-25
4
+
5
+ Tuile grows the navigation chrome an app builds its shell from — a `MenuBar`
6
+ driving a cascade of submenus, and `Tabs` / `TabSheet` — and gives up the last
7
+ row it reserved for itself: the framework status bar is gone, so `content`
8
+ fills the terminal and an app builds its own status line. A paste also stops
9
+ being a burst of keystrokes, arriving as one `Component#handle_paste` instead
10
+ of one keystroke per character.
11
+
12
+ - Add `Screen#on_focus_changed=` — a no-arg callback fired after the focused component *changes*, including to and from `nil` and on the focus repair a closing popup runs. Edge-triggered, so re-focusing what already has focus fires nothing. See `DECISIONS.md` `D-status-bar` and book ch5.
13
+ - Add `mnemonic:` to `Component::MenuBar#add_item` and `MenuBar::Item#add_item` — a letter that activates the item, underlined in its caption, matched level-scoped with no fallback, so only siblings can clash (which raises). See `DECISIONS.md` `D-menu-bar` and book ch7.
14
+ - Add `Component::List#select` and `Component::ListDropdown#select` — moves the cursor to an item by index, scrolling it into view and firing `on_cursor_changed`; the positional member of the `select_next` / `select_prev` family.
15
+ - Add `Component::MenuBar` — a one-row strip of menu captions, each dropping a cascade of submenus that nests without limit, built from `MenuBar::Item` handles minted and nested by `#add_item`, each carrying an optional no-arg `on_click`. See `DECISIONS.md` `D-menu-bar` and book ch7.
16
+ - Add a `MenuBar` shell to `examples/sampler.rb` — the side nav list becomes a menu bar grouped like the README's Components table, plus a `ComboBox` jump box at its right end, and the demo window fills everything below.
17
+ - Add a *MenuBar* pane to `examples/sampler.rb` — a three-deep File menu, an Edit menu, a top-level leaf that acts as a button, and a status line naming the last activated item.
18
+ - Add `Component::ListDropdown#anchor_beside` — places a panel against a row's right edge, flipping left when there is no room and sliding vertically, which is the cascading-submenu counterpart of `#anchor_to`.
19
+ - Add `Component::ListDropdown#cursor_row_rect` and `#on_cursor_changed=` — the highlighted row's rect, and the highlight-moved pass-through a cascading driver needs to drop the panels below the row it left.
20
+ - Add `Component::Tabs` — a one-row strip of captions with one selected: Left/Right switch immediately, a click selects, `#on_tab_selected` reports every change (`nil, nil` once the last tab goes), and tabs are `Tabs::Tab` handles minted by `#add_tab`. See `DECISIONS.md` `D-tabs` and book ch7.
21
+ - Add horizontal scrolling to `Component::Tabs` and `Component::MenuBar` — a strip narrower than its captions now scrolls to keep the selected or highlighted caption whole in view, instead of clipping it.
22
+ - Add a *TabSheet* pane to `examples/sampler.rb` — three tabs over a form, a `List` and a `TextView`, with a status line reporting every pane's state on each switch, so a hidden pane keeping its scroll position is visible rather than asserted.
23
+ - Add `Component::TabSheet` — a `Tabs` strip on its top row plus the selected tab's pane below it, added with `#add_tab(caption, pane)`; unselected panes are *detached* rather than hidden by a flag, so they keep their state and stay out of the Tab cycle. See `DECISIONS.md` `D-tabs`.
24
+ - Add `Screen#beep` and `Ansi::BEL` — rings the terminal bell for a keystroke that went nowhere. It writes immediately rather than riding the next frame, since the keys worth beeping at are precisely the ones that invalidate nothing.
25
+ - Add `StyledString#with_underline` — applies underline to every span, preserving each span's colors and other attributes; the underline counterpart of `#with_bold`. Slice and rejoin to underline part of a string, as a one-character mnemonic cue does.
26
+ - Add `StyledString#with_bold` — applies bold to every span, preserving each span's colors and other attributes; the bold-attribute counterpart of `#with_fg` / `#with_bg`. There is no `under_bold`, since bold has no inherited-unset state.
27
+ - Add `Component#handle_paste` — pasted text, whole and `\n`-normalized, delivered down the focus chain like a key but off the key ladder; the default returns false and `AbstractStringField` inserts at the caret in one mutation. See `DECISIONS.md` `D-bracketed-paste` and book ch5.
28
+ - Add `Screen#run_event_loop(bracketed_paste:)` — on by default, mirroring `capture_mouse:`; pass false for a terminal that mishandles mode 2004.
29
+ - Add `Keys::BRACKETED_PASTE_ON` / `_OFF`, `Keys::PASTE_START` / `PASTE_END`, `Keys.read_paste` and `Keys.normalize_paste` — the terminal-layer half: markers, a raw drain to the terminator, and CR/CRLF-to-`\n` plus a UTF-8 scrub.
30
+ - Add `EventQueue::PasteEvent` — the whole clipboard as one frozen event, posted by the key thread.
31
+ - Add `FakeScreen#paste` — normalizes and dispatches like the real key thread, so a spec can hand it the CR line endings terminals actually send.
32
+ - Add `Component::AbstractStringField#preprocess_paste` — the paste-side input filter; the base drops the C0 controls a text buffer cannot hold and `TextField` also flattens newlines to spaces and trims to `max_text_length`.
33
+ - Add a *Paste* pane to `examples/sampler.rb` — a submit-on-Enter prompt with submit/paste counters, so the distinction is visible (and PTY-testable).
34
+ - Add `Component::Popup#close_on_outside_click?` — a left click outside an open popup now closes it, modal or not (default true; `Component::Notification` opts out). Fixes dropdowns and menu cascades stranded by a click on inert decoration. See `DECISIONS.md` `D-outside-click`.
35
+ - Add `Component::Popup#owner` — names the component an overlay is part of, so a click inside it doesn't dismiss the popup hosting it. `ComboBox`, `Select` and each `MenuBar` cascade panel set it; an overlay without one is independent.
36
+ - Add `Component::Popup#on_close` — a no-arg callback fired once the popup has left the screen, however it left; for a driver keeping its own record of what is open.
37
+ - **Fix:** `Component::TabSheet` no longer holds a removed tab's pane against re-use — `Tabs::Tab#remove` bypasses `TabSheet#remove_tab`, and the stale mapping made `add_tab` reject that pane as still in use.
38
+ - **Fix:** a container whose children exactly tile its rect no longer swallows the repaint cascade — content under it survives an ancestor's `clear_background` instead of vanishing until the next unrelated repaint (visible in the sampler as rows blanking when focus moved). See `DECISIONS.md` `D-repaint-cascade`.
39
+ - **Fix:** a multi-line paste into a `Component::TextArea` subclass that rebinds ENTER no longer fires that binding once per pasted line ([#4](https://github.com/mvysny/tuile/issues/4)).
40
+ - **Fix:** `Component::TextArea`'s rdoc had the two line-break bytes backwards — a pasted break arrived as `\r`, not `\n`.
41
+ - **Fix:** `Component::TextView#handle_key` no longer refuses every key while the view is unfocused — a vestigial `active?` guard that 0.8.0's dispatch overhaul dropped from every other widget — so hand-feeding it a scroll key scrolls, as with any other component. See `DECISIONS.md` `D-text-view-scroll-verbs`.
42
+ - **Breaking:** `Component::TextField#left_column` is now private — the horizontal scroll offset was internal state with no caller outside the field. A spec asserting the scrolling reads it through `send(:left_column)`.
43
+ - **Breaking:** the framework status bar is removed — `ScreenPane#status_bar` is gone and the pane no longer reserves the bottom row, so `content` now fills the whole terminal. Build a status line into your own layout and fill it from `Screen#on_focus_changed=`; `examples/hello_world.rb` and `examples/file_commander.rb` show the shape. See `DECISIONS.md` `D-status-bar`.
44
+ - **Breaking:** `Component#keyboard_hint` is removed, along with its overrides on `Popup`, `PickerWindow`, `MenuBar`, `Tabs`, `Select`, `ComboBox` and `Notification` — four of them were unreachable in every configuration. Keep the method on your own window classes and call it from your status line; nothing in Tuile consults it now.
45
+ - **Breaking:** `Screen#register_global_shortcut` no longer takes `hint:` — the registry runs actions, it does not describe them. Drop the argument and write the hint into your own status line, next to the registration.
46
+ - **Breaking:** `Screen#active_window` is removed — it existed only to pick the component the status bar asked for a hint. Walk up from `Screen#focused` instead, which is the direction a key actually bubbles.
47
+
48
+ ## [0.12.0] - 2026-08-17
49
+
50
+ `Component::List` becomes a list of *items* rather than of pre-rendered rows: it
51
+ holds objects of any type plus a `renderer`, renders only what is on screen, and
52
+ hands your callbacks the item itself. The five components that compose a list are
53
+ folded onto it. The framework also settles its scrolling vocabulary in one
54
+ pass: `row` is the terminal grid unit everywhere, `line` means exactly what
55
+ `String#lines` returns, and `items` are the domain objects a widget renders.
56
+
57
+ - Add `Component::List#items` / `#items=` and `#renderer` — the list holds typed items, one row each, and a renderer turns an item into its row. See `DECISIONS.md` `D-list-items` and book ch7.
58
+ - Add `Component::List#refresh_rows` — re-renders every row when the renderer's *inputs* changed (a group's selection) while the items and the renderer did not.
59
+ - Add `Component::List#build_lines` — the verb-named builder that `#lines`'s block form used to be: it yields a growing `Array` and assigns it through `#lines=`.
60
+ - Add `Component::ListDropdown#items` / `#items=` / `#renderer=`, forwarding to its list.
61
+ - Add `Component::Notification` — the corner toast: `Notification.show("Saved")` floats a non-modal box in the top-right for three seconds, stacking a burst into one box that drains one message every tick. See `DECISIONS.md` `D-notification` and book ch7.
62
+ - Add `Component::TextArea#caret_row` and `#row_count` — the two readers that answer "has Up/Down anywhere left to go?", so a subclass can claim the key at the text's edge and delegate elsewhere. See `DECISIONS.md` `D-text-area-rows`.
63
+ - Add `Component::TextView#scroll_half_page_up` / `#scroll_half_page_down` — the programmatic form of `Ctrl+U` / `Ctrl+D`, for a host paging a view it does not focus. See `DECISIONS.md` `D-text-view-scroll-verbs`.
64
+ - Add `TERMINOLOGY.md` — a glossary of Tuile's house words, looked up by term; the rules live in `AGENTS.md`'s *Nomenclature* section and the reasoning in `DECISIONS.md` `D-scroll-nomenclature`.
65
+ - `Component::List` now renders lazily: only the rows in the viewport, memoized until `items=`, `renderer=` or a width change. A renderer therefore runs at paint time and must stay pure and cheap.
66
+ - `Component::List#lines=` is unchanged and stays supported: it splits on `\n` and stores the resulting `StyledString`s *as* the items, so a line-populated list behaves exactly as before.
67
+ - `Component::List::Cursor`'s count parameters are renamed `item_count` and `viewport_rows` (a cursor indexes items; only the paging half counts rows). They are positional, so no caller changes — a `Cursor` subclass overrides against the new names.
68
+ - `Component::Popup#open` now returns `self`, so construct-and-mount is one expression: `Popup.new(content: window).open`.
69
+ - **Fix:** `Component::TextView`'s half page is floored at one row, so `Ctrl+U` / `Ctrl+D` also move in a one-row viewport.
70
+ - **Fix:** `examples/file_commander.rb` navigates again — Enter on a directory called `Rainbow.uncolor` on a `StyledString` and raised.
71
+ - **Breaking:** `Component::List#on_item_chosen` and `#on_cursor_changed` now receive `(index, item)` rather than `(index, line)`. A list populated by `lines=` is unaffected (its items *are* the `StyledString` rows); one populated by `items=` must expect its own objects.
72
+ - **Breaking:** `Component::List#lines` (the reader) is removed — it returned the items, and the name lies once a `renderer` is set. Read `#items`; for the block form call `#build_lines`; to assert what a list *shows*, assert the painted buffer.
73
+ - **Breaking:** `Component::ListDropdown#lines=` / `#lines` are removed — use `#items=` with a `#renderer=`. See `DECISIONS.md` `D-list-items`.
74
+ - **Breaking:** `Component::List#add_line` and `#add_lines` are removed — an append is a statement about a collection the list owns, which a lazily-sourced provider has nothing to mutate. Keep your own array and assign it whole (`list.items = mine`); for incremental append use `Component::TextView`. See `DECISIONS.md` `D-list-items`.
75
+ - **Breaking:** `Component::Popup.open` (the class method) is removed — it hardcoded `Popup.new`, so every subclass inherited a factory that silently built a bare `Popup`. Write `Popup.new(...).open`, which now returns the popup. See `DECISIONS.md` `D-popup-open`.
76
+ - **Breaking:** `Buffer#set_line` is now `#set_text` and `Component#draw_line` is now `#draw_text` — both write a `StyledString` starting at `(x, y)` and never filled a row. Rename the calls; behavior is unchanged.
77
+ - **Breaking:** `Component::List#top_line`/`=` and `Component::TextView#top_line`/`=` are now `#scroll_top_row`/`=`, and `Component::TextArea#top_display_row` is now `#scroll_top_row`. Rename the accessors.
78
+ - **Breaking:** `VerticalScrollBar.new(line_count:, top_line:)` is now `.new(row_count:, scroll_top_row:)`. Rename the keywords.
79
+
3
80
  ## [0.11.0] - 2026-08-12
4
81
 
5
82
  - 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.