Class: Tuile::Component::ListDropdown
- Defined in:
- lib/tuile/component/list_dropdown.rb,
sig/tuile.rbs
Overview
A borderless, tinted, non-focusable floating selection list — the dropdown a driver drops open, drives by forwarding movement keys, and commits a pick from: an Overlay wrapping a List that never takes focus, so focus stays on the driver while the caller refills the rows, moves the highlight, and reads the pick.
drop = Component::ListDropdown.new
drop.renderer = ->(item, _w) { label_for(item) } # caller renders
drop.list.on_item_chosen { |e| commit(e.item) } # caller commits
# …then, from the driver's key handler:
drop.items = matches # caller filters
drop.anchor_to(self) # opens it below the driver, or flipped
return true if drop.move(key) # Up/Down/PgUp/PgDn/^U/^D → list scroll
drop.choose if key == Keys::ENTER # commit the highlight
It owns only what every such dropdown shares — placement included, via #anchor_to (below a field) and #anchor_beside (beside a parent row, for a cascading submenu). What stays with the driver: the width policy (neither placement method measures anything itself), filtering, row rendering, the commit action, and ESC/Enter handling. ESC and Enter carry driver-specific tails (ESC may revert a query; Enter may commit via #choose or via a separate submit path), so #move claims neither — the driver calls #choose and Overlay#close from its own branches.
Theming
Borderless, told apart from the content beneath by a background tint —
Theme#input_bg_color by default, assigned as a live Theme::Ref so it
tracks light/dark flips with no hook. Reassign #bg_color= for a
different tint (a Theme.ref(:token) keeps the flip-tracking).
UI-thread-confined, like every component (see Screen).
Defined Under Namespace
Constant Summary collapse
- MOVE_KEYS =
Cursor-movement keys forwarded to the list by #move: the two vertical arrows, page up/down, and Ctrl+U/D half-page jumps. Deliberately excludes Home/End and
j/k— a jump to the first/last row is the driver's call, and both drivers decline it (ComboBox's field needs Home/End for the caret; Select would spend a branch on what a second arrow press already does) — and Enter/ESC, which carry driver-specific tails (see the class docs).A driver only ever sees the keys its own children decline, so a ComboBox never gets Ctrl+U — its field claims it to clear the query — while Select, wrapping no editor, gets every one of these.
[Keys::UP_ARROW, Keys::DOWN_ARROW, Keys::PAGE_UP, Keys::PAGE_DOWN, Keys::CTRL_U, Keys::CTRL_D].freeze
- MAX_VISIBLE_ROWS =
Most rows shown before the list scrolls; #anchor_to's
max_rowsdefault. 10
Instance Attribute Summary collapse
-
#list ⇒ List
readonly
The wrapped list, exposed so a driver can register on Tuile::Component::List#on_item_chosen (its commit) and Tuile::Component::List#on_cursor_changed (a cascading driver drops the panels belonging to the row the highlight just left).
Attributes inherited from Overlay
#close_on_outside_click, #owner
Attributes included from HasContent
Instance Method Summary collapse
-
#anchor_beside(anchor, width:, max_rows: MAX_VISIBLE_ROWS) ⇒ void
sord duck - #call looks like a duck type, replacing with untyped Opens the dropdown beside
anchor— the placement a cascading submenu wants, where #anchor_to is the placement a field's dropdown wants — or moves it there if it is open already. -
#anchor_to(anchor, width: nil, max_rows: MAX_VISIBLE_ROWS) ⇒ void
sord duck - #call looks like a duck type, replacing with untyped Opens the dropdown against
anchor— directly beneath it, flipped above when its rows won't fit below, clamped (with the list scrolling) when neither side has room — or moves it there if it is open already. -
#anchor_with(placement) ⇒ void
Opens or moves the panel and settles the pass, because both anchor methods promise a panel that is placed: a driver reads #cursor_row_rect or forwards a key to the list in the same handler, and both measure rects this just assigned.
-
#choose ⇒ Boolean
Commits the highlighted row by firing Tuile::Component::List#on_item_chosen, exactly as pressing Enter on the focused list would — the driver calls this from its own Enter branch.
-
#cursor ⇒ List::Cursor
@return — the list's cursor (the current highlight).
-
#cursor=(cursor) ⇒ void
@param
cursor— the highlight; see Tuile::Component::List#cursor=. -
#cursor_row_rect ⇒ Rect?
The highlighted row's rect on screen — what a cascading submenu anchors against, via #anchor_beside.
-
#initialize ⇒ ListDropdown
constructor
A new instance of ListDropdown.
-
#items ⇒ ::Array[untyped]
@return — the items currently shown.
-
#items=(items) ⇒ void
@param
items— the items to show, one row each; see Tuile::Component::List#items=. -
#move(key) ⇒ Boolean
Forwards a cursor-movement key to the list.
-
#renderer=(proc) ⇒ void
@param
proc—(item, text_width) -> row; see Tuile::Component::List#renderer. -
#select(index) ⇒ Boolean
Moves the highlight to the item at
index, scrolling it into view; see Tuile::Component::List#select.
Methods inherited from Overlay
#close, #close_on_outside_click?, #declared_size_in, #default_placement, #focusable?, #handle_detached, #handle_focus, #handle_rect_changed, #modal?, #on_close, #open, #open?, #placement, #placement=, #relayout, #reposition, #tab_stop?, #visible=
Methods included from HasContent
Constructor Details
#initialize ⇒ ListDropdown
Returns a new instance of ListDropdown.
148 149 150 151 152 153 154 155 |
# File 'lib/tuile/component/list_dropdown.rb', line 148 def initialize @list = Menu.new @list.cursor = List::Cursor.new @list.show_cursor_when_inactive = true # highlight the selection though focus stays on the driver @list. = :auto # the list fills the panel, so the bar is on exactly when the rows outrun it super(content: @list) self.bg_color = Theme.ref(:input_bg_color) end |
Instance Attribute Details
#list ⇒ List (readonly)
The wrapped list, exposed so a driver can register on Tuile::Component::List#on_item_chosen
(its commit) and Tuile::Component::List#on_cursor_changed (a cascading driver drops the
panels belonging to the row the highlight just left). A driver tunes it
but never supplies it (D_has_content).
179 180 181 |
# File 'lib/tuile/component/list_dropdown.rb', line 179 def list @list end |
Instance Method Details
#anchor_beside(anchor, width:, max_rows: MAX_VISIBLE_ROWS) ⇒ void
This method returns an undefined value.
sord duck - #call looks like a duck type, replacing with untyped
Opens the dropdown beside anchor — the placement a cascading submenu
wants, where #anchor_to is the placement a field's dropdown wants —
or moves it there if it is open already.
sub.anchor_beside(parent.cursor_row_rect, width: measured)
Horizontally it sits against anchor's right edge, flipping to its
left when the right has no room (and clamping to the screen when neither
side does). Vertically it slides: the panel's first row lines up with
the anchored row, sliding up only far enough to keep the panel on screen.
The two axes are the mirror image of #anchor_to's, for the same reason: never cover the thing being chosen from. A field's dropdown must not cover the field, so it flips vertically and shares its columns; a submenu must not cover its parent panel, so it flips horizontally and shares its rows.
@param anchor — the row the submenu belongs to, in screen coordinates — typically the parent dropdown's #cursor_row_rect.
@param width — the panel's width in columns, clamped to the screen. Required, with no default: the anchor's width is the parent's and would be meaningless here, so the caller measures (see design/decisions.md D_select on why the width policy stays with the driver).
@param max_rows — rows shown before the list scrolls.
261 262 263 |
# File 'lib/tuile/component/list_dropdown.rb', line 261 def anchor_beside(anchor, width:, max_rows: MAX_VISIBLE_ROWS) anchor_with(Anchored.new(anchor:, side: :beside, width:, max_rows:)) end |
#anchor_to(anchor, width: nil, max_rows: MAX_VISIBLE_ROWS) ⇒ void
This method returns an undefined value.
sord duck - #call looks like a duck type, replacing with untyped
Opens the dropdown against anchor — directly beneath it, flipped above
when its rows won't fit below, clamped (with the list scrolling) when
neither side has room — or moves it there if it is open already.
Horizontally the left edges line up, sliding left only far enough to
keep the panel on screen.
drop.anchor_to(self) # follows the field, its width
drop.anchor_to(self, width: method(:menu_width))
Vertical flips but horizontal slides because covering the driver would hide what is being chosen, while sharing its columns is the point.
anchor is the region actually occupied, and may be taller than one
row — "beneath" means the row after it, so a multi-row driver (a
TextArea carrying an autocomplete menu) is cleared entirely
rather than overdrawn from its second row down. A component anchor is
read through its Tuile::Component#absolute_extent_rect, so a widget that
paints one row but is assigned more height (ComboBox, Select)
hangs the panel off its face.
The height is the item count, capped at max_rows, and follows
#items=. Settles before it returns, so a driver can forward a key to
the list, or read #cursor_row_rect, in the same handler.
@param anchor — the driver, followed wherever it moves, or a fixed region in screen coordinates. Of any height; the dropdown never covers it.
@param width — the panel's width in columns, clamped to the screen, or something answering it; nil (the default) takes the anchor's, which lines both edges up with a field. A driver that measures its labels passes its own. A label wider than the screen clips — Tuile::Component::List has no horizontal scrolling.
@param max_rows — rows shown before the list scrolls.
231 232 233 |
# File 'lib/tuile/component/list_dropdown.rb', line 231 def anchor_to(anchor, width: nil, max_rows: MAX_VISIBLE_ROWS) anchor_with(Anchored.new(anchor:, side: :below, width:, max_rows:)) end |
#anchor_with(placement) ⇒ void
This method returns an undefined value.
Opens or moves the panel and settles the pass, because both anchor methods promise a panel that is placed: a driver reads #cursor_row_rect or forwards a key to the list in the same handler, and both measure rects this just assigned.
@param placement
313 314 315 316 |
# File 'lib/tuile/component/list_dropdown.rb', line 313 def anchor_with(placement) open? ? self.placement = placement : self.open(placement) flush_layout end |
#choose ⇒ Boolean
Commits the highlighted row by firing Tuile::Component::List#on_item_chosen, exactly as pressing Enter on the focused list would — the driver calls this from its own Enter branch.
@return — true iff a row was chosen (false when the cursor is off-content).
303 |
# File 'lib/tuile/component/list_dropdown.rb', line 303 def choose = @list.handle_key?(Keys::ENTER) |
#cursor ⇒ List::Cursor
@return — the list's cursor (the current highlight).
188 |
# File 'lib/tuile/component/list_dropdown.rb', line 188 def cursor = @list.cursor |
#cursor=(cursor) ⇒ void
This method returns an undefined value.
@param cursor — the highlight; see Tuile::Component::List#cursor=.
183 184 185 |
# File 'lib/tuile/component/list_dropdown.rb', line 183 def cursor=(cursor) @list.cursor = cursor end |
#cursor_row_rect ⇒ Rect?
The highlighted row's rect on screen — what a cascading submenu
anchors against, via #anchor_beside. Screen coordinates because the
submenu is a sibling overlay rather than a child (D_relative_rect).
It lives here rather than in the driver because Tuile::Component::ListDropdown owns the
list's geometry: a driver computing top + position - scroll_top_row
itself would have to reach through to the private list.
@return — one row spanning the panel's width, or nil when
the cursor is off-content (Tuile::Component::List::Cursor::None, an empty list) or its
row is scrolled out of the viewport.
275 276 277 278 279 280 281 282 283 |
# File 'lib/tuile/component/list_dropdown.rb', line 275 def cursor_row_rect return nil if @list.rect.empty? return nil unless @list.cursor.position.between?(0, @list.items.size - 1) row = @list.cursor.position - @list.scroll_top_row return nil unless row.between?(0, @list.rect.height - 1) Rect.new(0, 0, @list.rect.width, 1).at(@list.to_screen(Point.new(0, row))) end |
#items ⇒ ::Array[untyped]
@return — the items currently shown.
166 |
# File 'lib/tuile/component/list_dropdown.rb', line 166 def items = @list.items |
#items=(items) ⇒ void
This method returns an undefined value.
@param items — the items to show, one row each; see Tuile::Component::List#items=. An open dropdown resizes to the new count on the next settle.
160 161 162 163 |
# File 'lib/tuile/component/list_dropdown.rb', line 160 def items=(items) @list.items = items reposition end |
#move(key) ⇒ Boolean
Forwards a cursor-movement key to the list. The driver calls this from its own key handler; a truthy return means "consumed — stop here", falsy means "not mine — proceed with normal editing/dispatch". Only MOVE_KEYS are claimed, and only while open.
@param key
@return — true iff the key was consumed.
291 292 293 294 295 296 |
# File 'lib/tuile/component/list_dropdown.rb', line 291 def move(key) return false unless open? && MOVE_KEYS.include?(key) @list.handle_key?(key) true end |
#renderer=(proc) ⇒ void
This method returns an undefined value.
@param proc — (item, text_width) -> row; see Tuile::Component::List#renderer.
170 171 172 |
# File 'lib/tuile/component/list_dropdown.rb', line 170 def renderer=(proc) @list.renderer = proc end |
#select(index) ⇒ Boolean
Moves the highlight to the item at index, scrolling it into view; see
Tuile::Component::List#select. The positional counterpart of #move, for a driver that
picked a row by something other than a key — a mnemonic letter, say.
@param index
@return — whether the highlight moved there.
195 |
# File 'lib/tuile/component/list_dropdown.rb', line 195 def select(index) = @list.select(index) |