tuile 0.16.0 → 0.17.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 +108 -0
- data/README.md +21 -12
- data/book/02-repaint.md +47 -19
- data/book/03-layout.md +98 -49
- data/book/04-event-loop.md +5 -4
- data/book/05-focus.md +12 -9
- data/book/06-theming.md +55 -17
- data/book/07-components.md +188 -40
- data/book/08-testing.md +115 -15
- data/book/10-locale.md +1 -1
- data/book/README.md +5 -5
- data/examples/file_commander.rb +38 -27
- data/examples/hello_world.rb +1 -1
- data/examples/sampler.rb +225 -169
- data/lib/tuile/buffer.rb +12 -1
- data/lib/tuile/canvas/backend.rb +46 -0
- data/lib/tuile/canvas.rb +212 -0
- data/lib/tuile/color.rb +38 -9
- data/lib/tuile/component/abstract_string_field.rb +81 -80
- data/lib/tuile/component/abstract_wrapping_field.rb +59 -54
- data/lib/tuile/component/big_decimal_field.rb +7 -6
- data/lib/tuile/component/button.rb +19 -11
- data/lib/tuile/component/checkbox.rb +12 -10
- data/lib/tuile/component/checkbox_group.rb +11 -13
- data/lib/tuile/component/combo_box.rb +30 -40
- data/lib/tuile/component/confirm_window.rb +27 -22
- data/lib/tuile/component/date_field.rb +27 -20
- data/lib/tuile/component/date_time_field.rb +75 -31
- data/lib/tuile/component/fill.rb +93 -0
- data/lib/tuile/component/float_field.rb +7 -6
- data/lib/tuile/component/form_item.rb +250 -0
- data/lib/tuile/component/form_layout.rb +206 -0
- data/lib/tuile/component/has_bad_input.rb +98 -27
- data/lib/tuile/component/has_caption.rb +14 -5
- data/lib/tuile/component/has_content.rb +5 -12
- data/lib/tuile/component/has_validation.rb +39 -13
- data/lib/tuile/component/has_value.rb +70 -16
- data/lib/tuile/component/integer_field.rb +7 -6
- data/lib/tuile/component/label.rb +8 -15
- data/lib/tuile/component/layout/absolute.rb +86 -0
- data/lib/tuile/component/layout/box.rb +38 -63
- data/lib/tuile/component/layout.rb +124 -10
- data/lib/tuile/component/list.rb +197 -94
- data/lib/tuile/component/list_dropdown.rb +148 -88
- data/lib/tuile/component/menu_bar/cascade.rb +97 -27
- data/lib/tuile/component/menu_bar.rb +84 -64
- data/lib/tuile/component/notification.rb +44 -31
- data/lib/tuile/component/overlay.rb +210 -52
- data/lib/tuile/component/password_field.rb +1 -8
- data/lib/tuile/component/picker_window.rb +15 -10
- data/lib/tuile/component/popup.rb +13 -24
- data/lib/tuile/component/progress_bar.rb +7 -7
- data/lib/tuile/component/radio_group.rb +10 -12
- data/lib/tuile/component/scroller.rb +266 -0
- data/lib/tuile/component/select.rb +15 -31
- data/lib/tuile/component/slot.rb +1 -2
- data/lib/tuile/component/tab_sheet.rb +21 -28
- data/lib/tuile/component/tabs.rb +39 -24
- data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
- data/lib/tuile/component/text_area.rb +21 -19
- data/lib/tuile/component/text_field.rb +55 -39
- data/lib/tuile/component/text_view.rb +143 -79
- data/lib/tuile/component/time_field.rb +26 -21
- data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
- data/lib/tuile/component/window.rb +27 -26
- data/lib/tuile/component.rb +481 -259
- data/lib/tuile/component_background.rb +177 -0
- data/lib/tuile/component_util.rb +43 -0
- data/lib/tuile/event.rb +29 -0
- data/lib/tuile/event_queue.rb +14 -0
- data/lib/tuile/fake_screen.rb +41 -10
- data/lib/tuile/keys.rb +15 -6
- data/lib/tuile/layout_pass.rb +180 -0
- data/lib/tuile/listeners.rb +219 -0
- data/lib/tuile/mouse/router.rb +51 -35
- data/lib/tuile/mouse.rb +96 -29
- data/lib/tuile/point.rb +6 -0
- data/lib/tuile/rect.rb +33 -0
- data/lib/tuile/screen.rb +419 -84
- data/lib/tuile/screen_pane.rb +144 -31
- data/lib/tuile/strict_layout.rb +127 -0
- data/lib/tuile/styled_string.rb +139 -9
- data/lib/tuile/testing/gestures.rb +35 -0
- data/lib/tuile/testing.rb +310 -36
- data/lib/tuile/theme.rb +170 -19
- data/lib/tuile/theme_def.rb +4 -0
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +53 -0
- data/sig/tuile.rbs +4951 -1158
- metadata +16 -2
- data/lib/tuile/vertical_scroll_bar.rb +0 -122
data/book/08-testing.md
CHANGED
|
@@ -66,16 +66,21 @@ components don't emit escape sequences — they write styled cells into
|
|
|
66
66
|
that means the buffer *is* the rendered screen, sitting in memory, fully
|
|
67
67
|
inspectable, before any diffing or I/O. You assert against it directly.
|
|
68
68
|
|
|
69
|
-
The rhythm is: build the component,
|
|
70
|
-
buffer back over that rect.
|
|
69
|
+
The rhythm is: build the component, have a parent place it at a rect,
|
|
70
|
+
repaint, read the buffer back over that rect. A component never takes a
|
|
71
|
+
rect from anyone but its parent's `relayout` — `rect=` raises anywhere
|
|
72
|
+
else — so a test holds it in a `Layout::Absolute`, which places each child
|
|
73
|
+
exactly where it was added:
|
|
71
74
|
|
|
72
75
|
```ruby
|
|
73
76
|
label = Component::Label.new
|
|
74
|
-
label.rect = Rect.new(0, 0, 10, 1)
|
|
75
77
|
label.text = "hi"
|
|
76
|
-
|
|
78
|
+
holder = Component::Layout::Absolute.new
|
|
79
|
+
holder.add(label, Rect.new(0, 0, 10, 1))
|
|
80
|
+
Screen.instance.content = holder
|
|
81
|
+
Screen.instance.repaint
|
|
77
82
|
|
|
78
|
-
assert_equal ["hi "], Screen.instance.buffer.region_text(label.
|
|
83
|
+
assert_equal ["hi "], Screen.instance.buffer.region_text(label.absolute_rect)
|
|
79
84
|
```
|
|
80
85
|
|
|
81
86
|
{Tuile::Buffer#region_text} returns the plain text of each row in the
|
|
@@ -88,6 +93,29 @@ active-background, say, or that a theme flip changed a hint's hue. And
|
|
|
88
93
|
pinpoint check. Everything is scoped to a `rect`, so you assert about a
|
|
89
94
|
component's own region without caring what surrounds it.
|
|
90
95
|
|
|
96
|
+
That is what the *user* sees at those cells, a popup drawn over them
|
|
97
|
+
included. Often a test wants something narrower — what this one component
|
|
98
|
+
paints — and {Tuile::Testing.paint} answers that without a screen round
|
|
99
|
+
trip. It paints the component and its whole subtree into a {Tuile::Buffer}
|
|
100
|
+
of the component's own size, whose `(0, 0)` is the component's top-left:
|
|
101
|
+
|
|
102
|
+
```ruby
|
|
103
|
+
window = Component::Window.new("Settings")
|
|
104
|
+
window.content = Component::Label.new("hi")
|
|
105
|
+
Testing.place(window, Rect.new(0, 0, 12, 3)) # sized; nothing to attach
|
|
106
|
+
|
|
107
|
+
assert_equal ["┌Settings──┐", "│hi │", "└──────────┘"], Testing.paint(window).text
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
`Testing.place` puts a component at a rect through whatever places it —
|
|
111
|
+
here a throwaway holder, since `window` has no parent — because `rect=`
|
|
112
|
+
raises anywhere but a parent's `relayout`. It never attaches, and
|
|
113
|
+
`Testing.paint` doesn't need it to. Because the buffer is the component's
|
|
114
|
+
own, `cell(1, 0)` means column 1 *of the window*, and the classic mistake of
|
|
115
|
+
reading the screen at `rect` instead of `absolute_rect` has nowhere to
|
|
116
|
+
happen. Ancestors don't clip the paint, though their background shows
|
|
117
|
+
through; popups aren't in the subtree, so they don't show at all.
|
|
118
|
+
|
|
91
119
|
What you do *not* assert content against is `prints`. On a FakeScreen,
|
|
92
120
|
`prints` captures only what actually went "to the wire" — cursor
|
|
93
121
|
positioning, housekeeping escapes, and the assembled frame string. Content
|
|
@@ -107,17 +135,33 @@ method* assembled, and the test never held a reference to it.
|
|
|
107
135
|
one component matching a spec:
|
|
108
136
|
|
|
109
137
|
```ruby
|
|
110
|
-
Testing.get(
|
|
138
|
+
Testing.get(id: :save).handle_key?(Keys::ENTER)
|
|
111
139
|
Testing.get(id: :amount).value = 42
|
|
112
140
|
```
|
|
113
141
|
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
142
|
+
Those two lines drive the component directly, which is the right altitude for
|
|
143
|
+
some tests and too low for others; the gestures later in this chapter are the
|
|
144
|
+
same two lines with "could a user have done this?" asked first.
|
|
145
|
+
|
|
146
|
+
The spec is a class, an `id`, a block, or any combination of them — never a
|
|
147
|
+
path through the hierarchy, which would break every time you nested one more
|
|
148
|
+
layout. The class slot also takes a *mixin*, which is where the `Has*` family
|
|
149
|
+
from chapter 7 pays off a second time:
|
|
118
150
|
`Testing.find(Component::HasBadInput)` finds every field in the tree whose
|
|
119
151
|
parse can fail, whatever their classes.
|
|
120
152
|
|
|
153
|
+
Notice what is *not* on that list: there is no way to look a component up by
|
|
154
|
+
the text it shows. That is deliberate. Those handles are structural — they
|
|
155
|
+
describe where a component sits and what kind of thing it is — while a caption
|
|
156
|
+
is copy, and copy gets reworded by people who are not thinking about your
|
|
157
|
+
specs. A test that breaks because "Save" became "Save changes" has told you
|
|
158
|
+
nothing. When you really do want the text, the block says so and reads better
|
|
159
|
+
for being explicit about the class:
|
|
160
|
+
|
|
161
|
+
```ruby
|
|
162
|
+
Testing.get(Component::Button) { _1.caption.to_s == "Save" }
|
|
163
|
+
```
|
|
164
|
+
|
|
121
165
|
The `id` in that second line is a plain `Symbol` tag you set on any
|
|
122
166
|
component, purely so a test can ask for it back:
|
|
123
167
|
|
|
@@ -196,16 +240,19 @@ by hand tests a third of what a click is. Drive it through
|
|
|
196
240
|
{Tuile::FakeScreen}, which posts the gesture the terminal would:
|
|
197
241
|
|
|
198
242
|
```ruby
|
|
199
|
-
|
|
200
|
-
list
|
|
243
|
+
holder = Component::Layout::Absolute.new
|
|
244
|
+
holder.add(list, Rect.new(0, 0, 10, 5))
|
|
245
|
+
screen.content = holder # a press focuses; focus needs a tree
|
|
201
246
|
screen.click(5, 2) # press then release, at that cell
|
|
202
247
|
```
|
|
203
248
|
|
|
204
249
|
`click` is the whole gesture; `press` / `release` are its halves, for a test
|
|
205
250
|
about what the grab does in between, and `scroll` / `move` post the other two
|
|
206
|
-
events. They all take screen-absolute, 0-based coordinates,
|
|
207
|
-
|
|
208
|
-
|
|
251
|
+
events. They all take screen-absolute, 0-based coordinates, because that is what a
|
|
252
|
+
terminal reports — so a test clicks at `button.absolute_rect.left`, not at
|
|
253
|
+
`button.rect.left`, which is measured inside the button's parent. The component
|
|
254
|
+
receives the press back in its own coordinates. A press on a cell no component
|
|
255
|
+
covers simply does nothing.
|
|
209
256
|
|
|
210
257
|
**High: go through the pane.** {Tuile::ScreenPane#handle_key?} runs the
|
|
211
258
|
dispatch rung from chapter 5 that routing is actually about: delivery to
|
|
@@ -239,6 +286,59 @@ invalidates and lets the loop coalesce.) And to check invalidation itself
|
|
|
239
286
|
`Screen.instance.invalidated?(component)` and `invalidated_clear` let you
|
|
240
287
|
assert on the set directly.
|
|
241
288
|
|
|
289
|
+
## Gestures: locating and driving in one line
|
|
290
|
+
|
|
291
|
+
Put the two halves together and a gap appears between them. `Testing.get`
|
|
292
|
+
refuses to hand you a hidden component, because it simulates a user — but the
|
|
293
|
+
moment you have a handle, `handle_key?` and `value=` will do as they are told
|
|
294
|
+
no matter what. A button under an open modal popup, a field in a collapsed
|
|
295
|
+
panel: both drive perfectly from a test, and both are unreachable in the app
|
|
296
|
+
you are shipping. The test passes; the feature is broken.
|
|
297
|
+
|
|
298
|
+
The gestures close it. Each one asks whether a user could have done this, and
|
|
299
|
+
raises with a dump of the tree when the answer is no:
|
|
300
|
+
|
|
301
|
+
```ruby
|
|
302
|
+
Testing.click(Testing.get(Component::Button, id: :save))
|
|
303
|
+
Testing.set_value(Testing.get(Component::TextField, id: :name), "Zaphod")
|
|
304
|
+
```
|
|
305
|
+
|
|
306
|
+
`Testing.click` is `screen.click` aimed by component rather than by cell: it
|
|
307
|
+
finds the top-left cell the component actually paints, checks a press there
|
|
308
|
+
*reaches* it, and posts the real press and release. What it refuses is
|
|
309
|
+
everything that would have clicked nothing — hidden, collapsed, or covered:
|
|
310
|
+
|
|
311
|
+
```
|
|
312
|
+
#<Button id=:save rect=(1,1 8x1) caption="Save"> is not clickable at 1,1:
|
|
313
|
+
a press there reaches nothing — a modal popup is open
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
It does *not* raise when the press lands and nobody claims it. Clicking a
|
|
317
|
+
`Label` is a thing a user can really do, and nothing happens; the gesture
|
|
318
|
+
asserts the click was possible, not that it achieved something.
|
|
319
|
+
|
|
320
|
+
`Testing.set_value` asks the keyboard's version of the same question: a field
|
|
321
|
+
outside the current key scope — behind an open modal — refuses. It moves no
|
|
322
|
+
focus, since no keystroke is involved, and assigns through `value=`, so it is
|
|
323
|
+
the value-level shortcut rather than a simulation of typing: the editor's input
|
|
324
|
+
filters never run.
|
|
325
|
+
|
|
326
|
+
Written out, those calls nest inside-out. Activate the refinement and they read
|
|
327
|
+
in the order they happen:
|
|
328
|
+
|
|
329
|
+
```ruby
|
|
330
|
+
using Tuile::Testing::Gestures # top of the file, or inside one describe
|
|
331
|
+
|
|
332
|
+
Testing.get(Component::TextField, id: :name)._value = "Zaphod"
|
|
333
|
+
Testing.get(Component::Button, id: :save)._click
|
|
334
|
+
```
|
|
335
|
+
|
|
336
|
+
The leading underscore is a borrowing from Karibu-Testing, and it earns its keep
|
|
337
|
+
on the first line: `_value =` sits one character from a real `value=`, and the
|
|
338
|
+
mark is what tells a reader which is running. Being a refinement, it exists only
|
|
339
|
+
where you `using` it — per file or per `describe`, never leaking to the next —
|
|
340
|
+
and nothing is added to `Component`, so an app never sees it.
|
|
341
|
+
|
|
242
342
|
## Why background code just works
|
|
243
343
|
|
|
244
344
|
Chapter 4's rule was that background threads marshal UI work back with
|
data/book/10-locale.md
CHANGED
|
@@ -201,7 +201,7 @@ Your own code does the same for a date you rendered into a
|
|
|
201
201
|
the listener:
|
|
202
202
|
|
|
203
203
|
```ruby
|
|
204
|
-
label.on_locale_changed
|
|
204
|
+
label.on_locale_changed { label.text = due.strftime(screen.locale.date_formats.first) }
|
|
205
205
|
```
|
|
206
206
|
|
|
207
207
|
One consequence to accept rather than defend against: a field holding a
|
data/book/README.md
CHANGED
|
@@ -18,7 +18,7 @@ links to the rdoc rather than restating it.
|
|
|
18
18
|
|
|
19
19
|
Chapters 1–2 are the **base vocabulary** — the component tree and the
|
|
20
20
|
repaint model that every later chapter leans on. Chapter 3 is the heart
|
|
21
|
-
of Tuile's design: layout is top-down and
|
|
21
|
+
of Tuile's design: layout is top-down and parent-relative, and the chapter
|
|
22
22
|
argues *why that is enough* — the "C64" case for hand-placed
|
|
23
23
|
coordinates on a character grid — rather than reaching for the
|
|
24
24
|
negotiated min/pref/max machinery of desktop and web toolkits.
|
|
@@ -47,13 +47,13 @@ one, not to fill an outline.
|
|
|
47
47
|
2. **[How the screen repaints](02-repaint.md).** Why components never
|
|
48
48
|
write to the terminal directly. `invalidate` → the back buffer →
|
|
49
49
|
a minimal diff → one synchronized flush per tick. The "cover your
|
|
50
|
-
own `rect`" contract, and why the whole
|
|
51
|
-
without damage tracking
|
|
50
|
+
own `rect`" contract, the bound that enforces it, and why the whole
|
|
51
|
+
model is flicker-free without damage tracking.
|
|
52
52
|
3. **[Layout: the parent sets the size](03-layout.md).** The heart of
|
|
53
|
-
the design. Top-down,
|
|
53
|
+
the design. Top-down, parent-relative, integer coordinates; a parent
|
|
54
54
|
assigns its children's `rect` and components never negotiate a size.
|
|
55
55
|
The C64 argument for *why simple layouting is enough* on a character
|
|
56
|
-
grid,
|
|
56
|
+
grid, the `relayout` override, `Layout::Absolute`, the `Vertical` /
|
|
57
57
|
`Horizontal` box layouts and their three constraints (`Fixed` /
|
|
58
58
|
`Percent` / `Expand`) as sugar over that same rule, `Fraction` for
|
|
59
59
|
sizing a popup against the screen, and resize as a discrete
|
data/examples/file_commander.rb
CHANGED
|
@@ -40,18 +40,26 @@ module FileCommanderExample
|
|
|
40
40
|
# navigation, and notifies a callback so the shared header label can be
|
|
41
41
|
# rebuilt without the panes knowing about each other.
|
|
42
42
|
class DirList < Tuile::Component::List
|
|
43
|
+
# What `on_cwd_changed` fires. An app's own event is a `Data.define`
|
|
44
|
+
# including the marker, exactly as the gem's are.
|
|
45
|
+
CwdChangedEvent = Data.define(:source, :cwd) { include Tuile::Event }
|
|
46
|
+
|
|
47
|
+
# @!method on_cwd_changed
|
|
48
|
+
# Fired whenever this pane's `cwd` changes, or it takes focus — the shared
|
|
49
|
+
# header rebuilds from it, so the two panes need not know about each other.
|
|
50
|
+
# @return [Tuile::Listeners]
|
|
51
|
+
listener :on_cwd_changed
|
|
52
|
+
|
|
43
53
|
def initialize(start_dir)
|
|
44
54
|
super()
|
|
45
55
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
46
|
-
self.renderer = ->(entry) { Rainbow(entry[:display]).color(TYPE_COLORS[entry[:type]]) }
|
|
56
|
+
self.renderer = ->(entry, _w) { Rainbow(entry[:display]).color(TYPE_COLORS[entry[:type]]) }
|
|
47
57
|
@cwd = File.expand_path(start_dir)
|
|
48
|
-
@on_cwd_changed = nil
|
|
49
58
|
load_entries
|
|
50
|
-
|
|
59
|
+
on_item_chosen << method(:descend)
|
|
51
60
|
end
|
|
52
61
|
|
|
53
62
|
attr_reader :cwd
|
|
54
|
-
attr_accessor :on_cwd_changed
|
|
55
63
|
|
|
56
64
|
def handle_key?(key)
|
|
57
65
|
return false unless active?
|
|
@@ -66,13 +74,13 @@ module FileCommanderExample
|
|
|
66
74
|
|
|
67
75
|
def handle_focus
|
|
68
76
|
super
|
|
69
|
-
|
|
77
|
+
fire_cwd_changed
|
|
70
78
|
end
|
|
71
79
|
|
|
72
80
|
private
|
|
73
81
|
|
|
74
|
-
def descend(
|
|
75
|
-
target = File.expand_path(File.join(@cwd,
|
|
82
|
+
def descend(event)
|
|
83
|
+
target = File.expand_path(File.join(@cwd, event.item[:name]))
|
|
76
84
|
change_to(target) if File.directory?(target)
|
|
77
85
|
end
|
|
78
86
|
|
|
@@ -81,13 +89,15 @@ module FileCommanderExample
|
|
|
81
89
|
change_to(parent) if parent != @cwd
|
|
82
90
|
end
|
|
83
91
|
|
|
92
|
+
def fire_cwd_changed = on_cwd_changed.fire(CwdChangedEvent.new(source: self, cwd: @cwd))
|
|
93
|
+
|
|
84
94
|
def change_to(path)
|
|
85
95
|
previous = @cwd
|
|
86
96
|
@cwd = path
|
|
87
97
|
load_entries
|
|
88
98
|
self.cursor = Tuile::Component::List::Cursor.new
|
|
89
99
|
self.scroll_top_row = 0
|
|
90
|
-
|
|
100
|
+
fire_cwd_changed
|
|
91
101
|
rescue SystemCallError => e
|
|
92
102
|
@cwd = previous
|
|
93
103
|
Tuile::Component::InfoWindow.open("Cannot open", "#{path}\n#{e.message}")
|
|
@@ -118,9 +128,9 @@ module FileCommanderExample
|
|
|
118
128
|
end
|
|
119
129
|
|
|
120
130
|
# Top-level layout. Header label on the first row, two side-by-side
|
|
121
|
-
# windows below. `
|
|
122
|
-
# so the split tracks the terminal size automatically.
|
|
123
|
-
class FileCommander < Tuile::Component::Layout
|
|
131
|
+
# windows below. `relayout` re-runs on the initial mount and on every
|
|
132
|
+
# WINCH, so the split tracks the terminal size automatically.
|
|
133
|
+
class FileCommander < Tuile::Component::Layout
|
|
124
134
|
def initialize(left_dir, right_dir)
|
|
125
135
|
super()
|
|
126
136
|
@header = Tuile::Component::Label.new
|
|
@@ -128,21 +138,21 @@ module FileCommanderExample
|
|
|
128
138
|
|
|
129
139
|
@left_window = Tuile::Component::Window.new
|
|
130
140
|
@left_list = DirList.new(left_dir)
|
|
131
|
-
@left_list.on_cwd_changed
|
|
141
|
+
@left_list.on_cwd_changed << method(:refresh_header)
|
|
132
142
|
@left_window.content = @left_list
|
|
133
143
|
@left_window.scrollbar = true
|
|
134
144
|
add(@left_window)
|
|
135
145
|
|
|
136
146
|
@right_window = Tuile::Component::Window.new
|
|
137
147
|
@right_list = DirList.new(right_dir)
|
|
138
|
-
@right_list.on_cwd_changed
|
|
148
|
+
@right_list.on_cwd_changed << method(:refresh_header)
|
|
139
149
|
@right_window.content = @right_list
|
|
140
150
|
@right_window.scrollbar = true
|
|
141
151
|
add(@right_window)
|
|
142
152
|
|
|
143
153
|
# The status line. Every key here works in both panes, so the row never
|
|
144
154
|
# changes and nothing needs to watch focus — a status line is only worth
|
|
145
|
-
# wiring to Tuile::Screen#on_focus_changed
|
|
155
|
+
# wiring to Tuile::Screen#on_focus_changed when its text actually varies
|
|
146
156
|
# with the focused component. `theme.fg` bakes its colors in, so the
|
|
147
157
|
# one thing this label does watch is a light/dark flip.
|
|
148
158
|
@status = Tuile::Component::Label.new
|
|
@@ -152,24 +162,25 @@ module FileCommanderExample
|
|
|
152
162
|
"Enter #{t.fg(:hint, "Open")} Bksp #{t.fg(:hint, "Up")}"
|
|
153
163
|
end
|
|
154
164
|
render_status.call
|
|
155
|
-
@status.on_theme_changed
|
|
165
|
+
@status.on_theme_changed << render_status
|
|
156
166
|
add(@status)
|
|
157
167
|
end
|
|
158
168
|
|
|
159
169
|
attr_reader :left_window
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
171
|
+
protected
|
|
172
|
+
|
|
173
|
+
# No `return if rect.empty?` guard: an empty pane still assigns every
|
|
174
|
+
# child, or they strand at their old coordinates (`D_empty_ancestor`).
|
|
175
|
+
def relayout
|
|
176
|
+
# A child's rect is relative to this layout, so nothing here names where
|
|
177
|
+
# the layout itself sits — moving it moves the whole pane for free.
|
|
178
|
+
@header.rect = Tuile::Rect.new(0, 0, width, 1)
|
|
179
|
+
@status.rect = Tuile::Rect.new(0, height - 1, width, 1)
|
|
180
|
+
body_height = [height - 2, 0].max
|
|
181
|
+
half = width / 2
|
|
182
|
+
@left_window.rect = Tuile::Rect.new(0, 1, half, body_height)
|
|
183
|
+
@right_window.rect = Tuile::Rect.new(half, 1, width - half, body_height)
|
|
173
184
|
end
|
|
174
185
|
|
|
175
186
|
private
|
data/examples/hello_world.rb
CHANGED
|
@@ -37,7 +37,7 @@ window.content = Tuile::Component::Label.new("Hello, world!")
|
|
|
37
37
|
status = Tuile::Component::Label.new
|
|
38
38
|
render_status = -> { status.text = "q #{screen.theme.fg(:hint, "quit")}" }
|
|
39
39
|
render_status.call
|
|
40
|
-
status.on_theme_changed
|
|
40
|
+
status.on_theme_changed << render_status
|
|
41
41
|
|
|
42
42
|
# One row for the status line, everything else to the window.
|
|
43
43
|
root = Tuile::Component::Layout::Vertical.new
|