Class: Tuile::Component::Popup

Inherits:
Overlay
  • Object
show all
Defined in:
lib/tuile/component/popup.rb,
sig/tuile.rbs

Overview

A modal dialog: an Overlay that centers itself on the screen, grabs focus, scopes keys to its own subtree, blocks clicks beneath it, and closes on ESC or q.

window = Component::Window.new("Help")
window.content = Component::List.new.tap { _1.lines = lines }
Component::Popup.new(content: window).open

Bare content also works (a Label, a List…), in which case the popup is borderless. For a floating layer that does not take focus or capture input — an autocomplete list anchored to a field, a toast — use Overlay directly.

The popup does not size itself to its content. Its box is set by #declared_size — a Fraction (resolved against the screen every layout pass, so it tracks resize) or an absolute Size (clamped to the screen). The default is Fraction::HALF: half the screen, centered. The wrapped content then fills that box and handles its own overflow by wrapping and scrolling, so use content that can — a TextView or TextArea — for anything longer than fits. A Label only truncates.

Implementation details

q and ESC close the popup — handled here, at the top of the popup's own subtree, so the key only arrives after every component on the focus chain declined it (see ScreenPane#handle_key). That's why typing q into a nested TextField doesn't dismiss the popup: the field consumes it first.

A left click outside the popup closes it too — see Overlay#close_on_outside_click? for the exact contract and Overlay#on_close= for the notice a driver hears when it happens.

UI-thread-confined, like every component (see Screen).

Instance Attribute Summary collapse

Attributes inherited from Overlay

#close_on_outside_click, #on_close, #owner

Attributes included from HasContent

#content

Instance Method Summary collapse

Methods inherited from Overlay

#close, #close_on_outside_click?, #layout, #on_detached, #on_focus, #open, #open?, #rect=, #tab_stop?

Methods included from HasContent

#on_focus, #rect=

Constructor Details

#initialize(content: nil, declared_size: Fraction::HALF, close_on_outside_click: true) ⇒ Popup

@param content — initial content; can be set later via HasContent#content=. The content fills the popup's Tuile::Component#rect; it does not determine the popup's size.

@param declared_size — the popup's box, applied top-down. A Fraction is resolved against the screen each layout pass; a Size is clamped to the screen. Defaults to Fraction::HALF.

@param close_on_outside_click — true (default) to dismiss on a left click that misses this popup. See Overlay#close_on_outside_click?.

Parameters:

  • content: (Component, nil) (defaults to: nil)
  • declared_size: (Size, Fraction) (defaults to: Fraction::HALF)
  • close_on_outside_click: (Boolean) (defaults to: true)


50
51
52
53
54
# File 'lib/tuile/component/popup.rb', line 50

def initialize(content: nil, declared_size: Fraction::HALF, close_on_outside_click: true)
  super(content: content, close_on_outside_click: close_on_outside_click)
  @declared_size = declared_size
  reposition
end

Instance Attribute Details

#declared_sizeSize, Fraction

The box this popup asks for, as set — a Fraction comes back unresolved. Tuile::Component#rect is the resolved answer. Named apart from size because a component's size is its rect.size, which this is not: it is an input re-read on every layout pass, not a report of the current geometry.

@return — see #declared_size=.

Returns:



62
63
64
# File 'lib/tuile/component/popup.rb', line 62

def declared_size
  @declared_size
end

Instance Method Details

#centervoid

This method returns an undefined value.

Recenters the popup on the screen, preserving its current width/height.



101
102
103
# File 'lib/tuile/component/popup.rb', line 101

def center
  self.rect = rect.centered(screen.size)
end

#focusable?Boolean

@return — true — ScreenPane#add_popup focuses a popup on open, and focus repair falls back to it when its subtree has no tab stop.

Returns:

  • (Boolean)


70
# File 'lib/tuile/component/popup.rb', line 70

def focusable? = true

#handle_key(key) ⇒ Boolean

q and ESC close the popup. The popup sits on the focus chain of whatever it wraps, so the key reaches here by bubbling up from the focused content after that content declined to handle it.

@param key

@return — true if the key was handled.

Parameters:

  • key (String)

Returns:

  • (Boolean)


110
111
112
113
114
115
116
117
# File 'lib/tuile/component/popup.rb', line 110

def handle_key(key)
  if [Keys::ESC, "q"].include?(key)
    close
    true
  else
    false
  end
end

#modal?Boolean

@return — true — a Popup scopes key dispatch, grabs focus on open, and blocks clicks on the content beneath it.

Returns:

  • (Boolean)


66
# File 'lib/tuile/component/popup.rb', line 66

def modal? = true

#repositionvoid

This method returns an undefined value.

Re-resolves #declared_size against the current screen and recenters the popup itself (this is not laying out content — the popup's own rect). Called on Overlay#open, on #declared_size=, and by the screen's layout pass, so a Fraction tracks SIGWINCH.

The final rect is computed and assigned in one step rather than sizing at the origin and then centering: the intermediate origin rect rarely covers the previous one, which would make Overlay#rect='s shrink/move detection fire a full repaint on every resize.



94
95
96
97
# File 'lib/tuile/component/popup.rb', line 94

def reposition
  size = @declared_size.is_a?(Fraction) ? @declared_size.resolve(screen.size) : @declared_size.clamp(screen.size)
  self.rect = Rect.new(rect.left, rect.top, size.width, size.height).centered(screen.size)
end