tuile 0.13.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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +150 -37
- data/COMPARISON.md +101 -0
- data/DECISIONS.md +4266 -226
- data/README.md +44 -24
- data/TERMINOLOGY.md +22 -7
- data/book/03-layout.md +17 -10
- data/book/05-focus.md +67 -3
- data/book/06-theming.md +153 -7
- data/book/07-components.md +643 -67
- data/book/08-testing.md +94 -0
- data/book/09-styled-text.md +3 -3
- data/book/10-locale.md +216 -0
- data/book/README.md +14 -5
- data/examples/file_commander.rb +1 -1
- data/examples/sampler.rb +402 -62
- data/ideas/arrow-key-navigation.md +2 -2
- data/ideas/binder.md +177 -0
- data/ideas/composite-field.md +77 -0
- data/ideas/focus-accent.md +116 -0
- data/ideas/form-layout.md +151 -0
- data/ideas/hover/probe.rb +241 -0
- data/ideas/hover/probe_spec.rb +82 -0
- data/ideas/hover.md +909 -0
- data/ideas/modal-backdrop.md +24 -0
- data/ideas/new-components.md +49 -29
- data/lib/tuile/buffer.rb +51 -3
- data/lib/tuile/color.rb +143 -0
- data/lib/tuile/color_depth.rb +80 -0
- data/lib/tuile/component/abstract_string_field.rb +106 -58
- data/lib/tuile/component/abstract_wrapping_field.rb +243 -0
- data/lib/tuile/component/big_decimal_field.rb +52 -79
- data/lib/tuile/component/button.rb +3 -3
- data/lib/tuile/component/checkbox.rb +3 -3
- data/lib/tuile/component/checkbox_group.rb +36 -20
- data/lib/tuile/component/combo_box.rb +68 -33
- data/lib/tuile/component/confirm_window.rb +442 -0
- data/lib/tuile/component/date_field.rb +322 -0
- data/lib/tuile/component/float_field.rb +57 -82
- data/lib/tuile/component/has_bad_input.rb +88 -0
- data/lib/tuile/component/has_caption.rb +8 -0
- data/lib/tuile/component/has_content.rb +43 -11
- data/lib/tuile/component/has_placeholder.rb +62 -0
- data/lib/tuile/component/has_validation.rb +115 -0
- data/lib/tuile/component/has_value.rb +28 -1
- data/lib/tuile/component/info_window.rb +64 -16
- data/lib/tuile/component/integer_field.rb +51 -78
- data/lib/tuile/component/label.rb +6 -38
- data/lib/tuile/component/layout/box.rb +87 -19
- data/lib/tuile/component/layout.rb +13 -13
- data/lib/tuile/component/list.rb +11 -6
- data/lib/tuile/component/list_dropdown.rb +22 -10
- data/lib/tuile/component/log_text_view.rb +71 -0
- data/lib/tuile/component/log_window.rb +13 -48
- data/lib/tuile/component/menu_bar/cascade.rb +3 -3
- data/lib/tuile/component/menu_bar.rb +5 -5
- data/lib/tuile/component/notification.rb +16 -34
- data/lib/tuile/component/overlay.rb +209 -0
- data/lib/tuile/component/popup.rb +59 -187
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +39 -22
- data/lib/tuile/component/select.rb +26 -10
- data/lib/tuile/component/slot.rb +54 -0
- data/lib/tuile/component/tab_sheet.rb +0 -11
- data/lib/tuile/component/tabs.rb +5 -5
- data/lib/tuile/component/text_area.rb +14 -8
- data/lib/tuile/component/text_field.rb +42 -15
- data/lib/tuile/component/text_view.rb +25 -8
- data/lib/tuile/component/time_field.rb +454 -0
- data/lib/tuile/component/window.rb +48 -59
- data/lib/tuile/component.rb +580 -54
- data/lib/tuile/event_queue.rb +21 -1
- data/lib/tuile/fake_screen.rb +37 -3
- data/lib/tuile/final.rb +75 -0
- data/lib/tuile/keys.rb +7 -0
- data/lib/tuile/locale.rb +851 -0
- data/lib/tuile/screen.rb +251 -55
- data/lib/tuile/screen_pane.rb +50 -44
- data/lib/tuile/styled_string.rb +40 -7
- data/lib/tuile/terminal_background.rb +74 -16
- data/lib/tuile/testing.rb +198 -0
- data/lib/tuile/theme.rb +100 -10
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +88 -12
- data/lib/tuile.rb +1 -0
- data/sig/tuile.rbs +4545 -770
- metadata +25 -1
data/book/08-testing.md
CHANGED
|
@@ -95,6 +95,84 @@ lives in the buffer; cursor behavior lives in `prints`. Keeping the two
|
|
|
95
95
|
apart is deliberate, and mixing them up is the most common way a first
|
|
96
96
|
Tuile test goes wrong.
|
|
97
97
|
|
|
98
|
+
## Finding the component to drive
|
|
99
|
+
|
|
100
|
+
Asserting on the buffer needs a `rect`; driving a component needs the
|
|
101
|
+
component itself. For a two-line test you have it already — you just built
|
|
102
|
+
it. The awkward case is the one that shows up as soon as an app grows: the
|
|
103
|
+
widget you want to poke is four levels down inside something a *builder
|
|
104
|
+
method* assembled, and the test never held a reference to it.
|
|
105
|
+
|
|
106
|
+
{Tuile::Testing} is the answer. `Testing.get` walks the tree and returns the
|
|
107
|
+
one component matching a spec:
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
Testing.get(Component::Button, caption: "Save").handle_key(Keys::ENTER)
|
|
111
|
+
Testing.get(id: :amount).value = 42
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The spec is a class, an `id`, a caption, a block, or any combination of
|
|
115
|
+
them — never a path through the hierarchy, which would break every time you
|
|
116
|
+
nested one more layout. The class slot also takes a *mixin*, which is where
|
|
117
|
+
the `Has*` family from chapter 7 pays off a second time:
|
|
118
|
+
`Testing.find(Component::HasBadInput)` finds every field in the tree whose
|
|
119
|
+
parse can fail, whatever their classes.
|
|
120
|
+
|
|
121
|
+
The `id` in that second line is a plain `Symbol` tag you set on any
|
|
122
|
+
component, purely so a test can ask for it back:
|
|
123
|
+
|
|
124
|
+
```ruby
|
|
125
|
+
amount = Component::IntegerField.new
|
|
126
|
+
amount.id = :amount # nothing paints this, and nothing reads it
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`get` is strict on purpose: it raises unless *exactly one* component
|
|
130
|
+
matches. That is the whole feature. The obvious thing to write by hand is a
|
|
131
|
+
walk that takes the first match —
|
|
132
|
+
|
|
133
|
+
```ruby
|
|
134
|
+
combo = nil
|
|
135
|
+
window.on_tree { |c| combo ||= c if c.is_a?(Component::ComboBox) }
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
— and the day the pane grows a second `ComboBox`, that silently re-points
|
|
139
|
+
your test at a different widget. Nothing fails; the assertions just start
|
|
140
|
+
describing something else. `get` calls that ambiguity what it is, and the
|
|
141
|
+
failure comes with a dump of the tree it searched:
|
|
142
|
+
|
|
143
|
+
```
|
|
144
|
+
expected 1 Component::ComboBox, found 2
|
|
145
|
+
searched:
|
|
146
|
+
#<ScreenPane rect=(0,0 160x50)>
|
|
147
|
+
#<Window rect=(0,0 40x10) caption="Settings">
|
|
148
|
+
→ #<ComboBox rect=(1,1 38x1) value=nil>
|
|
149
|
+
→ #<ComboBox rect=(1,2 38x1) value="dark">
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Which usually tells you the fix immediately: narrow the search. Every
|
|
153
|
+
lookup takes `in:` to scope it to a subtree, and by default searches the
|
|
154
|
+
whole screen — popups included, since chapter 1's pane holds them under the
|
|
155
|
+
same root as the content.
|
|
156
|
+
|
|
157
|
+
```ruby
|
|
158
|
+
Testing.get(Component::ComboBox, in: sampler.demo_window)
|
|
159
|
+
```
|
|
160
|
+
|
|
161
|
+
Its sibling `Testing.find` returns *all* matches as an array, and takes an
|
|
162
|
+
optional `count:` — an Integer for exactly, a Range for a bound — so an
|
|
163
|
+
assertion about how many of something exists is one call:
|
|
164
|
+
|
|
165
|
+
```ruby
|
|
166
|
+
Testing.find(Component::Checkbox, in: form, count: 3) # raises unless 3
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
Two habits worth forming. Call these qualified, as `Testing.get(...)`:
|
|
170
|
+
`find` and `get` are the most collision-prone names in a spec suite, so
|
|
171
|
+
Tuile deliberately neither installs them on `Component` nor asks you to mix
|
|
172
|
+
the module in. And remember this is *additive* — it makes driving a tree
|
|
173
|
+
terser, and changes nothing about the assertion channel. What a component
|
|
174
|
+
*shows* is still asserted on the buffer.
|
|
175
|
+
|
|
98
176
|
## Driving the system
|
|
99
177
|
|
|
100
178
|
There are two altitudes at which you feed input, and picking the right one
|
|
@@ -108,9 +186,25 @@ key, so you assert on that too:
|
|
|
108
186
|
|
|
109
187
|
```ruby
|
|
110
188
|
list.handle_key(Keys::DOWN_ARROW) # exercises the cursor directly
|
|
189
|
+
```
|
|
190
|
+
|
|
191
|
+
**A mouse test needs the component mounted, where a key test doesn't.** A
|
|
192
|
+
click doesn't only *do* something, it also *focuses* — and
|
|
193
|
+
{Tuile::Screen#focused=} refuses a component that isn't on the pane, so
|
|
194
|
+
`handle_mouse` on a component you never attached raises "is not attached to
|
|
195
|
+
this screen". Give it a tree first:
|
|
196
|
+
|
|
197
|
+
```ruby
|
|
198
|
+
screen.content = list # a click focuses; focus needs a tree
|
|
199
|
+
list.rect = Rect.new(0, 0, 10, 5)
|
|
111
200
|
list.handle_mouse(MouseEvent.new(:left, 5, 2))
|
|
112
201
|
```
|
|
113
202
|
|
|
203
|
+
That applies to containers too, and to more of them than you might expect:
|
|
204
|
+
a click descends to every child whose rect contains the point, so testing a
|
|
205
|
+
window's footer by clicking it exercises the window, the footer's slot and
|
|
206
|
+
the footer, all of which want to be attached.
|
|
207
|
+
|
|
114
208
|
**High: go through the pane.** {Tuile::ScreenPane#handle_key} runs the
|
|
115
209
|
dispatch rung from chapter 5 that routing is actually about: delivery to
|
|
116
210
|
{Tuile::Screen#focused}, then the bubble up its ancestor chain to the scope
|
data/book/09-styled-text.md
CHANGED
|
@@ -29,7 +29,7 @@ tangle has to be re-untangled on every edit.
|
|
|
29
29
|
{Tuile::StyledString} untangles it once, structurally. A styled string is
|
|
30
30
|
a sequence of **spans**, each a maximal run of characters that share one
|
|
31
31
|
complete {Tuile::StyledString::Style} — foreground, background, bold,
|
|
32
|
-
italic, underline, strikethrough. The spans are non-overlapping and tile
|
|
32
|
+
italic, underline, strikethrough, inverse. The spans are non-overlapping and tile
|
|
33
33
|
the whole string: every character belongs to exactly one span, and that
|
|
34
34
|
span's `style` *is* the character's style. There are no overlay layers to
|
|
35
35
|
merge, no running state to reconstruct. "What's the style at column 5?"
|
|
@@ -100,8 +100,8 @@ by default.**
|
|
|
100
100
|
|
|
101
101
|
Strict means it recognizes exactly the SGR codes that map to a
|
|
102
102
|
{Tuile::StyledString::Style}'s attributes — the foreground and background
|
|
103
|
-
colors, bold, italic, underline, strikethrough — and *raises* on
|
|
104
|
-
else. An unmodeled attribute like blink or
|
|
103
|
+
colors, bold, italic, underline, strikethrough, inverse — and *raises* on
|
|
104
|
+
anything else. An unmodeled attribute like blink or conceal, an unknown SGR
|
|
105
105
|
code, a non-SGR escape like a cursor move or an OSC sequence: all of them
|
|
106
106
|
are a {Tuile::StyledString::ParseError}, not a shrug. The reason is a
|
|
107
107
|
contract worth protecting: `parse(to_ansi(x)) == x`. If parsing silently
|
data/book/10-locale.md
ADDED
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
# 10. Locale: the conventions, not the words
|
|
2
|
+
|
|
3
|
+
Chapter 6 told one story about adapting to the user's environment: the
|
|
4
|
+
terminal reports whether it is light or dark, Tuile picks a matching
|
|
5
|
+
{Tuile::Theme}, and the whole UI restyles. This chapter tells the same
|
|
6
|
+
story about a different fact — not what the terminal looks like, but how
|
|
7
|
+
the *person* in front of it expects a date and a number to be spelled.
|
|
8
|
+
|
|
9
|
+
The two are shaped alike on purpose. A frozen value type, detected once
|
|
10
|
+
at startup, held on the screen, read at use time, replaceable at
|
|
11
|
+
runtime, with a hook for anything that baked the old answer in. If you
|
|
12
|
+
have read chapter 6, you already know the shape; what is worth your time
|
|
13
|
+
here is the *boundary* — because this is the feature most likely to grow
|
|
14
|
+
into something Tuile is not.
|
|
15
|
+
|
|
16
|
+
## The one rule: conventions, never prose
|
|
17
|
+
|
|
18
|
+
> A {Tuile::Locale} holds formatting conventions — how a value is
|
|
19
|
+
> rendered and parsed. It never holds prose.
|
|
20
|
+
|
|
21
|
+
That sentence is the whole design. It is what makes `Locale` a bag of
|
|
22
|
+
about eight members rather than an internationalization subsystem, and
|
|
23
|
+
it is the test a ninth member has to pass.
|
|
24
|
+
|
|
25
|
+
The distinction is easy to feel once you see the two halves side by
|
|
26
|
+
side. "Dates in this session are spelled `dd.mm.yyyy`" is a convention:
|
|
27
|
+
it is a *rule* about rendering, it applies to every date the app will
|
|
28
|
+
ever show, and Tuile can obey it without knowing anything about your
|
|
29
|
+
app. "The date you typed is not valid" is prose: it is one sentence, in
|
|
30
|
+
one language, that belongs to one widget, and translating it is a job
|
|
31
|
+
with a catalogue, interpolation, and pluralization behind it. Tuile does
|
|
32
|
+
the first and stays out of the second — a component's wording stays the
|
|
33
|
+
component's own, settable where it matters.
|
|
34
|
+
|
|
35
|
+
And the line is not Tuile's invention. POSIX drew it first: `LC_MESSAGES`
|
|
36
|
+
carries the language your programs speak, `LC_TIME` and `LC_NUMERIC`
|
|
37
|
+
carry the formatting conventions. Tuile reads the formatting categories
|
|
38
|
+
and never the message one. That is why a session can coherently ask for
|
|
39
|
+
an English UI *and* ISO dates *and* a decimal comma — three categories,
|
|
40
|
+
three independent answers, which is exactly what one of your author's
|
|
41
|
+
machines is configured to want.
|
|
42
|
+
|
|
43
|
+
## What is in one
|
|
44
|
+
|
|
45
|
+
```ruby
|
|
46
|
+
locale = Tuile::Screen.instance.locale
|
|
47
|
+
|
|
48
|
+
locale.date_formats # => ["%Y-%m-%d"] strftime patterns, primary first
|
|
49
|
+
locale.time_formats # => ["%H:%M:%S"] ditto, at full precision
|
|
50
|
+
locale.calendar_start # => Date::GREGORIAN
|
|
51
|
+
locale.first_weekday # => 1 Monday, in Date#wday numbering
|
|
52
|
+
locale.month_names[9] # => "September"
|
|
53
|
+
locale.abbr_day_names[1] # => "Mon"
|
|
54
|
+
locale.decimal_separator # => "."
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Two things about those last few are worth pausing on.
|
|
58
|
+
|
|
59
|
+
**The name tables are keyed by the `Date` accessor that reads them.**
|
|
60
|
+
`month_names[date.month]`, `day_names[date.wday]` — so you never do
|
|
61
|
+
arithmetic to look a name up. That is why the two have different shapes:
|
|
62
|
+
`Date#month` is 1-based, so the month tables are `Hash`es keyed `1..12`;
|
|
63
|
+
`Date#wday` is 0-based, so the day tables are plain seven-element
|
|
64
|
+
arrays. It looks like an inconsistency and is the opposite of one. A
|
|
65
|
+
0-based month array would answer `month_names[9] # => "October"` — a
|
|
66
|
+
*plausible wrong answer*, silently, in the calendar header of a
|
|
67
|
+
month-view someone ships. The `Hash` makes that mistake impossible to
|
|
68
|
+
express instead of merely documented against.
|
|
69
|
+
|
|
70
|
+
**`date_formats` is a list, and the first entry is special.** Parsing
|
|
71
|
+
tries them in order and the first whole match wins; the primary —
|
|
72
|
+
`formats.first` — is also what gets *written* when a value is assigned or
|
|
73
|
+
a loosely typed buffer is canonicalized. Lenient in, strict out. Which
|
|
74
|
+
means only the primary has to survive a round-trip; a later entry only
|
|
75
|
+
ever parses, so it is allowed to be lossy. Chapter 7 has the field-side
|
|
76
|
+
story; {Tuile::Component::DateField} has the details.
|
|
77
|
+
|
|
78
|
+
**`time_formats` keeps its seconds, and the field is what drops them.**
|
|
79
|
+
This is the one member whose detected value is deliberately *more* than
|
|
80
|
+
any field shows. glibc's `t_fmt` — where the conventions come from — is a
|
|
81
|
+
**clock display** format, so it carries seconds nearly everywhere:
|
|
82
|
+
`%H:%M:%S` under C, `%H.%M.%S` under Finnish. Honoring that in a form
|
|
83
|
+
would put `:00` in front of every user who never asked for it, which is
|
|
84
|
+
what WinForms and Ant Design both actually ship.
|
|
85
|
+
|
|
86
|
+
So the seconds are dropped — but *in the field*, not here, and the
|
|
87
|
+
distinction is the whole point. Dropping them is a **policy**, and a
|
|
88
|
+
lossy one: a status-bar clock or a log timestamp legitimately wants
|
|
89
|
+
`13:45:30`, and if the locale had already thrown the seconds away, it
|
|
90
|
+
would have no way to ask for them back. A convention that carries less
|
|
91
|
+
than the system said is not a convention any more. So the locale keeps
|
|
92
|
+
the full spelling, {Tuile::Component::TimeField} reduces it to whatever
|
|
93
|
+
its `step` calls for, and a consumer that wants the other answer still
|
|
94
|
+
has one.
|
|
95
|
+
|
|
96
|
+
What *does* happen at the boundary is expansion: libc's shorthands are
|
|
97
|
+
rewritten into their parts (`%T` → `%H:%M:%S`, `%r` → `%I:%M:%S %p`) so
|
|
98
|
+
no consumer has to know them. That is a representation change and
|
|
99
|
+
nothing more, which is the line between what normalizes here and what
|
|
100
|
+
does not.
|
|
101
|
+
|
|
102
|
+
## Detection: ask the system, and only when it was asked
|
|
103
|
+
|
|
104
|
+
Ruby exposes no locale data at all. There is no `nl_langinfo` binding,
|
|
105
|
+
nothing on `Date`, nothing in `Etc`; `Date::MONTHNAMES` is frozen
|
|
106
|
+
English under every `LC_ALL` you set, and `strftime("%x")` is a fixed
|
|
107
|
+
American `%m/%d/%y` that merely *looks* like a locale channel. So
|
|
108
|
+
{Tuile::Locale.system} shells out to `locale -k` — one subprocess of
|
|
109
|
+
about a millisecond, whose answers are already strftime patterns, which
|
|
110
|
+
is Tuile's own vocabulary rather than a second grammar to translate.
|
|
111
|
+
|
|
112
|
+
The interesting part is not the subprocess. It is the gate in front of
|
|
113
|
+
it.
|
|
114
|
+
|
|
115
|
+
The C/POSIX default date format is American. That means "the user said
|
|
116
|
+
nothing" and "the user wants `%m/%d/%y`" are *indistinguishable* from
|
|
117
|
+
the answer — so a container with no `LANG` set would confidently show
|
|
118
|
+
American dates to a Norwegian. Tuile therefore detects only when the
|
|
119
|
+
environment actually said something: if the POSIX chain for a category
|
|
120
|
+
is empty, or names `C` or `POSIX`, the probe's answer for that category
|
|
121
|
+
is thrown away and {Tuile::Locale::ISO} stands.
|
|
122
|
+
|
|
123
|
+
And the gate is *per category*, which is the part that surprises people.
|
|
124
|
+
Gating once on "any locale variable is set" is wrong on a real machine:
|
|
125
|
+
a session exporting only `LC_NUMERIC=de_DE.UTF-8` would open the gate,
|
|
126
|
+
and then `d_fmt` — resolving through an unset `LC_TIME` and an unset
|
|
127
|
+
`LANG` — comes back American. So the date conventions are kept only if
|
|
128
|
+
`LC_ALL` / `LC_TIME` / `LANG` speaks, and the numeric one only if
|
|
129
|
+
`LC_ALL` / `LC_NUMERIC` / `LANG` does. One subprocess, two gates.
|
|
130
|
+
|
|
131
|
+
Everything else about detection is defensive, because none of it can be
|
|
132
|
+
allowed to fail loudly at startup: `locale(1)`'s exit status is useless
|
|
133
|
+
in both directions (a bad locale name exits 0 and quietly returns C; an
|
|
134
|
+
unknown keyword exits 1 while printing every good key), so the status is
|
|
135
|
+
ignored and each value is validated on its own. Anything that does not
|
|
136
|
+
hold up falls back to its `ISO` member individually — not
|
|
137
|
+
all-or-nothing. A missing binary, a Windows box, a musl container: `ISO`,
|
|
138
|
+
no exception raised.
|
|
139
|
+
|
|
140
|
+
## The floor is ISO 8601, and that is an argument
|
|
141
|
+
|
|
142
|
+
{Tuile::Locale::ISO} is the only constant Tuile ships. There is no
|
|
143
|
+
`EN_US`, no `DE`, and there will not be: two presets are a catalogue, a
|
|
144
|
+
catalogue implies completeness, and completeness is a promise about the
|
|
145
|
+
world's date conventions that a TUI toolkit has no business making. You
|
|
146
|
+
build your own from `ISO`:
|
|
147
|
+
|
|
148
|
+
```ruby
|
|
149
|
+
screen.locale = Tuile::Locale::ISO.with(date_formats: ["%d.%m.%Y", "%Y-%m-%d"])
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
Every member is validated in the constructor, and `Data#with` re-runs the
|
|
153
|
+
constructor — so an invalid `Locale` is unreachable by either route.
|
|
154
|
+
|
|
155
|
+
`ISO` is a defensible floor rather than a default someone picked, because
|
|
156
|
+
three of its members cite the same standard: ISO 8601 dates, an ISO 8601
|
|
157
|
+
week starting Monday, and the proleptic Gregorian calendar ISO 8601
|
|
158
|
+
mandates. The names are Ruby's own frozen English tables, re-keyed but
|
|
159
|
+
not authored — so Tuile ships zero locale data of its own, even in the
|
|
160
|
+
fallback. And the decimal separator is `"."` not because ISO says so (ISO
|
|
161
|
+
31-0 rather prefers the comma) but because `Float#to_s` writes a dot and
|
|
162
|
+
a field's `value=` goes through it: any other floor would make a numeric
|
|
163
|
+
field disagree with itself before you configured anything.
|
|
164
|
+
|
|
165
|
+
## Fields follow the session unless you say otherwise
|
|
166
|
+
|
|
167
|
+
A {Tuile::Component::DateField} takes both of its conventions from
|
|
168
|
+
`Screen#locale` until you assign them. So the common cases are one line
|
|
169
|
+
each, and they compose:
|
|
170
|
+
|
|
171
|
+
```ruby
|
|
172
|
+
screen.locale = Locale::ISO.with(date_formats: ["%d.%m.%Y"]) # every field
|
|
173
|
+
field.formats = "%Y-%m-%d" # this one only
|
|
174
|
+
field.formats = nil # follow again
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
`nil` restoring inheritance is the same shape `placeholder=` and
|
|
178
|
+
`bg_color=` already use, and it is what keeps the two decisions
|
|
179
|
+
independent: overriding one field is not opting it out of the session
|
|
180
|
+
forever.
|
|
181
|
+
|
|
182
|
+
## When the locale changes under you
|
|
183
|
+
|
|
184
|
+
{Tuile::Screen#locale=} fires {Tuile::Component#on_locale_changed}
|
|
185
|
+
across the attached tree and then invalidates all of it — the same
|
|
186
|
+
machinery {Tuile::Screen#theme=} uses, for the same reason.
|
|
187
|
+
|
|
188
|
+
Because everything repaints, anything your code *pulls* at paint or parse
|
|
189
|
+
time needs no hook at all. A date field parses its buffer on read; a
|
|
190
|
+
calendar grid would look month names up while painting. Both simply come
|
|
191
|
+
out right on the next frame.
|
|
192
|
+
|
|
193
|
+
The hook is for state you *pushed* somewhere when you last read the
|
|
194
|
+
conventions. A date field's typing hint is the worked example: it lives
|
|
195
|
+
in its editor's `placeholder`, written when the formats were last set, so
|
|
196
|
+
a repaint alone would faithfully repaint the stale `dd.mm.yyyy`. The
|
|
197
|
+
field overrides `on_locale_changed` to re-derive it — and, while it is
|
|
198
|
+
there, to rewrite a buffer that still parses into the new primary format.
|
|
199
|
+
Your own code does the same for a date you rendered into a
|
|
200
|
+
{Tuile::Component::Label}, either by overriding the hook or by assigning
|
|
201
|
+
the listener:
|
|
202
|
+
|
|
203
|
+
```ruby
|
|
204
|
+
label.on_locale_changed = -> { label.text = due.strftime(screen.locale.date_formats.first) }
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
One consequence to accept rather than defend against: a field holding a
|
|
208
|
+
half-typed buffer when the grammar changes under it may stop parsing, and
|
|
209
|
+
will then read as bad input. That is correct — the text is still there,
|
|
210
|
+
the user can see it, and they can fix it. A locale change is a
|
|
211
|
+
once-a-session event, not something worth contorting a field to survive.
|
|
212
|
+
|
|
213
|
+
The other rule inherited from chapter 6 applies unchanged: **read the
|
|
214
|
+
locale at use time; never cache it in an ivar.** A cached one strands on
|
|
215
|
+
the old conventions the moment someone assigns a new locale, and nothing
|
|
216
|
+
raises to tell you.
|
data/book/README.md
CHANGED
|
@@ -29,9 +29,10 @@ theming (including live OS light/dark flips). Chapters 7–8 close out
|
|
|
29
29
|
**narratively** — a tour of the shipped component toolbox framed around
|
|
30
30
|
when and why to reach for each, and how to test a Tuile app end to end.
|
|
31
31
|
Those two lean on the rdoc for the exact APIs; the guide keeps to the
|
|
32
|
-
walkthroughs and use-cases.
|
|
33
|
-
`Tuile::StyledString`, the text
|
|
34
|
-
|
|
32
|
+
walkthroughs and use-cases. Chapters 9–10 are **deep dives** on one
|
|
33
|
+
piece each, to read when you need them: `Tuile::StyledString`, the text
|
|
34
|
+
primitive under everything the framework draws, and `Tuile::Locale`, the
|
|
35
|
+
formatting conventions a date or a number is spelled by.
|
|
35
36
|
|
|
36
37
|
The book grows organically — a chapter exists when a concept has earned
|
|
37
38
|
one, not to fill an outline.
|
|
@@ -80,10 +81,18 @@ one, not to fill an outline.
|
|
|
80
81
|
window conveniences — framed around *when and why* you reach for each.
|
|
81
82
|
Signatures stay in the rdoc.
|
|
82
83
|
8. **[Testing a Tuile app](08-testing.md).** The testing approach:
|
|
83
|
-
`FakeScreen`, asserting against the painted buffer,
|
|
84
|
-
|
|
84
|
+
`FakeScreen`, asserting against the painted buffer, locating the
|
|
85
|
+
component to drive with `Tuile::Testing`, driving invalidation, and
|
|
86
|
+
PTY-based end-to-end tests of runnable scripts.
|
|
85
87
|
9. **[Styled text](09-styled-text.md).** A deep dive on
|
|
86
88
|
`Tuile::StyledString`, the span-based "text plus styling" value type
|
|
87
89
|
under everything Tuile draws: why spans instead of a `String` full of
|
|
88
90
|
escape codes, the style-aware algebra (slice/wrap/concat by display
|
|
89
91
|
column), minimal-diff rendering, and the strict-vs-lenient parser.
|
|
92
|
+
10. **[Locale](10-locale.md).** The other environment fact, shaped like
|
|
93
|
+
chapter 6's theme: `Tuile::Locale` holds *formatting conventions and
|
|
94
|
+
never prose*, detected from `locale(1)` only when the environment
|
|
95
|
+
actually asked, with `Locale::ISO` as the floor. Why the name tables
|
|
96
|
+
are keyed by the `Date` accessor that reads them, how a field follows
|
|
97
|
+
the session until you override it, and what `on_locale_changed` is
|
|
98
|
+
for.
|
data/examples/file_commander.rb
CHANGED
|
@@ -82,7 +82,7 @@ module FileCommanderExample
|
|
|
82
82
|
@on_cwd_changed&.call
|
|
83
83
|
rescue SystemCallError => e
|
|
84
84
|
@cwd = previous
|
|
85
|
-
Tuile::Component::InfoWindow.open("Cannot open",
|
|
85
|
+
Tuile::Component::InfoWindow.open("Cannot open", "#{path}\n#{e.message}")
|
|
86
86
|
end
|
|
87
87
|
|
|
88
88
|
def load_entries
|