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/lib/tuile/component.rb
CHANGED
|
@@ -11,16 +11,20 @@ module Tuile
|
|
|
11
11
|
#
|
|
12
12
|
# == Handlers and listener slots
|
|
13
13
|
#
|
|
14
|
-
# Two families
|
|
14
|
+
# Two families — `handle_` is the override point, `on_` the listener slot:
|
|
15
15
|
#
|
|
16
16
|
# class Trimmed < Component::TextField
|
|
17
17
|
# def handle_blur # handle_ — the override point
|
|
18
18
|
# super
|
|
19
|
-
# self.
|
|
19
|
+
# self.value = text.strip
|
|
20
20
|
# end
|
|
21
21
|
# end
|
|
22
22
|
#
|
|
23
|
-
# label.on_theme_changed
|
|
23
|
+
# label.on_theme_changed { … } # on_… — the listener slot, a {Listeners}
|
|
24
|
+
#
|
|
25
|
+
# A slot holds *many* listeners and has no setter: register with the reader,
|
|
26
|
+
# remove with {Listeners#remove}, and nothing you add can displace what the
|
|
27
|
+
# widget or another app wired there.
|
|
24
28
|
#
|
|
25
29
|
# **What a handler returns is per hook**, declared in its own rdoc. Only the
|
|
26
30
|
# ones a dispatcher routes answer at all — {#handle_key?},
|
|
@@ -28,27 +32,27 @@ module Tuile
|
|
|
28
32
|
# took this, stop bubbling". The rest, {#handle_paste} included, return `void`.
|
|
29
33
|
#
|
|
30
34
|
# An override calls `super`, even where the base body is empty: that is what
|
|
31
|
-
# lets a hook grow an `on_foo
|
|
35
|
+
# lets a hook grow an `on_foo` slot without breaking you. The one carve-out is
|
|
32
36
|
# {#handle_child_removed}, whose base does real work and whose overrides
|
|
33
37
|
# replace it. `D_handler_naming` carries the argument.
|
|
34
38
|
class Component
|
|
35
39
|
extend Final
|
|
40
|
+
extend Listeners::Declare
|
|
36
41
|
|
|
37
42
|
# Each method's own rdoc says what an override would break; `D_final_tree`
|
|
38
43
|
# carries the full argument.
|
|
39
44
|
final :children, :parent, :parent=, :add_child, :remove_child, :detach_child,
|
|
40
|
-
:
|
|
45
|
+
:bg
|
|
41
46
|
|
|
42
47
|
def initialize
|
|
43
48
|
Component.verify_final!(self.class)
|
|
44
49
|
@rect = Rect.new(0, 0, 0, 0)
|
|
45
50
|
@visible = true
|
|
46
51
|
@active = false
|
|
47
|
-
@
|
|
48
|
-
@on_locale_changed = nil
|
|
49
|
-
@bg_color = nil
|
|
52
|
+
@bg = ComponentBackground.new(self)
|
|
50
53
|
@children = []
|
|
51
54
|
@id = nil
|
|
55
|
+
@layout_dirty = false
|
|
52
56
|
end
|
|
53
57
|
|
|
54
58
|
# A tag for finding this component again — nothing paints it, and the
|
|
@@ -74,7 +78,17 @@ module Tuile
|
|
|
74
78
|
@id = new_id
|
|
75
79
|
end
|
|
76
80
|
|
|
77
|
-
#
|
|
81
|
+
# The rectangle the component occupies **inside its parent**: `(0, 0)` is
|
|
82
|
+
# the parent's top-left, not the screen's. {#absolute_rect} is where that
|
|
83
|
+
# lands on screen, and {#local_rect} is this same rectangle with the
|
|
84
|
+
# position taken out.
|
|
85
|
+
#
|
|
86
|
+
# **Layout is deferred, so a rect read in the same turn that dirtied it is
|
|
87
|
+
# the *previous* pass's** — a plausible rectangle, not zeros.
|
|
88
|
+
# {#flush_layout} brings it up to date (`D_deferred_layout`),
|
|
89
|
+
# {#rect_stale?} answers whether it is one, and {Tuile.strict_layout} makes
|
|
90
|
+
# every such read say so.
|
|
91
|
+
# @return [Rect]
|
|
78
92
|
attr_reader :rect
|
|
79
93
|
|
|
80
94
|
# The three readers below report the geometry a parent *assigned*, as
|
|
@@ -101,7 +115,9 @@ module Tuile
|
|
|
101
115
|
#
|
|
102
116
|
# It always sits at {#rect}'s top-left — which is why this is a {Size} and
|
|
103
117
|
# not a {Rect}: an offset extent is not merely unsupported, it is
|
|
104
|
-
# unrepresentable. Use {#
|
|
118
|
+
# unrepresentable. Use {#local_extent_rect} or {#absolute_extent_rect} where
|
|
119
|
+
# coordinates are wanted — there is no parent-space form, because nothing
|
|
120
|
+
# asks the question in that space.
|
|
105
121
|
#
|
|
106
122
|
# **`nil` is not the same as `rect.size`.** `nil` says "I have not declared
|
|
107
123
|
# what I paint, so clear everything before I do", which is what a
|
|
@@ -116,8 +132,9 @@ module Tuile
|
|
|
116
132
|
# so {#rect} still means exactly what the parent assigned (`D_extent`). Three
|
|
117
133
|
# things read it, all of them this component or the framework painting it:
|
|
118
134
|
# {#clear_outside_extent} blanks the dead tail, {Mouse::Router} hit-tests
|
|
119
|
-
# against
|
|
120
|
-
# dropdown anchors under
|
|
135
|
+
# against {#local_extent_rect} so a click on that tail doesn't activate the
|
|
136
|
+
# widget, and a dropdown anchors under {#absolute_extent_rect} rather than
|
|
137
|
+
# under unused space.
|
|
121
138
|
#
|
|
122
139
|
# **An override promises to paint the extent in full**, so `super` in
|
|
123
140
|
# {#repaint} blanks only what is outside it. The arithmetic is each widget's
|
|
@@ -126,36 +143,198 @@ module Tuile
|
|
|
126
143
|
# @return [Size, nil]
|
|
127
144
|
def extent = nil
|
|
128
145
|
|
|
129
|
-
# {#
|
|
130
|
-
#
|
|
131
|
-
#
|
|
132
|
-
#
|
|
146
|
+
# {#rect} with the position taken out: the same size at `(0, 0)`.
|
|
147
|
+
#
|
|
148
|
+
# **Two things live in these coordinates**, and that is the whole point of
|
|
149
|
+
# them being one space: what this component *paints* ({Screen#canvas_for}
|
|
150
|
+
# puts the canvas here), and what its children's {#rect}s are measured in.
|
|
151
|
+
# So a container divides `local_rect` among its children and blanks its own
|
|
152
|
+
# gaps in the very same numbers:
|
|
153
|
+
#
|
|
154
|
+
# private def relayout
|
|
155
|
+
# half = width / 2 # no `rect.left +` anywhere:
|
|
156
|
+
# left.rect = Rect.new(0, 0, half, height)
|
|
157
|
+
# right.rect = Rect.new(half, 0, width - half, height)
|
|
158
|
+
# end
|
|
159
|
+
#
|
|
160
|
+
# @return [Rect]
|
|
161
|
+
def local_rect = Rect.new(0, 0, rect.width, rect.height)
|
|
162
|
+
|
|
163
|
+
# {#extent} at `(0, 0)`, {#local_rect}'s counterpart — what
|
|
164
|
+
# {#clear_inside_extent} blanks and what {Mouse::Router} hit-tests. Total:
|
|
165
|
+
# an undeclared {#extent} yields {#local_rect}, so a generic caller never
|
|
166
|
+
# sees `nil`.
|
|
133
167
|
# @return [Rect]
|
|
134
|
-
def
|
|
168
|
+
def local_extent_rect
|
|
135
169
|
e = extent
|
|
136
|
-
e.nil? ?
|
|
170
|
+
e.nil? ? local_rect : Rect.new(0, 0, e.width, e.height)
|
|
137
171
|
end
|
|
138
172
|
|
|
139
|
-
#
|
|
140
|
-
#
|
|
141
|
-
# {#
|
|
173
|
+
# {#rect} in **screen** coordinates — every ancestor's offset summed in.
|
|
174
|
+
# The form the three consumers outside this component's own frame need: the
|
|
175
|
+
# {Canvas#origin} {Screen#canvas_for} builds, an overlay's anchor (an
|
|
176
|
+
# overlay hangs off {ScreenPane}, so it shares no offset with its driver),
|
|
177
|
+
# and a spec clicking a component by where it sits.
|
|
142
178
|
#
|
|
143
|
-
#
|
|
179
|
+
# Derived on every call and never cached — a parent may move this subtree
|
|
180
|
+
# between two reads, and nothing announces it (`D_relative_rect`).
|
|
181
|
+
# @return [Rect]
|
|
182
|
+
def absolute_rect = rect.at(to_screen(Point::ZERO))
|
|
183
|
+
|
|
184
|
+
# {#local_extent_rect} in screen coordinates, {#absolute_rect}'s
|
|
185
|
+
# counterpart — what a dropdown anchors against.
|
|
186
|
+
# @return [Rect]
|
|
187
|
+
def absolute_extent_rect = local_extent_rect.at(to_screen(Point::ZERO))
|
|
188
|
+
|
|
189
|
+
# Converts a point in *this component's own* coordinates — the ones it
|
|
190
|
+
# paints in, the ones a {Mouse::Event} reaches it in — to screen
|
|
191
|
+
# coordinates, by walking up and adding each ancestor's offset.
|
|
192
|
+
#
|
|
193
|
+
# # a strip anchoring a panel under one of its own segments
|
|
194
|
+
# Rect.new(0, 0, width, 1).at(to_screen(Point.new(column, 0)))
|
|
195
|
+
#
|
|
196
|
+
# Iterative and summing into two locals rather than recursing through a
|
|
197
|
+
# {Point} per level: this runs once per component per repaint, from
|
|
198
|
+
# {Screen#canvas_for}.
|
|
199
|
+
# @param point [Point] in this component's coordinates.
|
|
200
|
+
# @return [Point] in screen coordinates.
|
|
201
|
+
def to_screen(point)
|
|
202
|
+
x = point.x
|
|
203
|
+
y = point.y
|
|
204
|
+
node = self
|
|
205
|
+
until node.nil?
|
|
206
|
+
x += node.rect.left
|
|
207
|
+
y += node.rect.top
|
|
208
|
+
node = node.parent
|
|
209
|
+
end
|
|
210
|
+
Point.new(x, y)
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
# {#to_screen}'s inverse: a screen point in this component's own
|
|
214
|
+
# coordinates. The result may be negative or past {#size} — a point outside
|
|
215
|
+
# the component converts perfectly well, which is what a grabbed component's
|
|
216
|
+
# {Mouse::DragEvent} relies on.
|
|
217
|
+
# @param point [Point] in screen coordinates.
|
|
218
|
+
# @return [Point] in this component's coordinates.
|
|
219
|
+
def to_local(point)
|
|
220
|
+
x = point.x
|
|
221
|
+
y = point.y
|
|
222
|
+
node = self
|
|
223
|
+
until node.nil?
|
|
224
|
+
x -= node.rect.left
|
|
225
|
+
y -= node.rect.top
|
|
226
|
+
node = node.parent
|
|
227
|
+
end
|
|
228
|
+
Point.new(x, y)
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Places the component **inside its parent**: `(0, 0)` is the parent's
|
|
232
|
+
# top-left, so a container divides its own {#local_rect} and never adds its
|
|
233
|
+
# own position in. {#absolute_rect} is where the result lands on screen.
|
|
234
|
+
#
|
|
235
|
+
# A component that sticks outside its parent's {#local_rect}, or paints
|
|
236
|
+
# outside this rectangle, is cut to it — {Screen#canvas_for} bounds every
|
|
237
|
+
# component by its own rect and every ancestor's (`D_clip`). Overrunning is
|
|
238
|
+
# still a bug; it now shows as truncation rather than as a corrupt neighbour.
|
|
239
|
+
#
|
|
240
|
+
# The component is invalidated and will paint over the new rectangle, and
|
|
241
|
+
# so is its parent, which owns the cells the old position vacated.
|
|
242
|
+
#
|
|
243
|
+
# **The children do not move yet**: this only marks a {#relayout}, so a
|
|
244
|
+
# child's rect read back in the same turn is still the previous pass's —
|
|
245
|
+
# {#flush_layout} first.
|
|
144
246
|
#
|
|
145
|
-
#
|
|
146
|
-
#
|
|
247
|
+
# **Only the parent's {#relayout} may call this**, and it raises from
|
|
248
|
+
# anywhere else, a component with no parent included. To move a child,
|
|
249
|
+
# change what its parent places it by — {Layout::Absolute#constrain},
|
|
250
|
+
# {Layout::Box#constrain}, {Component::Overlay#placement=} — and to size a
|
|
251
|
+
# tree that has no screen, hold it in a {Layout::Absolute}:
|
|
252
|
+
#
|
|
253
|
+
# holder = Component::Layout::Absolute.new
|
|
254
|
+
# holder.add(tree, Rect.new(0, 0, 40, 10))
|
|
255
|
+
# holder.flush_layout
|
|
256
|
+
#
|
|
257
|
+
# A subclass reacts to a new rect in {#handle_rect_changed}; it cannot
|
|
258
|
+
# override this, because a `protected` override is callable only from its
|
|
259
|
+
# own class, and the parent calling it is not one.
|
|
147
260
|
# @param new_rect [Rect] new position. Does nothing if the new rectangle is
|
|
148
261
|
# the same as the old one.
|
|
262
|
+
# @raise [Tuile::Error] unless the parent's {#relayout} is running.
|
|
149
263
|
def rect=(new_rect)
|
|
150
264
|
raise TypeError, "expected Rect, got #{new_rect.inspect}" unless new_rect.is_a? Rect
|
|
265
|
+
|
|
266
|
+
LayoutPass.check(self)
|
|
267
|
+
LayoutPass.note_placement(self)
|
|
151
268
|
return if @rect == new_rect
|
|
152
269
|
|
|
153
|
-
|
|
270
|
+
old_rect = @rect
|
|
154
271
|
@rect = new_rect
|
|
155
|
-
|
|
272
|
+
handle_rect_changed(old_rect)
|
|
156
273
|
invalidate
|
|
274
|
+
# Nothing else blanks the cells the old rect vacated — the same reason
|
|
275
|
+
# {#visible=} invalidates the parent.
|
|
276
|
+
parent&.invalidate
|
|
277
|
+
invalidate_layout
|
|
278
|
+
end
|
|
279
|
+
protected :rect=
|
|
280
|
+
|
|
281
|
+
# Runs every {#relayout} this component's *tree* owes, so its rects are
|
|
282
|
+
# current — the force-now that makes a detached tree measurable:
|
|
283
|
+
#
|
|
284
|
+
# layout = Component::Layout::Vertical.new # no Screen in the process
|
|
285
|
+
# layout.add(label, Fixed[1])
|
|
286
|
+
# layout.rect = Rect.new(0, 0, 20, 10)
|
|
287
|
+
# layout.flush_layout
|
|
288
|
+
# label.rect # => Rect(0, 0, 20, 1)
|
|
289
|
+
#
|
|
290
|
+
# Attached, {Screen#dispatch} already flushes after every event, so ask for
|
|
291
|
+
# this by hand only to read a rect in the *same* turn that dirtied it —
|
|
292
|
+
# what Swing spells `validate()` and Tk `update idletasks`.
|
|
293
|
+
#
|
|
294
|
+
# **Overwhelmingly that means a spec**: an app mutates and lets the settle
|
|
295
|
+
# run, so a `flush_layout` in app code is usually a sign the *read* wants
|
|
296
|
+
# deferring instead.
|
|
297
|
+
#
|
|
298
|
+
# == Implementation details
|
|
299
|
+
#
|
|
300
|
+
# The whole tree, never this subtree, whichever end it is asked from: a
|
|
301
|
+
# pending ancestor pass would overwrite whatever a narrower one wrote.
|
|
302
|
+
# Attached that is {Screen#flush_layout}; detached it is {LayoutPass.drain}
|
|
303
|
+
# from {#root}, the same fixpoint the screen runs.
|
|
304
|
+
# @raise [Tuile::Error] when the tree has not settled after
|
|
305
|
+
# {LayoutPass::MAX_ROUNDS} rounds — a relayout feeding its own input — or
|
|
306
|
+
# when called from inside a {#relayout}.
|
|
307
|
+
# @return [void]
|
|
308
|
+
def flush_layout
|
|
309
|
+
return screen.flush_layout if attached?
|
|
310
|
+
|
|
311
|
+
LayoutPass.refuse_nested
|
|
312
|
+
LayoutPass.drain(root)
|
|
157
313
|
end
|
|
158
314
|
|
|
315
|
+
# Whether this container owes a {#relayout} — that its *children*'s rects
|
|
316
|
+
# are out of date.
|
|
317
|
+
#
|
|
318
|
+
# **It says nothing about this component's own {#rect}**, which no pass of
|
|
319
|
+
# its own touches: the flag that makes *this* rect stale sits on the parent
|
|
320
|
+
# that assigns it, and {#rect_stale?} is that question asked from here.
|
|
321
|
+
# Survives detaching, so {#handle_attached} can hand the mark to the {Screen}.
|
|
322
|
+
# @return [Boolean]
|
|
323
|
+
def layout_dirty? = @layout_dirty
|
|
324
|
+
|
|
325
|
+
# Whether {#rect} is the *previous* pass's rectangle, because some ancestor
|
|
326
|
+
# owes a {#relayout} and that pass reassigns every rect below it — or is
|
|
327
|
+
# running it right now and has not reached this branch yet.
|
|
328
|
+
#
|
|
329
|
+
# holder.constrain(pane, Rect.new(0, 0, 100, 26))
|
|
330
|
+
# pane.left.rect_stale? # => true — `pane.left.rect` is still the old half
|
|
331
|
+
#
|
|
332
|
+
# {#flush_layout} makes it false; {Tuile.strict_layout} reports every read
|
|
333
|
+
# taken while it is true. One walk to {#root} per call, which is why no
|
|
334
|
+
# framework read consults it outside strict mode.
|
|
335
|
+
# @return [Boolean]
|
|
336
|
+
def rect_stale? = !stale_layout_ancestor.nil?
|
|
337
|
+
|
|
159
338
|
# This component's own flag — **not** whether the user can see it, which
|
|
160
339
|
# also depends on its ancestors: a shown field inside a hidden panel
|
|
161
340
|
# answers `true`.
|
|
@@ -185,7 +364,9 @@ module Tuile
|
|
|
185
364
|
# {#handle_child_removed}), and does not hand it back on the way in.
|
|
186
365
|
#
|
|
187
366
|
# For "invisible but still occupying its space", use a
|
|
188
|
-
# {Component::Slot} with no content (`D_slots`).
|
|
367
|
+
# {Component::Slot} with no content (`D_slots`). The parent's own
|
|
368
|
+
# {#relayout} may flip it — before placing any child, or it runs twice
|
|
369
|
+
# ({#invalidate_layout}).
|
|
189
370
|
# @param value [Boolean]
|
|
190
371
|
# @raise [Tuile::Error] when the UI is locked, or always from
|
|
191
372
|
# {Component::Overlay#visible=}.
|
|
@@ -196,9 +377,12 @@ module Tuile
|
|
|
196
377
|
|
|
197
378
|
screen.check_locked if attached?
|
|
198
379
|
@visible = value
|
|
199
|
-
#
|
|
200
|
-
#
|
|
201
|
-
|
|
380
|
+
# Both on the parent, because the child just vacated (or re-claimed) cells
|
|
381
|
+
# the parent owns *and* may have changed how it divides its space. A
|
|
382
|
+
# hidden component paints nothing itself, so nothing else would blank what
|
|
383
|
+
# it left behind.
|
|
384
|
+
parent&.invalidate
|
|
385
|
+
parent&.invalidate_layout
|
|
202
386
|
repair_focus_after_hiding unless value
|
|
203
387
|
walk_tree { |c| screen.invalidate(c) } if attached?
|
|
204
388
|
end
|
|
@@ -212,35 +396,54 @@ module Tuile
|
|
|
212
396
|
screen.focused = self
|
|
213
397
|
end
|
|
214
398
|
|
|
215
|
-
#
|
|
216
|
-
#
|
|
217
|
-
#
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
#
|
|
221
|
-
# resolution skips this component's {#default_bg_color} and takes whatever
|
|
222
|
-
# surrounds it. CSS's `background: inherit`, and the reason a widget with a
|
|
223
|
-
# well can be made to sit flush in a tinted panel:
|
|
399
|
+
# Asks whatever scrolls above to bring `rect` into view — "show me this",
|
|
400
|
+
# made by the component that wants to be seen, never polled for by a
|
|
401
|
+
# container:
|
|
402
|
+
#
|
|
403
|
+
# row.scroll_to_visible # all of me
|
|
404
|
+
# editor.scroll_to_visible(caret_row_rect) # this much of me
|
|
224
405
|
#
|
|
225
|
-
#
|
|
406
|
+
# The request climbs the parent chain re-expressed a level at a time, so
|
|
407
|
+
# with nothing scrolling on the way it reaches the root and does nothing.
|
|
408
|
+
# {Screen#focused=} makes the no-argument call on every focus assignment,
|
|
409
|
+
# which is the whole of making Tab follow the view: a child scrolled out of
|
|
410
|
+
# sight keeps its rect, its keys and its tab stop (`D_empty_ancestor`).
|
|
226
411
|
#
|
|
227
|
-
#
|
|
228
|
-
#
|
|
229
|
-
#
|
|
230
|
-
#
|
|
231
|
-
|
|
412
|
+
# A container that scrolls overrides it, and the shape is the contract:
|
|
413
|
+
#
|
|
414
|
+
# def scroll_to_visible(rect = local_extent_rect)
|
|
415
|
+
# delta = ... # the minimum that makes rect visible
|
|
416
|
+
# move_scroll_top_row_by(delta)
|
|
417
|
+
# super(rect.moved_by(Point.new(0, -delta)))
|
|
418
|
+
# end
|
|
419
|
+
#
|
|
420
|
+
# **The minimum** distance, so the far edge of the viewport is the one
|
|
421
|
+
# allowed to cut a child in half. And `super` takes the rect **where the
|
|
422
|
+
# scroll left it**, so an outer scroller is asked about cells that exist and
|
|
423
|
+
# nested scrollers settle inner-first.
|
|
424
|
+
#
|
|
425
|
+
# A hidden component raises, checked a level at a time as the request
|
|
426
|
+
# climbs — so it fails late: a scroller below a hidden ancestor has
|
|
427
|
+
# already scrolled.
|
|
428
|
+
# @param rect [Rect] in *this* component's coordinates; defaults to
|
|
429
|
+
# {#local_extent_rect}, so a widget asks for what it paints.
|
|
430
|
+
# @raise [Tuile::Error] when this component or an ancestor is hidden.
|
|
431
|
+
# @return [void]
|
|
432
|
+
def scroll_to_visible(rect = local_extent_rect)
|
|
433
|
+
raise Tuile::Error, "#{self} is hidden; it cannot be scrolled into view" unless visible?
|
|
232
434
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
#
|
|
237
|
-
#
|
|
238
|
-
|
|
435
|
+
parent&.scroll_to_visible(rect.moved_by(self.rect.top_left))
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
# @return [Color, Theme::Ref, Hash{Symbol => Color, Theme::Ref}, Symbol, nil]
|
|
439
|
+
# this component's own background — the value as set, so a {Theme::Ref}
|
|
440
|
+
# comes back unresolved and a state map comes back a Hash; `nil` when
|
|
441
|
+
# unset, in which case the widget's own well and then the parent answer.
|
|
442
|
+
def bg_color = @bg.color
|
|
239
443
|
|
|
240
444
|
# Tints this component and every descendant that doesn't set its own
|
|
241
|
-
# background
|
|
242
|
-
#
|
|
243
|
-
# subtree so it repaints.
|
|
445
|
+
# background — set it once on a container / {Component::Popup} to tint a
|
|
446
|
+
# whole subtree. Invalidates the subtree so it repaints.
|
|
244
447
|
#
|
|
245
448
|
# A {Theme::Ref} is re-resolved against the theme each paint, so it tracks
|
|
246
449
|
# light/dark flips with no {#handle_theme_changed} hook; a {Color} is fixed:
|
|
@@ -248,9 +451,9 @@ module Tuile
|
|
|
248
451
|
# panel.bg_color = Theme.ref(:panel_bg) # theme-tracked
|
|
249
452
|
# panel.bg_color = Color::GREY27 # fixed
|
|
250
453
|
#
|
|
251
|
-
# A Hash keyed by {
|
|
252
|
-
# that highlights itself on focus needs, and the reason
|
|
253
|
-
# on one is a *choice* rather than a trap:
|
|
454
|
+
# A Hash keyed by {ComponentBackground::STATES} gives a color per state —
|
|
455
|
+
# the shape a widget that highlights itself on focus needs, and the reason
|
|
456
|
+
# setting a flat color on one is a *choice* rather than a trap:
|
|
254
457
|
#
|
|
255
458
|
# field.bg_color = grey # flat: focused or not
|
|
256
459
|
# field.bg_color = { normal: grey, active: blue } # the pair
|
|
@@ -258,27 +461,23 @@ module Tuile
|
|
|
258
461
|
# # well, override focus
|
|
259
462
|
#
|
|
260
463
|
# A state whose key is absent is not answered here at all: resolution falls
|
|
261
|
-
# through to
|
|
464
|
+
# through to the widget's own well and then to the parent, exactly as `nil`
|
|
262
465
|
# does. That is what makes the third line above mean what it reads as.
|
|
263
466
|
#
|
|
264
467
|
# This does *not* win over a validation error: {#error_bg_color} resolves
|
|
265
468
|
# first, so tinting a panel cannot switch off the error well on the fields
|
|
266
|
-
# inside it.
|
|
469
|
+
# inside it. {ComponentBackground} carries the whole chain.
|
|
267
470
|
#
|
|
268
471
|
# @param color [Color, Theme::Ref, Hash, Symbol, Integer, Array<Integer>, nil]
|
|
269
|
-
# a {Theme::Ref}, {
|
|
270
|
-
# coerced via {Color.coerce}; `nil` unsets
|
|
271
|
-
#
|
|
272
|
-
#
|
|
472
|
+
# a {Theme::Ref}, {ComponentBackground::INHERIT}, a state Hash, else a
|
|
473
|
+
# color coerced via {Color.coerce}; `nil` unsets.
|
|
474
|
+
# @raise [ArgumentError] when a Hash carries a key outside
|
|
475
|
+
# {ComponentBackground::STATES}.
|
|
273
476
|
# @raise [KeyError] when a {Theme::Ref} names an absent custom token —
|
|
274
477
|
# validated eagerly at assignment, not deferred to paint.
|
|
275
478
|
# @return [void]
|
|
276
479
|
def bg_color=(color)
|
|
277
|
-
color =
|
|
278
|
-
return if @bg_color == color
|
|
279
|
-
|
|
280
|
-
@bg_color = color
|
|
281
|
-
walk_tree { |c| screen.invalidate(c) } if attached?
|
|
480
|
+
@bg.color = color
|
|
282
481
|
end
|
|
283
482
|
|
|
284
483
|
# Repaints the component. The default does the bookkeeping most components
|
|
@@ -303,7 +502,7 @@ module Tuile
|
|
|
303
502
|
# **The children are re-invalidated whether or not they tile.** A container
|
|
304
503
|
# that paints nothing of its own can only redraw its area *through* them, so
|
|
305
504
|
# a tiling container that skipped this would be a dead end in the cascade: an
|
|
306
|
-
# ancestor's
|
|
505
|
+
# ancestor's background clear wipes the whole ancestor rect — siblings and
|
|
307
506
|
# grandchildren included — and re-invalidates only its *direct* children, so
|
|
308
507
|
# the notice has to keep travelling down or the cleared cells are never
|
|
309
508
|
# repainted. Cheap by construction: repainting the same glyphs leaves
|
|
@@ -312,13 +511,25 @@ module Tuile
|
|
|
312
511
|
# A container that skips `super` because it paints its own rect must still
|
|
313
512
|
# call {#invalidate_children} — that is the half of this that cannot be
|
|
314
513
|
# dropped.
|
|
514
|
+
#
|
|
515
|
+
# **Paint onto `canvas`, never onto {Screen#canvas} by name.** It arrives
|
|
516
|
+
# already loaded with this component's {ComponentBackground#effective}, so every write
|
|
517
|
+
# through it inherits; reach for the screen's own and inheritance silently
|
|
518
|
+
# stops (`D_canvas`).
|
|
519
|
+
#
|
|
520
|
+
# **The canvas paints in this component's own coordinates**: `(0, 0)` is {#rect}'s
|
|
521
|
+
# top-left, so `rect.left` has no place in a `repaint` — adding it lands
|
|
522
|
+
# the write at twice the offset, with nothing raising. {#local_rect} is
|
|
523
|
+
# the region argument to reach for; {Canvas} carries the two spaces.
|
|
524
|
+
# @param canvas [Canvas] the paint context, from {Screen#canvas_for}.
|
|
525
|
+
# Required: a canvas carries state, so there is no default worth inventing.
|
|
315
526
|
# @return [void]
|
|
316
|
-
def repaint
|
|
527
|
+
def repaint(canvas)
|
|
317
528
|
return if rect.empty?
|
|
318
529
|
|
|
319
530
|
unless children.any? && children_tile_rect?
|
|
320
|
-
clear_outside_extent
|
|
321
|
-
clear_inside_extent if extent && children.any?
|
|
531
|
+
clear_outside_extent(canvas)
|
|
532
|
+
clear_inside_extent(canvas) if extent && children.any?
|
|
322
533
|
end
|
|
323
534
|
invalidate_children
|
|
324
535
|
end
|
|
@@ -364,7 +575,7 @@ module Tuile
|
|
|
364
575
|
# def handle_mouse_down?(event)
|
|
365
576
|
# return false unless event.button == :left
|
|
366
577
|
#
|
|
367
|
-
#
|
|
578
|
+
# on_click.fire(ClickEvent.new(source: self))
|
|
368
579
|
# true
|
|
369
580
|
# end
|
|
370
581
|
#
|
|
@@ -531,23 +742,37 @@ module Tuile
|
|
|
531
742
|
# @return [void]
|
|
532
743
|
def handle_focus; end
|
|
533
744
|
|
|
534
|
-
#
|
|
535
|
-
#
|
|
536
|
-
#
|
|
745
|
+
# What {#on_theme_changed} fires.
|
|
746
|
+
#
|
|
747
|
+
# @!attribute [r] source
|
|
748
|
+
# @return [Component] the component whose theme changed.
|
|
749
|
+
ThemeChangedEvent = Data.define(:source) { include Tuile::Event }
|
|
750
|
+
|
|
751
|
+
# What {#on_locale_changed} fires.
|
|
537
752
|
#
|
|
538
|
-
#
|
|
753
|
+
# @!attribute [r] source
|
|
754
|
+
# @return [Component] the component whose locale changed.
|
|
755
|
+
LocaleChangedEvent = Data.define(:source) { include Tuile::Event }
|
|
756
|
+
|
|
757
|
+
# @!method on_theme_changed
|
|
758
|
+
# Fired by the base {#handle_theme_changed} — the composition-style
|
|
759
|
+
# alternative to overriding the method, for apps that assemble stock
|
|
760
|
+
# components rather than subclass:
|
|
761
|
+
#
|
|
762
|
+
# label.on_theme_changed { label.text = render_status_line }
|
|
539
763
|
#
|
|
540
|
-
#
|
|
541
|
-
|
|
764
|
+
# @return [Listeners]
|
|
765
|
+
listener :on_theme_changed
|
|
542
766
|
|
|
543
|
-
#
|
|
544
|
-
#
|
|
545
|
-
#
|
|
767
|
+
# @!method on_locale_changed
|
|
768
|
+
# Fired by the base {#handle_locale_changed} — the composition-style
|
|
769
|
+
# alternative to overriding the method, for an app that rendered a date or
|
|
770
|
+
# a number into a stock component:
|
|
546
771
|
#
|
|
547
|
-
#
|
|
772
|
+
# label.on_locale_changed { label.text = due_date.strftime(fmt) }
|
|
548
773
|
#
|
|
549
|
-
#
|
|
550
|
-
|
|
774
|
+
# @return [Listeners]
|
|
775
|
+
listener :on_locale_changed
|
|
551
776
|
|
|
552
777
|
# Whether this component's tree is mounted on a UI, {ScreenPane} being the
|
|
553
778
|
# root of every displayed tree.
|
|
@@ -555,7 +780,7 @@ module Tuile
|
|
|
555
780
|
# A property of the parent chain alone — no {Screen} is consulted, so
|
|
556
781
|
# assembling a tree needs no screen in the process at all:
|
|
557
782
|
#
|
|
558
|
-
# layout = Component::Layout::
|
|
783
|
+
# layout = Component::Layout::Vertical.new
|
|
559
784
|
# layout.add(label) # legal with no Screen; neither is attached yet
|
|
560
785
|
# screen.content = layout # now both are
|
|
561
786
|
#
|
|
@@ -596,10 +821,13 @@ module Tuile
|
|
|
596
821
|
end
|
|
597
822
|
|
|
598
823
|
# Where the hardware terminal cursor should sit when this component is the
|
|
599
|
-
# cursor owner
|
|
600
|
-
#
|
|
824
|
+
# cursor owner, **in this component's own coordinates** — the ones it paints
|
|
825
|
+
# in, so a caret is `Point.new(column, row)` with no position added.
|
|
826
|
+
# {Screen#cursor_position} converts it. Returns `nil` to hide the cursor.
|
|
827
|
+
#
|
|
828
|
+
# The {Screen} positions the hardware cursor after each repaint cycle by
|
|
601
829
|
# consulting the {Screen#focused} component only.
|
|
602
|
-
# @return [Point, nil]
|
|
830
|
+
# @return [Point, nil] in this component's coordinates, or nil to hide.
|
|
603
831
|
def cursor_position = nil
|
|
604
832
|
|
|
605
833
|
# One line naming the component, its {#id} and its rect, plus whatever
|
|
@@ -643,6 +871,8 @@ module Tuile
|
|
|
643
871
|
# @param at [Integer, nil] index to insert at; appends when nil.
|
|
644
872
|
# @raise [TypeError] if `child` is not a {Component}.
|
|
645
873
|
# @raise [ArgumentError] if `child` already has a parent.
|
|
874
|
+
# @raise [Tuile::Error] if `child` is a {Component::Overlay} not being
|
|
875
|
+
# adopted as one of {ScreenPane#popups}.
|
|
646
876
|
# @return [void]
|
|
647
877
|
#
|
|
648
878
|
# Final: one of the three mutators that write {#children} and the parent
|
|
@@ -651,8 +881,18 @@ module Tuile
|
|
|
651
881
|
raise TypeError, "expected Component, got #{child.inspect}" unless child.is_a? Component
|
|
652
882
|
raise ArgumentError, "#{child} already has a parent #{child.parent}" unless child.parent.nil?
|
|
653
883
|
|
|
884
|
+
# An overlay's placement, `open?`, `visible=` and outside-click dismissal
|
|
885
|
+
# all come from the pane's popup list, so anywhere else it would be
|
|
886
|
+
# unplaceable and undismissable. Membership, not the pane's identity,
|
|
887
|
+
# which would let the `content` slot through; checked before the push, so
|
|
888
|
+
# a refusal leaves the tree untouched.
|
|
889
|
+
if child.is_a?(Component::Overlay) && !(is_a?(ScreenPane) && has_popup?(child))
|
|
890
|
+
raise Tuile::Error, "#{child.class} belongs on the popup stack — open it (#{child.class}#open) " \
|
|
891
|
+
"rather than adding it to #{self.class}"
|
|
892
|
+
end
|
|
654
893
|
at.nil? ? @children.push(child) : @children.insert(at, child)
|
|
655
894
|
child.parent = self
|
|
895
|
+
invalidate_layout
|
|
656
896
|
end
|
|
657
897
|
|
|
658
898
|
# Drops `child` and notifies {#handle_child_removed}.
|
|
@@ -688,6 +928,7 @@ module Tuile
|
|
|
688
928
|
|
|
689
929
|
@children.delete(child)
|
|
690
930
|
child.parent = nil
|
|
931
|
+
invalidate_layout
|
|
691
932
|
end
|
|
692
933
|
|
|
693
934
|
# Called once this component's tree has been mounted on a {ScreenPane},
|
|
@@ -766,33 +1007,24 @@ module Tuile
|
|
|
766
1007
|
def fire_lifecycle(attached)
|
|
767
1008
|
kids = children.dup
|
|
768
1009
|
attached ? handle_attached : handle_detached
|
|
1010
|
+
# A mark taken while detached had no screen to go to; hand it over now
|
|
1011
|
+
# that there is one (the `attached?` re-check covers a hook that detached
|
|
1012
|
+
# us again). Flutter's `RenderObject#attach` does the same.
|
|
1013
|
+
screen.invalidate_layout(self) if attached && @layout_dirty && attached?
|
|
769
1014
|
kids.each { _1.fire_lifecycle(attached) if _1.attached? == attached }
|
|
770
1015
|
end
|
|
771
1016
|
|
|
772
|
-
# Called
|
|
773
|
-
#
|
|
774
|
-
def handle_width_changed; end
|
|
775
|
-
|
|
776
|
-
# Called on the parent after a direct child's {#visible=} flipped, so a
|
|
777
|
-
# container that divides space can re-divide it:
|
|
778
|
-
#
|
|
779
|
-
# def handle_child_visibility_changed(_child)
|
|
780
|
-
# super
|
|
781
|
-
# relayout
|
|
782
|
-
# end
|
|
1017
|
+
# Called once the parent has given this component a different rect, before
|
|
1018
|
+
# anything repaints. Does nothing by default.
|
|
783
1019
|
#
|
|
784
|
-
#
|
|
785
|
-
#
|
|
786
|
-
#
|
|
787
|
-
#
|
|
788
|
-
#
|
|
789
|
-
#
|
|
790
|
-
# grandchild. {#visible=} repairs focus itself, so an override has nothing
|
|
791
|
-
# to inherit — it still calls `super`, per the class doc. Reached through
|
|
792
|
-
# `__send__`, so it may declare any visibility (`D_hook_visibility`).
|
|
793
|
-
# @param _child [Component] the direct child whose flag changed.
|
|
1020
|
+
# The *edge*: for a reaction to the change itself, one that needs the old
|
|
1021
|
+
# rect or must not repeat — closing a menu, escalating a repaint. State
|
|
1022
|
+
# that merely *follows* from the size — a wrap, a scroll clamp — is
|
|
1023
|
+
# re-derived in {#relayout} instead, which runs after every rect change
|
|
1024
|
+
# on either axis and after every other mark too.
|
|
1025
|
+
# @param _old_rect [Rect] the rect it had.
|
|
794
1026
|
# @return [void]
|
|
795
|
-
def
|
|
1027
|
+
def handle_rect_changed(_old_rect); end
|
|
796
1028
|
|
|
797
1029
|
# Mirror of {#handle_focus}: the component just lost focus, to another component
|
|
798
1030
|
# or to nothing. The commit point a Tab-away still reaches — Tab is
|
|
@@ -802,7 +1034,7 @@ module Tuile
|
|
|
802
1034
|
# class TrimmedField < Component::TextField
|
|
803
1035
|
# protected def handle_blur
|
|
804
1036
|
# super
|
|
805
|
-
# self.
|
|
1037
|
+
# self.value = text.strip
|
|
806
1038
|
# false
|
|
807
1039
|
# end
|
|
808
1040
|
# end
|
|
@@ -840,8 +1072,8 @@ module Tuile
|
|
|
840
1072
|
#
|
|
841
1073
|
# Runs on the UI thread with {Screen#theme} already updated, so mutating
|
|
842
1074
|
# content (`text=`, `lines=`, …) is safe. Do not assign {Screen#theme=}
|
|
843
|
-
# here. Subclasses overriding this must call `super` so
|
|
844
|
-
# {#on_theme_changed
|
|
1075
|
+
# here. Subclasses overriding this must call `super` so any
|
|
1076
|
+
# {#on_theme_changed} listener keeps firing.
|
|
845
1077
|
#
|
|
846
1078
|
# Plumbing an app overrides and never calls, hence protected — and
|
|
847
1079
|
# {Screen}, not being a {Component}, fans it out through `__send__`, so an
|
|
@@ -849,7 +1081,7 @@ module Tuile
|
|
|
849
1081
|
# @return [void]
|
|
850
1082
|
# is whatever the app's lambda happened to return.
|
|
851
1083
|
def handle_theme_changed
|
|
852
|
-
|
|
1084
|
+
on_theme_changed.fire(ThemeChangedEvent.new(source: self))
|
|
853
1085
|
end
|
|
854
1086
|
|
|
855
1087
|
# Called on every attached component (pre-order, popups included) when
|
|
@@ -859,7 +1091,7 @@ module Tuile
|
|
|
859
1091
|
# change invalidates the whole tree.
|
|
860
1092
|
#
|
|
861
1093
|
# Runs on the UI thread with {Screen#locale} already updated. Subclasses
|
|
862
|
-
# overriding it must call `super` so
|
|
1094
|
+
# overriding it must call `super` so any {#on_locale_changed}
|
|
863
1095
|
# listener keeps firing.
|
|
864
1096
|
#
|
|
865
1097
|
# Plumbing an app overrides and never calls, hence protected — {Screen}
|
|
@@ -868,7 +1100,7 @@ module Tuile
|
|
|
868
1100
|
# @return [void]
|
|
869
1101
|
# is whatever the app's lambda happened to return.
|
|
870
1102
|
def handle_locale_changed
|
|
871
|
-
|
|
1103
|
+
on_locale_changed.fire(LocaleChangedEvent.new(source: self))
|
|
872
1104
|
end
|
|
873
1105
|
|
|
874
1106
|
# The formatting conventions to render and parse by ({Screen#locale}), or
|
|
@@ -894,12 +1126,89 @@ module Tuile
|
|
|
894
1126
|
screen.invalidate(self)
|
|
895
1127
|
end
|
|
896
1128
|
|
|
1129
|
+
# Marks this container as owing a {#relayout}: its children's rects are out
|
|
1130
|
+
# of date, and the next {#flush_layout} will bring them up to date.
|
|
1131
|
+
#
|
|
1132
|
+
# def spacing=(cells)
|
|
1133
|
+
# @spacing = cells
|
|
1134
|
+
# invalidate_layout # every input to the arithmetic ends here
|
|
1135
|
+
# end
|
|
1136
|
+
#
|
|
1137
|
+
# The framework marks after `rect=`, after the three tree mutators and
|
|
1138
|
+
# after a child's {#visible=} flips; a container marks for every *other*
|
|
1139
|
+
# input to its own arithmetic.
|
|
1140
|
+
#
|
|
1141
|
+
# **The mark never runs the pass** — not even detached, where there is no
|
|
1142
|
+
# settle to defer to and {#flush_layout} has to be asked for. That is what
|
|
1143
|
+
# lets a constructor `add_child` before its ivars are written, and a
|
|
1144
|
+
# container mutate its own bookkeeping in whatever order reads best: no
|
|
1145
|
+
# `relayout` ever observes a container mid-configuration.
|
|
1146
|
+
#
|
|
1147
|
+
# Unlike {#invalidate}, a detached mark is *remembered* rather than
|
|
1148
|
+
# dropped: attaching hands it to the {Screen}, so a tree assembled with no
|
|
1149
|
+
# screen lays out as soon as it is mounted.
|
|
1150
|
+
#
|
|
1151
|
+
# **A mark made during this container's own {#relayout}, before it places
|
|
1152
|
+
# its first child, is dropped** — the pass that would answer it is the one
|
|
1153
|
+
# running. So hide or add a child, or set your own `spacing`, *first*:
|
|
1154
|
+
#
|
|
1155
|
+
# def relayout
|
|
1156
|
+
# @sidebar.visible = width >= 60 # before `super`: one pass
|
|
1157
|
+
# super
|
|
1158
|
+
# end
|
|
1159
|
+
#
|
|
1160
|
+
# Once a child is placed, the division may already be stale, so a later
|
|
1161
|
+
# mark is kept and costs a second pass.
|
|
1162
|
+
# @return [void]
|
|
1163
|
+
def invalidate_layout
|
|
1164
|
+
return if LayoutPass.before_first_placement?(self)
|
|
1165
|
+
|
|
1166
|
+
@layout_dirty = true
|
|
1167
|
+
screen.invalidate_layout(self) if attached?
|
|
1168
|
+
end
|
|
1169
|
+
|
|
1170
|
+
# Assigns every child's rect, and is the only place a container may.
|
|
1171
|
+
#
|
|
1172
|
+
# private def relayout
|
|
1173
|
+
# half = width / 2 # `local_rect`, so no rect.left:
|
|
1174
|
+
# @left.rect = Rect.new(0, 0, half, height)
|
|
1175
|
+
# @right.rect = Rect.new(half, 0, width - half, height)
|
|
1176
|
+
# end
|
|
1177
|
+
#
|
|
1178
|
+
# **`relayout` : geometry :: {#repaint} : ink.** Invoked by the framework,
|
|
1179
|
+
# never called directly; derives every rect from current state, so it is
|
|
1180
|
+
# idempotent and safe to run twice. It assigns *every* child on every pass,
|
|
1181
|
+
# including when {#rect} is empty — a `return if rect.empty?` guard strands
|
|
1182
|
+
# children at stale coordinates that the next full repaint paints them at
|
|
1183
|
+
# (`D_empty_ancestor`).
|
|
1184
|
+
#
|
|
1185
|
+
# **It is also where a component re-derives what depends on its own size**
|
|
1186
|
+
# — a wrap, a padded row cache, a scroll offset clamped to the viewport —
|
|
1187
|
+
# because {#rect=} marks the component itself, so this runs after every
|
|
1188
|
+
# rect change, width *or* height. It runs after every other mark as well,
|
|
1189
|
+
# so a costly derivation keys itself on the size it was built at —
|
|
1190
|
+
# {Component::TextView}'s:
|
|
1191
|
+
#
|
|
1192
|
+
# def relayout
|
|
1193
|
+
# rewrap unless @wrapped_at == wrap_width # not on every append
|
|
1194
|
+
# update_scroll_top_row_if_auto_scroll # a taller viewport moves the bottom
|
|
1195
|
+
# @scrollbar.rect = …
|
|
1196
|
+
# end
|
|
1197
|
+
#
|
|
1198
|
+
# There is no width-changed hook; {#handle_rect_changed} is the edge, for a
|
|
1199
|
+
# reaction to the change itself.
|
|
1200
|
+
#
|
|
1201
|
+
# Reached through `__send__`, so an override may be protected or private
|
|
1202
|
+
# (`D_hook_visibility`). A leaf inherits the empty body and costs nothing.
|
|
1203
|
+
# @return [void]
|
|
1204
|
+
def relayout; end
|
|
1205
|
+
|
|
897
1206
|
# Whether direct children fully tile {#rect}. Used by the default
|
|
898
1207
|
# {#repaint} to decide whether the framework needs to wipe gaps.
|
|
899
1208
|
#
|
|
900
1209
|
# Approximated by area: sum of (non-empty) child areas vs the parent's
|
|
901
1210
|
# area. Cheap, and correct as long as siblings don't overlap each other
|
|
902
|
-
# — which Tuile already requires
|
|
1211
|
+
# — which Tuile already requires of a tiled layout.
|
|
903
1212
|
# Children with empty rects contribute zero, since they paint nothing.
|
|
904
1213
|
#
|
|
905
1214
|
# A **hidden** child contributes zero for the same reason, and that is what
|
|
@@ -916,18 +1225,20 @@ module Tuile
|
|
|
916
1225
|
# below it. A `nil` extent declares nothing, so the whole rect is blanked.
|
|
917
1226
|
# Called by the default {#repaint}; a self-painter that skips `super` calls
|
|
918
1227
|
# it directly.
|
|
1228
|
+
# @param canvas [Canvas] the paint context, at this component's own background.
|
|
919
1229
|
# @return [void]
|
|
920
|
-
def clear_outside_extent
|
|
1230
|
+
def clear_outside_extent(canvas)
|
|
921
1231
|
e = extent
|
|
922
|
-
return
|
|
1232
|
+
return canvas.fill(local_rect) if e.nil? # nothing declared: all of it is fair game
|
|
923
1233
|
|
|
924
|
-
right = Rect.new(
|
|
925
|
-
below = Rect.new(
|
|
1234
|
+
right = Rect.new(e.width, 0, rect.width - e.width, e.height)
|
|
1235
|
+
below = Rect.new(0, e.height, rect.width, rect.height - e.height)
|
|
926
1236
|
# Not this widget's own surface: a one-row Select handed a 25-row rect
|
|
927
1237
|
# would otherwise flood the other 24 with its field well.
|
|
928
|
-
bg
|
|
929
|
-
|
|
930
|
-
|
|
1238
|
+
canvas.with(bg_color: bg.ambient) do |ambient|
|
|
1239
|
+
ambient.fill(right) unless right.empty?
|
|
1240
|
+
ambient.fill(below) unless below.empty?
|
|
1241
|
+
end
|
|
931
1242
|
end
|
|
932
1243
|
|
|
933
1244
|
# Blanks the {#extent} itself, for a *container* whose children don't cover
|
|
@@ -943,64 +1254,35 @@ module Tuile
|
|
|
943
1254
|
# extent itself, and blanking that first is the re-emit `D_progress_bar`
|
|
944
1255
|
# bought back. Override it to decline when you paint your own ink into a
|
|
945
1256
|
# face cell no child covers.
|
|
1257
|
+
# @param canvas [Canvas] the paint context, at this component's own background.
|
|
946
1258
|
# @return [void]
|
|
947
|
-
def clear_inside_extent
|
|
948
|
-
|
|
1259
|
+
def clear_inside_extent(canvas)
|
|
1260
|
+
canvas.with(bg_color: bg.ambient) { _1.fill(local_extent_rect) }
|
|
949
1261
|
end
|
|
950
1262
|
|
|
951
|
-
#
|
|
952
|
-
#
|
|
953
|
-
# me shows through". A widget that paints an opaque surface overrides it, and
|
|
954
|
-
# inheritance stops there: that is what keeps a form's fields looking like
|
|
955
|
-
# fields inside a tinted panel. Declare it unconditionally — a widget owned
|
|
956
|
-
# by a bigger one is told so with {BG_INHERIT}, and must not try to work it
|
|
957
|
-
# out from where it sits in the tree.
|
|
958
|
-
#
|
|
959
|
-
# # a field: its own well, brighter while focused
|
|
960
|
-
# def default_bg_color = active? ? screen.theme.active_bg_color : screen.theme.input_bg_color
|
|
1263
|
+
# This component's background — protected, because stating a widget's own
|
|
1264
|
+
# well is the widget's business; an app tints through {#bg_color=}.
|
|
961
1265
|
#
|
|
962
|
-
#
|
|
963
|
-
# Hash. Branching on {#active?} and handing back one {Color}, as above, is
|
|
964
|
-
# the cheap form and allocates nothing on the paint path.
|
|
1266
|
+
# bg.default_color = ComponentBackground::INPUT_WELL # in a field's initialize
|
|
965
1267
|
#
|
|
966
|
-
#
|
|
967
|
-
#
|
|
968
|
-
|
|
969
|
-
def default_bg_color = nil
|
|
970
|
-
|
|
971
|
-
# Final, and protected: it answers what the *framework* paints with, and an
|
|
972
|
-
# app never needs it — {#clear_background} / {#draw_text} / {#draw_char}
|
|
973
|
-
# apply it already. A component states its own opinion by overriding
|
|
974
|
-
# {#default_bg_color}, an app by setting {#bg_color}; neither takes this
|
|
975
|
-
# over. Protected rather than private because the chain below is an
|
|
976
|
-
# explicit-receiver call, which Ruby forbids for a private method.
|
|
977
|
-
# @return [Color, nil] the background actually painted, for the state this
|
|
978
|
-
# component is in right now: its {#error_bg_color}, else its {#bg_color},
|
|
979
|
-
# else its {#default_bg_color}, else the nearest ancestor answering one of
|
|
980
|
-
# those, else `nil` (terminal default). Resolved at paint time — never
|
|
981
|
-
# cached, so the subtree tracks an ancestor's {#bg_color=}, a
|
|
982
|
-
# {Screen#theme=}, a focus change and a validation verdict on its next
|
|
983
|
-
# repaint.
|
|
984
|
-
def effective_bg_color
|
|
985
|
-
own = resolve_bg_color(error_bg_color) || resolve_bg_color(@bg_color) || resolve_bg_color(default_bg_color)
|
|
986
|
-
return parent&.effective_bg_color if own.nil? || own == BG_INHERIT
|
|
987
|
-
|
|
988
|
-
own
|
|
989
|
-
end
|
|
1268
|
+
# Final: {Screen#canvas_for} and every descendant's chain read it.
|
|
1269
|
+
# @return [ComponentBackground]
|
|
1270
|
+
attr_reader :bg
|
|
990
1271
|
|
|
991
1272
|
# The background a component paints while it is in an *error* state —
|
|
992
1273
|
# `nil` by default, meaning "I am not signalling one". {HasValidation}
|
|
993
1274
|
# overrides it, so every field has it and nothing else does.
|
|
994
1275
|
#
|
|
995
|
-
# It sits **above** {#bg_color} in
|
|
996
|
-
#
|
|
1276
|
+
# It sits **above** {#bg_color} in the chain rather than under it, unlike
|
|
1277
|
+
# {ComponentBackground#default_color}. That is deliberate: an app tinting a panel
|
|
997
1278
|
# would otherwise switch the validation signal off on the fields inside it,
|
|
998
1279
|
# silently. An app that wants different error colors changes the
|
|
999
1280
|
# {Theme#error_bg_color} tokens.
|
|
1000
1281
|
#
|
|
1001
|
-
#
|
|
1002
|
-
#
|
|
1003
|
-
#
|
|
1282
|
+
# A hook rather than a {ComponentBackground} setter because it follows the
|
|
1283
|
+
# validation state, and a pulled answer cannot go stale. Read the theme
|
|
1284
|
+
# here rather than in an ivar, and hand back one {Color}: this runs first
|
|
1285
|
+
# on every paint.
|
|
1004
1286
|
# @return [Color, Theme::Ref, Hash, nil]
|
|
1005
1287
|
def error_bg_color = nil
|
|
1006
1288
|
|
|
@@ -1009,11 +1291,11 @@ module Tuile
|
|
|
1009
1291
|
# self-painting container can drop the default's blanket clear without also
|
|
1010
1292
|
# dropping this by accident ({Component::Window} is the case):
|
|
1011
1293
|
#
|
|
1012
|
-
# def repaint
|
|
1294
|
+
# def repaint(canvas)
|
|
1013
1295
|
# return if rect.empty?
|
|
1014
1296
|
#
|
|
1015
1297
|
# invalidate_children # never optional
|
|
1016
|
-
# paint_my_own_chrome
|
|
1298
|
+
# paint_my_own_chrome(canvas)
|
|
1017
1299
|
# end
|
|
1018
1300
|
#
|
|
1019
1301
|
# @return [void]
|
|
@@ -1021,54 +1303,42 @@ module Tuile
|
|
|
1021
1303
|
children.each { |c| screen.invalidate(c) }
|
|
1022
1304
|
end
|
|
1023
1305
|
|
|
1024
|
-
|
|
1025
|
-
# {#effective_bg_color} (the terminal default when none is inherited).
|
|
1026
|
-
#
|
|
1027
|
-
# A component that paints part of its {#rect} itself passes just the part it
|
|
1028
|
-
# *doesn't* — blanking a cell it is about to overwrite anyway makes that cell
|
|
1029
|
-
# dirty, and {Buffer#flush} then re-emits it even though nothing visibly
|
|
1030
|
-
# changed.
|
|
1031
|
-
# @param area [Rect] the region to blank; defaults to the whole {#rect}.
|
|
1032
|
-
# @param bg [Color, nil] the color to blank with; defaults to
|
|
1033
|
-
# {#effective_bg_color}, i.e. this component's own surface.
|
|
1034
|
-
# @return [void]
|
|
1035
|
-
def clear_background(area = rect, bg = effective_bg_color)
|
|
1036
|
-
screen.buffer.fill(area, bg ? StyledString::Style.new(bg:) : StyledString::Style::DEFAULT)
|
|
1037
|
-
end
|
|
1306
|
+
private
|
|
1038
1307
|
|
|
1039
|
-
#
|
|
1040
|
-
#
|
|
1041
|
-
# {#bg_color} — or an invalid field's error well — shows through the content
|
|
1042
|
-
# a component paints. A no-op layer when none is inherited. Self-painters
|
|
1043
|
-
# (those skipping the {#repaint} auto-clear) paint through this instead of
|
|
1044
|
-
# {Screen#buffer} directly.
|
|
1045
|
-
# @param x [Integer] starting column.
|
|
1046
|
-
# @param y [Integer] row.
|
|
1047
|
-
# @param styled [StyledString]
|
|
1308
|
+
# Clears the mark and runs {#relayout} — the sole invocation site of
|
|
1309
|
+
# `relayout`, reached only from {LayoutPass.drain}.
|
|
1048
1310
|
# @return [void]
|
|
1049
|
-
def
|
|
1050
|
-
|
|
1311
|
+
def perform_relayout
|
|
1312
|
+
@layout_dirty = false
|
|
1313
|
+
LayoutPass.run(self) { relayout }
|
|
1051
1314
|
end
|
|
1052
1315
|
|
|
1053
|
-
#
|
|
1054
|
-
#
|
|
1055
|
-
#
|
|
1056
|
-
#
|
|
1057
|
-
#
|
|
1058
|
-
#
|
|
1059
|
-
#
|
|
1060
|
-
#
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1316
|
+
# The nearest ancestor whose pending or running {#relayout} would rewrite
|
|
1317
|
+
# this component's {#rect}, or `nil` when the rect is current —
|
|
1318
|
+
# {#rect_stale?}'s answer, with the culprit kept for {StrictLayout}'s
|
|
1319
|
+
# message.
|
|
1320
|
+
#
|
|
1321
|
+
# Two ways to owe it: a marked ancestor, and one whose pass is running and
|
|
1322
|
+
# has not yet placed the child on the way down — a hook fired mid-pass, a
|
|
1323
|
+
# focus repair from a child hidden there, reading a sibling. Strictly
|
|
1324
|
+
# ancestors, never `self`: a container's own flag means its children are
|
|
1325
|
+
# stale, and {#perform_relayout} clears the flag before the body runs, so a
|
|
1326
|
+
# `relayout` reading its own `width` is asking a settled question.
|
|
1327
|
+
# @return [Component, nil]
|
|
1328
|
+
def stale_layout_ancestor
|
|
1329
|
+
node = self
|
|
1330
|
+
until node.parent.nil?
|
|
1331
|
+
return node.parent if node.parent.layout_dirty? || LayoutPass.unplaced?(node)
|
|
1332
|
+
|
|
1333
|
+
node = node.parent
|
|
1334
|
+
end
|
|
1335
|
+
nil
|
|
1065
1336
|
end
|
|
1066
1337
|
|
|
1067
|
-
private
|
|
1068
|
-
|
|
1069
1338
|
# Hands focus out of the subtree just hidden, if it was in there, through
|
|
1070
1339
|
# the parent's {#handle_child_removed} — see there for why hiding reuses the
|
|
1071
|
-
# removal repair instead of growing a second one.
|
|
1340
|
+
# removal repair instead of growing a second one. {List#interactive=}
|
|
1341
|
+
# reuses it too, for a component that stays shown but stops taking focus.
|
|
1072
1342
|
#
|
|
1073
1343
|
# The parent is necessarily showing (focus was inside it a moment ago, and
|
|
1074
1344
|
# {Screen#focused=} refuses a hidden target), so its assignment can't bounce.
|
|
@@ -1080,53 +1350,5 @@ module Tuile
|
|
|
1080
1350
|
cursor = cursor.parent until cursor.nil? || cursor.equal?(self)
|
|
1081
1351
|
parent.handle_child_removed(self) unless cursor.nil?
|
|
1082
1352
|
end
|
|
1083
|
-
|
|
1084
|
-
# What surrounds this component — an app-set {#bg_color}, else whatever the
|
|
1085
|
-
# parent paints where this component is not. Skips {#default_bg_color}, the
|
|
1086
|
-
# one thing that colors this widget's *own* surface, which is what makes it
|
|
1087
|
-
# the right answer for the dead tail outside {#extent}.
|
|
1088
|
-
# @return [Color, nil]
|
|
1089
|
-
def ambient_bg_color
|
|
1090
|
-
own = resolve_bg_color(@bg_color)
|
|
1091
|
-
return parent&.effective_bg_color if own.nil? || own == BG_INHERIT
|
|
1092
|
-
|
|
1093
|
-
own
|
|
1094
|
-
end
|
|
1095
|
-
|
|
1096
|
-
# Collapses one level of the background chain to the {Color} it means right
|
|
1097
|
-
# now: picks the entry for this component's current state out of a state
|
|
1098
|
-
# Hash, and resolves a {Theme::Ref} against the live theme. An absent state
|
|
1099
|
-
# key yields `nil`, so resolution falls through to the next level — which is
|
|
1100
|
-
# what lets `bg_color = { active: … }` keep the widget's own normal well.
|
|
1101
|
-
# @param value [Color, Theme::Ref, Hash, nil]
|
|
1102
|
-
# @return [Color, nil]
|
|
1103
|
-
def resolve_bg_color(value)
|
|
1104
|
-
case value
|
|
1105
|
-
when nil then nil
|
|
1106
|
-
when Hash then resolve_bg_color(value[active? ? :active : :normal])
|
|
1107
|
-
when Theme::Ref then value.resolve(screen.theme)
|
|
1108
|
-
else value
|
|
1109
|
-
end
|
|
1110
|
-
end
|
|
1111
|
-
|
|
1112
|
-
# Validates and normalizes what {#bg_color=} was handed, so a bad token or a
|
|
1113
|
-
# misspelled state raises at the assignment rather than deep in a repaint.
|
|
1114
|
-
# @param value [Object]
|
|
1115
|
-
# @return [Color, Theme::Ref, Hash, nil]
|
|
1116
|
-
# @raise [ArgumentError] on a Hash key outside {BG_STATES}.
|
|
1117
|
-
# @raise [KeyError] on a {Theme::Ref} naming an absent custom token.
|
|
1118
|
-
def coerce_bg_color(value)
|
|
1119
|
-
case value
|
|
1120
|
-
when nil, Color, BG_INHERIT then value
|
|
1121
|
-
when Theme::Ref then value.tap { _1.resolve(screen.theme) }
|
|
1122
|
-
when Hash
|
|
1123
|
-
unknown = value.keys - BG_STATES
|
|
1124
|
-
raise ArgumentError, "unknown background state(s) #{unknown.join(", ")}; known: #{BG_STATES.join(", ")}" \
|
|
1125
|
-
unless unknown.empty?
|
|
1126
|
-
|
|
1127
|
-
value.to_h { |state, color| [state, coerce_bg_color(color)] }.freeze
|
|
1128
|
-
else Color.coerce(value)
|
|
1129
|
-
end
|
|
1130
|
-
end
|
|
1131
1353
|
end
|
|
1132
1354
|
end
|