Class: Tuile::Component
- Inherits:
-
Object
- Object
- Tuile::Component
- Defined in:
- lib/tuile/component.rb,
lib/tuile/component/list.rb,
lib/tuile/component/label.rb,
lib/tuile/component/popup.rb,
lib/tuile/component/button.rb,
lib/tuile/component/layout.rb,
lib/tuile/component/window.rb,
lib/tuile/component/text_area.rb,
lib/tuile/component/text_view.rb,
lib/tuile/component/log_window.rb,
lib/tuile/component/text_field.rb,
lib/tuile/component/text_input.rb,
lib/tuile/component/has_content.rb,
lib/tuile/component/info_window.rb,
lib/tuile/component/picker_window.rb,
sig/tuile.rbs
Overview
A UI component which is positioned on the screen and draws characters into its bounding rectangle (in #repaint).
Painting is gated by attachment: a detached component (one whose #root isn't Screen#pane) is never enqueued for repaint via #invalidate, and any stale invalidation entries are filtered out at drain time. Subclasses can paint freely in #repaint without re-asserting attachment.
Direct Known Subclasses
Defined Under Namespace
Modules: HasContent Classes: Button, InfoWindow, Label, Layout, List, LogWindow, PickerWindow, Popup, TextArea, TextField, TextInput, TextView, Window
Instance Attribute Summary collapse
-
#key_shortcut ⇒ String?
A global keyboard shortcut.
-
#on_theme_changed ⇒ Proc?
Called on every attached component (pre-order, popups included) when Screen#theme changes — at Screen#theme= / Screen#theme_def= assignment and on OS appearance flips.
-
#parent ⇒ Component?
@return — the parent component or nil if the component has no parent.
-
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
Instance Method Summary collapse
-
#active=(active) ⇒ void
@param
active— true if active. -
#active? ⇒ Boolean
@return — true if the component is on the active chain — i.e.
-
#attached? ⇒ Boolean
@return — true if this component's tree is currently mounted on the Screen, i.e.
-
#children ⇒ ::Array[Component]
List of child components, defaults to an empty array.
-
#children_tile_rect? ⇒ Boolean
Whether direct children fully tile #rect.
-
#clear_background ⇒ void
Clears the background: prints spaces into all characters occupied by the component's rect.
-
#cursor_position ⇒ Point?
Where the hardware terminal cursor should sit when this component is the cursor owner.
-
#depth ⇒ Integer
@return — the distance from the root component; 0 if #parent is nil.
-
#find_shortcut_component(key) ⇒ Component?
@param
key— keyboard key to look up. -
#focus ⇒ void
Focuses this component.
-
#focusable? ⇒ Boolean
Whether this component is a valid focus target.
-
#handle_key(_key) ⇒ Boolean
Called when a character is pressed on the keyboard.
-
#handle_mouse(event) ⇒ void
Handles mouse event.
-
#initialize ⇒ Component
constructor
A new instance of Component.
- #invalidate ⇒ void
-
#keyboard_hint ⇒ String
@return — formatted keyboard hint surfaced in the status bar by Screen when this component is the active tiled window or the topmost popup.
-
#on_child_removed(child) ⇒ void
Called by container components after
childhas been detached fromself.children(itsparentis already nil and it is no longer in the children list). -
#on_focus ⇒ void
Called when the component receives focus.
-
#on_tree(&block) ⇒ void
Calls block for this component and for every descendant component.
-
#on_width_changed ⇒ void
Called whenever the component width changes.
-
#repaint ⇒ void
Repaints the component.
-
#root ⇒ Component
@return — the root component of this component hierarchy.
-
#screen ⇒ Screen
@return — the screen which owns this component.
-
#tab_stop? ⇒ Boolean
Whether this component participates in Tab / Shift+Tab focus cycling.
Constructor Details
Instance Attribute Details
#key_shortcut ⇒ String?
A global keyboard shortcut. When pressed, will focus this component.
@return — shortcut, nil by default.
103 104 105 |
# File 'lib/tuile/component.rb', line 103 def key_shortcut @key_shortcut end |
#on_theme_changed ⇒ Proc?
Called on every attached component (pre-order, popups included) when Screen#theme changes — at Screen#theme= / Screen#theme_def= assignment and on OS appearance flips.
Built-in components read Screen#theme at paint time, so their accents
restyle automatically; this hook exists for content whose colors the
app baked in from the old theme — a Tuile::Component::Label#text / Tuile::Component::List#lines /
Tuile::Component::TextView#text StyledString styled with theme[:accent] and the
like. Only the app knows which of its colors were theme-derived (as
opposed to inherent to the data, e.g. log-level colors), so it rebuilds
them here, re-running the same code that rendered them initially.
Runs on the UI thread; Screen#theme already returns the new theme.
Mutating content (text=, lines=, …) is safe — repaint coalesces per
event-loop tick. Do not assign Screen#theme= from inside the hook.
Subclasses overriding this should call super so an assigned
#on_theme_changed= listener keeps firing.
222 223 224 |
# File 'lib/tuile/component.rb', line 222 def on_theme_changed @on_theme_changed&.call end |
#parent ⇒ Component?
@return — the parent component or nil if the component has no parent.
166 167 168 |
# File 'lib/tuile/component.rb', line 166 def parent @parent end |
#rect ⇒ Rect
@return — the rectangle the component occupies on screen.
19 20 21 |
# File 'lib/tuile/component.rb', line 19 def rect @rect end |
Instance Method Details
#active=(active) ⇒ void
This method returns an undefined value.
@param active — true if active. Set by Screen#focused= as it marks the focus chain (root → focused); not meant to be called directly.
133 134 135 136 137 138 139 |
# File 'lib/tuile/component.rb', line 133 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=.
128 |
# File 'lib/tuile/component.rb', line 128 def active? = @active |
#attached? ⇒ Boolean
@return — true if this component's tree is currently mounted on the Screen, i.e. its root is the ScreenPane.
228 |
# File 'lib/tuile/component.rb', line 228 def attached? = root == screen.pane |
#children ⇒ ::Array[Component]
List of child components, defaults to an empty array.
@return — child components. Must not be mutated! May be empty.
178 |
# File 'lib/tuile/component.rb', line 178 def children = [] |
#children_tile_rect? ⇒ Boolean
Whether direct children fully tile #rect. Used by the default #repaint to decide whether the framework needs to wipe gaps.
Approximated by area: sum of (non-empty) child areas vs the parent's area. Cheap, and correct as long as siblings don't overlap each other — which Tuile already requires (no clipping in the tiled tree). Children with empty rects contribute zero, since they paint nothing.
298 299 300 301 |
# File 'lib/tuile/component.rb', line 298 def children_tile_rect? total = children.sum { |c| c.rect.empty? ? 0 : c.rect.width * c.rect.height } total >= rect.width * rect.height end |
#clear_background ⇒ void
This method returns an undefined value.
Clears the background: prints spaces into all characters occupied by the component's rect.
306 307 308 |
# File 'lib/tuile/component.rb', line 306 def clear_background screen.buffer.fill(rect) end |
#cursor_position ⇒ Point?
Where the hardware terminal cursor should sit when this component is the
cursor owner. Returns nil to indicate the cursor should be hidden. The
Screen positions the hardware cursor after each repaint cycle by
consulting the Screen#focused component only.
@return — absolute screen coordinates, or nil to hide.
259 |
# File 'lib/tuile/component.rb', line 259 def cursor_position = nil |
#depth ⇒ Integer
@return — the distance from the root component; 0 if #parent is nil.
170 |
# File 'lib/tuile/component.rb', line 170 def depth = parent.nil? ? 0 : parent.depth + 1 |
#find_shortcut_component(key) ⇒ Component?
@param key — keyboard key to look up.
@return — the component whose #key_shortcut matches key,
or nil.
108 109 110 111 112 113 114 115 116 |
# File 'lib/tuile/component.rb', line 108 def find_shortcut_component(key) return self if key_shortcut == key children.each do |child| sc = child.find_shortcut_component(key) return sc unless sc.nil? end nil end |
#focus ⇒ void
This method returns an undefined value.
Focuses this component. Equivalent to screen.focused = self.
46 47 48 |
# File 'lib/tuile/component.rb', line 46 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 (#handle_mouse) and the focus-cascade
in container components (Tuile::Component::HasContent#on_focus, Tuile::Component::Layout#on_focus).
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.
See also #tab_stop?: focusable controls can receive focus (via click or programmatic assignment), but only tab stops participate in Tab / Shift+Tab cycling. Containers like Window and Popup are focusable (so a click on chrome lands focus) but are not tab stops.
@return — true if this component can be focused.
154 |
# File 'lib/tuile/component.rb', line 154 def focusable? = false |
#handle_key(_key) ⇒ Boolean
Called when a character is pressed on the keyboard. The default does nothing and reports the key as unhandled; input components (TextField, List, Button, …) override it to act on keys they care about.
Dispatch is owned by ScreenPane#handle_key: a #key_shortcut match anywhere in the active scope is captured first (suppressed while a cursor-owner is mid-edit), then the key is delivered to Screen#focused and bubbles up its ancestor chain until some component handles it. A component therefore only ever receives keys when it is on the focus chain — or when app code hands it a key directly — so it acts on the key alone and must never gate on its own #active? state.
@param _key — a key.
@return — true if the key was handled, false if not.
97 98 99 |
# File 'lib/tuile/component.rb', line 97 def handle_key(_key) false end |
#handle_mouse(event) ⇒ void
This method returns an undefined value.
Handles mouse event. Default implementation focuses this component when clicked (if #focusable?).
@param event
122 123 124 |
# File 'lib/tuile/component.rb', line 122 def handle_mouse(event) screen.focused = self unless event. != :left || active? || !focusable? end |
#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.
284 285 286 287 288 |
# File 'lib/tuile/component.rb', line 284 def invalidate return unless attached? screen.invalidate(self) end |
#keyboard_hint ⇒ String
@return — formatted keyboard hint surfaced in the status bar by Screen when this component is the active tiled window or the topmost popup. Empty by default; override to advertise shortcuts.
264 |
# File 'lib/tuile/component.rb', line 264 def keyboard_hint = "" |
#on_child_removed(child) ⇒ void
This method returns an undefined value.
Called by container components after child has been detached from
self.children (its parent is already nil and it is no longer in the
children list). Default behavior repairs dangling focus: if the focused
component lived inside the removed subtree, focus shifts to self so the
cursor doesn't dangle on a detached component. No-op if self is not
attached to the screen — focus state in a detached subtree is moot.
@param child — the just-detached child.
238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 |
# File 'lib/tuile/component.rb', line 238 def on_child_removed(child) return unless attached? f = screen.focused return if f.nil? cursor = f until cursor.nil? if cursor == child screen.focused = self return end cursor = cursor.parent end end |
#on_focus ⇒ void
This method returns an undefined value.
Called when the component receives focus.
192 |
# File 'lib/tuile/component.rb', line 192 def on_focus; end |
#on_tree(&block) ⇒ void
This method returns an undefined value.
Calls block for this component and for every descendant component.
185 186 187 188 |
# File 'lib/tuile/component.rb', line 185 def on_tree(&block) block.call(self) children.each { _1.on_tree(&block) } end |
#on_width_changed ⇒ void
This method returns an undefined value.
Called whenever the component width changes. Does nothing by default.
273 |
# File 'lib/tuile/component.rb', line 273 def on_width_changed; end |
#repaint ⇒ void
This method returns an undefined value.
Repaints the component.
The default does the bookkeeping that almost every component would otherwise have to remember: it clears the background and re-invalidates any direct children whose rects leave gaps in #rect. Concretely:
- Leaf (no children): always clears, so subclasses can paint their
content directly without an explicit
clear_backgroundcall. - Container with children that fully tile #rect: skipped — the children themselves will repaint and cover everything.
- Container with gappy children (e.g. a form layout where widgets don't tile): clears, then invalidates the children so they re-paint on top of the cleared background. This is what makes mixed field/button forms safe without each container learning a custom damage-tracking pass.
Subclasses that paint their entire rect themselves (e.g. Window's border draws over the area the default would clear; List explicitly paints every row) may skip super and take full responsibility for #rect. Everything else should call super.
A component must not draw outside of #rect.
Only called when the component is attached.
75 76 77 78 79 80 81 |
# File 'lib/tuile/component.rb', line 75 def repaint return if rect.empty? return if children.any? && children_tile_rect? clear_background children.each { |c| screen.invalidate(c) } end |
#root ⇒ Component
@return — the root component of this component hierarchy.
173 |
# File 'lib/tuile/component.rb', line 173 def root = parent.nil? ? self : parent.root |
#screen ⇒ Screen
@return — the screen which owns this component.
42 |
# File 'lib/tuile/component.rb', line 42 def screen = Screen.instance |
#tab_stop? ⇒ Boolean
Whether this component participates in Tab / Shift+Tab focus cycling.
false by default. Only true on components that accept direct user
input (e.g. TextField, List, Button). Implies
#focusable? — Screen will skip non-focusable tab stops, but in
practice every override should keep the two consistent.
@return — true if Tab / Shift+Tab should land on this component.
162 |
# File 'lib/tuile/component.rb', line 162 def tab_stop? = false |