Class: Tuile::Component
- Inherits:
-
Object
- Object
- Tuile::Component
- Extended by:
- Final
- 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/date_field.rb,
lib/tuile/component/layout/box.rb,
lib/tuile/component/log_window.rb,
lib/tuile/component/text_field.rb,
lib/tuile/component/time_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/has_bad_input.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/has_validation.rb,
lib/tuile/component/password_field.rb,
lib/tuile/component/date_time_field.rb,
lib/tuile/component/has_placeholder.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,
lib/tuile/component/abstract_wrapping_field.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.
Handlers and listener slots
Two families, told apart by the =:
class Trimmed < Component::TextField
def handle_blur # handle_ — the override point
super
self.text = text.strip
end
end
label.on_theme_changed = -> { … } # on_…= — the listener slot
What a handler returns is per hook, declared in its own rdoc. Only the
ones a dispatcher routes answer at all — #handle_key?,
#handle_text_input_key?, MenuBar#handle_mnemonic? — where true means "I
took this, stop bubbling". The rest, #handle_paste included, return void.
An override calls super, even where the base body is empty: that is what
lets a hook grow an on_foo= slot without breaking you. The one carve-out is
#handle_child_removed, whose base does real work and whose overrides
replace it. D_handler_naming carries the argument.
Direct Known Subclasses
Defined Under Namespace
Modules: HasBadInput, HasCaption, HasContent, HasPlaceholder, HasValidation, HasValue Classes: AbstractStringField, AbstractWrappingField, BigDecimalField, Button, Checkbox, CheckboxGroup, ComboBox, ConfirmWindow, DateField, DateTimeField, FloatField, InfoWindow, IntegerField, Label, Layout, List, ListDropdown, LogTextView, LogWindow, MenuBar, Notification, Overlay, PasswordField, PickerWindow, Popup, ProgressBar, RadioGroup, Select, Slot, TabSheet, Tabs, TextArea, TextField, TextView, TimeField, Window
Constant Summary collapse
- BG_STATES =
The states a background may be keyed by. Closed and framework-defined: a key is added when Tuile grows the state, never to let an app invent one.
%i[normal active].freeze
- BG_INHERIT =
Assign to #bg_color to say "I contribute no background of my own" — resolution skips this component's #default_bg_color and takes whatever surrounds it. CSS's
background: inherit, and the reason a widget with a well can be made to sit flush in a tinted panel:field.bg_color = Component::BG_INHERIT # no well; take the pane's tintDistinct from
nil, which falls through to #default_bg_color first. There is deliberately no counterpart forcing the terminal default despite a tinted ancestor (D_bg_inherit). :inherit
Instance Attribute Summary collapse
-
#bg_color ⇒ Color, ...
@return — this component's own background — the value as set, so a Theme::Ref comes back unresolved and a state map comes back a Hash;
nilwhen unset, in which case the component falls back to #default_bg_color and then to its parent. -
#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.
-
#id ⇒ Symbol?
A tag for finding this component again — nothing paints it, and the framework never reads it:.
-
#on_locale_changed ⇒ Proc?
Optional zero-arg listener fired by the base #handle_locale_changed — the composition-style alternative to overriding the method, for an app that rendered a date or a number into a stock component:.
-
#on_theme_changed ⇒ Proc?
Optional zero-arg listener fired by the base #handle_theme_changed — the composition-style alternative to overriding the method, for apps that assemble stock components rather than subclass:.
-
#parent ⇒ Component?
Final: the parent chain is one half of the tree's single source of truth — #attached? walks it while every subtree walk uses #children, so a derived pointer leaves a component attached but never painted, with nothing raising (
D_final_tree). -
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
Class Method Summary collapse
-
.final ⇒ Symbol, ::Array[Symbol]
Marks each of
namesnon-overridable by subclasses. -
.final_methods ⇒ ::Array[Symbol]
@return — the methods marked Final#final on this class.
-
.verify_final! ⇒ void
Raises unless
klassinherits every Final#final_methods entry from this class.
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. -
#ambient_bg_color ⇒ Color?
What surrounds this component — an app-set #bg_color, else whatever the parent paints where this component is not.
-
#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, bg = effective_bg_color) ⇒ void
Clears the background: fills every cell with a blank in the #effective_bg_color (the terminal default when none is inherited).
-
#clear_inside_extent ⇒ void
Blanks the #extent itself, for a container whose children don't cover it — a Layout::Box's
spacingcolumn, the slack past the last child, the span a child abandoned by going hidden or by a narrowing resize. - #clear_outside_extent ⇒ void
-
#coerce_bg_color(value) ⇒ Color, ...
Validates and normalizes what #bg_color= was handed, so a bad token or a misspelled state raises at the assignment rather than deep in a repaint.
-
#cursor_position ⇒ Point?
Where the hardware terminal cursor should sit when this component is the cursor owner.
-
#default_bg_color ⇒ Color, ...
The background this component paints when the app has set no #bg_color —
nilby default, meaning "I have no surface of my own; whatever is behind me shows through". -
#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 #handle_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 background 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 — or an invalid field's error well — shows through the content a component paints.
-
#effective_bg_color ⇒ Color?
Final, and protected: it answers what the framework paints with, and an app never needs it — #clear_background / #draw_text / #draw_char apply it already.
-
#error_bg_color ⇒ Color, ...
The background a component paints while it is in an error state —
nilby default, meaning "I am not signalling one". - #extent ⇒ Size?
-
#extent_rect ⇒ Rect
#extent placed at #rect's top-left, for the consumers that need coordinates:
extent_rect.contains?(point)in Mouse::Router, 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_attached ⇒ void
Called once this component's tree has been mounted on a ScreenPane, i.e.
-
#handle_blur ⇒ void
Mirror of #handle_focus: the component just lost focus, to another component or to nothing.
-
#handle_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). -
#handle_child_visibility_changed(_child) ⇒ void
Called on the parent after a direct child's #visible= flipped, so a container that divides space can re-divide it:.
-
#handle_detached ⇒ void
Mirror of #handle_attached, called once the tree has been unmounted — see there for the contract.
-
#handle_focus ⇒ void
Called when the component receives focus — on this component alone, never on the ancestors that light up with it.
-
#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_locale_changed ⇒ void
Called on every attached component (pre-order, popups included) when Screen#locale changes — for state derived from the old conventions and pushed somewhere, such as a date already rendered into a Label's text.
-
#handle_mouse_down?(_event) ⇒ Boolean
Called when a mouse button goes down over this component; answer
trueto claim the press. -
#handle_mouse_drag(_event) ⇒ void
Called on the component that claimed a press whenever the pointer moves while the button is held, wherever the pointer is.
-
#handle_mouse_enter ⇒ void
Called when the pointer comes over this component or any of its descendants — down the chain, root first, and after every #handle_mouse_exit the same move fires.
-
#handle_mouse_exit ⇒ void
The other half of #handle_mouse_enter; also fires when this component is detached or hidden while hovered, innermost first.
-
#handle_mouse_move?(_event) ⇒ Boolean
Called when the pointer moves over this component with nothing grabbed; answer
trueto consume the move. -
#handle_mouse_scroll?(_event) ⇒ Boolean
Called when the wheel turns over this component; answer
trueto consume the notch. -
#handle_mouse_up(_event) ⇒ void
Called on the component that claimed a press when the button comes up, ending the grab.
-
#handle_paste(_text) ⇒ void
Called when text is pasted while this component is Screen#focused; override to accept it.
-
#handle_theme_changed ⇒ void
Called on every attached component (pre-order, popups included) when Screen#theme changes — at Screen#theme= / Screen#theme_def= and on OS appearance flips.
-
#handle_width_changed ⇒ void
Called whenever the component width changes.
-
#height ⇒ Integer
@return —
rect.height. -
#initialize ⇒ Component
constructor
A new instance of Component.
-
#inspect ⇒ String
One line naming the component, its #id and its rect, plus whatever #inspect_details adds:.
-
#inspect_details ⇒ ::Array[String]
What this component adds to its #inspect, as
key=valuestrings. - #invalidate ⇒ void
-
#invalidate_children ⇒ void
Passes the repaint cascade on to the direct children — the one thing a container may never skip, whatever else its #repaint does.
-
#locale ⇒ Locale
The formatting conventions to render and parse by (Screen#locale), or Locale::ISO when there is no screen in the process — which a tree assembled outside a UI legitimately is, and Screen.instance raises rather than answering.
-
#remove_child(child) ⇒ void
Drops
childand notifies #handle_child_removed. -
#repaint ⇒ void
Repaints the component.
-
#repair_focus_after_hiding ⇒ void
Hands focus out of the subtree just hidden, if it was in there, through the parent's #handle_child_removed — see there for why hiding reuses the removal repair instead of growing a second one.
-
#resolve_bg_color(value) ⇒ Color?
Collapses one level of the background chain to the Color it means right now: picks the entry for this component's current state out of a state Hash, and resolves a Theme::Ref against the live theme.
-
#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.
-
#visible=(value) ⇒ void
Hides or shows the component:
falsemeans as if detached — but it stays in the tree. -
#visible? ⇒ Boolean
This component's own flag — not whether the user can see it, which also depends on its ancestors: a shown field inside a hidden panel answers
true. -
#walk_shown_tree(&block) ⇒ void
#walk_tree, pruned: a hidden subtree is skipped whole, this component included when it is itself hidden (in which case nothing is yielded).
-
#walk_tree(&block) ⇒ void
Calls block for this component and for every descendant component.
-
#width ⇒ Integer
@return —
rect.width.
Methods included from Final
final, final_methods, verify_final!
Constructor Details
#initialize ⇒ Component
Returns a new instance of Component.
42 43 44 45 46 47 48 49 50 51 52 |
# File 'lib/tuile/component.rb', line 42 def initialize Component.verify_final!(self.class) @rect = Rect.new(0, 0, 0, 0) @visible = true @active = false @on_theme_changed = nil @on_locale_changed = nil @bg_color = nil @children = [] @id = nil end |
Instance Attribute Details
#bg_color ⇒ Color, ...
@return — this
component's own background — the value as set, so a Theme::Ref comes
back unresolved and a state map comes back a Hash; nil when unset, in
which case the component falls back to #default_bg_color and then to
its parent. #effective_bg_color is the resolved Tuile::Color to paint.
238 239 240 |
# File 'lib/tuile/component.rb', line 238 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.
Final: a container that computed this from its own slots would disagree
with the parent pointers #attached? walks, silently (D_final_tree).
Named slots are readers over this array (Window#footer), never a
second copy of it; for a swappable region hold a Slot.
@return — child components. Must not be mutated by callers! May be empty.
489 490 491 |
# File 'lib/tuile/component.rb', line 489 def children @children end |
#id ⇒ Symbol?
A tag for finding this component again — nothing paints it, and the framework never reads it:
field.id = :name
Testing.get(id: :name).value = "Zaphod"
Nothing enforces uniqueness, here or anywhere in production; two components may carry the same id, and only Testing.get — which raises on an ambiguous match — will ever say so.
64 65 66 |
# File 'lib/tuile/component.rb', line 64 def id @id end |
#on_locale_changed ⇒ Proc?
Optional zero-arg listener fired by the base #handle_locale_changed — the composition-style alternative to overriding the method, for an app that rendered a date or a number into a stock component:
label.on_locale_changed = -> { label.text = due_date.strftime(fmt) }
550 551 552 |
# File 'lib/tuile/component.rb', line 550 def on_locale_changed @on_locale_changed end |
#on_theme_changed ⇒ Proc?
Optional zero-arg listener fired by the base #handle_theme_changed — the composition-style alternative to overriding the method, for apps that assemble stock components rather than subclass:
label.on_theme_changed = -> { label.text = render_status_line }
541 542 543 |
# File 'lib/tuile/component.rb', line 541 def on_theme_changed @on_theme_changed end |
#parent ⇒ Component?
Final: the parent chain is one half of the tree's single source of truth
— #attached? walks it while every subtree walk uses #children, so a
derived pointer leaves a component attached but never painted, with
nothing raising (D_final_tree). Reparent through #add_child /
#remove_child / #detach_child.
@return — the parent component or nil if the component has no parent.
471 472 473 |
# File 'lib/tuile/component.rb', line 471 def parent @parent end |
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
78 79 80 |
# File 'lib/tuile/component.rb', line 78 def rect @rect end |
Class Method Details
.final ⇒ Symbol, ::Array[Symbol]
Marks each of names non-overridable by subclasses. Declare them in one
call near the top of the class; the names may be forward references,
since nothing is resolved until Final#verify_final! runs.
final :parent, :children, :add_child
It reads better as a keyword on the definition (final def foo, which
parses — def hands back its name), but don't: YARD has no handler for
the macro, so a decorated def loses its parameter list and sord then
generates def foo: () -> void into sig/, silently.
@param names — method names.
@return — the names — a bare Symbol when exactly one was given.
3659 |
# File 'sig/tuile.rbs', line 3659
def self.final: (*::Array[(Symbol | ::Array[Symbol])] names) -> (Symbol | ::Array[Symbol])
|
.final_methods ⇒ ::Array[Symbol]
@return — the methods marked Final#final on this class.
3662 |
# File 'sig/tuile.rbs', line 3662
def self.final_methods: () -> ::Array[Symbol]
|
.verify_final! ⇒ void
This method returns an undefined value.
Raises unless klass inherits every Final#final_methods entry from this
class. Memoized per class, so a construction-time call costs one hash
lookup after the first instance.
@param klass — the class being instantiated.
3669 |
# File 'sig/tuile.rbs', line 3669
def self.verify_final!: (Class klass) -> void
|
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.
438 439 440 441 442 443 444 |
# File 'lib/tuile/component.rb', line 438 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=.
433 |
# File 'lib/tuile/component.rb', line 433 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
Final: one of the three mutators that write #children and the parent pointer in the same call, which is what keeps them in agreement.
@param child — must not already have a parent.
@param at — index to insert at; appends when nil.
650 651 652 653 654 655 656 |
# File 'lib/tuile/component.rb', line 650 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 |
#ambient_bg_color ⇒ Color?
What surrounds this component — an app-set #bg_color, else whatever the parent paints where this component is not. Skips #default_bg_color, the one thing that colors this widget's own surface, which is what makes it the right answer for the dead tail outside #extent.
1089 1090 1091 1092 1093 1094 |
# File 'lib/tuile/component.rb', line 1089 def ambient_bg_color own = resolve_bg_color(@bg_color) return parent&.effective_bg_color if own.nil? || own == BG_INHERIT own 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.
563 |
# File 'lib/tuile/component.rb', line 563 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.
A hidden child contributes zero for the same reason, and that is what erases it: its cells become a gap this component then blanks.
908 909 910 911 |
# File 'lib/tuile/component.rb', line 908 def children_tile_rect? total = children.sum { |c| c.rect.empty? || !c.visible? ? 0 : c.rect.width * c.rect.height } total >= rect.width * rect.height end |
#clear_background(area = rect, bg = effective_bg_color) ⇒ 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.
@param bg — the color to blank with; defaults to #effective_bg_color, i.e. this component's own surface.
1035 1036 1037 |
# File 'lib/tuile/component.rb', line 1035 def clear_background(area = rect, bg = effective_bg_color) screen.buffer.fill(area, bg ? StyledString::Style.new(bg:) : StyledString::Style::DEFAULT) end |
#clear_inside_extent ⇒ void
This method returns an undefined value.
Blanks the #extent itself, for a container whose children don't cover
it — a Tuile::Component::Layout::Box's spacing column, the slack past the last
child, the span a child abandoned by going hidden or by a narrowing resize.
In the ambient background, the same answer #clear_outside_extent gives the dead tail: a gap between two children is not this widget's ink, so an app's #bg_color tint covers it but a well of its own — a field's, a validation error's — must not bleed into it.
Called by the default #repaint for a container only; a leaf paints its
extent itself, and blanking that first is the re-emit D_progress_bar
bought back. Override it to decline when you paint your own ink into a
face cell no child covers.
947 948 949 |
# File 'lib/tuile/component.rb', line 947 def clear_inside_extent clear_background(extent_rect, ambient_bg_color) 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.
920 921 922 923 924 925 926 927 928 929 930 931 |
# File 'lib/tuile/component.rb', line 920 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) # Not this widget's own surface: a one-row Select handed a 25-row rect # would otherwise flood the other 24 with its field well. bg = ambient_bg_color clear_background(right, bg) unless right.empty? clear_background(below, bg) unless below.empty? end |
#coerce_bg_color(value) ⇒ Color, ...
Validates and normalizes what #bg_color= was handed, so a bad token or a misspelled state raises at the assignment rather than deep in a repaint.
@param value
1118 1119 1120 1121 1122 1123 1124 1125 1126 1127 1128 1129 1130 |
# File 'lib/tuile/component.rb', line 1118 def coerce_bg_color(value) case value when nil, Color, BG_INHERIT then value when Theme::Ref then value.tap { _1.resolve(screen.theme) } when Hash unknown = value.keys - BG_STATES raise ArgumentError, "unknown background state(s) #{unknown.join(", ")}; known: #{BG_STATES.join(", ")}" \ unless unknown.empty? value.to_h { |state, color| [state, coerce_bg_color(color)] }.freeze else Color.coerce(value) end 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.
603 |
# File 'lib/tuile/component.rb', line 603 def cursor_position = nil |
#default_bg_color ⇒ Color, ...
The background this component paints when the app has set no #bg_color —
nil by default, meaning "I have no surface of my own; whatever is behind
me shows through". A widget that paints an opaque surface overrides it, and
inheritance stops there: that is what keeps a form's fields looking like
fields inside a tinted panel. Declare it unconditionally — a widget owned
by a bigger one is told so with BG_INHERIT, and must not try to work it
out from where it sits in the tree.
# a field: its own well, brighter while focused
def default_bg_color = active? ? screen.theme.active_bg_color : screen.theme.input_bg_color
Return whatever #bg_color accepts — a Tuile::Color, a Theme::Ref or a state Hash. Branching on #active? and handing back one Tuile::Color, as above, is the cheap form and allocates nothing on the paint path.
Read the theme here rather than in an ivar: this runs at paint time, so a Screen#theme= restyles the widget with no #handle_theme_changed hook.
969 |
# File 'lib/tuile/component.rb', line 969 def default_bg_color = nil |
#depth ⇒ Integer
@return — the distance from the root component; 0 if #parent is nil.
475 |
# File 'lib/tuile/component.rb', line 475 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 #handle_child_removed call once the new occupant is wired:
detach_child(old)
@content = new
add_child(new, at: 0)
handle_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. Final: one of the three mutators that write #children and the parent pointer in the same call, which is what keeps them in agreement.
@param child
686 687 688 689 690 691 |
# File 'lib/tuile/component.rb', line 686 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 background of its
own.
@param x — column.
@param y — row.
@param grapheme — one grapheme cluster.
@param style
1061 1062 1063 1064 1065 |
# File 'lib/tuile/component.rb', line 1061 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 — or an invalid field's error well — shows through the content a component paints. A no-op layer when none 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
1049 1050 1051 |
# File 'lib/tuile/component.rb', line 1049 def draw_text(x, y, styled) screen.buffer.set_text(x, y, styled.under_bg(effective_bg_color)) end |
#effective_bg_color ⇒ Color?
Final, and protected: it answers what the framework paints with, and an app never needs it — #clear_background / #draw_text / #draw_char apply it already. A component states its own opinion by overriding #default_bg_color, an app by setting #bg_color; neither takes this over. Protected rather than private because the chain below is an explicit-receiver call, which Ruby forbids for a private method.
@return — the background actually painted, for the state this
component is in right now: its #error_bg_color, else its #bg_color,
else its #default_bg_color, else the nearest ancestor answering one of
those, else nil (terminal default). Resolved at paint time — never
cached, so the subtree tracks an ancestor's #bg_color=, a
Screen#theme=, a focus change and a validation verdict on its next
repaint.
984 985 986 987 988 989 |
# File 'lib/tuile/component.rb', line 984 def effective_bg_color own = resolve_bg_color(error_bg_color) || resolve_bg_color(@bg_color) || resolve_bg_color(default_bg_color) return parent&.effective_bg_color if own.nil? || own == BG_INHERIT own end |
#error_bg_color ⇒ Color, ...
The background a component paints while it is in an error state —
nil by default, meaning "I am not signalling one". HasValidation
overrides it, so every field has it and nothing else does.
It sits above #bg_color in #effective_bg_color rather than under it, unlike #default_bg_color. That is deliberate: an app tinting a panel would otherwise switch the validation signal off on the fields inside it, silently. An app that wants different error colors changes the Theme#error_bg_color tokens.
Read the theme here rather than in an ivar, and hand back one Tuile::Color rather than a state Hash — #default_bg_color's reasons, and this runs one level earlier than that on the same paint path.
1005 |
# File 'lib/tuile/component.rb', line 1005 def error_bg_color = nil |
#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, Mouse::Router 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).
127 |
# File 'lib/tuile/component.rb', line 127 def extent = nil |
#extent_rect ⇒ Rect
#extent placed at #rect's top-left, for the consumers that need
coordinates: extent_rect.contains?(point) in Mouse::Router, and
the anchor a dropdown hangs from. Total — an undeclared #extent yields
the whole #rect, so a generic caller never sees nil.
134 135 136 137 |
# File 'lib/tuile/component.rb', line 134 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 hearhandle_detachedat all. The reverse case (pulled out during an attach walk) getshandle_detachedfrom its ownparent=and nohandle_attached, which is why the hooks are required to be idempotent: an unpaired detach releases nothing, whereas firinghandle_attachedat a component that is no longer attached would start a ticker nothing stops.
@param attached — true to fire #handle_attached, false for #handle_detached.
766 767 768 769 770 |
# File 'lib/tuile/component.rb', line 766 def fire_lifecycle(attached) kids = children.dup attached ? handle_attached : handle_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.
211 212 213 |
# File 'lib/tuile/component.rb', line 211 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.
454 |
# File 'lib/tuile/component.rb', line 454 def focusable? = false |
#handle_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 handle_attached
@ticker = screen.event_queue.tick_fps(10) { advance }
end
def handle_detached
@ticker&.cancel
@ticker = nil
end
handle_attached starts what handle_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 #handle_detached — nothing else will. Not a destructor:
process teardown does not fire #handle_detached.
#invalidate needs no guard: #attached? is already true here (and already false in #handle_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.
717 |
# File 'lib/tuile/component.rb', line 717 def handle_attached; end |
#handle_blur ⇒ void
This method returns an undefined value.
Mirror of #handle_focus: the component just lost focus, to another component or to nothing. The commit point a Tab-away still reaches — Tab is unconditional, so Tuile::Component::TextField#on_enter never fires for a user who tabs out of a half-typed field:
class TrimmedField < Component::TextField
protected def handle_blur
super
self.text = text.strip
false
end
end
Edge-triggered, and fired on the blurred component alone — never on the ancestors leaving the active chain with it, so a composed widget asking "did focus leave me and my children" overrides #active= instead (ComboBox closes its dropdown from there). Focus that merely passes through does blur: a container forwarding focus from #handle_focus is blurred by its own forward.
A notification, not a veto — focus has already moved, and the active-flag cascade has already run. Reassigning Screen#focused from here is honored: that assignment wins, and the one that blurred you abandons the rest of its work.
Implementation details
It fires wherever focus is dropped, not only where a user moved it, so
two paths reach it with the tree mid-flight: the popup-close repair blurs
an already-detached component, where #invalidate is the same silent
no-op as in #handle_detached, and Screen#close blurs on its way out. Keep
it cheap; a raise propagates out of Screen#focused=. Protected because
the framework calls it and an app never does — Screen reaches it with
__send__, so an override may declare any visibility (D_hook_visibility).
832 |
# File 'lib/tuile/component.rb', line 832 def handle_blur; end |
#handle_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.
#visible= reuses this when it hides a subtree holding focus — the
same question, answered once so hide and remove can't drift apart. It
passes the hidden child, which is still in children with self as
its parent: an override may repair focus however it likes, but must not
assume the child is gone. Removal bookkeeping belongs in the remover.
The one hook whose base body does real work, so an override replaces it
(as Slot and ScreenPane do) instead of calling super.
@param child — the just-detached, or just-hidden, child.
582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 |
# File 'lib/tuile/component.rb', line 582 def handle_child_removed(child) return unless attached? f = screen.focused return if f.nil? cursor = f until cursor.nil? if cursor == child screen.focused = self break end cursor = cursor.parent end end |
#handle_child_visibility_changed(_child) ⇒ void
This method returns an undefined value.
Called on the parent after a direct child's #visible= flipped, so a container that divides space can re-divide it:
def handle_child_visibility_changed(_child)
super
relayout
end
A container with layout arithmetic owes this override, or a hidden
child keeps its slot and its gap — the hole the flag exists to close.
Tuile::Component::Layout::Absolute owes nothing: its rect= is app
arithmetic, and an app wanting the space back reads visible? there.
Fires on the flip only, before the subtree is invalidated, never for a
grandchild. #visible= repairs focus itself, so an override has nothing
to inherit — it still calls super, per the class doc. Reached through
__send__, so it may declare any visibility (D_hook_visibility).
@param _child — the direct child whose flag changed.
795 |
# File 'lib/tuile/component.rb', line 795 def handle_child_visibility_changed(_child); end |
#handle_detached ⇒ void
This method returns an undefined value.
Mirror of #handle_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.
725 |
# File 'lib/tuile/component.rb', line 725 def handle_detached; end |
#handle_focus ⇒ void
This method returns an undefined value.
Called when the component receives focus — on this component alone, never on the ancestors that light up with it. #handle_blur is the other half.
Unlike handle_blur it is not edge-triggered: it fires on every
Screen#focused=, re-assigning the component that already has focus
included, which is what lets a container forward focus into its content
from here.
532 |
# File 'lib/tuile/component.rb', line 532 def handle_focus; end |
#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.
The ? reads like Set#add?: calling it delivers the key and reports
whether it was taken, so it is never a "would you handle this?" probe.
@param _key — a key.
@return — true if the key was handled, false if not.
336 337 338 |
# File 'lib/tuile/component.rb', line 336 def handle_key?(_key) false end |
#handle_locale_changed ⇒ void
This method returns an undefined value.
Called on every attached component (pre-order, popups included) when Screen#locale changes — for state derived from the old conventions and pushed somewhere, such as a date already rendered into a Label's text. Anything read at paint or parse time needs no override: the locale change invalidates the whole tree.
Runs on the UI thread with Screen#locale already updated. Subclasses
overriding it must call super so an assigned #on_locale_changed=
listener keeps firing.
Plumbing an app overrides and never calls, hence protected — Screen
fans it out through __send__, so an override may declare any visibility
(D_hook_visibility).
@return — is whatever the app's lambda happened to return.
870 871 872 |
# File 'lib/tuile/component.rb', line 870 def handle_locale_changed @on_locale_changed&.call end |
#handle_mouse_down?(_event) ⇒ Boolean
Called when a mouse button goes down over this component; answer true to
claim the press. The default claims nothing.
def handle_mouse_down?(event)
return false unless event.button == :left
@on_click&.call
true
end
Mouse::Router delivers it to the innermost component under the pointer
and bubbles it up the ancestors until one answers true; the claimant
then holds the grab, and the button's #handle_mouse_drag and
#handle_mouse_up go to it alone. The press has already moved focus by
the time it arrives, and it only arrives where #extent_rect contains the
point, so an override needs neither super nor a hit test of its own.
Activate here, on the press: Tuile synthesizes no click, because a release
is losable over ssh and tmux (D_mouse_dispatch).
@param _event
@return — whether this component claimed the press.
382 |
# File 'lib/tuile/component.rb', line 382 def handle_mouse_down?(_event) = false |
#handle_mouse_drag(_event) ⇒ void
This method returns an undefined value.
Called on the component that claimed a press whenever the pointer moves
while the button is held, wherever the pointer is. Needs
capture_mouse: :drag or :hover.
@param _event — its point may lie outside #rect.
413 |
# File 'lib/tuile/component.rb', line 413 def handle_mouse_drag(_event); end |
#handle_mouse_enter ⇒ void
This method returns an undefined value.
Called when the pointer comes over this component or any of its
descendants — down the chain, root first, and after every
#handle_mouse_exit the same move fires. Needs capture_mouse: :hover,
and is suspended while a press is grabbed.
Never a commit point: no terminal reports the pointer leaving the window, so the matching #handle_mouse_exit may arrive late or not at all. Anything done here must be cosmetic and survive that.
424 |
# File 'lib/tuile/component.rb', line 424 def handle_mouse_enter; end |
#handle_mouse_exit ⇒ void
This method returns an undefined value.
The other half of #handle_mouse_enter; also fires when this component is detached or hidden while hovered, innermost first.
429 |
# File 'lib/tuile/component.rb', line 429 def handle_mouse_exit; end |
#handle_mouse_move?(_event) ⇒ Boolean
Called when the pointer moves over this component with nothing grabbed;
answer true to consume the move. Bubbles as #handle_mouse_down? does.
Arrives only under run_event_loop(capture_mouse: :hover), at up to ~84
events a second, which is why the default passes it on untouched.
@param _event
@return — whether this component consumed the move.
398 |
# File 'lib/tuile/component.rb', line 398 def handle_mouse_move?(_event) = false |
#handle_mouse_scroll?(_event) ⇒ Boolean
Called when the wheel turns over this component; answer true to consume
the notch. Bubbles exactly as #handle_mouse_down? does, but grabs
nothing — so a scroller already at its limit answers false and its
ancestor scrolls instead.
@param _event
@return — whether this component consumed the notch.
390 |
# File 'lib/tuile/component.rb', line 390 def handle_mouse_scroll?(_event) = false |
#handle_mouse_up(_event) ⇒ void
This method returns an undefined value.
Called on the component that claimed a press when the button comes up, ending the grab. For press feedback and for ending a drag — never for activation: a release may never arrive, and any key or the next press ends the grab without it.
@param _event
406 |
# File 'lib/tuile/component.rb', line 406 def handle_mouse_up(_event); end |
#handle_paste(_text) ⇒ void
This method returns an undefined value.
Called when text is pasted while this component is Screen#focused;
override to accept it. The default drops the text. It 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]"
end
No verdict, unlike #handle_key?: a paste reaches the focused component
and stops, so one that declines has nowhere to hand it on to
(D_bracketed_paste).
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.
359 |
# File 'lib/tuile/component.rb', line 359 def handle_paste(_text); end |
#handle_theme_changed ⇒ void
This method returns an undefined value.
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).
@return — is whatever the app's lambda happened to return.
851 852 853 |
# File 'lib/tuile/component.rb', line 851 def handle_theme_changed @on_theme_changed&.call end |
#handle_width_changed ⇒ void
This method returns an undefined value.
Called whenever the component width changes. Does nothing by default.
774 |
# File 'lib/tuile/component.rb', line 774 def handle_width_changed; end |
#height ⇒ Integer
@return — rect.height.
93 |
# File 'lib/tuile/component.rb', line 93 def height = rect.height |
#inspect ⇒ String
One line naming the component, its #id and its rect, plus whatever #inspect_details adds:
#<Tuile::Component::Button id=:save rect=(2,3 8x1) caption="Save">
Deliberately shallow — it never walks #parent or #children, so inspecting one component does not dump the whole UI.
613 614 615 616 617 618 |
# File 'lib/tuile/component.rb', line 613 def inspect parts = [self.class.to_s] # not .name — an anonymous class has none parts << "id=#{@id.inspect}" unless @id.nil? parts << "rect=(#{rect})" "#<#{(parts + inspect_details).join(" ")}>" end |
#inspect_details ⇒ ::Array[String]
What this component adds to its #inspect, as key=value strings.
def inspect_details = super + ["items=#{items.size}"]
Always super — this is the seam several mixins share, and each one
appends to what the last returned. Override #inspect instead of this
and you drop whichever details the mixins contribute. They come out in
reverse include order, the last-included module running first.
The base contributes a bare hidden when #visible? is false, and
nothing when it is true — so in a Testing.dump the marker sits on the
hidden ancestor, not on each component under it.
635 |
# File 'lib/tuile/component.rb', line 635 def inspect_details = @visible ? [] : ["hidden"] |
#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.
891 892 893 894 895 |
# File 'lib/tuile/component.rb', line 891 def invalidate return unless attached? screen.invalidate(self) end |
#invalidate_children ⇒ void
This method returns an undefined value.
Passes the repaint cascade on to the direct children — the one thing a container may never skip, whatever else its #repaint does. Named so a self-painting container can drop the default's blanket clear without also dropping this by accident (Window is the case):
def repaint
return if rect.empty?
invalidate_children # never optional
paint_my_own_chrome
end
1020 1021 1022 |
# File 'lib/tuile/component.rb', line 1020 def invalidate_children children.each { |c| screen.invalidate(c) } end |
#locale ⇒ Locale
The formatting conventions to render and parse by (Screen#locale), or Locale::ISO when there is no screen in the process — which a tree assembled outside a UI legitimately is, and Screen.instance raises rather than answering. Read it here, at use time; never cache it, since Screen#locale= can replace it.
880 |
# File 'lib/tuile/component.rb', line 880 def locale = Screen.instance? ? screen.locale : Locale::ISO |
#remove_child(child) ⇒ void
This method returns an undefined value.
Drops child and notifies #handle_child_removed.
Final: one of the three mutators that write #children and the parent
pointer in the same call, which is what keeps them in agreement.
@param child
665 666 667 668 |
# File 'lib/tuile/component.rb', line 665 def remove_child(child) detach_child(child) handle_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).
That saving is a leaf's: a container's children paint its extent for it,
so a cell among them that none covers still gets blanked — an extent
narrows which cells are yours, never whether your gaps are wiped.
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.
A container that skips super because it paints its own rect must still
call #invalidate_children — that is the half of this that cannot be
dropped.
316 317 318 319 320 321 322 323 324 |
# File 'lib/tuile/component.rb', line 316 def repaint return if rect.empty? unless children.any? && children_tile_rect? clear_outside_extent clear_inside_extent if extent && children.any? end invalidate_children end |
#repair_focus_after_hiding ⇒ void
This method returns an undefined value.
Hands focus out of the subtree just hidden, if it was in there, through the parent's #handle_child_removed — see there for why hiding reuses the removal repair instead of growing a second one.
The parent is necessarily showing (focus was inside it a moment ago, and Screen#focused= refuses a hidden target), so its assignment can't bounce.
1076 1077 1078 1079 1080 1081 1082 |
# File 'lib/tuile/component.rb', line 1076 def repair_focus_after_hiding return unless attached? cursor = screen.focused cursor = cursor.parent until cursor.nil? || cursor.equal?(self) parent.handle_child_removed(self) unless cursor.nil? end |
#resolve_bg_color(value) ⇒ Color?
Collapses one level of the background chain to the Tuile::Color it means right
now: picks the entry for this component's current state out of a state
Hash, and resolves a Theme::Ref against the live theme. An absent state
key yields nil, so resolution falls through to the next level — which is
what lets bg_color = { active: … } keep the widget's own normal well.
@param value
1103 1104 1105 1106 1107 1108 1109 1110 |
# File 'lib/tuile/component.rb', line 1103 def resolve_bg_color(value) case value when nil then nil when Hash then resolve_bg_color(value[active? ? :active : :normal]) when Theme::Ref then value.resolve(screen.theme) else value end end |
#root ⇒ Component
@return — the root component of this component hierarchy.
478 |
# File 'lib/tuile/component.rb', line 478 def root = parent.nil? ? self : parent.root |
#screen ⇒ Screen
@return — the screen which owns this component.
207 |
# File 'lib/tuile/component.rb', line 207 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.
462 |
# File 'lib/tuile/component.rb', line 462 def tab_stop? = false |
#visible=(value) ⇒ void
This method returns an undefined value.
Hides or shows the component: false means as if detached — but it
stays in the tree.
company.visible = business_customer.checked? # a conditional form field
Hidden, it paints nothing, takes no space in a
Tuile::Component::Layout::Box (nor the spacing around it), and is
unreachable by focus, Tab, keys, the cursor, the mouse and
Testing.find. Unlike a detached component it keeps its #parent,
#rect, box constraints, state and any resource it holds, and fires no
lifecycle hook — so a hidden pane may go on running a job. When you
want the hooks, remove it from the tree instead.
Two contracts worth knowing before you meet them as bugs:
- Ancestor-inclusive. Hiding a container hides its whole subtree whatever those components' own flags say; the flags are remembered, so showing it restores exactly the subtree that was showing before.
- Focus never stays on what the user cannot see. Hiding the subtree holding focus repairs it exactly as removing that subtree would (see #handle_child_removed), and does not hand it back on the way in.
For "invisible but still occupying its space", use a
Slot with no content (D_slots).
@param value
193 194 195 196 197 198 199 200 201 202 203 204 |
# File 'lib/tuile/component.rb', line 193 def visible=(value) value = value ? true : false return if @visible == value screen.check_locked if attached? @visible = value # `__send__` for the same reason `Screen#theme=` uses it: the hook is # protected (`D_hook_visibility`). parent&.__send__(:handle_child_visibility_changed, self) repair_focus_after_hiding unless value walk_tree { |c| screen.invalidate(c) } if attached? end |
#visible? ⇒ Boolean
This component's own flag — not whether the user can see it, which
also depends on its ancestors: a shown field inside a hidden panel
answers true.
163 |
# File 'lib/tuile/component.rb', line 163 def visible? = @visible |
#walk_shown_tree(&block) ⇒ void
This method returns an undefined value.
#walk_tree, pruned: a hidden subtree is skipped whole, this component included when it is itself hidden (in which case nothing is yielded).
stops = []
scope.walk_shown_tree { |c| stops << c if c.tab_stop? }
Use this for anything asking "can the user reach it", #walk_tree for
what the framework does to a component regardless — lifecycle, theme
fan-out, invalidation — which a hidden component still gets. Writing the
first as walk_tree plus a visible? test is the trap: that is this walk
with the ancestor case missing, so a field under a hidden panel is back
in the Tab cycle (D_visibility).
517 518 519 520 521 522 |
# File 'lib/tuile/component.rb', line 517 def walk_shown_tree(&block) return unless visible? block.call(self) children.each { _1.walk_shown_tree(&block) } end |
#walk_tree(&block) ⇒ void
This method returns an undefined value.
Calls block for this component and for every descendant component.
496 497 498 499 |
# File 'lib/tuile/component.rb', line 496 def walk_tree(&block) block.call(self) children.each { _1.walk_tree(&block) } end |
#width ⇒ Integer
@return — rect.width.
90 |
# File 'lib/tuile/component.rb', line 90 def width = rect.width |