tuile 0.15.0 → 0.16.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 +121 -80
- data/README.md +28 -12
- data/book/04-event-loop.md +12 -12
- data/book/05-focus.md +95 -26
- data/book/06-theming.md +58 -26
- data/book/07-components.md +61 -6
- data/book/08-testing.md +24 -22
- data/book/10-locale.md +2 -2
- data/book/README.md +6 -5
- data/examples/file_commander.rb +14 -5
- data/examples/hello_world.rb +17 -4
- data/examples/sampler.rb +392 -18
- data/lib/tuile/component/abstract_string_field.rb +16 -18
- data/lib/tuile/component/abstract_wrapping_field.rb +47 -11
- data/lib/tuile/component/button.rb +8 -8
- data/lib/tuile/component/checkbox.rb +9 -9
- data/lib/tuile/component/checkbox_group.rb +6 -5
- data/lib/tuile/component/combo_box.rb +50 -35
- data/lib/tuile/component/confirm_window.rb +7 -5
- data/lib/tuile/component/date_field.rb +28 -3
- data/lib/tuile/component/date_time_field.rb +275 -0
- data/lib/tuile/component/has_bad_input.rb +2 -2
- data/lib/tuile/component/has_content.rb +3 -3
- data/lib/tuile/component/has_placeholder.rb +1 -1
- data/lib/tuile/component/has_validation.rb +2 -2
- data/lib/tuile/component/has_value.rb +1 -1
- data/lib/tuile/component/label.rb +1 -1
- data/lib/tuile/component/layout/box.rb +4 -1
- data/lib/tuile/component/layout.rb +3 -3
- data/lib/tuile/component/list.rb +42 -32
- data/lib/tuile/component/list_dropdown.rb +3 -3
- data/lib/tuile/component/menu_bar/cascade.rb +5 -5
- data/lib/tuile/component/menu_bar.rb +18 -18
- data/lib/tuile/component/notification.rb +32 -18
- data/lib/tuile/component/overlay.rb +9 -8
- data/lib/tuile/component/picker_window.rb +27 -8
- data/lib/tuile/component/popup.rb +2 -2
- data/lib/tuile/component/progress_bar.rb +10 -4
- data/lib/tuile/component/radio_group.rb +6 -5
- data/lib/tuile/component/select.rb +11 -12
- data/lib/tuile/component/slot.rb +3 -3
- data/lib/tuile/component/tab_sheet.rb +6 -6
- data/lib/tuile/component/tabs.rb +11 -11
- data/lib/tuile/component/text_area.rb +12 -10
- data/lib/tuile/component/text_field.rb +14 -12
- data/lib/tuile/component/text_view.rb +15 -11
- data/lib/tuile/component/time_field.rb +29 -4
- data/lib/tuile/component.rb +201 -93
- data/lib/tuile/event_queue.rb +4 -4
- data/lib/tuile/fake_event_queue.rb +1 -1
- data/lib/tuile/fake_screen.rb +84 -2
- data/lib/tuile/mouse/router.rb +217 -0
- data/lib/tuile/mouse.rb +177 -0
- data/lib/tuile/screen.rb +98 -61
- data/lib/tuile/screen_pane.rb +41 -36
- data/lib/tuile/styled_string.rb +5 -5
- data/lib/tuile/testing.rb +8 -8
- data/lib/tuile/theme.rb +22 -34
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +1 -1
- data/sig/tuile.rbs +1211 -427
- metadata +4 -16
- data/COMPARISON.md +0 -101
- data/DECISIONS.md +0 -8562
- data/TERMINOLOGY.md +0 -85
- data/ideas/arrow-key-navigation.md +0 -221
- data/ideas/binder.md +0 -177
- data/ideas/composite-field.md +0 -77
- data/ideas/focus-accent.md +0 -116
- data/ideas/form-layout.md +0 -151
- data/ideas/hover/probe.rb +0 -241
- data/ideas/hover/probe_spec.rb +0 -82
- data/ideas/hover.md +0 -909
- data/ideas/modal-backdrop.md +0 -24
- data/ideas/new-components.md +0 -144
- data/ideas/per-component-buffers.md +0 -55
- data/lib/tuile/mouse_event.rb +0 -68
data/ideas/modal-backdrop.md
DELETED
|
@@ -1,24 +0,0 @@
|
|
|
1
|
-
# Modal backdrop — dim the content under a popup, or cast a shadow
|
|
2
|
-
|
|
3
|
-
**Status:** seed, 2026-08-31. Deliberately not brainstormed yet; spun off from
|
|
4
|
-
the `ConfirmWindow` design (`D_confirm_window`'s sizing paragraph).
|
|
5
|
-
|
|
6
|
-
**The problem.** A modal `Popup` floats over the tiled content with no visual
|
|
7
|
-
separation beyond its own border: the content underneath is neither dimmed nor
|
|
8
|
-
shadowed. A small popup — a `ConfirmWindow` measuring a one-line "Overwrite?" —
|
|
9
|
-
can sit in the middle of a busy screen and simply not be noticed.
|
|
10
|
-
|
|
11
|
-
**The two candidate treatments** (every GUI stack ships at least one):
|
|
12
|
-
|
|
13
|
-
- **Dim/tint** the non-popup cells under the topmost modal.
|
|
14
|
-
- **A drop shadow** — a one-cell dark offset under/right of the popup box.
|
|
15
|
-
|
|
16
|
-
**Hooks that exist today, for whoever picks this up:** `Screen#repaint`
|
|
17
|
-
already partitions tiled vs. popup subtrees and repaints popups on top, so a
|
|
18
|
-
dim pass has a natural slot between the two. Terminal cells are opaque
|
|
19
|
-
(`D_bg_inherit`), so "dim" means restyling cells, not compositing — and
|
|
20
|
-
`Color` has no darken/blend operation yet, which a dim factor would need.
|
|
21
|
-
|
|
22
|
-
**Open when picked up:** flush-time transform in `Buffer` vs. repaint-time
|
|
23
|
-
style override in components; does a shadow belong to `Overlay` or only
|
|
24
|
-
`Popup`; interaction with themes and with the terminal-default (unset) bg.
|
data/ideas/new-components.md
DELETED
|
@@ -1,144 +0,0 @@
|
|
|
1
|
-
# Components Vaadin has and Tuile doesn't — the survey
|
|
2
|
-
|
|
3
|
-
**Status:** survey done 2026-07-25 against Vaadin **25.2** (54 free/OSS
|
|
4
|
-
components, via the Vaadin docs MCP). This file is the roadmap; each
|
|
5
|
-
component we actually decide to build gets its own `ideas/<name>.md`.
|
|
6
|
-
Retire this file once the interesting part of the list is either built or
|
|
7
|
-
explicitly rejected — the tiering below is the only nugget worth keeping,
|
|
8
|
-
and it belongs here, not in a durable doc, because it goes stale as we
|
|
9
|
-
build.
|
|
10
|
-
|
|
11
|
-
Batch 1 ("field components only") is **done** — every idea filed under it has
|
|
12
|
-
graduated: `checkbox` (`DECISIONS.md` `D_boolean_fields`) and `checkbox-group`
|
|
13
|
-
(`D_checkbox_group`), both built 2026-07-30; `radio-group` (`D_radio_group`),
|
|
14
|
-
built 2026-07-31; `progress-bar` (`D_color_slots`, book ch7 "Reporting
|
|
15
|
-
progress") and `password-field` (`D_integer_field`'s taxonomy, book ch7
|
|
16
|
-
"Editing text"), both built 2026-08-02.
|
|
17
|
-
|
|
18
|
-
The **box layouts** that headed the gating list below are done too
|
|
19
|
-
(`D_box_layouts`, 2026-08-07) — `Layout::Vertical` / `::Horizontal`, plus the
|
|
20
|
-
sampler ported onto them.
|
|
21
|
-
|
|
22
|
-
## What Tuile already has
|
|
23
|
-
|
|
24
|
-
Seven of the 54 have a counterpart: Button, Text Field, Text Area,
|
|
25
|
-
Integer Field, Combo Box (1:1), Dialog ({Tuile::Component::Popup} plus
|
|
26
|
-
`Window`/`InfoWindow`/`PickerWindow`), Themable Mixin
|
|
27
|
-
({Tuile::Theme}/{Tuile::ThemeDef}). **List Box** is half-there:
|
|
28
|
-
{Tuile::Component::List} takes typed items and a renderer since 2026-08-14
|
|
29
|
-
(`D_list_items`), but has no multi-select — {Tuile::Component::CheckboxGroup}
|
|
30
|
-
is the nearest thing.
|
|
31
|
-
|
|
32
|
-
Tuile also has pieces Vaadin doesn't name as components — `ListDropdown`,
|
|
33
|
-
`TextView`, `LogWindow`, `VerticalScrollBar` — so the gap is not
|
|
34
|
-
symmetric.
|
|
35
|
-
|
|
36
|
-
That leaves ~46 gaps.
|
|
37
|
-
|
|
38
|
-
## Tier 1 — reachable from what exists (S–M each)
|
|
39
|
-
|
|
40
|
-
| Component | Builds on | Note |
|
|
41
|
-
|---|---|---|
|
|
42
|
-
| ~~Box layouts (H/V)~~ | `Layout` | **built** 2026-08-07 (`D_box_layouts`, book ch3); `Vertical`/`Horizontal` over `Box`, additive sugar on top of `Absolute` — no foundation change |
|
|
43
|
-
| ~~Checkbox~~ | `HasValue` | **built** 2026-07-30 (`D_boolean_fields`); tri-state still deferred |
|
|
44
|
-
| ~~Radio Group~~ | `List` + `HasValue` | **built** 2026-07-31 (`D_radio_group`); composes a `List`, cursor roams and Space selects |
|
|
45
|
-
| ~~Checkbox Group~~ | `List` + `HasValue` | **built** 2026-07-30 (`D_checkbox_group`); composes a `List`, frozen `Set` value |
|
|
46
|
-
| ~~Select~~ | `ListDropdown` + `HasValue` | **built** 2026-08-12 (`D_select`, book ch7); a *second driver* of `ListDropdown`, not "ComboBox − filter" — it paints its own one-row face and needs no read-only axis. Claims no printable but Space |
|
|
47
|
-
| ~~Password Field~~ | `TextField` | **built** 2026-08-02 (`D_integer_field`'s taxonomy — subclass, since a password's value *is* its text; mask default in `D_ambiguous_width`); a `display_text` seam, one mask glyph per character |
|
|
48
|
-
| ~~Number Field~~ | `IntegerField` twin | **built** 2026-08-07 as `FloatField` (`D_float_field`) and `BigDecimalField` (`D_bigdecimal_field`, on Tuile's first optional dep); each named for its Ruby value type, deliberate copies of `IntegerField` |
|
|
49
|
-
| ~~Progress Bar~~ | `draw_line` + `EventQueue#tick_fps` | **built** 2026-08-02 (`D_progress_bar`, book ch7); a `value` that stays out of `HasValue`, ticker synced from `attached? && indeterminate?` |
|
|
50
|
-
| ~~Notification~~ | `Popup` + `Ticker` | **built** 2026-08-17 (`D_notification`, book ch7); one non-modal top-right box, N messages, one 3 s ticker retiring the oldest. Corner anchor is its own `reposition` override, so `Popup` was untouched — and the `Popover` extraction still waits for a second *kind* of anchoring |
|
|
51
|
-
| ~~Confirm Dialog~~ | `Popup`+`Window`+`Button` | **built** 2026-08-31 as `ConfirmWindow` (`D_confirm_window`, book ch7); the component is the builder — `#button` plus the `alert`/`confirm`/`yes_no` factories — every button dismisses, MenuBar-shaped mnemonics with `q`/`g`/`G` reserved. The fold-`PickerWindow`-in idea is **rejected**: the two disagree on every semantic that matters (cursor, default, ESC, close-on-pick) and share only API shape |
|
|
52
|
-
| Details → Accordion | `HasContent` | Details is the atom, Accordion the group |
|
|
53
|
-
| ~~Tabs → TabSheet~~ | plain `Component` + the tree API | **built** 2026-08-23 (`D_tabs`, book ch7); a strip (one tab stop, Left/Right, immediate activation) plus a sheet whose `children` are `[strip, pane]`. Neither is `HasValue` — a tab selection is view state — and neither is `HasContent`; a `TabSheet` hides a pane by *detaching* it, deliberately — that is what fires the lifecycle hooks on every switch, and it stays the choice now that `Component#visible=` exists (`D_visibility`); the strip owns mutable `Tabs::Tab` handles rather than the `items`/`item_label` shell. Hidden/disabled/closeable tabs, lazy panes and a scrolling strip are deferred, each additive (`D_tabs`) |
|
|
54
|
-
| Popover | `ListDropdown#anchor_to` (extracted 2026-08-12) | generalize the anchored non-modal overlay: `anchor_to` moves down to it and `ListDropdown` inherits it. Build it when the second *kind* of anchoring appears (a point; a right edge that flips) — not the second caller of the same kind. Nothing gates on it today: Menu Bar shipped without it |
|
|
55
|
-
| ~~Menu Bar~~ | `ListDropdown` (+ `anchor_beside`) | **built** 2026-08-24 (`D_menu_bar`, book ch7), mnemonics the same day. Turned out *not* to need the Popover extraction: a focused strip drives a cascade of non-modal `ListDropdown`s the way `Select` drives one, so the additions were a side-anchor method, a highlighted-row rect and a cursor pass-through. Unlimited submenu depth; checkable/disabled items and global-shortcut activation deferred indefinitely |
|
|
56
|
-
| ~~DateField~~ | `AbstractWrappingField` + `HasBadInput` | **built** 2026-09-04 (`D_date_field`, book ch7), after re-tiering out of Tier 2 the same day: the v1 is manual entry only, so the calendar popup it was blocked on became a phase 2 (still Tier 2 below). Value is stdlib `Date`; a per-instance list of strftime `formats`, first whole match winning and `formats.first` written back on blur/ENTER. It filters *nothing* — a date's grammar is not prefix-closed, so the residue is reported through `HasBadInput` — and its empty well derives its own `HasPlaceholder` hint from the primary format. `%y` is rejected outright and the calendar is proleptic Gregorian, not Ruby's `Date::ITALY` |
|
|
57
|
-
| Slider | `draw_line` | arrows/PgUp; 25.2 also has a two-thumb *range* variant |
|
|
58
|
-
| Breadcrumbs | `Label`/`StyledString` | clickable path segments |
|
|
59
|
-
| Markdown | `TextView` + `StyledString` | Markdown subset → styled text; high value on a TTY |
|
|
60
|
-
|
|
61
|
-
## Tier 2 — worth doing, each blocked on a new seam
|
|
62
|
-
|
|
63
|
-
| Component | Blocked on |
|
|
64
|
-
|---|---|
|
|
65
|
-
| **Grid** (the flagship gap) | column model + renderer strategies + typed items + horizontal scroll (L) |
|
|
66
|
-
| Form Layout | a field label/helper seam (Vaadin's `HasLabel`) — Tuile fields carry no caption by decision (`D_caption_ownership`), so the seam is the layout's own cells: `ideas/form-layout.md` |
|
|
67
|
-
| Email Field | nothing, per `D_bad_input` — its value *is* its input, so it has no bad-input state and contributes only a packaged regex; **re-tiered toward reject** |
|
|
68
|
-
| Calendar grid for `DateField`, and the ~~Time~~ / DateTime twins | the grid needs the calendar popup over Popover (L). It is **phase 2** of a field that already ships (`D_date_field`, Tier 1 above), so nothing is blocked on it: a `DateField` is fully usable by typing, and the grid is additive — a second way to set the same `value`, placed with `ListDropdown#anchor_to`. Two things it inherits rather than re-decides: the month names it paints are the locale question of `ideas/locale.md`, and `PageUp`/`PageDown` stepping a month is deferred there too. The Time / DateTime twins no longer wait on that seam — it shipped (`D_locale`) — and `TimeField` shipped under `D_time_field` (2026-09-05) with **no** picker dropdown: a list of times carries no information a user lacks, so it fails the test the calendar grid passes (`D_mouse`). PageUp/PageDown step an hour instead, and its phase 2 is segment-aware Up/Down (the Qt / `dialog --timebox` model), shared with `DateField` |
|
|
69
|
-
| Multi Select Combo Box | Checkbox Group + ComboBox |
|
|
70
|
-
| Split Layout → Master Detail Layout | mouse **motion/drag**: Tuile runs X10 mode 1000 (press only, no release, no motion) |
|
|
71
|
-
| Virtual List | a lazy data-provider strategy on `List` |
|
|
72
|
-
| Side Nav | hierarchical collapsible list (the sampler's nav is the prototype) |
|
|
73
|
-
| App Layout | shell: title bar + drawer + content slot |
|
|
74
|
-
| Custom Field | formalize the composed typed-field pattern — or *reject* it as a shared base (COP: inherit to *be*, not to share); `D_integer_field` already sketches the taxonomy |
|
|
75
|
-
| Message Input / Message List / Login | nothing — pure assemblies, good example fodder |
|
|
76
|
-
| Upload | reinterpret as a file-chooser dialog (`file_commander` has the ingredients) |
|
|
77
|
-
| Icon | a glyph / Nerd-Font constants module |
|
|
78
|
-
|
|
79
|
-
## Tier 3 — design tension or marginal
|
|
80
|
-
|
|
81
|
-
- **Scroller** — scrolling *arbitrary* content needs clipping/viewport
|
|
82
|
-
machinery and pushes against the top-down layout invariant (it wants to
|
|
83
|
-
measure content). Best kept as a documented road-not-taken.
|
|
84
|
-
- **Tooltip** — competes with Tuile's status-bar `keyboard_hint` idiom.
|
|
85
|
-
- **Card** — overlaps `Window` almost entirely.
|
|
86
|
-
- **Avatar / Avatar Group** — initials in a box; little value on a TTY.
|
|
87
|
-
- **Not applicable:** Field Highlighter (collaboration), Themable Mixin
|
|
88
|
-
(covered by `Theme`).
|
|
89
|
-
|
|
90
|
-
## Infrastructure that gates clusters
|
|
91
|
-
|
|
92
|
-
These are prerequisites, not components, and each deserves its own idea
|
|
93
|
-
file when its cluster comes up:
|
|
94
|
-
|
|
95
|
-
1. ~~**Box layouts** (H/V)~~ — **done** 2026-08-07 (`D_box_layouts`). Turned
|
|
96
|
-
out *not* to be structural: a `Box` is an `Absolute` subclass with a `rect=`
|
|
97
|
-
override, so it unblocked the form-shaped cluster without touching the
|
|
98
|
-
foundation. A future Grid should reuse its `Fixed`/`Percent`/`Expand`
|
|
99
|
-
constraints per row and column rather than invent a second vocabulary.
|
|
100
|
-
2. **Field label + helper text seam** → Form Layout. Note this is what Form
|
|
101
|
-
Layout is actually blocked on — the layout half now exists. Designed
|
|
102
|
-
2026-09-03 and **half shipped the same day**: the caption goes to the layout
|
|
103
|
-
(fields do *not* include `HasCaption` — `D_caption_ownership`), and the error
|
|
104
|
-
*verdict* stays on the field as `Component::HasValidation` (one member,
|
|
105
|
-
`error_message`, `HasValue` includes it, painted as a red *well* —
|
|
106
|
-
`D_has_validation`). What is left is the container that reads the *message*
|
|
107
|
-
off the field and paints it inline-right, plus the required marker and helper
|
|
108
|
-
text: `ideas/form-layout.md`. Items 2 and 3 turn out to be the same seam cut
|
|
109
|
-
in two — this half is the *geometry*, item 3 is the *signals*.
|
|
110
|
-
3. **Validation seam** → forms generally. Designed 2026-09-03 and split three
|
|
111
|
-
ways. The field-side channel **shipped the same day**: `HasBadInput`
|
|
112
|
-
(`D_bad_input`) holds the one fact only the field can know, because
|
|
113
|
-
`on_value_change` is a diff over values and every unrepresentable input
|
|
114
|
-
collapses onto the same one, and `on_blur` — the commit point it wanted —
|
|
115
|
-
shipped 2026-09-04 (`D_on_blur`); only the push notice is still deferred, for
|
|
116
|
-
want of a consumer. Still open: `ideas/binder.md`
|
|
117
|
-
(the consumer, and the four-layer model/transformations/value/input
|
|
118
|
-
vocabulary the whole cluster now uses). The `HasValidation` half **shipped
|
|
119
|
-
the same day** as well (`D_has_validation`), so what is left of item 2's note
|
|
120
|
-
is the container that paints the message. `Date Picker` is the component that
|
|
121
|
-
actually forces it; Email Field is re-tiered toward *reject*.
|
|
122
|
-
4. **Anchored Popover extraction** → pickers, Tooltip. **No longer gates Menu
|
|
123
|
-
Bar** — `D_menu_bar` argues the side-anchor is a sibling method on
|
|
124
|
-
`ListDropdown`, since both callers still wrap a `List`; the extraction's
|
|
125
|
-
trigger is now the first non-`List` content that wants anchoring.
|
|
126
|
-
5. **Mouse motion/drag** (modes 1002/1006, release events) → Split
|
|
127
|
-
divider, Slider drag, scrollbar drag.
|
|
128
|
-
6. **Typed items + data provider on `List`** → List Box, Grid, Virtual
|
|
129
|
-
List. **Half done** 2026-08-14 (`D_list_items`): `List` takes `items` +
|
|
130
|
-
a `renderer` and renders only the visible rows, and the five composers
|
|
131
|
-
are folded onto it. The remaining half is *sourcing* items lazily (a
|
|
132
|
-
data provider behind `items`), which lazy rendering was chosen to keep
|
|
133
|
-
reachable without a redesign.
|
|
134
|
-
|
|
135
|
-
Vaadin's `Binder` is the natural companion for the forms cluster but is
|
|
136
|
-
not a component; `D_has_value` already parks the forms-layer questions
|
|
137
|
-
(converters, read-only, required indicator).
|
|
138
|
-
|
|
139
|
-
## ~~Cross-cutting open question: component color slots vs. theme tokens~~
|
|
140
|
-
|
|
141
|
-
**Settled 2026-08-01 as `DECISIONS.md` `D_color_slots`** — the slot, defaulting
|
|
142
|
-
to `nil`. Slider and Badge are bound by it when they land; Badge's promotion
|
|
143
|
-
trigger (a *second* built-in needing the same semantic color) is written up
|
|
144
|
-
there, so neither has to re-argue it.
|
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
# Per-component back buffers + a z-order compositor
|
|
2
|
-
|
|
3
|
-
**Status:** deferred, not worth doing yet. Graduated out of `Buffer`'s
|
|
4
|
-
rdoc (it was speculative design musing, not caller contract — see the
|
|
5
|
-
doc-kinds rules in AGENTS.md). Parked here so the thinking isn't lost.
|
|
6
|
-
|
|
7
|
-
## The idea
|
|
8
|
-
|
|
9
|
-
Today there is one global {Tuile::Buffer}: every component paints into
|
|
10
|
-
the same grid via `set_line` / `set_char` / `fill`, and {Tuile::Screen}
|
|
11
|
-
flushes its minimal diff to the terminal.
|
|
12
|
-
|
|
13
|
-
Components already paint through that drawing surface *without knowing*
|
|
14
|
-
whether it's the one global buffer or a private one — the indirection is
|
|
15
|
-
deliberate. So per-component back buffers plus a z-order compositor could
|
|
16
|
-
drop in **without touching component code**: give each component its own
|
|
17
|
-
`Buffer`, let it paint into that, and have a compositor blend the
|
|
18
|
-
per-component buffers in stacking order into the frame the terminal sees.
|
|
19
|
-
|
|
20
|
-
## Why it's not worth doing yet
|
|
21
|
-
|
|
22
|
-
The current single-buffer diff already captures most of the win a
|
|
23
|
-
compositor would give:
|
|
24
|
-
|
|
25
|
-
- The flush drops unchanged cells from the wire, so an unchanged region
|
|
26
|
-
costs nothing on the terminal side regardless of how many components
|
|
27
|
-
overlap it.
|
|
28
|
-
- An occluded component that didn't change is never repainted at all —
|
|
29
|
-
invalidation gates it out before `repaint` runs.
|
|
30
|
-
|
|
31
|
-
So a compositor would only save **residual `repaint` CPU**: the cost of a
|
|
32
|
-
component re-rendering its content into a buffer, when that content then
|
|
33
|
-
turns out to be occluded or unchanged after compositing.
|
|
34
|
-
|
|
35
|
-
## The one regime where it pays off
|
|
36
|
-
|
|
37
|
-
High repeat-rate scroll — a held arrow key or a spun mouse wheel — over a
|
|
38
|
-
**large** component on a **large** screen, where re-rendering the
|
|
39
|
-
component's content on every repeat is the dominant cost. A per-component
|
|
40
|
-
buffer would let an unchanged-but-scrolled component be re-composited
|
|
41
|
-
(cheap) instead of re-rendered (expensive) each repeat.
|
|
42
|
-
|
|
43
|
-
Until Tuile has a real workload in that regime, the extra machinery
|
|
44
|
-
(per-component buffer allocation, a compositor pass, dirty propagation
|
|
45
|
-
across buffer layers) buys nothing over what the single-buffer diff
|
|
46
|
-
already delivers.
|
|
47
|
-
|
|
48
|
-
## If we revisit
|
|
49
|
-
|
|
50
|
-
- The component-facing drawing API (`set_line` / `set_char` / `fill`)
|
|
51
|
-
stays the same — that's the whole point of the existing indirection.
|
|
52
|
-
- What changes is *who owns the buffer* and the addition of a
|
|
53
|
-
composite-into-frame step between `repaint` and `Buffer#flush`.
|
|
54
|
-
- Measure first: prove the residual-`repaint`-CPU regime is real and
|
|
55
|
-
material before adding a layer.
|
data/lib/tuile/mouse_event.rb
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module Tuile
|
|
4
|
-
# A mouse event.
|
|
5
|
-
#
|
|
6
|
-
# @!attribute [r] button
|
|
7
|
-
# @return [Symbol, nil] one of `:left`, `:middle`, `:right`, `:scroll_up`,
|
|
8
|
-
# `:scroll_down`, `:scroll_left`, `:scroll_right`; `nil` if not known.
|
|
9
|
-
# @!attribute [r] x
|
|
10
|
-
# @return [Integer] x coordinate, 0-based.
|
|
11
|
-
# @!attribute [r] y
|
|
12
|
-
# @return [Integer] y coordinate, 0-based.
|
|
13
|
-
class MouseEvent < Data.define(:button, :x, :y)
|
|
14
|
-
# @return [Point] the event's position.
|
|
15
|
-
def point = Point.new(x, y)
|
|
16
|
-
|
|
17
|
-
# Checks whether given key is a mouse event key. Returns true on the X10
|
|
18
|
-
# `\e[M` prefix regardless of length — {.parse} is the place that
|
|
19
|
-
# validates the full 6-byte shape and raises on malformed input.
|
|
20
|
-
# @param key [String] key read via {Keys.getkey}
|
|
21
|
-
# @return [Boolean] true if it is a mouse event
|
|
22
|
-
def self.mouse_event?(key)
|
|
23
|
-
key.start_with?("\e[M")
|
|
24
|
-
end
|
|
25
|
-
|
|
26
|
-
# Parses an X10 mouse report (`\e[M` + 3 bytes: button, x, y).
|
|
27
|
-
#
|
|
28
|
-
# Raises {Tuile::Error} when `key` starts with the mouse prefix but is
|
|
29
|
-
# not exactly 6 bytes long. Both shorter and longer inputs are bugs in
|
|
30
|
-
# the upstream key-reader: a shorter prefix means the tail was lost on
|
|
31
|
-
# the way in, and a longer one means we over-consumed into the next
|
|
32
|
-
# escape sequence. We refuse to silently truncate either case because
|
|
33
|
-
# the trailing `\e` of an over-read corrupts the *next* getkey, and the
|
|
34
|
-
# corruption then surfaces as garbled keystrokes in focused inputs
|
|
35
|
-
# rather than as a parser failure pointing at the actual cause.
|
|
36
|
-
# @param key [String] key read via {Keys.getkey}
|
|
37
|
-
# @return [MouseEvent, nil] `nil` if `key` is not a mouse event
|
|
38
|
-
# @raise [Tuile::Error] if `key` is a malformed mouse event
|
|
39
|
-
def self.parse(key)
|
|
40
|
-
return nil unless mouse_event?(key)
|
|
41
|
-
unless key.bytesize == 6
|
|
42
|
-
raise Tuile::Error,
|
|
43
|
-
"malformed mouse event: expected 6 bytes after \\e[M prefix, got #{key.bytesize}: #{key.inspect}"
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
button = key[3].ord - 32
|
|
47
|
-
# XTerm reports coordinates 1-based (column N is encoded as N + 32);
|
|
48
|
-
# subtract 33 so that `x` and `y` are 0-based.
|
|
49
|
-
x = key[4].ord - 33
|
|
50
|
-
y = key[5].ord - 33
|
|
51
|
-
button = case button
|
|
52
|
-
when 0 then :left
|
|
53
|
-
when 2 then :right
|
|
54
|
-
when 1 then :middle
|
|
55
|
-
when 64 then :scroll_up
|
|
56
|
-
when 65 then :scroll_down
|
|
57
|
-
when 66 then :scroll_left
|
|
58
|
-
when 67 then :scroll_right
|
|
59
|
-
end
|
|
60
|
-
MouseEvent.new(button, x, y)
|
|
61
|
-
end
|
|
62
|
-
|
|
63
|
-
# @return [String]
|
|
64
|
-
def self.start_tracking = "\e[?1000h"
|
|
65
|
-
# @return [String]
|
|
66
|
-
def self.stop_tracking = "\e[?1000l"
|
|
67
|
-
end
|
|
68
|
-
end
|