Class: Tuile::Component

Inherits:
Object
  • Object
show all
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

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

Button, Label, Layout, List, Popup, TextInput, TextView, Window, ScreenPane

Defined Under Namespace

Modules: HasContent Classes: Button, InfoWindow, Label, Layout, List, LogWindow, PickerWindow, Popup, TextArea, TextField, TextInput, TextView, Window

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeComponent

Returns a new instance of Component.



12
13
14
15
16
17
# File 'lib/tuile/component.rb', line 12

def initialize
  @rect = Rect.new(0, 0, 0, 0)
  @active = false
  @content_size = Size::ZERO
  @on_theme_changed = nil
end

Instance Attribute Details

#content_sizeSize

The Size big enough to show the entire component contents without scrolling. Plain components have no intrinsic content and report Size::ZERO; content-bearing components (e.g. Label, List, TextView, Window) maintain it eagerly via #content_size= from their mutators, so reads are O(1). Used by callers like Popup to auto-size to whatever content was assigned, regardless of its concrete type, and by Sizing::WRAP_CONTENT slots.

Returns:



263
264
265
# File 'lib/tuile/component.rb', line 263

def content_size
  @content_size
end

#key_shortcutString?

A global keyboard shortcut. When pressed, will focus this component.

Returns:

  • (String, nil)

    shortcut, ‘nil` by default.



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

def key_shortcut
  @key_shortcut
end

#on_theme_changedvoid

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



223
224
225
# File 'lib/tuile/component.rb', line 223

def on_theme_changed
  @on_theme_changed&.call
end

#parentComponent?

Returns the parent component or nil if the component has no parent.

Returns:

  • (Component, nil)

    the parent component or nil if the component has no parent.



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

def parent
  @parent
end

#rectRect

Returns the rectangle the component occupies on screen.

Returns:

  • (Rect)

    the rectangle the component occupies on screen.



20
21
22
# File 'lib/tuile/component.rb', line 20

def rect
  @rect
end

Instance Method Details

#active=(active) ⇒ void

This method returns an undefined value.

Parameters:

  • active (Boolean)

    true if active. Set by Screen#focused= as it marks the focus chain (root → focused); not meant to be called directly.



134
135
136
137
138
139
140
# File 'lib/tuile/component.rb', line 134

def active=(active)
  active = active ? true : false
  return unless @active != active

  @active = active
  invalidate
end

#active?Boolean

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

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



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

def active? = @active

#attached?Boolean

Returns true if this component’s tree is currently mounted on the Screen, i.e. its root is the ScreenPane.

Returns:

  • (Boolean)

    true if this component’s tree is currently mounted on the Screen, i.e. its root is the ScreenPane.



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

def attached? = root == screen.pane

#childrenArray<Component>

List of child components, defaults to an empty array.

Returns:

  • (Array<Component>)

    child components. Must not be mutated! May be empty.



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

def children = []

#cursor_positionPoint?

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.

Returns:

  • (Point, nil)

    absolute screen coordinates, or nil to hide.



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

def cursor_position = nil

#depthInteger

Returns the distance from the root component; 0 if #parent is nil.

Returns:

  • (Integer)

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



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

def depth = parent.nil? ? 0 : parent.depth + 1

#find_shortcut_component(key) ⇒ Component?

Returns the component whose #key_shortcut matches ‘key`, or nil.

Parameters:

  • key (String)

    keyboard key to look up.

Returns:



109
110
111
112
113
114
115
116
117
# File 'lib/tuile/component.rb', line 109

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

#focusvoid

This method returns an undefined value.

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



47
48
49
# File 'lib/tuile/component.rb', line 47

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.

Returns:

  • (Boolean)

    true if this component can be focused.



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

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.

Parameters:

  • _key (String)

    a key.

Returns:

  • (Boolean)

    true if the key was handled, false if not.



98
99
100
# File 'lib/tuile/component.rb', line 98

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

Parameters:



123
124
125
# File 'lib/tuile/component.rb', line 123

def handle_mouse(event)
  screen.focused = self unless event.button != :left || active? || !focusable?
end

#keyboard_hintString

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

Returns:

  • (String)

    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.



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

def keyboard_hint = ""

#on_child_content_size_changed(child) ⇒ void

This method returns an undefined value.

Called by a child component whose #content_size just changed (fired from the child’s #content_size=). Does nothing by default — a plain container is not size-coupled to its children. Containers that derive their own natural size or child layout from a child’s natural size override this (e.g. Window re-lays-out a Sizing::WRAP_CONTENT footer and recomputes its own size from content; Popup re-self-sizes). If the receiver’s own #content_size changes as a consequence, its #content_size= notifies its parent in turn — so the event bubbles exactly as far as geometry keeps changing, and stops where it doesn’t.

Parameters:

  • child (Component)

    the resized direct child.



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

def on_child_content_size_changed(child); end

#on_child_removed(child) ⇒ void

This method returns an undefined value.

Called by container components after ‘child` has been detached from `self.children` (its `parent` is already nil and it is no longer in the children list). Default behavior repairs dangling focus: if the focused component lived inside the removed subtree, focus shifts to `self` so the cursor doesn’t dangle on a detached component. No-op if ‘self` is not attached to the screen — focus state in a detached subtree is moot.

Parameters:

  • child (Component)

    the just-detached child.



239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# File 'lib/tuile/component.rb', line 239

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_focusvoid

This method returns an undefined value.

Called when the component receives focus.



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

def on_focus; end

#on_tree {|component| ... } ⇒ void

This method returns an undefined value.

Calls block for this component and for every descendant component.

Yields:

  • (component)

Yield Parameters:

Yield Returns:

  • (void)


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

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

Advice to a wrapping Popup on the maximum height this component may grow to when shown in a popup. ‘nil` (the default) means no preference — the popup uses its own Tuile::Component::Popup#max_height.

Returns:

  • (Integer, nil)


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

def popup_max_height = nil

Advice to a wrapping Popup on the minimum height this component prefers to occupy when shown in a popup. ‘nil` (the default) means no preference — the popup uses its own Tuile::Component::Popup#min_height. Override in a content component that should not collapse to a couple of rows when sparse (e.g. LogWindow).

Returns:

  • (Integer, nil)


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

def popup_min_height = nil

#repaintvoid

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_background` call.

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



76
77
78
79
80
81
82
# File 'lib/tuile/component.rb', line 76

def repaint
  return if rect.empty?
  return if children.any? && children_tile_rect?

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

#rootComponent

Returns the root component of this component hierarchy.

Returns:

  • (Component)

    the root component of this component hierarchy.



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

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

#screenScreen

Returns the screen which owns this component.

Returns:

  • (Screen)

    the screen which owns this component.



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

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.

Returns:

  • (Boolean)

    true if Tab / Shift+Tab should land on this component.



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

def tab_stop? = false