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,
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

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
# File 'lib/tuile/component.rb', line 12

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

Instance Attribute Details

#key_shortcutString?

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

@return — shortcut, nil by default.

Returns:

  • (String, nil)


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

def key_shortcut
  @key_shortcut
end

#on_theme_changedProc?

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.

Returns:

  • (Proc, nil)


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

def on_theme_changed
  @on_theme_changed&.call
end

#parentComponent?

@return — the parent component or nil if the component has no parent.

Returns:



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

def parent
  @parent
end

#rectRect

@return — the rectangle the component occupies on screen.

Returns:



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.

Parameters:

  • active (Boolean)


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

Returns:

  • (Boolean)


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.

Returns:

  • (Boolean)


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.

Returns:



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.

Returns:

  • (Boolean)


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_backgroundvoid

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

@return — absolute screen coordinates, or nil to hide.

Returns:



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

def cursor_position = nil

#depthInteger

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

Returns:

  • (Integer)


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.

Parameters:

  • key (String)

Returns:



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

#focusvoid

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.

Returns:

  • (Boolean)


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.

Parameters:

  • _key (String)

Returns:

  • (Boolean)


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

Parameters:



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

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

#invalidatevoid

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_hintString

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

Returns:

  • (String)


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.

Parameters:



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_focusvoid

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_changedvoid

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

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



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

#rootComponent

@return — the root component of this component hierarchy.

Returns:



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

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

#screenScreen

@return — the screen which owns this component.

Returns:



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.

Returns:

  • (Boolean)


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

def tab_stop? = false