Class: Tuile::Component
- Inherits:
-
Object
- Object
- Tuile::Component
- Defined in:
- lib/tuile/component.rb,
lib/tuile/component/list.rb,
lib/tuile/component/slot.rb,
lib/tuile/component/tabs.rb,
lib/tuile/component/label.rb,
lib/tuile/component/popup.rb,
lib/tuile/component/button.rb,
lib/tuile/component/layout.rb,
lib/tuile/component/select.rb,
lib/tuile/component/window.rb,
lib/tuile/component/overlay.rb,
lib/tuile/component/checkbox.rb,
lib/tuile/component/menu_bar.rb,
lib/tuile/component/combo_box.rb,
lib/tuile/component/has_value.rb,
lib/tuile/component/tab_sheet.rb,
lib/tuile/component/text_area.rb,
lib/tuile/component/text_view.rb,
lib/tuile/component/layout/box.rb,
lib/tuile/component/log_window.rb,
lib/tuile/component/text_field.rb,
lib/tuile/component/float_field.rb,
lib/tuile/component/has_caption.rb,
lib/tuile/component/has_content.rb,
lib/tuile/component/info_window.rb,
lib/tuile/component/radio_group.rb,
lib/tuile/component/notification.rb,
lib/tuile/component/progress_bar.rb,
lib/tuile/component/integer_field.rb,
lib/tuile/component/list_dropdown.rb,
lib/tuile/component/log_text_view.rb,
lib/tuile/component/picker_window.rb,
lib/tuile/component/checkbox_group.rb,
lib/tuile/component/confirm_window.rb,
lib/tuile/component/password_field.rb,
lib/tuile/component/layout/vertical.rb,
lib/tuile/component/menu_bar/cascade.rb,
lib/tuile/component/big_decimal_field.rb,
lib/tuile/component/layout/horizontal.rb,
lib/tuile/component/abstract_string_field.rb,
lib/tuile/component/text_area/wrapped_text.rb,
sig/tuile.rbs
Overview
A UI component which is positioned on the screen and draws characters into its bounding rectangle (in #repaint).
Painting is gated by attachment: a detached component (one whose #root isn't Screen#pane) is never enqueued for repaint via #invalidate, and any stale invalidation entries are filtered out at drain time. Subclasses can paint freely in #repaint without re-asserting attachment.
Direct Known Subclasses
Defined Under Namespace
Modules: HasCaption, HasContent, HasValue Classes: AbstractStringField, BigDecimalField, Button, Checkbox, CheckboxGroup, ComboBox, ConfirmWindow, FloatField, InfoWindow, IntegerField, Label, Layout, List, ListDropdown, LogTextView, LogWindow, MenuBar, Notification, Overlay, PasswordField, PickerWindow, Popup, ProgressBar, RadioGroup, Select, Slot, TabSheet, Tabs, TextArea, TextField, TextView, Window
Constant Summary collapse
- FINAL_METHODS =
The methods that define the tree, and so may never be overridden — #attached? walks the parent chain while every subtree walk uses #children, and an override that makes those two disagree leaves a component attached but never painted, with nothing raising (
D_final_tree). Enforced by verify_final!; reparent through #add_child / #remove_child / #detach_child, and hold a Slot for a swappable region. %i[children parent parent= add_child remove_child detach_child].freeze
Instance Attribute Summary collapse
-
#bg_color ⇒ Color, ...
@return — this component's own background — the value as set, so a Theme::Ref comes back unresolved;
nilwhen unset, in which case it inherits from the parent (see #effective_bg_color), ultimately the terminal default. -
#children ⇒ ::Array[Component]
readonly
Child components in paint order (siblings left to right, earlier ones painted under later ones), maintained by #add_child / #remove_child.
-
#on_theme_changed ⇒ Proc?
Called on every attached component (pre-order, popups included) when Screen#theme changes — at Screen#theme= / Screen#theme_def= and on OS appearance flips.
-
#parent ⇒ Component?
@return — the parent component or nil if the component has no parent.
-
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
Class Method Summary collapse
-
.verify_final!(klass) ⇒ void
Raises unless
klassinherits every FINAL_METHODS entry from Component; memoized, so it costs one hash lookup per construction.
Instance Method Summary collapse
-
#active=(active) ⇒ void
@param
active— true if active. -
#active? ⇒ Boolean
@return — true if the component is on the active chain — i.e.
-
#add_child(child, at: nil) ⇒ void
Adopts
child: places it in #children and wires its parent pointer. -
#attached? ⇒ Boolean
Whether this component's tree is mounted on a UI, ScreenPane being the root of every displayed tree.
-
#children_tile_rect? ⇒ Boolean
Whether direct children fully tile #rect.
-
#clear_background(area = rect) ⇒ void
Clears the background: fills every cell with a blank in the #effective_bg_color (the terminal default when none is inherited).
- #clear_outside_extent ⇒ void
-
#cursor_position ⇒ Point?
Where the hardware terminal cursor should sit when this component is the cursor owner.
-
#depth ⇒ Integer
@return — the distance from the root component; 0 if #parent is nil.
-
#detach_child(child) ⇒ void
Drops
childwithout notifying — for a container swapping a named slot, which owes the #on_child_removed call once the new occupant is wired:. -
#draw_char(x, y, grapheme, style = StyledString::Style::DEFAULT) ⇒ Object
#draw_text's single-grapheme counterpart: writes
graphemeat(x, y), filling #effective_bg_color whenstylecarries no bg of its own. -
#draw_text(x, y, styled) ⇒ void
Buffer#set_text wrapper that fills #effective_bg_color behind any span with no bg of its own (via StyledString#under_bg), so an inherited #bg_color shows through the content a component paints.
-
#effective_bg_color ⇒ Color?
@return — the background actually painted: this component's own #bg_color if set (a Theme::Ref resolved against the current theme), else the nearest ancestor's, else
nil(terminal default). - #extent ⇒ Size?
-
#extent_rect ⇒ Rect
#extent placed at #rect's top-left, for the consumers that need coordinates:
extent_rect.contains?(event.point)in a #handle_mouse, and the anchor a dropdown hangs from. -
#fire_lifecycle(attached) ⇒ void
Walks self-then-children calling one lifecycle hook, delivering at most one call per component per transition however the hooks mutate the tree.
-
#focus ⇒ void
Focuses this component.
-
#focusable? ⇒ Boolean
Whether this component is a valid focus target.
-
#handle_key(_key) ⇒ Boolean
Called when a key is pressed; override to act on keys you care about (the default reports every key unhandled).
-
#handle_mouse(event) ⇒ void
Focuses this component when left-clicked (if #focusable?), then hands the event down to every child whose #rect contains the point — which is how a click descends the tiled tree to a leaf.
-
#handle_paste(_text) ⇒ Boolean
Called when text is pasted while this component is on the focus chain; override to accept it (the default reports every paste unhandled, and unhandled text is dropped).
-
#height ⇒ Integer
@return —
rect.height. -
#initialize ⇒ Component
constructor
A new instance of Component.
- #invalidate ⇒ void
-
#on_attached ⇒ void
Called once this component's tree has been mounted on a ScreenPane, i.e.
-
#on_child_removed(child) ⇒ void
Called by container components after
childhas been detached fromself.children(itsparentis already nil and it is no longer in the children list). -
#on_detached ⇒ void
Mirror of #on_attached, called once the tree has been unmounted — see there for the contract.
-
#on_focus ⇒ void
Called when the component receives focus.
-
#on_tree(&block) ⇒ void
Calls block for this component and for every descendant component.
-
#on_width_changed ⇒ void
Called whenever the component width changes.
-
#remove_child(child) ⇒ void
Drops
childand notifies #on_child_removed. -
#repaint ⇒ void
Repaints the component.
-
#root ⇒ Component
@return — the root component of this component hierarchy.
-
#screen ⇒ Screen
@return — the screen which owns this component.
-
#size ⇒ Size
@return —
rect.size. -
#tab_stop? ⇒ Boolean
Whether this component participates in Tab / Shift+Tab focus cycling.
-
#width ⇒ Integer
@return —
rect.width.
Constructor Details
#initialize ⇒ Component
Returns a new instance of Component.
47 48 49 50 51 52 53 54 |
# File 'lib/tuile/component.rb', line 47 def initialize Component.verify_final!(self.class) @rect = Rect.new(0, 0, 0, 0) @active = false @on_theme_changed = nil @bg_color = nil @children = [] end |
Instance Attribute Details
#bg_color ⇒ Color, ...
@return — this component's own background — the
value as set, so a Theme::Ref comes back unresolved; nil when unset,
in which case it inherits from the parent (see #effective_bg_color),
ultimately the terminal default. #effective_bg_color is the resolved
Tuile::Color to paint.
152 153 154 |
# File 'lib/tuile/component.rb', line 152 def bg_color @bg_color end |
#children ⇒ ::Array[Component] (readonly)
Child components in paint order (siblings left to right, earlier ones painted under later ones), maintained by #add_child / #remove_child.
Not meant to be overridden: a container that computed this from its own
slots could disagree with the parent pointers, and #attached? walks the
chain while subtree walks use this list. Named slots are readers over
the array (Window#footer), never a second copy of it.
@return — child components. Must not be mutated by callers! May be empty.
325 326 327 |
# File 'lib/tuile/component.rb', line 325 def children @children end |
#on_theme_changed ⇒ Proc?
Called on every attached component (pre-order, popups included) when
Screen#theme changes — at Screen#theme= / Screen#theme_def= and on
OS appearance flips. The hook exists for app content whose colors were
baked in from the old theme (a Tuile::Component::Label#text / Tuile::Component::List#lines= StyledString
styled with theme[:accent]); rebuild it here by re-running the code that
rendered it. See book ch6 for why built-in accents need no such handling.
Runs on the UI thread with Screen#theme already updated, so mutating
content (text=, lines=, …) is safe. Do not assign Screen#theme=
here. Subclasses overriding this must call super so an assigned
#on_theme_changed= listener keeps firing.
Plumbing an app overrides and never calls, hence protected — and
Screen, not being a Tuile::Component, fans it out through __send__, so an
override is free to declare any visibility (D_hook_visibility).
539 540 541 |
# File 'lib/tuile/component.rb', line 539 def on_theme_changed @on_theme_changed&.call end |
#parent ⇒ Component?
@return — the parent component or nil if the component has no parent.
307 308 309 |
# File 'lib/tuile/component.rb', line 307 def parent @parent end |
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
57 58 59 |
# File 'lib/tuile/component.rb', line 57 def rect @rect end |
Class Method Details
.verify_final!(klass) ⇒ void
This method returns an undefined value.
Raises unless klass inherits every FINAL_METHODS entry from
Tuile::Component; memoized, so it costs one hash lookup per construction.
Comparing each resolved method's owner is what makes this catch all
four routes in — def, define_method, an included module, a prepend.
A method_added hook would fire earlier but see only the first two.
@param klass — the class being instantiated.
32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
# File 'lib/tuile/component.rb', line 32 def self.verify_final!(klass) return if @final_verified.key?(klass) overridden = FINAL_METHODS.reject { klass.instance_method(_1).owner == Component } unless overridden.empty? raise Error, "#{klass} overrides #{overridden.join(", ")}, which are final on Component. " \ "The tree is the framework's single source of truth — #attached? walks the " \ "parent chain while subtree walks use #children, so an override desyncs them " \ "silently. Reparent through add_child / remove_child / detach_child; for a " \ "swappable region, hold a Component::Slot." end @final_verified[klass] = true end |
Instance Method Details
#active=(active) ⇒ void
This method returns an undefined value.
@param active — true if active. Set by Screen#focused= as it marks the focus chain (root → focused); not meant to be called directly.
279 280 281 282 283 284 285 |
# File 'lib/tuile/component.rb', line 279 def active=(active) active = active ? true : false return unless @active != active @active = active invalidate end |
#active? ⇒ Boolean
@return — true if the component is on the active chain — i.e. it is the focused component or an ancestor of it. Set by Screen#focused=.
274 |
# File 'lib/tuile/component.rb', line 274 def active? = @active |
#add_child(child, at: nil) ⇒ void
This method returns an undefined value.
Adopts child: places it in #children and wires its parent pointer.
add_child(content, at: 0) # the tiled layer, painted beneath …
add_child(@footer) # … and chrome appended, painted over it
@param child — must not already have a parent.
@param at — index to insert at; appends when nil.
406 407 408 409 410 411 412 |
# File 'lib/tuile/component.rb', line 406 def add_child(child, at: nil) raise TypeError, "expected Component, got #{child.inspect}" unless child.is_a? Component raise ArgumentError, "#{child} already has a parent #{child.parent}" unless child.parent.nil? at.nil? ? @children.push(child) : @children.insert(at, child) child.parent = self end |
#attached? ⇒ Boolean
Whether this component's tree is mounted on a UI, ScreenPane being the root of every displayed tree.
A property of the parent chain alone — no Screen is consulted, so assembling a tree needs no screen in the process at all:
layout = Component::Layout::Absolute.new
layout.add(label) # legal with no Screen; neither is attached yet
screen.content = layout # now both are
@return — true if #root is a ScreenPane.
361 |
# File 'lib/tuile/component.rb', line 361 def attached? = root.is_a?(ScreenPane) |
#children_tile_rect? ⇒ Boolean
Whether direct children fully tile #rect. Used by the default #repaint to decide whether the framework needs to wipe gaps.
Approximated by area: sum of (non-empty) child areas vs the parent's area. Cheap, and correct as long as siblings don't overlap each other — which Tuile already requires (no clipping in the tiled tree). Children with empty rects contribute zero, since they paint nothing.
566 567 568 569 |
# File 'lib/tuile/component.rb', line 566 def children_tile_rect? total = children.sum { |c| c.rect.empty? ? 0 : c.rect.width * c.rect.height } total >= rect.width * rect.height end |
#clear_background(area = rect) ⇒ void
This method returns an undefined value.
Clears the background: fills every cell with a blank in the #effective_bg_color (the terminal default when none is inherited).
A component that paints part of its #rect itself passes just the part it doesn't — blanking a cell it is about to overwrite anyway makes that cell dirty, and Buffer#flush then re-emits it even though nothing visibly changed.
@param area — the region to blank; defaults to the whole #rect.
597 598 599 600 |
# File 'lib/tuile/component.rb', line 597 def clear_background(area = rect) bg = effective_bg_color screen.buffer.fill(area, bg ? StyledString::Style.new(bg:) : StyledString::Style::DEFAULT) end |
#clear_outside_extent ⇒ void
This method returns an undefined value.
Blanks the part of #rect outside #extent — the dead tail a widget that
paints less than it was given must not leave stale. Up to two regions,
since a narrowed extent leaves an L: the columns right of it, and the rows
below it. A nil extent declares nothing, so the whole rect is blanked.
Called by the default #repaint; a self-painter that skips super calls
it directly.
578 579 580 581 582 583 584 585 586 |
# File 'lib/tuile/component.rb', line 578 def clear_outside_extent e = extent return clear_background if e.nil? # nothing declared: all of it is fair game right = Rect.new(rect.left + e.width, rect.top, rect.width - e.width, e.height) below = Rect.new(rect.left, rect.top + e.height, rect.width, rect.height - e.height) clear_background(right) unless right.empty? clear_background(below) unless below.empty? end |
#cursor_position ⇒ Point?
Where the hardware terminal cursor should sit when this component is the
cursor owner. Returns nil to indicate the cursor should be hidden. The
Screen positions the hardware cursor after each repaint cycle by
consulting the Screen#focused component only.
@return — absolute screen coordinates, or nil to hide.
392 |
# File 'lib/tuile/component.rb', line 392 def cursor_position = nil |
#depth ⇒ Integer
@return — the distance from the root component; 0 if #parent is nil.
311 |
# File 'lib/tuile/component.rb', line 311 def depth = parent.nil? ? 0 : parent.depth + 1 |
#detach_child(child) ⇒ void
This method returns an undefined value.
Drops child without notifying — for a container swapping a named slot,
which owes the #on_child_removed call once the new occupant is wired:
detach_child(old)
@content = new
add_child(new, at: 0)
on_child_removed(old) # focus repair cascades into the *new* content
The child leaves #children before its pointer is cleared, so nothing observes a child whose parent has disowned it while still listing it.
@param child
436 437 438 439 440 441 |
# File 'lib/tuile/component.rb', line 436 def detach_child(child) raise ArgumentError, "#{child} is not a child of #{self}" unless @children.include?(child) @children.delete(child) child.parent = nil end |
#draw_char(x, y, grapheme, style = StyledString::Style::DEFAULT) ⇒ Object
#draw_text's single-grapheme counterpart: writes grapheme at (x, y),
filling #effective_bg_color when style carries no bg of its own.
@param x — column.
@param y — row.
@param grapheme — one grapheme cluster.
@param style
622 623 624 625 626 |
# File 'lib/tuile/component.rb', line 622 def draw_char(x, y, grapheme, style = StyledString::Style::DEFAULT) bg = effective_bg_color style = style.merge(bg:) if bg && style.bg.nil? screen.buffer.set_char(x, y, grapheme, style) end |
#draw_text(x, y, styled) ⇒ void
This method returns an undefined value.
Buffer#set_text wrapper that fills #effective_bg_color behind any span with no bg of its own (via StyledString#under_bg), so an inherited #bg_color shows through the content a component paints. A no-op layer when nothing is inherited. Self-painters (those skipping the #repaint auto-clear) paint through this instead of Screen#buffer directly.
@param x — starting column.
@param y — row.
@param styled
611 612 613 |
# File 'lib/tuile/component.rb', line 611 def draw_text(x, y, styled) screen.buffer.set_text(x, y, styled.under_bg(effective_bg_color)) end |
#effective_bg_color ⇒ Color?
@return — the background actually painted: this component's own
#bg_color if set (a Theme::Ref resolved against the current theme),
else the nearest ancestor's, else nil (terminal default). Resolved at
paint time — never cached, so the subtree tracks both an ancestor's
#bg_color= and a Screen#theme= on its next repaint.
185 186 187 188 189 |
# File 'lib/tuile/component.rb', line 185 def effective_bg_color own = @bg_color own = own.resolve(screen.theme) if own.is_a?(Theme::Ref) own || parent&.effective_bg_color end |
#extent ⇒ Size?
The size of the region this component paints, or nil (the default) to
declare nothing — in which case the whole #rect is treated as fair game
and the default #repaint blanks all of it. Override it when you paint
less: a one-row Checkbox handed a tall column, or a
Select used as a Popup's content and assigned the
whole inner box.
It always sits at #rect's top-left — which is why this is a Size and not a Rect: an offset extent is not merely unsupported, it is unrepresentable. Use #extent_rect where coordinates are wanted.
nil is not the same as rect.size. nil says "I have not declared
what I paint, so clear everything before I do", which is what a
Label with short text needs. A declared extent — even one that
happens to equal the rect, as a one-row Select in a one-row
rect does — says "I paint this in full, don't blank it", which is what
keeps the default #repaint from dirtying cells it is about to redraw
(D_progress_bar). The base cannot tell those apart from the value alone;
that is what the nil carries.
It flows downward only: no container consults it when dividing space,
so #rect still means exactly what the parent assigned (D_extent). Three
things read it, all of them this component or the framework painting it:
#clear_outside_extent blanks the dead tail, #handle_mouse hit-tests
against it so a click on that tail doesn't activate the widget, and a
dropdown anchors under it rather than under unused space.
An override promises to paint the extent in full, so super in
#repaint blanks only what is outside it. The arithmetic is each widget's
own — caption width, painted strip, one row — and must not vary with
#bg_color (D_boolean_fields).
106 |
# File 'lib/tuile/component.rb', line 106 def extent = nil |
#extent_rect ⇒ Rect
#extent placed at #rect's top-left, for the consumers that need
coordinates: extent_rect.contains?(event.point) in a #handle_mouse, and
the anchor a dropdown hangs from. Total — an undeclared #extent yields
the whole #rect, so a generic caller never sees nil.
113 114 115 116 |
# File 'lib/tuile/component.rb', line 113 def extent_rect e = extent e.nil? ? rect : Rect.new(rect.left, rect.top, e.width, e.height) end |
#fire_lifecycle(attached) ⇒ void
This method returns an undefined value.
Walks self-then-children calling one lifecycle hook, delivering at most one call per component per transition however the hooks mutate the tree. Two guards, because a hook runs before its own children are visited:
- the snapshot covers a child a hook adds — it isn't in
kids, and fires exactly once through its ownparent=; - the state re-check covers a child a hook removes. Matching on
current attachedness rather than on
parent.equal?(self): a child pulled out during a detach walk is already detached, so its ownparent=saw no transition and stayed silent — a parentage check would skip it too and it would never hearon_detachedat all. The reverse case (pulled out during an attach walk) getson_detachedfrom its ownparent=and noon_attached, which is why the hooks are required to be idempotent: an unpaired detach releases nothing, whereas firingon_attachedat a component that is no longer attached would start a ticker nothing stops.
@param attached — true to fire #on_attached, false for #on_detached.
513 514 515 516 517 |
# File 'lib/tuile/component.rb', line 513 def fire_lifecycle(attached) kids = children.dup attached ? on_attached : on_detached kids.each { _1.fire_lifecycle(attached) if _1.attached? == attached } end |
#focus ⇒ void
This method returns an undefined value.
Focuses this component. Equivalent to screen.focused = self.
143 144 145 |
# File 'lib/tuile/component.rb', line 143 def focus screen.focused = self end |
#focusable? ⇒ Boolean
Whether this component is a valid focus target. false by default —
passive components like Label are decoration and don't accept focus.
The flag gates click-to-focus and the container focus-cascade. Independent
from #active?: every component carries the active flag, but only
focusable ones can become a focus target that puts themselves and their
ancestors on the active chain. Focusable is broader than #tab_stop? —
a Window is focusable (a click on chrome lands focus) but not a tab stop.
@return — true if this component can be focused.
295 |
# File 'lib/tuile/component.rb', line 295 def focusable? = false |
#handle_key(_key) ⇒ Boolean
Called when a key is pressed; override to act on keys you care about (the default reports every key unhandled). A component only receives keys while it's on the focus chain — or when app code hands it one directly — so act on the key alone and never gate on your own #active? state. See book ch5 for how a keystroke is routed to reach here.
@param _key — a key.
@return — true if the key was handled, false if not.
230 231 232 |
# File 'lib/tuile/component.rb', line 230 def handle_key(_key) false end |
#handle_mouse(event) ⇒ void
This method returns an undefined value.
Focuses this component when left-clicked (if #focusable?), then hands the event down to every child whose #rect contains the point — which is how a click descends the tiled tree to a leaf.
A widget that resolves clicks inside its own rect — mapping a point to a
row, or toggling an overlay — overrides this and does not call super.
Such an override hit-tests #extent_rect rather than #rect, so a click
on the tail it doesn't paint never activates it.
@param event
265 266 267 268 269 270 |
# File 'lib/tuile/component.rb', line 265 def handle_mouse(event) screen.focused = self unless event. != :left || active? || !focusable? # Snapshot: a handler may add or remove siblings (a click that swaps a # slot's occupant), and `each` over a mutating array skips an entry. children.dup.each { |c| c.handle_mouse(event) if c.rect.contains?(event.point) } end |
#handle_paste(_text) ⇒ Boolean
Called when text is pasted while this component is on the focus chain;
override to accept it (the default reports every paste unhandled, and
unhandled text is dropped). Arrives whole and \n-normalized, so
text.lines.size is the paste's line count and a single mutation can
absorb it:
def handle_paste(text)
self.caption = "[Pasted #{text.lines.size} lines]"
true
end
Reaching here means the terminal said "this came from the clipboard" — AbstractStringField inserts it at the caret, which is why a subclass that rebinds ENTER to submit needs no paste handling of its own to stop firing once per pasted line.
@param _text — the pasted text.
@return — true if the paste was consumed.
251 252 253 |
# File 'lib/tuile/component.rb', line 251 def handle_paste(_text) false end |
#height ⇒ Integer
@return — rect.height.
72 |
# File 'lib/tuile/component.rb', line 72 def height = rect.height |
#invalidate ⇒ void
This method returns an undefined value.
Invalidates the component: Screen records this component as needs-repaint and once all events are processed, will call #repaint.
No-op when the component is not #attached? — a detached component has
no place on the screen to paint to, so Screen must never end up
repainting it. Callers don't need to guard their own invalidate calls;
mutating a detached component (e.g. setting lines= on a List sitting
inside a closed Popup) is silent.
552 553 554 555 556 |
# File 'lib/tuile/component.rb', line 552 def invalidate return unless attached? screen.invalidate(self) end |
#on_attached ⇒ void
This method returns an undefined value.
Called once this component's tree has been mounted on a ScreenPane, i.e. when #attached? flips to true — the place to acquire whatever is supposed to live for exactly as long as the component is on screen:
def on_attached
@ticker = screen.event_queue.tick_fps(10) { advance }
end
def on_detached
@ticker&.cancel
@ticker = nil
end
on_attached starts what on_detached stops; both must be cheap and
idempotent, since a component moved between parents is genuinely detached
in between and gets both, in that order. Whatever you acquire here you
must release in #on_detached — nothing else will. Not a destructor:
process teardown does not fire #on_detached.
#invalidate needs no guard: #attached? is already true here (and already false in #on_detached, where it no-ops). Do not read #rect — a parent assigns it after wiring, so it is still stale. Runs on the thread that owns the UI.
467 |
# File 'lib/tuile/component.rb', line 467 def on_attached; end |
#on_child_removed(child) ⇒ void
This method returns an undefined value.
Called by container components after child has been detached from
self.children (its parent is already nil and it is no longer in the
children list). Default behavior repairs dangling focus: if the focused
component lived inside the removed subtree, focus shifts to self so the
cursor doesn't dangle on a detached component. No-op if self is not
attached to the screen — focus state in a detached subtree is moot.
@param child — the just-detached child.
371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 |
# File 'lib/tuile/component.rb', line 371 def on_child_removed(child) return unless attached? f = screen.focused return if f.nil? cursor = f until cursor.nil? if cursor == child screen.focused = self return end cursor = cursor.parent end end |
#on_detached ⇒ void
This method returns an undefined value.
Mirror of #on_attached, called once the tree has been unmounted — see there for the contract. Two things are still mid-flight when it runs, both deliberate: Screen#focused may still point into this subtree (repair happens after), and the ex-parent's own bookkeeping may not be finished. So release resources here and don't inspect the tree around you.
475 |
# File 'lib/tuile/component.rb', line 475 def on_detached; end |
#on_focus ⇒ void
This method returns an undefined value.
Called when the component receives focus.
339 |
# File 'lib/tuile/component.rb', line 339 def on_focus; end |
#on_tree(&block) ⇒ void
This method returns an undefined value.
Calls block for this component and for every descendant component.
332 333 334 335 |
# File 'lib/tuile/component.rb', line 332 def on_tree(&block) block.call(self) children.each { _1.on_tree(&block) } end |
#on_width_changed ⇒ void
This method returns an undefined value.
Called whenever the component width changes. Does nothing by default.
521 |
# File 'lib/tuile/component.rb', line 521 def on_width_changed; end |
#remove_child(child) ⇒ void
418 419 420 421 |
# File 'lib/tuile/component.rb', line 418 def remove_child(child) detach_child(child) on_child_removed(child) end |
#repaint ⇒ void
This method returns an undefined value.
Repaints the component. The default does the bookkeeping most components need: it clears the background — unless the direct children already tile #rect, in which case there is no gap to wipe and blanking cells they are about to repaint would only make them dirty — and then re-invalidates those children so they paint over the cleared area. That is what makes mixed-width form layouts safe.
Call super from your own repaint to inherit this. Skip it only if you
paint the whole #rect yourself (Window's border, List's
row-by-row paint). Never draw outside #rect. Only called when attached.
A widget that paints less than its rect declares an #extent rather than
skipping super. The clear then covers only what is outside it, so the
cells it is about to repaint are not blanked first — blanking them would
mark them dirty and make Buffer#flush re-emit them (D_progress_bar).
The children are re-invalidated whether or not they tile. A container
that paints nothing of its own can only redraw its area through them, so
a tiling container that skipped this would be a dead end in the cascade: an
ancestor's clear_background wipes the whole ancestor rect — siblings and
grandchildren included — and re-invalidates only its direct children, so
the notice has to keep travelling down or the cleared cells are never
repainted. Cheap by construction: repainting the same glyphs leaves
Buffer::Cell unchanged, so nothing extra reaches the wire.
216 217 218 219 220 221 |
# File 'lib/tuile/component.rb', line 216 def repaint return if rect.empty? clear_outside_extent unless children.any? && children_tile_rect? children.each { |c| screen.invalidate(c) } end |
#root ⇒ Component
@return — the root component of this component hierarchy.
314 |
# File 'lib/tuile/component.rb', line 314 def root = parent.nil? ? self : parent.root |
#screen ⇒ Screen
@return — the screen which owns this component.
139 |
# File 'lib/tuile/component.rb', line 139 def screen = Screen.instance |
#tab_stop? ⇒ Boolean
Whether this component participates in Tab / Shift+Tab focus cycling.
false by default. Only true on components that accept direct user
input (e.g. TextField, List, Button). Implies
#focusable? — Screen will skip non-focusable tab stops, but in
practice every override should keep the two consistent.
@return — true if Tab / Shift+Tab should land on this component.
303 |
# File 'lib/tuile/component.rb', line 303 def tab_stop? = false |
#width ⇒ Integer
@return — rect.width.
69 |
# File 'lib/tuile/component.rb', line 69 def width = rect.width |