Class: Tuile::Component::Overlay

Inherits:
Component
  • Object
show all
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:

overlay = Component::Overlay.new(content: Component::Label.new("saved"))
overlay.open(Overlay::At[Rect.new(10, 4, 20, 1)])   # mounts it on the Screen
overlay.placement = Overlay::At[Rect.new(10, 6, 20, 1)]   # moves it
overlay.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

ListDropdown, Notification, Popup

Defined Under Namespace

Modules: Placement Classes: At, Centered, CloseEvent, TopRight

Instance Attribute Summary collapse

Attributes included from HasContent

#content

Instance Method Summary collapse

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

Parameters:

  • content: (Component, nil) (defaults to: nil)
  • close_on_outside_click: (Boolean) (defaults to: true)


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

Parameters:

  • value (Boolean)


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.

Returns:



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.

Returns:

  • (Boolean)


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

def close_on_outside_click? = @close_on_outside_click

#declared_size_in(_screen_size) ⇒ Size

The box this overlay asks for on a screen of screen_size — what Centered and TopRight place. A bare overlay declares none.

@param _screen_size

Parameters:

  • _screen_size (Size)

Returns:



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.

Returns:



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

Returns:

  • (Boolean)


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

Parameters:



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

Returns:

  • (Boolean)


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

Returns:



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.

Parameters:

  • placement (Placement) (defaults to: default_placement)

Returns:

  • (self)


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.

Returns:

  • (Boolean)


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.

Returns:



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.

Parameters:



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

Returns:

  • (Boolean)


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

Parameters:

  • _value (Boolean)


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