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, and has an open/close lifecycle. It opens with a placement — where it wants to be — and the ScreenPane's layout pass turns that into its rect, again on every resize:
= Component::Overlay.new(content: Component::Label.new("saved"))
.open(Overlay::At[Rect.new(10, 4, 20, 1)]) # mounts it on the Screen
.placement = Overlay::At[Rect.new(10, 6, 20, 1)] # moves it
.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, centering, focus and key handling.
Where it goes is a Placement: At a fixed rect, Centered and TopRight an overlay that declares its size (#declared_size_in), ListDropdown::Anchored a dropdown hanging off a field.
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.
An overlay whose size changes calls #reposition, which asks the pane to place it again; the placement itself stays put. That is how a growing Notification or a re-measured ConfirmWindow gets its new rect.
UI-thread-confined, like every component (see Screen).
Direct Known Subclasses
Defined Under Namespace
Modules: Placement Classes: At, Centered, CloseEvent, TopRight
Instance Attribute Summary collapse
-
#close_on_outside_click ⇒ Object
writeonly
@return — see #close_on_outside_click?.
-
#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).
- #declared_size_in(_screen_size) ⇒ Size
-
#default_placement ⇒ Placement
The placement a bare #open uses.
-
#focusable? ⇒ Boolean
@return — false — a bare overlay leaves focus where it was.
-
#handle_detached ⇒ void
Fires #on_close.
- #handle_focus ⇒ void
-
#handle_rect_changed(old_rect) ⇒ void
Escalates to a full scene repaint when an open overlay shrinks or moves so its new rect no longer covers the cells it previously painted.
-
#initialize(content: nil, close_on_outside_click: true) ⇒ Overlay
constructor
@param
content— initial content; can be set later via HasContent#content=. -
#modal? ⇒ Boolean
@return — false — a bare overlay scopes no keys and blocks no clicks.
-
#on_close ⇒ Listeners
Fired with a CloseEvent once this overlay has left the screen — however it left: #close, a direct Screen#remove_popup, an outside click, or teardown via Screen#close.
-
#open(placement = default_placement) ⇒ self
Mounts this overlay on the Screen at
placement; the pane assigns the rect on the next settle. -
#open? ⇒ Boolean
@return — true if this overlay is currently mounted on the screen.
-
#placement ⇒ Placement?
Where this overlay wants to be, or
nilwhile closed. -
#placement=(placement) ⇒ void
Moves the open overlay; it takes the new rect on the next settle.
-
#relayout ⇒ void
Content fills the overlay's full rect — an Overlay has no border to subtract.
-
#reposition ⇒ void
Asks the pane to place this overlay again, because something its placement reads — its declared size, its row count — changed.
-
#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?.
164 165 166 167 168 169 170 |
# File 'lib/tuile/component/overlay.rb', line 164 def initialize(content: nil, close_on_outside_click: true) super() @close_on_outside_click = close_on_outside_click @owner = 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?.
226 227 228 |
# File 'lib/tuile/component/overlay.rb', line 226 def close_on_outside_click=(value) @close_on_outside_click = value 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.
242 243 244 |
# File 'lib/tuile/component/overlay.rb', line 242 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.
317 318 319 |
# File 'lib/tuile/component/overlay.rb', line 317 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.
223 |
# File 'lib/tuile/component/overlay.rb', line 223 def close_on_outside_click? = @close_on_outside_click |
#declared_size_in(_screen_size) ⇒ Size
270 271 272 |
# File 'lib/tuile/component/overlay.rb', line 270 def declared_size_in(_screen_size) raise Tuile::Error, "#{self.class} declares no size — place it with Overlay::At[rect]" end |
#default_placement ⇒ Placement
The placement a bare #open uses. A bare overlay has none, because nothing about it says where it goes.
278 279 280 |
# File 'lib/tuile/component/overlay.rb', line 278 def default_placement raise ArgumentError, "#{self.class} has no default placement — open(Overlay::At[rect])" end |
#focusable? ⇒ Boolean
@return — false — a bare overlay leaves focus where it was. See the class docs: override it only together with #modal?.
178 |
# File 'lib/tuile/component/overlay.rb', line 178 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.
353 354 355 356 |
# File 'lib/tuile/component/overlay.rb', line 353 def handle_detached super on_close.fire(CloseEvent.new(source: self)) end |
#handle_focus ⇒ void
This method returns an undefined value.
7145 |
# File 'sig/tuile.rbs', line 7145
def handle_focus: () -> void
|
#handle_rect_changed(old_rect) ⇒ void
This method returns an undefined value.
Escalates 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 old_rect
291 292 293 294 |
# File 'lib/tuile/component/overlay.rb', line 291 def handle_rect_changed(old_rect) super screen.needs_full_repaint if open? && !rect.contains_rect?(old_rect) end |
#modal? ⇒ Boolean
@return — false — a bare overlay scopes no keys and blocks no clicks. Popup overrides it, together with #focusable?.
174 |
# File 'lib/tuile/component/overlay.rb', line 174 def modal? = false |
#on_close ⇒ Listeners
Fired with a CloseEvent 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).
263 |
# File 'lib/tuile/component/overlay.rb', line 263 listener :on_close |
#open(placement = default_placement) ⇒ self
Mounts this overlay on the Screen at placement; the pane assigns the
rect on the next settle.
Component::Popup.new(content: window).open # its default: centered
overlay.open(Overlay::At[Rect.new(10, 4, 20, 1)])
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.
@param placement — where it wants to be.
310 311 312 313 |
# File 'lib/tuile/component/overlay.rb', line 310 def open(placement = default_placement) screen.add_popup(self, placement) self end |
#open? ⇒ Boolean
@return — true if this overlay is currently mounted on the screen.
322 323 324 |
# File 'lib/tuile/component/overlay.rb', line 322 def open? screen.has_popup?(self) end |
#placement ⇒ Placement?
Where this overlay wants to be, or nil while closed.
@return — the placement it was opened or moved with.
328 |
# File 'lib/tuile/component/overlay.rb', line 328 def placement = open? ? screen.pane.placement(self) : nil |
#placement=(placement) ⇒ void
This method returns an undefined value.
Moves the open overlay; it takes the new rect on the next settle.
@param placement — see the class docs.
336 337 338 339 340 |
# File 'lib/tuile/component/overlay.rb', line 336 def placement=(placement) raise Tuile::Error, "#{self} is not open — pass the placement to #open" unless open? screen.pane.constrain(self, placement) end |
#relayout ⇒ void
This method returns an undefined value.
Content fills the overlay's full rect — an Overlay has no border to subtract.
363 364 365 |
# File 'lib/tuile/component/overlay.rb', line 363 def relayout content&.rect = local_rect end |
#reposition ⇒ void
This method returns an undefined value.
Asks the pane to place this overlay again, because something its placement reads — its declared size, its row count — changed. A no-op while closed: #open places it anyway.
346 347 348 |
# File 'lib/tuile/component/overlay.rb', line 346 def reposition parent&.invalidate_layout end |
#tab_stop? ⇒ Boolean
@return — false — Tab never lands on the overlay wrapper itself (its content may still carry stops).
182 |
# File 'lib/tuile/component/overlay.rb', line 182 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
197 198 199 |
# File 'lib/tuile/component/overlay.rb', line 197 def visible=(_value) raise Tuile::Error, "#{self.class} cannot be hidden — close it instead (Overlay#close)" end |