Class: Tuile::Component::Overlay
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::Overlay
- Includes:
- HasContent
- Defined in:
- lib/tuile/component/overlay.rb,
sig/tuile.rbs
Overview
A component mounted on the Screen's overlay stack rather than in the tiled tree: it floats above the content at a rect the caller assigns, and has an open/close lifecycle instead of a parent that lays it out.
= Component::Overlay.new(content: Component::Label.new("saved"))
.rect = Rect.new(10, 4, 20, 1) # you place it — nothing else does
.open # mounts it on the Screen
.close
That is the whole of it — floating, plus the lifecycle, #on_close, outside-click dismissal and #owner. Popup is the subclass that adds a declared size, self-centering, focus and key handling.
The wrapped content fills the overlay's whole #rect; for a frame and a caption, wrap a Window and let it draw its own border.
Implementation details
#focusable? and #modal? move together — flip both or neither. The
defaults here are inert (false, false): a bare overlay floats without
disturbing focus or key dispatch. Popup flips both. What must
not appear is a focusable non-modal overlay: ScreenPane#handle_key?
scopes delivery to the topmost modal popup or else the tiled content, so
such an overlay would hold focus outside the key scope, where delivery
reaches nobody and every keystroke goes dead until Tab recovers. A
non-modal overlay is therefore driven from its owner's key handler
(Select forwarding to its dropdown) instead of claiming focus.
A derived position needs a #reposition override. The default is a
no-op: the rect is whatever the caller last assigned. An overlay whose
position is computed — from the screen, or from an anchor — must recompute
it there, or it keeps a stale rect after a SIGWINCH and sits off-screen
entirely if the terminal narrowed. Closing on resize is equally legal
(MenuBar drops its cascade from rect= rather than walking
every level to re-anchor it).
UI-thread-confined, like every component (see Screen).
Direct Known Subclasses
Instance Attribute Summary collapse
-
#close_on_outside_click ⇒ Object
writeonly
@return — see #close_on_outside_click?.
-
#on_close ⇒ Proc?
A callback taking no arguments, fired once this overlay has left the screen — however it left: #close, a direct Screen#remove_popup, an outside click, or teardown via Screen#close.
-
#owner ⇒ Component?
The component this overlay is part of, or
nil(the default) when it is an overlay in its own right.
Attributes included from HasContent
Instance Method Summary collapse
-
#close ⇒ void
Removes this overlay from the Screen.
-
#close_on_outside_click? ⇒ Boolean
Whether a left click outside this overlay closes it (default true).
-
#focusable? ⇒ Boolean
@return — false — a bare overlay leaves focus where it was.
-
#handle_detached ⇒ void
Fires #on_close.
- #handle_focus ⇒ void
-
#initialize(content: nil, close_on_outside_click: true) ⇒ Overlay
constructor
@param
content— initial content; can be set later via HasContent#content=. -
#layout(content) ⇒ void
Content fills the overlay's full rect — an Overlay has no border to subtract.
-
#modal? ⇒ Boolean
@return — false — a bare overlay scopes no keys and blocks no clicks.
-
#open ⇒ self
Mounts this overlay on the Screen.
-
#open? ⇒ Boolean
@return — true if this overlay is currently mounted on the screen.
-
#rect=(new_rect) ⇒ void
Reassigns the overlay's rect, escalating to a full scene repaint when an open overlay shrinks or moves so its new rect no longer covers the cells it previously painted.
-
#reposition ⇒ void
Recomputes this overlay's own rect (not its content's layout).
-
#tab_stop? ⇒ Boolean
@return — false — Tab never lands on the overlay wrapper itself (its content may still carry stops).
-
#visible=(_value) ⇒ void
Refused: an overlay is dismissed, not hidden.
Constructor Details
#initialize(content: nil, close_on_outside_click: true) ⇒ Overlay
@param content — initial content; can be set later via HasContent#content=. It fills the overlay's Tuile::Component#rect and does not determine it.
@param close_on_outside_click — true (default) to dismiss on a left click that misses this overlay. See #close_on_outside_click?.
49 50 51 52 53 54 55 56 |
# File 'lib/tuile/component/overlay.rb', line 49 def initialize(content: nil, close_on_outside_click: true) super() @close_on_outside_click = close_on_outside_click @owner = nil @on_close = nil @content = nil self.content = content unless content.nil? end |
Instance Attribute Details
#close_on_outside_click=(value) ⇒ Object (writeonly)
@return — see #close_on_outside_click?.
112 113 114 |
# File 'lib/tuile/component/overlay.rb', line 112 def close_on_outside_click=(value) @close_on_outside_click = value end |
#on_close ⇒ Proc?
A callback taking no arguments, fired once this overlay has left the screen — however it left: #close, a direct Screen#remove_popup, an outside click, or teardown via Screen#close. That unconditionality is the point, so it hangs off #handle_detached rather than #close; a driver keeping its own record of open overlays reconciles it here and cannot drift (MenuBar::Cascade is the worked example).
It fires after the overlay is detached, so #open? is already false and the usual Tuile::Component#handle_detached caveats apply: release state, don't inspect the tree, keep it trivial (it may run while the pane is mid-way through closing a batch of overlays, and a raise propagates).
142 143 144 |
# File 'lib/tuile/component/overlay.rb', line 142 def on_close @on_close end |
#owner ⇒ Component?
The component this overlay is part of, or nil (the default) when it
is an overlay in its own right. It exists for outside-click dismissal: a
click inside this overlay also counts as inside whatever overlay encloses
its owner, so the host is not dismissed by a click on a panel it put
there. See #close_on_outside_click?.
Set it to the driver — ComboBox hands its dropdown self
— rather than to the enclosing overlay: the driver knows what it is,
while the overlay above it is a tree relationship the pane resolves at
click time (so it cannot go stale). Any Tuile::Component is accepted, and an
Overlay resolves to itself, which is how a
MenuBar::Cascade chains each panel to the one it dropped out
of.
128 129 130 |
# File 'lib/tuile/component/overlay.rb', line 128 def owner @owner end |
Instance Method Details
#close ⇒ void
This method returns an undefined value.
Removes this overlay from the Screen. No-op if not currently open.
175 176 177 |
# File 'lib/tuile/component/overlay.rb', line 175 def close screen.remove_popup(self) end |
#close_on_outside_click? ⇒ Boolean
Whether a left click outside this overlay closes it (default true). The
pane does the closing — ScreenPane#dismissing_popups_outside snapshots
the open overlays before the press is routed and closes the
dismissable ones after, so a widget that toggles its own overlay from a click on its
face (a Select, a MenuBar title) still toggles
correctly: the delivered click closes the overlay and the dismissal then
no-ops on it, rather than closing and reopening it. Only :left
dismisses; scroll and right clicks never do.
"Outside" spans the #owner chain, not just this rect. A click counts as inside this overlay when it lands in its rect or in any overlay that belongs to it — so a dialog is not dismissed by a click on a dropdown its own field opened, and a menu cascade is not dismissed by a click on one of its deeper panels. Overlays with no owner relationship are independent: clicking one dismisses the other, which is what a window-like overlay should do. One that must survive unrelated clicks entirely (Notification) sets this false.
Every dismissable overlay closes, not just the topmost, and stacking order plays no part: a MenuBar cascade must vanish whole on one click on the background, not peel one panel per click.
109 |
# File 'lib/tuile/component/overlay.rb', line 109 def close_on_outside_click? = @close_on_outside_click |
#focusable? ⇒ Boolean
@return — false — a bare overlay leaves focus where it was. See the class docs: override it only together with #modal?.
64 |
# File 'lib/tuile/component/overlay.rb', line 64 def focusable? = false |
#handle_detached ⇒ void
This method returns an undefined value.
Fires #on_close. A subclass overriding this must call super, or
the overlay's driver never hears that it closed.
194 195 196 197 |
# File 'lib/tuile/component/overlay.rb', line 194 def handle_detached super @on_close&.call end |
#handle_focus ⇒ void
This method returns an undefined value.
5723 |
# File 'sig/tuile.rbs', line 5723
def handle_focus: () -> void
|
#layout(content) ⇒ void
This method returns an undefined value.
Content fills the overlay's full rect — an Overlay has no border to subtract.
@param content
205 206 207 |
# File 'lib/tuile/component/overlay.rb', line 205 def layout(content) content.rect = rect end |
#modal? ⇒ Boolean
@return — false — a bare overlay scopes no keys and blocks no clicks. Popup overrides it, together with #focusable?.
60 |
# File 'lib/tuile/component/overlay.rb', line 60 def modal? = false |
#open ⇒ self
Mounts this overlay on the Screen.
overlay = Component::Overlay.new(content: label).open # construct and mount
There is deliberately no class-level Overlay.open factory — see
design/decisions.md D_popup_open; returning self is what keeps the
one-liner above available without one.
167 168 169 170 171 |
# File 'lib/tuile/component/overlay.rb', line 167 def open reposition screen.add_popup(self) self end |
#open? ⇒ Boolean
@return — true if this overlay is currently mounted on the screen.
180 181 182 |
# File 'lib/tuile/component/overlay.rb', line 180 def open? screen.has_popup?(self) end |
#rect=(new_rect) ⇒ void
This method returns an undefined value.
Reassigns the overlay's rect, escalating to a full scene repaint when an open overlay shrinks or moves so its new rect no longer covers the cells it previously painted. An overlay overdraws the scene without clipping and nothing clears underneath it, so Screen#repaint's overlay-only fast path would repaint into the new rect and leave the vacated cells showing stale content. When the new rect fully covers the old one (the overlay only grew), the fast path is correct and the full repaint is skipped.
@param new_rect
153 154 155 156 157 |
# File 'lib/tuile/component/overlay.rb', line 153 def rect=(new_rect) old_rect = rect super screen.needs_full_repaint if open? && !new_rect.contains_rect?(old_rect) end |
#reposition ⇒ void
This method returns an undefined value.
Recomputes this overlay's own rect (not its content's layout). A no-op here — the rect is whatever the caller assigned — and the hook a subclass with a derived position overrides; see the class docs. Called on #open and by the screen's layout pass, so an override tracks SIGWINCH.
189 |
# File 'lib/tuile/component/overlay.rb', line 189 def reposition; end |
#tab_stop? ⇒ Boolean
@return — false — Tab never lands on the overlay wrapper itself (its content may still carry stops).
68 |
# File 'lib/tuile/component/overlay.rb', line 68 def tab_stop? = false |
#visible=(_value) ⇒ void
This method returns an undefined value.
Refused: an overlay is dismissed, not hidden. Use #close and #open — which already keep it alive between showings, the one thing hiding would buy — or hide the content if only a piece of it comes and goes.
The pane resolves clicks and key scope from its popup list, which does not consult this flag, so a hidden overlay would paint nothing while still swallowing every click and, if modal, scoping every key: the invisible-modal trap the class docs warn about for #focusable? and #modal?.
@param _value
83 84 85 |
# File 'lib/tuile/component/overlay.rb', line 83 def visible=(_value) raise Tuile::Error, "#{self.class} cannot be hidden — close it instead (Overlay#close)" end |