Class: Tuile::ScreenPane

Inherits:
Component show all
Defined in:
lib/tuile/screen_pane.rb

Overview

The structural root of the Screen‘s component tree.

Screen is a singleton runtime owner (event loop, lock, terminal IO, invalidation set). All actual UI lives under a ScreenPane: the tiled #content, the modal #popups stack, and the bottom #status_bar. Putting them under a single Component parent gives focus traversal a real root, makes Component#attached? a one-liner, and lets popup-focus repair fall out of the standard Component#on_child_removed hook.

The pane is not a Component::Layout: popups deliberately overlap content (Z-ordered, full overdraw, no clipping) and key/mouse dispatch follows modal-popup rules rather than active-child dispatch.

Instance Attribute Summary collapse

Attributes inherited from Component

#key_shortcut, #parent, #rect

Instance Method Summary collapse

Methods inherited from Component

#active=, #active?, #attached?, #content_size, #cursor_position, #depth, #find_shortcut_component, #focus, #keyboard_hint, #on_focus, #on_tree, #root, #screen

Constructor Details

#initializeScreenPane

Returns a new instance of ScreenPane.



17
18
19
20
21
22
23
24
25
26
27
# File 'lib/tuile/screen_pane.rb', line 17

def initialize
  super
  @popups = []
  # Per-popup snapshot of {Screen#focused} taken just before the popup was
  # added. Restored when the popup closes so focus returns to where the
  # user was, instead of falling through to {#content} and getting
  # cascaded to the first focusable child.
  @popup_prior_focus = {}
  @status_bar = Component::Label.new
  @status_bar.parent = self
end

Instance Attribute Details

#contentComponent?

Returns the tiled content component.

Returns:

  • (Component, nil)

    the tiled content component.



30
31
32
# File 'lib/tuile/screen_pane.rb', line 30

def content
  @content
end

#popupsArray<Component> (readonly)

Returns modal popups in stacking order; last is topmost. The array must not be mutated by callers.

Returns:

  • (Array<Component>)

    modal popups in stacking order; last is topmost. The array must not be mutated by callers.



33
34
35
# File 'lib/tuile/screen_pane.rb', line 33

def popups
  @popups
end

#status_barComponent::Label (readonly)

Returns the bottom status bar.

Returns:



35
36
37
# File 'lib/tuile/screen_pane.rb', line 35

def status_bar
  @status_bar
end

Instance Method Details

#add_popup(window) ⇒ void

This method returns an undefined value.

Adds a popup, centers it, focuses it, and invalidates it for repaint.

Parameters:

Raises:

  • (TypeError)


63
64
65
66
67
68
69
70
71
72
73
# File 'lib/tuile/screen_pane.rb', line 63

def add_popup(window)
  raise TypeError, "expected Popup, got #{window.inspect}" unless window.is_a? Component::Popup
  raise ArgumentError, "#{window} already has a parent #{window.parent}" unless window.parent.nil?

  @popup_prior_focus[window] = screen.focused
  @popups << window
  window.parent = self
  window.center
  screen.focused = window
  screen.invalidate(window)
end

#childrenObject

Children for tree traversal: content first, popups in stacking order, status bar last.



41
# File 'lib/tuile/screen_pane.rb', line 41

def children = [*[@content].compact, *@popups, @status_bar]

#focusable?Boolean

Returns:

  • (Boolean)


37
# File 'lib/tuile/screen_pane.rb', line 37

def focusable? = false

#handle_key(key) ⇒ Object

Topmost popup is modal: it eats keys. Falls through to content only when no popup is open.



128
129
130
131
132
133
134
# File 'lib/tuile/screen_pane.rb', line 128

def handle_key(key)
  topmost = @popups.last
  return topmost.handle_key(key) unless topmost.nil?
  return @content.handle_key(key) unless @content.nil?

  false
end

#handle_mouse(event) ⇒ void

This method returns an undefined value.

Mouse events check popups in reverse stacking order (topmost first), and fall through to content only when no popup is hit and there are no popups open. This preserves modal click-blocking: an open popup eats clicks even outside its rect.

Parameters:



142
143
144
145
146
# File 'lib/tuile/screen_pane.rb', line 142

def handle_mouse(event)
  clicked = @popups.rfind { it.rect.contains?(event.point) }
  clicked = @content if clicked.nil? && @popups.empty?
  clicked&.handle_mouse(event)
end

#has_popup?(window) ⇒ Boolean

Returns true if this pane currently hosts the popup.

Parameters:

Returns:

  • (Boolean)

    true if this pane currently hosts the popup.



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

def has_popup?(window) = @popups.include?(window) # rubocop:disable Naming/PredicatePrefix

#layoutvoid

This method returns an undefined value.

Lays out content (full pane minus the bottom row) and the status bar (bottom row). Popups self-position via Component::Popup#center.



114
115
116
117
118
119
120
# File 'lib/tuile/screen_pane.rb', line 114

def layout
  return if rect.empty?

  @content.rect = Rect.new(rect.left, rect.top, rect.width, [rect.height - 1, 0].max) unless @content.nil?
  @popups.each(&:center)
  @status_bar.rect = Rect.new(rect.left, rect.top + rect.height - 1, rect.width, 1)
end

#on_child_removed(child) ⇒ void

This method returns an undefined value.

Focus repair when a child detaches. Default Component#on_child_removed would refocus to ‘self` (the pane), which isn’t a useful focus target. Instead, route focus to the now-topmost popup, then to the prior focus snapshotted when this popup was opened (if still attached), then to content, then nil.

Parameters:



155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
# File 'lib/tuile/screen_pane.rb', line 155

def on_child_removed(child)
  return unless attached?

  f = screen.focused
  return if f.nil?

  cursor = f
  while cursor
    if cursor == child
      fallback = @popups.last
      fallback ||= @removing_popup_prior if @removing_popup_prior&.attached?
      fallback ||= @content
      screen.focused = fallback
      return
    end
    cursor = cursor.parent
  end
end

#rect=(new_rect) ⇒ void

This method returns an undefined value.

Re-lays out children whenever the pane’s own rect changes.

Parameters:



106
107
108
109
# File 'lib/tuile/screen_pane.rb', line 106

def rect=(new_rect)
  super
  layout
end

#remove_popup(window) ⇒ void

This method returns an undefined value.

Removes a popup. If the popup held focus, focus shifts to the now-topmost remaining popup, falling back to the focus snapshotted when the popup was opened (if still attached), then to #content, then to nil.

Parameters:



80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
# File 'lib/tuile/screen_pane.rb', line 80

def remove_popup(window)
  raise Tuile::Error, "#{window} is not an open popup on this pane" unless @popups.delete(window)

  prior = @popup_prior_focus.delete(window)
  # Detach first so the popup becomes its own root; then any prior
  # pointing *inside* that popup is detectable via `p.root == window`.
  window.parent = nil
  # If any other popup recorded its prior focus inside the popup we're
  # removing, forward it to *our* prior so chained closures still climb
  # back to the original owner instead of stopping at a detached
  # component.
  @popup_prior_focus.transform_values! { |p| p && p.root == window ? prior : p }

  @removing_popup_prior = prior
  on_child_removed(window)
ensure
  @removing_popup_prior = nil
end

#repaintvoid

This method returns an undefined value.

Pane paints nothing itself; its children paint over the entire rect.



124
# File 'lib/tuile/screen_pane.rb', line 124

def repaint; end