Class: Tuile::Component::ListDropdown

Inherits:
Popup
  • Object
show all
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: a non-modal Popup 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.on_item_chosen = ->(index, _line) { commit(index) } # caller commits
# …then, from the driver's key handler:
drop.lines = matches.map { |m| render(m) }   # caller filters + renders
drop.anchor_to(rect, rows: matches.size)     # below the driver, or flipped
drop.open
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. What stays with the driver: the width policy (#anchor_to measures nothing 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 Popup#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

Classes: Menu

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

Returns:

  • (Array<String>)
[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_rows default.

Returns:

  • (Integer)
10

Instance Attribute Summary

Attributes inherited from Popup

#size

Attributes included from HasContent

#content

Instance Method Summary collapse

Methods inherited from Popup

#center, #close, #focusable?, #handle_key, #handle_mouse, #keyboard_hint, #layout, #modal?, #on_focus, #open, open, #open?, #rect=, #reposition

Methods included from HasContent

#handle_mouse, #on_focus, #rect=

Constructor Details

#initializeListDropdown

Returns a new instance of ListDropdown.



61
62
63
64
65
66
67
# File 'lib/tuile/component/list_dropdown.rb', line 61

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
  super(content: @list, modal: false)
  self.bg_color = Theme.ref(:input_bg_color)
end

Instance Method Details

#anchor_to(anchor, rows:, width: anchor.width, max_rows: MAX_VISIBLE_ROWS) ⇒ Object

Sizes and places the dropdown against anchor: directly beneath it, flipped above when rows won't fit below, clamped — with the list scrolling — when neither side has room. Horizontally the left edges line up, sliding left only far enough to keep the panel on screen.

drop.anchor_to(field.rect, rows: matches.size)            # field width
drop.anchor_to(rect, rows: items.size, width: measured)   # own width

Vertical flips but horizontal slides because covering the driver would hide what is being chosen, while sharing its columns is the point.

@param anchor — the driver's rect; the dropdown never covers it.

@param rows — how many rows there are to show — the content count, not the height: more than fits turns the scrollbar on. 0 collapses the dropdown to an empty rect (drivers close instead).

@param width — the panel's width in columns, clamped to the screen. Defaults to the anchor's, which lines both edges up with a field; a driver that measured 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.



114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/tuile/component/list_dropdown.rb', line 114

def anchor_to(anchor, rows:, width: anchor.width, max_rows: MAX_VISIBLE_ROWS)
  desired = [rows, max_rows].min
  below = screen.size.height - (anchor.top + 1)
  above = anchor.top
  if desired <= below
    top = anchor.top + 1
    height = desired
  elsif above >= below
    height = [desired, above].min
    top = anchor.top - height
  else
    height = below
    top = anchor.top + 1
  end
  width = [width, screen.size.width].min
  self.size = Size.new(width, height)
  self.rect = Rect.new([anchor.left, screen.size.width - width].min.clamp(0, nil), top, width, height)
  # After the geometry: the setter rebuilds the list's padded rows against
  # the width it can see, and the gutter takes a column off it.
  @list.scrollbar_visibility = rows > height ? :visible : :gone
end

#chooseBoolean

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

Returns:

  • (Boolean)


154
# File 'lib/tuile/component/list_dropdown.rb', line 154

def choose = @list.handle_key(Keys::ENTER)

#cursorList::Cursor

@return — the list's cursor (the current highlight).

Returns:



91
# File 'lib/tuile/component/list_dropdown.rb', line 91

def cursor = @list.cursor

#cursor=(cursor) ⇒ void

This method returns an undefined value.

@param cursor — the highlight; see Tuile::Component::List#cursor=.

Parameters:



86
87
88
# File 'lib/tuile/component/list_dropdown.rb', line 86

def cursor=(cursor)
  @list.cursor = cursor
end

#lines::Array[StyledString]

@return — the current rows.

Returns:



76
# File 'lib/tuile/component/list_dropdown.rb', line 76

def lines = @list.lines

#lines=(lines) ⇒ void

This method returns an undefined value.

@param lines — the rows to show; see Tuile::Component::List#lines=.

Parameters:

  • lines (::Array[untyped])


71
72
73
# File 'lib/tuile/component/list_dropdown.rb', line 71

def lines=(lines)
  @list.lines = lines
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.

Parameters:

  • key (String)

Returns:

  • (Boolean)


142
143
144
145
146
147
# File 'lib/tuile/component/list_dropdown.rb', line 142

def move(key)
  return false unless open? && MOVE_KEYS.include?(key)

  @list.handle_key(key)
  true
end

#on_item_chosen=(proc) ⇒ void

This method returns an undefined value.

@param proc — commit callback; see Tuile::Component::List#on_item_chosen.

Parameters:

  • proc (Proc, Method, nil)


80
81
82
# File 'lib/tuile/component/list_dropdown.rb', line 80

def on_item_chosen=(proc)
  @list.on_item_chosen = proc
end