Class: Tuile::Component

Inherits:
Object
  • Object
show all
Extended by:
Final, Listeners::Declare
Defined in:
lib/tuile/component.rb,
lib/tuile/component/fill.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/scroller.rb,
lib/tuile/component/combo_box.rb,
lib/tuile/component/form_item.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/form_layout.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/absolute.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/vertical_scroll_bar.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 — handle_ is the override point, on_ the listener slot:

class Trimmed < Component::TextField
def handle_blur                  # handle_ — the override point
  super
  self.value = text.strip
end
end

label.on_theme_changed { … }        # on_… — the listener slot, a {Listeners}

A slot holds many listeners and has no setter: register with the reader, remove with Listeners#remove, and nothing you add can displace what the widget or another app wired there.

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

ScreenPane

Defined Under Namespace

Modules: HasBadInput, HasCaption, HasContent, HasPlaceholder, HasValidation, HasValue Classes: AbstractStringField, AbstractWrappingField, BigDecimalField, Button, Checkbox, CheckboxGroup, ComboBox, ConfirmWindow, DateField, DateTimeField, Fill, FloatField, FormItem, FormLayout, InfoWindow, IntegerField, Label, Layout, List, ListDropdown, LocaleChangedEvent, LogTextView, LogWindow, MenuBar, Notification, Overlay, PasswordField, PickerWindow, Popup, ProgressBar, RadioGroup, Scroller, Select, Slot, TabSheet, Tabs, TextArea, TextField, TextView, ThemeChangedEvent, TimeField, VerticalScrollBar, Window

Instance Attribute Summary collapse

Class Method Summary collapse

Instance Method Summary collapse

Methods included from Final

final, final_methods, verify_final!

Methods included from Listeners::Declare

listener

Constructor Details

#initialize ⇒ Component

Returns a new instance of Component.



47
48
49
50
51
52
53
54
55
56
# File 'lib/tuile/component.rb', line 47

def initialize
  Component.verify_final!(self.class)
  @rect = Rect.new(0, 0, 0, 0)
  @visible = true
  @active = false
  @bg = ComponentBackground.new(self)
  @children = []
  @id = nil
  @layout_dirty = false
end

Instance Attribute Details

#bg ⇒ ComponentBackground (readonly)

This component's background — protected, because stating a widget's own well is the widget's business; an app tints through #bg_color=.

bg.default_color = ComponentBackground::INPUT_WELL   # in a field's initialize

Final: Screen#canvas_for and every descendant's chain read it.

Returns:



1270
1271
1272
# File 'lib/tuile/component.rb', line 1270

def bg
  @bg
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.

Returns:



700
701
702
# File 'lib/tuile/component.rb', line 700

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.

Returns:

  • (Symbol, nil)


68
69
70
# File 'lib/tuile/component.rb', line 68

def id
  @id
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.

Returns:



682
683
684
# File 'lib/tuile/component.rb', line 682

def parent
  @parent
end

#rect ⇒ Rect

The rectangle the component occupies inside its parent: (0, 0) is the parent's top-left, not the screen's. #absolute_rect is where that lands on screen, and #local_rect is this same rectangle with the position taken out.

Layout is deferred, so a rect read in the same turn that dirtied it is the previous pass's — a plausible rectangle, not zeros. #flush_layout brings it up to date (D_deferred_layout), #rect_stale? answers whether it is one, and Tuile.strict_layout makes every such read say so.

Returns:



92
93
94
# File 'lib/tuile/component.rb', line 92

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.

Parameters:

  • names (::Array[(Symbol | ::Array[Symbol])])

Returns:

  • (Symbol, ::Array[Symbol])


4660
# File 'sig/tuile.rbs', line 4660

def self.final: (*::Array[(Symbol | ::Array[Symbol])] names) -> (Symbol | ::Array[Symbol])

.final_methods ⇒ ::Array[Symbol]

@return — the methods marked Final#final on this class.

Returns:

  • (::Array[Symbol])


4663
# File 'sig/tuile.rbs', line 4663

def self.final_methods: () -> ::Array[Symbol]

.listener ⇒ void

This method returns an undefined value.

Defines the slot's reader, which returns the Listeners — or, given a block, registers it and returns the Proc.

An optional block is the slot's transition block, run on the owner whenever the list becomes non-empty or empty again. It is what lets a widget install a bridge only while somebody is listening:

listener :on_enter do |claimed|
claimed ? editor.on_enter << @bridge : editor.on_enter.remove(@bridge)
end

The list is built on first read, so no mixin has to remember a constructor line — which means in-class code goes through the reader and never touches @on_foo, nil until somebody asks.

@param name — the slot's full name, on_-prefixed. Spelling it out is what keeps on_value_change greppable from its declaration.

@return — name.

Parameters:

  • name (Symbol)


4690
# File 'sig/tuile.rbs', line 4690

def self.listener: (Symbol name) ?{ (bool claimed) -> void } -> 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.

Parameters:

  • klass (Class)


4670
# File 'sig/tuile.rbs', line 4670

def self.verify_final!: (Class klass) -> void

Instance Method Details

#absolute_extent_rect ⇒ Rect

#local_extent_rect in screen coordinates, #absolute_rect's counterpart — what a dropdown anchors against.

Returns:



187
# File 'lib/tuile/component.rb', line 187

def absolute_extent_rect = local_extent_rect.at(to_screen(Point::ZERO))

#absolute_rect ⇒ Rect

#rect in screen coordinates — every ancestor's offset summed in. The form the three consumers outside this component's own frame need: the Tuile::Canvas#origin Screen#canvas_for builds, an overlay's anchor (an overlay hangs off ScreenPane, so it shares no offset with its driver), and a spec clicking a component by where it sits.

Derived on every call and never cached — a parent may move this subtree between two reads, and nothing announces it (D_relative_rect).

Returns:



182
# File 'lib/tuile/component.rb', line 182

def absolute_rect = rect.at(to_screen(Point::ZERO))

#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.

Parameters:

  • active (Boolean)


649
650
651
652
653
654
655
# File 'lib/tuile/component.rb', line 649

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=.

Returns:

  • (Boolean)


644
# File 'lib/tuile/component.rb', line 644

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.

Parameters:

  • child (Component)
  • at: (Integer, nil) (defaults to: nil)


880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
# File 'lib/tuile/component.rb', line 880

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?

  # An overlay's placement, `open?`, `visible=` and outside-click dismissal
  # all come from the pane's popup list, so anywhere else it would be
  # unplaceable and undismissable. Membership, not the pane's identity,
  # which would let the `content` slot through; checked before the push, so
  # a refusal leaves the tree untouched.
  if child.is_a?(Component::Overlay) && !(is_a?(ScreenPane) && has_popup?(child))
    raise Tuile::Error, "#{child.class} belongs on the popup stack — open it (#{child.class}#open) " \
                        "rather than adding it to #{self.class}"
  end
  at.nil? ? @children.push(child) : @children.insert(at, child)
  child.parent = self
  invalidate_layout
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::Vertical.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.

Returns:

  • (Boolean)


788
# File 'lib/tuile/component.rb', line 788

def attached? = root.is_a?(ScreenPane)

#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 widget's own well and then the parent answer.

Returns:



442
# File 'lib/tuile/component.rb', line 442

def bg_color = @bg.color

#bg_color=(color) ⇒ void

This method returns an undefined value.

Tints this component and every descendant that doesn't set its own background — set it once on a container / Popup to tint a whole subtree. Invalidates the subtree so it repaints.

A Theme::Ref is re-resolved against the theme each paint, so it tracks light/dark flips with no #handle_theme_changed hook; a Tuile::Color is fixed:

panel.bg_color = Theme.ref(:panel_bg)   # theme-tracked
panel.bg_color = Color::GREY27          # fixed

A Hash keyed by Tuile::ComponentBackground::STATES gives a color per state — the shape a widget that highlights itself on focus needs, and the reason setting a flat color on one is a choice rather than a trap:

field.bg_color = grey                            # flat: focused or not
field.bg_color = { normal: grey, active: blue }  # the pair
field.bg_color = { active: blue }                # keep the widget's own
                                               # well, override focus

A state whose key is absent is not answered here at all: resolution falls through to the widget's own well and then to the parent, exactly as nil does. That is what makes the third line above mean what it reads as.

This does not win over a validation error: #error_bg_color resolves first, so tinting a panel cannot switch off the error well on the fields inside it. Tuile::ComponentBackground carries the whole chain.

@param color — a Theme::Ref, Tuile::ComponentBackground::INHERIT, a state Hash, else a color coerced via Tuile::Color.coerce; nil unsets.

Parameters:

  • color (Color, Theme::Ref, ::Hash[untyped, untyped], Symbol, Integer, ::Array[Integer], nil)


479
480
481
# File 'lib/tuile/component.rb', line 479

def bg_color=(color)
  @bg.color = color
end

#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 of a tiled layout. 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.

Returns:

  • (Boolean)


1217
1218
1219
1220
# File 'lib/tuile/component.rb', line 1217

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_inside_extent(canvas) ⇒ 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.

@param canvas — the paint context, at this component's own background.

Parameters:



1259
1260
1261
# File 'lib/tuile/component.rb', line 1259

def clear_inside_extent(canvas)
  canvas.with(bg_color: bg.ambient) { _1.fill(local_extent_rect) }
end

#clear_outside_extent(canvas) ⇒ 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.

@param canvas — the paint context, at this component's own background.

Parameters:



1230
1231
1232
1233
1234
1235
1236
1237
1238
1239
1240
1241
1242
# File 'lib/tuile/component.rb', line 1230

def clear_outside_extent(canvas)
  e = extent
  return canvas.fill(local_rect) if e.nil? # nothing declared: all of it is fair game

  right = Rect.new(e.width, 0, rect.width - e.width, e.height)
  below = Rect.new(0, 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.
  canvas.with(bg_color: bg.ambient) do |ambient|
    ambient.fill(right) unless right.empty?
    ambient.fill(below) unless below.empty?
  end
end

#cursor_position ⇒ Point?

Where the hardware terminal cursor should sit when this component is the cursor owner, in this component's own coordinates — the ones it paints in, so a caret is Point.new(column, row) with no position added. Screen#cursor_position converts it. Returns nil to hide the cursor.

The Screen positions the hardware cursor after each repaint cycle by consulting the Screen#focused component only.

@return — in this component's coordinates, or nil to hide.

Returns:



831
# File 'lib/tuile/component.rb', line 831

def cursor_position = nil

#depth ⇒ Integer

@return — the distance from the root component; 0 if #parent is nil.

Returns:

  • (Integer)


686
# File 'lib/tuile/component.rb', line 686

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

Parameters:



926
927
928
929
930
931
932
# File 'lib/tuile/component.rb', line 926

def detach_child(child)
  raise ArgumentError, "#{child} is not a child of #{self}" unless @children.include?(child)

  @children.delete(child)
  child.parent = nil
  invalidate_layout
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 the chain rather than under it, unlike Tuile::ComponentBackground#default_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.

A hook rather than a Tuile::ComponentBackground setter because it follows the validation state, and a pulled answer cannot go stale. Read the theme here rather than in an ivar, and hand back one Tuile::Color: this runs first on every paint.

Returns:



1287
# File 'lib/tuile/component.rb', line 1287

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 #local_extent_rect or #absolute_extent_rect where coordinates are wanted — there is no parent-space form, because nothing asks the question in that space.

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 #local_extent_rect so a click on that tail doesn't activate the widget, and a dropdown anchors under #absolute_extent_rect 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).

Returns:



144
# File 'lib/tuile/component.rb', line 144

def extent = nil

#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 own parent=;
  • 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 own parent= saw no transition and stayed silent — a parentage check would skip it too and it would never hear handle_detached at all. The reverse case (pulled out during an attach walk) gets handle_detached from its own parent= and no handle_attached, which is why the hooks are required to be idempotent: an unpaired detach releases nothing, whereas firing handle_attached at a component that is no longer attached would start a ticker nothing stops.

@param attached — true to fire #handle_attached, false for #handle_detached.

Parameters:

  • attached (Boolean)


1007
1008
1009
1010
1011
1012
1013
1014
1015
# File 'lib/tuile/component.rb', line 1007

def fire_lifecycle(attached)
  kids = children.dup
  attached ? handle_attached : handle_detached
  # A mark taken while detached had no screen to go to; hand it over now
  # that there is one (the `attached?` re-check covers a hook that detached
  # us again). Flutter's `RenderObject#attach` does the same.
  screen.invalidate_layout(self) if attached && @layout_dirty && attached?
  kids.each { _1.fire_lifecycle(attached) if _1.attached? == attached }
end

#flush_layout ⇒ void

This method returns an undefined value.

Runs every #relayout this component's tree owes, so its rects are current — the force-now that makes a detached tree measurable:

layout = Component::Layout::Vertical.new   # no Screen in the process
layout.add(label, Fixed[1])
layout.rect = Rect.new(0, 0, 20, 10)
layout.flush_layout
label.rect                                 # => Rect(0, 0, 20, 1)

Attached, Screen#dispatch already flushes after every event, so ask for this by hand only to read a rect in the same turn that dirtied it — what Swing spells validate() and Tk update idletasks.

Overwhelmingly that means a spec: an app mutates and lets the settle run, so a flush_layout in app code is usually a sign the read wants deferring instead.

Implementation details

The whole tree, never this subtree, whichever end it is asked from: a pending ancestor pass would overwrite whatever a narrower one wrote. Attached that is Screen#flush_layout; detached it is LayoutPass.drain from #root, the same fixpoint the screen runs.



308
309
310
311
312
313
# File 'lib/tuile/component.rb', line 308

def flush_layout
  return screen.flush_layout if attached?

  LayoutPass.refuse_nested
  LayoutPass.drain(root)
end

#focus ⇒ void

This method returns an undefined value.

Focuses this component. Equivalent to screen.focused = self.



395
396
397
# File 'lib/tuile/component.rb', line 395

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.

Returns:

  • (Boolean)


665
# File 'lib/tuile/component.rb', line 665

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.



958
# File 'lib/tuile/component.rb', line 958

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.value = 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).



1064
# File 'lib/tuile/component.rb', line 1064

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.

Parameters:



807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
# File 'lib/tuile/component.rb', line 807

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_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.



966
# File 'lib/tuile/component.rb', line 966

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.



743
# File 'lib/tuile/component.rb', line 743

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.

Parameters:

  • _key (String)

Returns:

  • (Boolean)


547
548
549
# File 'lib/tuile/component.rb', line 547

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 any #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.



1102
1103
1104
# File 'lib/tuile/component.rb', line 1102

def handle_locale_changed
  on_locale_changed.fire(LocaleChangedEvent.new(source: self))
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.fire(ClickEvent.new(source: self))
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.

Parameters:

Returns:

  • (Boolean)


593
# File 'lib/tuile/component.rb', line 593

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.

Parameters:



624
# File 'lib/tuile/component.rb', line 624

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.



635
# File 'lib/tuile/component.rb', line 635

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.



640
# File 'lib/tuile/component.rb', line 640

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.

Parameters:

Returns:

  • (Boolean)


609
# File 'lib/tuile/component.rb', line 609

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.

Parameters:

Returns:

  • (Boolean)


601
# File 'lib/tuile/component.rb', line 601

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

Parameters:



617
# File 'lib/tuile/component.rb', line 617

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.

Parameters:

  • _text (String)


570
# File 'lib/tuile/component.rb', line 570

def handle_paste(_text); end

#handle_rect_changed(_old_rect) ⇒ void

This method returns an undefined value.

Called once the parent has given this component a different rect, before anything repaints. Does nothing by default.

The edge: for a reaction to the change itself, one that needs the old rect or must not repeat — closing a menu, escalating a repaint. State that merely follows from the size — a wrap, a scroll clamp — is re-derived in #relayout instead, which runs after every rect change on either axis and after every other mark too.

@param _old_rect — the rect it had.

Parameters:



1027
# File 'lib/tuile/component.rb', line 1027

def handle_rect_changed(_old_rect); 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 any #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.



1083
1084
1085
# File 'lib/tuile/component.rb', line 1083

def handle_theme_changed
  on_theme_changed.fire(ThemeChangedEvent.new(source: self))
end

#height ⇒ Integer

@return — rect.height.

Returns:

  • (Integer)


107
# File 'lib/tuile/component.rb', line 107

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.

Returns:

  • (String)


841
842
843
844
845
846
# File 'lib/tuile/component.rb', line 841

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.

Returns:

  • (::Array[String])


863
# File 'lib/tuile/component.rb', line 863

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.



1123
1124
1125
1126
1127
# File 'lib/tuile/component.rb', line 1123

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(canvas)
return if rect.empty?

invalidate_children      # never optional
paint_my_own_chrome(canvas)
end


1302
1303
1304
# File 'lib/tuile/component.rb', line 1302

def invalidate_children
  children.each { |c| screen.invalidate(c) }
end

#invalidate_layout ⇒ void

This method returns an undefined value.

Marks this container as owing a #relayout: its children's rects are out of date, and the next #flush_layout will bring them up to date.

def spacing=(cells)
@spacing = cells
invalidate_layout      # every input to the arithmetic ends here
end

The framework marks after rect=, after the three tree mutators and after a child's #visible= flips; a container marks for every other input to its own arithmetic.

The mark never runs the pass — not even detached, where there is no settle to defer to and #flush_layout has to be asked for. That is what lets a constructor add_child before its ivars are written, and a container mutate its own bookkeeping in whatever order reads best: no relayout ever observes a container mid-configuration.

Unlike #invalidate, a detached mark is remembered rather than dropped: attaching hands it to the Screen, so a tree assembled with no screen lays out as soon as it is mounted.

A mark made during this container's own #relayout, before it places its first child, is dropped — the pass that would answer it is the one running. So hide or add a child, or set your own spacing, first:

def relayout
@sidebar.visible = width >= 60   # before `super`: one pass
super
end

Once a child is placed, the division may already be stale, so a later mark is kept and costs a second pass.



1163
1164
1165
1166
1167
1168
# File 'lib/tuile/component.rb', line 1163

def invalidate_layout
  return if LayoutPass.before_first_placement?(self)

  @layout_dirty = true
  screen.invalidate_layout(self) if attached?
end

#layout_dirty? ⇒ Boolean

Whether this container owes a #relayout — that its children's rects are out of date.

It says nothing about this component's own #rect, which no pass of its own touches: the flag that makes this rect stale sits on the parent that assigns it, and #rect_stale? is that question asked from here. Survives detaching, so #handle_attached can hand the mark to the Screen.

Returns:

  • (Boolean)


323
# File 'lib/tuile/component.rb', line 323

def layout_dirty? = @layout_dirty

#local_extent_rect ⇒ Rect

#extent at (0, 0), #local_rect's counterpart — what #clear_inside_extent blanks and what Mouse::Router hit-tests. Total: an undeclared #extent yields #local_rect, so a generic caller never sees nil.

Returns:



168
169
170
171
# File 'lib/tuile/component.rb', line 168

def local_extent_rect
  e = extent
  e.nil? ? local_rect : Rect.new(0, 0, e.width, e.height)
end

#local_rect ⇒ Rect

#rect with the position taken out: the same size at (0, 0).

Two things live in these coordinates, and that is the whole point of them being one space: what this component paints (Screen#canvas_for puts the canvas here), and what its children's #rects are measured in. So a container divides local_rect among its children and blanks its own gaps in the very same numbers:

private def relayout
half = width / 2                       # no `rect.left +` anywhere:
left.rect  = Rect.new(0, 0, half, height)
right.rect = Rect.new(half, 0, width - half, height)
end

Returns:



161
# File 'lib/tuile/component.rb', line 161

def local_rect = Rect.new(0, 0, rect.width, rect.height)

#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.

Returns:



1112
# File 'lib/tuile/component.rb', line 1112

def locale = Screen.instance? ? screen.locale : Locale::ISO

#on_locale_changed ⇒ Listeners

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) }

Returns:



775
# File 'lib/tuile/component.rb', line 775

listener :on_locale_changed

#on_theme_changed ⇒ Listeners

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 }

Returns:



765
# File 'lib/tuile/component.rb', line 765

listener :on_theme_changed

#perform_relayout ⇒ void

This method returns an undefined value.

Clears the mark and runs #relayout — the sole invocation site of relayout, reached only from LayoutPass.drain.



1311
1312
1313
1314
# File 'lib/tuile/component.rb', line 1311

def perform_relayout
  @layout_dirty = false
  LayoutPass.run(self) { relayout }
end

#rect_stale? ⇒ Boolean

Whether #rect is the previous pass's rectangle, because some ancestor owes a #relayout and that pass reassigns every rect below it — or is running it right now and has not reached this branch yet.

holder.constrain(pane, Rect.new(0, 0, 100, 26))
pane.left.rect_stale?   # => true — `pane.left.rect` is still the old half

#flush_layout makes it false; Tuile.strict_layout reports every read taken while it is true. One walk to #root per call, which is why no framework read consults it outside strict mode.

Returns:

  • (Boolean)


336
# File 'lib/tuile/component.rb', line 336

def rect_stale? = !stale_layout_ancestor.nil?

#relayout ⇒ void

This method returns an undefined value.

Assigns every child's rect, and is the only place a container may.

private def relayout
half = width / 2                       # `local_rect`, so no rect.left:
@left.rect  = Rect.new(0, 0, half, height)
@right.rect = Rect.new(half, 0, width - half, height)
end

relayout : geometry :: #repaint : ink. Invoked by the framework, never called directly; derives every rect from current state, so it is idempotent and safe to run twice. It assigns every child on every pass, including when #rect is empty — a return if rect.empty? guard strands children at stale coordinates that the next full repaint paints them at (D_empty_ancestor).

It is also where a component re-derives what depends on its own size — a wrap, a padded row cache, a scroll offset clamped to the viewport — because #rect= marks the component itself, so this runs after every rect change, width or height. It runs after every other mark as well, so a costly derivation keys itself on the size it was built at — TextView's:

def relayout
rewrap unless @wrapped_at == wrap_width   # not on every append
update_scroll_top_row_if_auto_scroll      # a taller viewport moves the bottom
@scrollbar.rect = …
end

There is no width-changed hook; #handle_rect_changed is the edge, for a reaction to the change itself.

Reached through __send__, so an override may be protected or private (D_hook_visibility). A leaf inherits the empty body and costs nothing.



1204
# File 'lib/tuile/component.rb', line 1204

def relayout; end

#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

Parameters:



905
906
907
908
# File 'lib/tuile/component.rb', line 905

def remove_child(child)
  detach_child(child)
  handle_child_removed(child)
end

#repaint(canvas) ⇒ 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 background clear 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.

Paint onto canvas, never onto Screen#canvas by name. It arrives already loaded with this component's Tuile::ComponentBackground#effective, so every write through it inherits; reach for the screen's own and inheritance silently stops (D_canvas).

The canvas paints in this component's own coordinates: (0, 0) is #rect's top-left, so rect.left has no place in a repaint — adding it lands the write at twice the offset, with nothing raising. #local_rect is the region argument to reach for; Tuile::Canvas carries the two spaces.

@param canvas — the paint context, from Screen#canvas_for. Required: a canvas carries state, so there is no default worth inventing.

Parameters:



527
528
529
530
531
532
533
534
535
# File 'lib/tuile/component.rb', line 527

def repaint(canvas)
  return if rect.empty?

  unless children.any? && children_tile_rect?
    clear_outside_extent(canvas)
    clear_inside_extent(canvas) 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. Tuile::Component::List#interactive= reuses it too, for a component that stays shown but stops taking focus.

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.



1346
1347
1348
1349
1350
1351
1352
# File 'lib/tuile/component.rb', line 1346

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

#root ⇒ Component

@return — the root component of this component hierarchy.

Returns:



689
# File 'lib/tuile/component.rb', line 689

def root = parent.nil? ? self : parent.root

#screen ⇒ Screen

@return — the screen which owns this component.

Returns:



391
# File 'lib/tuile/component.rb', line 391

def screen = Screen.instance

#scroll_to_visible(rect = local_extent_rect) ⇒ void

This method returns an undefined value.

Asks whatever scrolls above to bring rect into view — "show me this", made by the component that wants to be seen, never polled for by a container:

row.scroll_to_visible                      # all of me
editor.scroll_to_visible(caret_row_rect)   # this much of me

The request climbs the parent chain re-expressed a level at a time, so with nothing scrolling on the way it reaches the root and does nothing. Screen#focused= makes the no-argument call on every focus assignment, which is the whole of making Tab follow the view: a child scrolled out of sight keeps its rect, its keys and its tab stop (D_empty_ancestor).

A container that scrolls overrides it, and the shape is the contract:

def scroll_to_visible(rect = local_extent_rect)
delta = ...                     # the minimum that makes rect visible
move_scroll_top_row_by(delta)
super(rect.moved_by(Point.new(0, -delta)))
end

The minimum distance, so the far edge of the viewport is the one allowed to cut a child in half. And super takes the rect where the scroll left it, so an outer scroller is asked about cells that exist and nested scrollers settle inner-first.

A hidden component raises, checked a level at a time as the request climbs — so it fails late: a scroller below a hidden ancestor has already scrolled.

@param rect — in this component's coordinates; defaults to #local_extent_rect, so a widget asks for what it paints.

Parameters:

  • rect (Rect) (defaults to: local_extent_rect)


432
433
434
435
436
# File 'lib/tuile/component.rb', line 432

def scroll_to_visible(rect = local_extent_rect)
  raise Tuile::Error, "#{self} is hidden; it cannot be scrolled into view" unless visible?

  parent&.scroll_to_visible(rect.moved_by(self.rect.top_left))
end

#size ⇒ Size

@return — rect.size.

Returns:



101
# File 'lib/tuile/component.rb', line 101

def size = rect.size

#stale_layout_ancestor ⇒ Component?

The nearest ancestor whose pending or running #relayout would rewrite this component's #rect, or nil when the rect is current — #rect_stale?'s answer, with the culprit kept for StrictLayout's message.

Two ways to owe it: a marked ancestor, and one whose pass is running and has not yet placed the child on the way down — a hook fired mid-pass, a focus repair from a child hidden there, reading a sibling. Strictly ancestors, never self: a container's own flag means its children are stale, and #perform_relayout clears the flag before the body runs, so a relayout reading its own width is asking a settled question.

Returns:



1328
1329
1330
1331
1332
1333
1334
1335
1336
# File 'lib/tuile/component.rb', line 1328

def stale_layout_ancestor
  node = self
  until node.parent.nil?
    return node.parent if node.parent.layout_dirty? || LayoutPass.unplaced?(node)

    node = node.parent
  end
  nil
end

#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.

Returns:

  • (Boolean)


673
# File 'lib/tuile/component.rb', line 673

def tab_stop? = false

#to_local(point) ⇒ Point

#to_screen's inverse: a screen point in this component's own coordinates. The result may be negative or past #size — a point outside the component converts perfectly well, which is what a grabbed component's Mouse::DragEvent relies on.

@param point — in screen coordinates.

@return — in this component's coordinates.

Parameters:

Returns:



219
220
221
222
223
224
225
226
227
228
229
# File 'lib/tuile/component.rb', line 219

def to_local(point)
  x = point.x
  y = point.y
  node = self
  until node.nil?
    x -= node.rect.left
    y -= node.rect.top
    node = node.parent
  end
  Point.new(x, y)
end

#to_screen(point) ⇒ Point

Converts a point in this component's own coordinates — the ones it paints in, the ones a Mouse::Event reaches it in — to screen coordinates, by walking up and adding each ancestor's offset.

# a strip anchoring a panel under one of its own segments
Rect.new(0, 0, width, 1).at(to_screen(Point.new(column, 0)))

Iterative and summing into two locals rather than recursing through a Point per level: this runs once per component per repaint, from Screen#canvas_for.

@param point — in this component's coordinates.

@return — in screen coordinates.

Parameters:

Returns:



201
202
203
204
205
206
207
208
209
210
211
# File 'lib/tuile/component.rb', line 201

def to_screen(point)
  x = point.x
  y = point.y
  node = self
  until node.nil?
    x += node.rect.left
    y += node.rect.top
    node = node.parent
  end
  Point.new(x, y)
end

#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). The parent's own #relayout may flip it — before placing any child, or it runs twice (#invalidate_layout).

@param value

Parameters:

  • value (Boolean)


374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
# File 'lib/tuile/component.rb', line 374

def visible=(value)
  value = value ? true : false
  return if @visible == value

  screen.check_locked if attached?
  @visible = value
  # Both on the parent, because the child just vacated (or re-claimed) cells
  # the parent owns *and* may have changed how it divides its space. A
  # hidden component paints nothing itself, so nothing else would blank what
  # it left behind.
  parent&.invalidate
  parent&.invalidate_layout
  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.

Returns:

  • (Boolean)


342
# File 'lib/tuile/component.rb', line 342

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).



728
729
730
731
732
733
# File 'lib/tuile/component.rb', line 728

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.



707
708
709
710
# File 'lib/tuile/component.rb', line 707

def walk_tree(&block)
  block.call(self)
  children.each { _1.walk_tree(&block) }
end

#width ⇒ Integer

@return — rect.width.

Returns:

  • (Integer)


104
# File 'lib/tuile/component.rb', line 104

def width = rect.width