Class: Tuile::ScreenPane
- 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
-
#content ⇒ Component?
The tiled content component.
-
#popups ⇒ Array<Component>
readonly
Modal popups in stacking order; last is topmost.
-
#status_bar ⇒ Component::Label
readonly
The bottom status bar.
Attributes inherited from Component
Instance Method Summary collapse
-
#add_popup(window) ⇒ void
Adds a popup, centers it, focuses it, and invalidates it for repaint.
-
#children ⇒ Object
Children for tree traversal: content first, popups in stacking order, status bar last.
- #focusable? ⇒ Boolean
-
#handle_key(key) ⇒ Object
Topmost popup is modal: it eats keys.
-
#handle_mouse(event) ⇒ void
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.
-
#has_popup?(window) ⇒ Boolean
True if this pane currently hosts the popup.
-
#initialize ⇒ ScreenPane
constructor
A new instance of ScreenPane.
-
#layout ⇒ void
Lays out content (full pane minus the bottom row) and the status bar (bottom row).
-
#on_child_removed(child) ⇒ void
Focus repair when a child detaches.
-
#rect=(new_rect) ⇒ void
Re-lays out children whenever the pane’s own rect changes.
-
#remove_popup(window) ⇒ void
Removes a popup.
-
#repaint ⇒ void
Pane paints nothing itself; its children paint over the entire rect.
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
#initialize ⇒ ScreenPane
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
#content ⇒ Component?
Returns the tiled content component.
30 31 32 |
# File 'lib/tuile/screen_pane.rb', line 30 def content @content end |
#popups ⇒ Array<Component> (readonly)
Returns 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_bar ⇒ Component::Label (readonly)
Returns the bottom status bar.
35 36 37 |
# File 'lib/tuile/screen_pane.rb', line 35 def @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.
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 |
#children ⇒ Object
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
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.
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.
101 |
# File 'lib/tuile/screen_pane.rb', line 101 def has_popup?(window) = @popups.include?(window) # rubocop:disable Naming/PredicatePrefix |
#layout ⇒ void
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.
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.
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.
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 |
#repaint ⇒ void
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 |