Class: Tuile::Component
- Inherits:
-
Object
- Object
- Tuile::Component
- 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
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
-
#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=.
-
#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:.
-
#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
The rectangle the component occupies inside its parent:
(0, 0)is the parent's top-left, not the screen's.
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.
-
.listener ⇒ void
Defines the slot's reader, which returns the Listeners — or, given a block, registers it and returns the
Proc. -
.verify_final! ⇒ void
Raises unless
klassinherits every Final#final_methods entry from this class.
Instance Method Summary collapse
-
#absolute_extent_rect ⇒ Rect
#local_extent_rect in screen coordinates, #absolute_rect's counterpart — what a dropdown anchors against.
-
#absolute_rect ⇒ Rect
#rect in screen coordinates — every ancestor's offset summed in.
-
#active=(active) ⇒ void
@param
active— true if active. -
#active? ⇒ Boolean
@return — true if the component is on the active chain — i.e.
-
#add_child(child, at: nil) ⇒ void
Adopts
child: places it in #children and wires its parent pointer. -
#attached? ⇒ Boolean
Whether this component's tree is mounted on a UI, ScreenPane being the root of every displayed tree.
-
#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 widget's own well and then the parent answer. -
#bg_color=(color) ⇒ void
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.
-
#children_tile_rect? ⇒ Boolean
Whether direct children fully tile #rect.
-
#clear_inside_extent(canvas) ⇒ 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(canvas) ⇒ void
-
#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. -
#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:. -
#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?
-
#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.
-
#flush_layout ⇒ void
Runs every #relayout this component's tree owes, so its rects are current — the force-now that makes a detached tree measurable:.
-
#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_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_rect_changed(_old_rect) ⇒ void
Called once the parent has given this component a different rect, before anything repaints.
-
#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.
-
#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.
-
#invalidate_layout ⇒ void
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.
-
#layout_dirty? ⇒ Boolean
Whether this container owes a #relayout — that its children's rects are out of date.
-
#local_extent_rect ⇒ Rect
#extent at
(0, 0), #local_rect's counterpart — what #clear_inside_extent blanks and what Mouse::Router hit-tests. -
#local_rect ⇒ Rect
#rect with the position taken out: the same size at
(0, 0). -
#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.
-
#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:.
-
#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:.
-
#perform_relayout ⇒ void
Clears the mark and runs #relayout — the sole invocation site of
relayout, reached only from LayoutPass.drain. - #rect_stale? ⇒ Boolean
-
#relayout ⇒ void
Assigns every child's rect, and is the only place a container may.
-
#remove_child(child) ⇒ void
Drops
childand notifies #handle_child_removed. -
#repaint(canvas) ⇒ 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.
-
#root ⇒ Component
@return — the root component of this component hierarchy.
-
#screen ⇒ Screen
@return — the screen which owns this component.
-
#scroll_to_visible(rect = local_extent_rect) ⇒ void
Asks whatever scrolls above to bring
rectinto view — "show me this", made by the component that wants to be seen, never polled for by a container:. -
#size ⇒ Size
@return —
rect.size. -
#stale_layout_ancestor ⇒ Component?
The nearest ancestor whose pending or running #relayout would rewrite this component's #rect, or
nilwhen the rect is current — #rect_stale?'s answer, with the culprit kept for StrictLayout's message. -
#tab_stop? ⇒ Boolean
Whether this component participates in Tab / Shift+Tab focus cycling.
-
#to_local(point) ⇒ Point
#to_screen's inverse: a screen point in this component's own coordinates.
-
#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.
-
#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!
Methods included from Listeners::Declare
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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).
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.
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=.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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).
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 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.
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.
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.
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.
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.
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.
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.
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.
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
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.
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.
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.
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.
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.
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.
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.
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
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.
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) }
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 }
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.
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
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.
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.
689 |
# File 'lib/tuile/component.rb', line 689 def root = parent.nil? ? self : parent.root |
#screen ⇒ Screen
@return — the screen which owns this component.
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.
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.
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.
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.
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.
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.
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
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.
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.
104 |
# File 'lib/tuile/component.rb', line 104 def width = rect.width |