Class: Tuile::Component::Popup

Inherits:
Tuile::Component show all
Includes:
HasContent
Defined in:
lib/tuile/component/popup.rb

Overview

An overlay that wraps any Tuile::Component as its content. Popup itself paints nothing — it’s a transparent host that handles its lifecycle (#open / #close / #open?, ESC/q to close) and auto-sizes to the wrapped content.

Modal by default: it centers on the screen, grabs focus, eats keys, and blocks clicks beneath it. Pass ‘modal: false` for a non-modal overlay that floats above the content (still painted on top, still auto-sized) without taking focus or capturing input — the caller positions it (via #rect=) and drives it from app code. That is the building block for an autocomplete/slash-command list anchored to a TextField or TextArea caret: typing keeps focus (and the cursor) in the input, an TextInput#on_change listener refills the list, and an TextInput#on_key interceptor forwards Up/Down/Enter to it.

The wrapped content fills the popup’s full #rect; if you want a frame and caption, wrap a Window (or any subclass — including LogWindow) and let it draw its own border:

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.

‘q` and ESC close the popup. Any nested TextField that owns the hardware cursor swallows printable keys first via the standard cursor-owner suppression in #handle_key, so typing `q` into a text field doesn’t dismiss the popup.

Instance Attribute Summary

Attributes included from HasContent

#content

Attributes inherited from Tuile::Component

#content_size, #key_shortcut, #on_theme_changed, #parent, #rect

Class Method Summary collapse

Instance Method Summary collapse

Methods included from HasContent

#children, #handle_mouse, #on_focus

Methods inherited from Tuile::Component

#active=, #active?, #attached?, #children, #cursor_position, #depth, #find_shortcut_component, #focus, #handle_mouse, #on_child_removed, #on_focus, #on_tree, #popup_max_height, #popup_min_height, #repaint, #root, #screen, #tab_stop?

Constructor Details

#initialize(content: nil, modal: true) ⇒ Popup

Returns a new instance of Popup.

Parameters:

  • content (Component, nil) (defaults to: nil)

    initial content; can be set later via #content=. When provided here, the popup auto-sizes to fit.

  • modal (Boolean) (defaults to: true)

    true (default) for a centered, focus-grabbing, input-capturing modal; false for a non-modal overlay the caller positions and drives (see the class docs).



43
44
45
46
47
48
# File 'lib/tuile/component/popup.rb', line 43

def initialize(content: nil, modal: true)
  super()
  @modal = modal
  @content = nil
  self.content = content unless content.nil?
end

Class Method Details

.open(content: nil) ⇒ Popup

Constructs and opens a popup in one call.

Parameters:

  • content (Component, nil) (defaults to: nil)

Returns:

  • (Popup)

    the opened popup.



82
83
84
# File 'lib/tuile/component/popup.rb', line 82

def self.open(content: nil)
  Popup.new(content: content).tap(&:open)
end

Instance Method Details

#centervoid

This method returns an undefined value.

Recenters the popup on the screen, preserving its current width/height. Called automatically by the screen’s layout pass and by #content= when the popup is open.



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

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

#closevoid

This method returns an undefined value.

Removes this popup from the Screen. No-op if not currently open.



88
89
90
# File 'lib/tuile/component/popup.rb', line 88

def close
  screen.remove_popup(self)
end

#content=(new_content) ⇒ Object

Sets the popup’s content and auto-sizes the popup to fit.

Parameters:



122
123
124
125
# File 'lib/tuile/component/popup.rb', line 122

def content=(new_content)
  super
  update_rect unless new_content.nil?
end

#focusable?Boolean

Returns:

  • (Boolean)


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

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.

Parameters:

  • key (String)

Returns:

  • (Boolean)

    true if the key was handled.



150
151
152
153
154
155
156
157
# File 'lib/tuile/component/popup.rb', line 150

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

#keyboard_hintString

Hint for the status bar: own “q Close” plus the wrapped content’s hint.

Returns:

  • (String)


139
140
141
142
143
# File 'lib/tuile/component/popup.rb', line 139

def keyboard_hint
  prefix = "q #{screen.theme.hint("Close")}"
  child_hint = @content&.keyboard_hint.to_s
  child_hint.empty? ? prefix : "#{prefix}  #{child_hint}"
end

#max_heightInteger

Returns max height the popup will grow to fit its content. Defers to the content’s Tuile::Component#popup_max_height advice when it gives one, else defaults to 12. Override in a subclass to allow taller popups regardless of content.

Returns:

  • (Integer)

    max height the popup will grow to fit its content. Defers to the content’s Tuile::Component#popup_max_height advice when it gives one, else defaults to 12. Override in a subclass to allow taller popups regardless of content.



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

def max_height = @content&.popup_max_height || 12

#min_heightInteger

Returns min height the popup occupies even when its content is shorter. Defers to the content’s Tuile::Component#popup_min_height advice when it gives one, else defaults to 0 (size purely to content) — so a LogWindow stays readable while only a few lines are in without callers wiring up a subclass. Override in a subclass to keep any popup from collapsing to a couple of rows. Capped at the same 4/5-of-screen ceiling #update_rect applies.

Returns:

  • (Integer)

    min height the popup occupies even when its content is shorter. Defers to the content’s Tuile::Component#popup_min_height advice when it gives one, else defaults to 0 (size purely to content) — so a LogWindow stays readable while only a few lines are in without callers wiring up a subclass. Override in a subclass to keep any popup from collapsing to a couple of rows. Capped at the same 4/5-of-screen ceiling #update_rect applies.



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

def min_height = @content&.popup_min_height || 0

#modal?Boolean

Returns whether this popup is modal. See #initialize.

Returns:

  • (Boolean)

    whether this popup is modal. See #initialize.



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

def modal? = @modal

#on_child_content_size_changed(_child) ⇒ void

This method returns an undefined value.

Re-sizes (and recenters, when open) whenever the wrapped content’s natural size changes — e.g. a Label‘s `text=`, a List’s ‘add_line`, or a nested Window whose own content grew (the window recomputes its Tuile::Component#content_size and the change bubbles here).

Parameters:



133
134
135
# File 'lib/tuile/component/popup.rb', line 133

def on_child_content_size_changed(_child)
  update_rect
end

#openvoid

This method returns an undefined value.

Mounts this popup on the Screen. Recomputes the popup’s size from the current content first, so reopening a popup whose content has grown or shrunk while closed picks up the new size.



74
75
76
77
# File 'lib/tuile/component/popup.rb', line 74

def open
  update_rect unless @content.nil?
  screen.add_popup(self)
end

#open?Boolean

Returns true if this popup is currently mounted on the screen.

Returns:

  • (Boolean)

    true if this popup is currently mounted on the screen.



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

def open?
  screen.has_popup?(self)
end

#rect=(new_rect) ⇒ void

This method returns an undefined value.

Reassigns the popup’s rect, escalating to a full scene repaint when an open popup shrinks or moves so its new rect no longer covers the cells it previously painted. A popup overdraws the scene without clipping and nothing clears underneath it, so Screen#repaint‘s popup-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 popup only grew), the fast path is correct and the full repaint is skipped.

Parameters:



64
65
66
67
68
# File 'lib/tuile/component/popup.rb', line 64

def rect=(new_rect)
  old_rect = rect
  super
  screen.needs_full_repaint if open? && !new_rect.contains_rect?(old_rect)
end