Class: Tuile::Component::List

Inherits:
Tuile::Component show all
Defined in:
lib/tuile/component/list.rb

Overview

A scrollable list of items with cursor support.

Items are modeled as StyledStrings and painted directly into the component’s #rect. Lines wider than the viewport are ellipsized via StyledString#ellipsize (span styles are preserved across the cut —unlike the older ANSI-as-bytes truncation, color does not get dropped on the surviving characters). Vertical scrolling is supported via #top_line; the list can also automatically scroll to the bottom if #auto_scroll is enabled.

Cursor is supported; call #cursor= to change cursor behavior. The cursor responds to arrows, ‘jk`, Home/End, Ctrl+U/D and scrolls the list automatically. The cursor highlight overlays Theme#active_bg_color while preserving each span’s foreground color.

Defined Under Namespace

Classes: Cursor

Instance Attribute Summary collapse

Attributes inherited from Tuile::Component

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

Instance Method Summary collapse

Methods inherited from Tuile::Component

#active=, #active?, #attached?, #children, #cursor_position, #depth, #find_shortcut_component, #focus, #keyboard_hint, #on_child_content_size_changed, #on_child_removed, #on_focus, #on_tree, #root, #screen

Constructor Details

#initializeList

Returns a new instance of List.



20
21
22
23
24
25
26
27
28
29
30
31
32
33
# File 'lib/tuile/component/list.rb', line 20

def initialize
  super
  @lines = []
  @padded_lines = []
  @blank_padded = StyledString::EMPTY
  @auto_scroll = false
  @top_line = 0
  @cursor = Cursor::None.new
  @scrollbar_visibility = :gone
  @show_cursor_when_inactive = false
  @on_item_chosen = nil
  @on_cursor_changed = nil
  @last_cursor_state = cursor_state
end

Instance Attribute Details

#auto_scrollBoolean

Returns if true and a line is added or new content is set, auto-scrolls to the bottom.

Returns:

  • (Boolean)

    if true and a line is added or new content is set, auto-scrolls to the bottom.



54
55
56
# File 'lib/tuile/component/list.rb', line 54

def auto_scroll
  @auto_scroll
end

#cursorCursor

Returns the list’s cursor.

Returns:

  • (Cursor)

    the list’s cursor.



60
61
62
# File 'lib/tuile/component/list.rb', line 60

def cursor
  @cursor
end

#on_cursor_changedProc?

Returns callback fired when the ‘(index, line)` tuple under the cursor changes. Called as `proc.call(index, line)` where `line` is the StyledString at the cursor, or `nil` when the cursor is off-content (Tuile::Component::List::Cursor::None, empty list, or `index` past the last line). Fires on cursor moves (key, mouse, search), on #cursor=, and on #lines=/#add_lines when the line at the cursor’s index changes (or its in-range/out-of-range status flips). Useful for keeping a details pane in sync with the highlighted row.

Returns:

  • (Proc, nil)

    callback fired when the ‘(index, line)` tuple under the cursor changes. Called as `proc.call(index, line)` where `line` is the StyledString at the cursor, or `nil` when the cursor is off-content (Tuile::Component::List::Cursor::None, empty list, or `index` past the last line). Fires on cursor moves (key, mouse, search), on #cursor=, and on #lines=/#add_lines when the line at the cursor’s index changes (or its in-range/out-of-range status flips). Useful for keeping a details pane in sync with the highlighted row.



50
51
52
# File 'lib/tuile/component/list.rb', line 50

def on_cursor_changed
  @on_cursor_changed
end

#on_item_chosenProc?

Returns callback fired when an item is chosen — by pressing Enter on the cursor’s item, or by left-clicking an item. Called as ‘proc.call(index, line)` with the chosen 0-based index and its StyledString line. Never fires when the cursor’s position is outside the content (e.g. Tuile::Component::List::Cursor::None, or empty content).

Returns:

  • (Proc, nil)

    callback fired when an item is chosen — by pressing Enter on the cursor’s item, or by left-clicking an item. Called as ‘proc.call(index, line)` with the chosen 0-based index and its StyledString line. Never fires when the cursor’s position is outside the content (e.g. Tuile::Component::List::Cursor::None, or empty content).



40
41
42
# File 'lib/tuile/component/list.rb', line 40

def on_item_chosen
  @on_item_chosen
end

#scrollbar_visibilitySymbol

Returns scrollbar visibility: ‘:gone` or `:visible`.

Returns:

  • (Symbol)

    scrollbar visibility: ‘:gone` or `:visible`.



63
64
65
# File 'lib/tuile/component/list.rb', line 63

def scrollbar_visibility
  @scrollbar_visibility
end

#show_cursor_when_inactiveBoolean

Returns when true, the cursor highlight is painted even while the list is inactive (e.g. when focus is on a sibling search field). Defaults to false.

Returns:

  • (Boolean)

    when true, the cursor highlight is painted even while the list is inactive (e.g. when focus is on a sibling search field). Defaults to false.



68
69
70
# File 'lib/tuile/component/list.rb', line 68

def show_cursor_when_inactive
  @show_cursor_when_inactive
end

#top_lineInteger

Returns top line of the viewport. 0 or positive.

Returns:

  • (Integer)

    top line of the viewport. 0 or positive.



57
58
59
# File 'lib/tuile/component/list.rb', line 57

def top_line
  @top_line
end

Instance Method Details

#add_line(line) ⇒ void

This method returns an undefined value.

Adds a line.

Parameters:

Raises:

  • (ArgumentError)


164
165
166
167
# File 'lib/tuile/component/list.rb', line 164

def add_line(line)
  raise ArgumentError, "line is nil" if line.nil?
  add_lines [line]
end

#add_lines(lines) ⇒ void

This method returns an undefined value.

Appends given lines. Each entry is parsed the same way as in #lines=: coerced to a StyledString, split on ‘n`, with trailing empty pieces dropped and trailing ASCII whitespace stripped.

Parameters:

  • lines (Array)

    entries are ‘String`, `StyledString`, or anything that responds to `#to_s`.



175
176
177
178
179
180
181
182
183
184
# File 'lib/tuile/component/list.rb', line 175

def add_lines(lines)
  screen.check_locked
  new_lines = parse_input_lines(lines)
  @lines += new_lines
  @padded_lines += new_lines.map { |line| pad_to_row(line) }
  update_top_line_if_auto_scroll
  notify_cursor_changed
  invalidate
  grow_content_size(new_lines)
end

#focusable?Boolean

Returns:

  • (Boolean)


186
# File 'lib/tuile/component/list.rb', line 186

def focusable? = true

#handle_key(key) ⇒ Boolean

Returns true if the key was handled.

Parameters:

  • key (String)

    a key.

Returns:

  • (Boolean)

    true if the key was handled.



192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
# File 'lib/tuile/component/list.rb', line 192

def handle_key(key)
  if !active?
    false
  elsif super
    true
  elsif key == Keys::PAGE_UP
    move_top_line_by(-viewport_lines)
    true
  elsif key == Keys::PAGE_DOWN
    move_top_line_by(viewport_lines)
    true
  elsif key == Keys::ENTER && cursor_on_item?
    fire_item_chosen
    true
  elsif @cursor.handle_key(key, @lines.size, viewport_lines)
    move_viewport_to_cursor
    notify_cursor_changed
    invalidate
    true
  else
    false
  end
end

#handle_mouse(event) ⇒ void

This method returns an undefined value.

Parameters:



243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
# File 'lib/tuile/component/list.rb', line 243

def handle_mouse(event)
  super
  if event.button == :scroll_down
    move_top_line_by(4)
  elsif event.button == :scroll_up
    move_top_line_by(-4)
  else
    return unless rect.contains?(event.point)

    line = event.y - rect.top + top_line
    if @cursor.handle_mouse(line, event, @lines.size)
      move_viewport_to_cursor
      notify_cursor_changed
      invalidate
    end
    fire_item_chosen if event.button == :left && line >= 0 && line < @lines.size && cursor_on_item?
  end
end

#lines {|buffer| ... } ⇒ Array<StyledString>

Without a block, returns the current lines. With a block, fully re-populates the list: “‘ruby list.lines do |buffer|

buffer << "Hello!"

end “‘

Yields:

  • (buffer)

Yield Parameters:

  • buffer (Array)

    mutable buffer to push lines into. Each entry is parsed the same way as the items passed to #lines=.

Yield Returns:

  • (void)

Returns:

  • (Array<StyledString>)

    current lines (when called without a block).



153
154
155
156
157
158
159
# File 'lib/tuile/component/list.rb', line 153

def lines
  return @lines unless block_given?

  buffer = []
  yield buffer
  self.lines = buffer
end

#lines=(lines) ⇒ void

This method returns an undefined value.

Sets new lines. Each entry is coerced into a StyledString (a ‘String` is parsed via StyledString.parse, so embedded ANSI is honored; a StyledString is used as-is; anything else is stringified via `#to_s` first), then split on `n` into separate lines via StyledString#lines, with trailing empty pieces dropped and trailing ASCII whitespace stripped — symmetric with #add_lines, so the stored `@lines` is always `Array<StyledString>`.

Parameters:

  • lines (Array)

    entries are ‘String`, `StyledString`, or anything that responds to `#to_s`.

Raises:

  • (TypeError)


129
130
131
132
133
134
135
136
137
138
# File 'lib/tuile/component/list.rb', line 129

def lines=(lines)
  raise TypeError, "expected Array, got #{lines.inspect}" unless lines.is_a? Array

  @lines = parse_input_lines(lines)
  rebuild_padded_lines
  update_top_line_if_auto_scroll
  notify_cursor_changed
  invalidate
  self.content_size = compute_content_size
end

#repaintvoid

This method returns an undefined value.

Paints the list items into Tuile::Component#rect.

Skips the Tuile::Component#repaint default’s auto-clear: every row of Tuile::Component#rect is painted below (with blank padding past the last item), so the parent contract — “fully draw over your rect” — is met without an upfront wipe.



269
270
271
272
273
274
275
276
277
278
279
# File 'lib/tuile/component/list.rb', line 269

def repaint
  return if rect.empty?

  scrollbar = if scrollbar_visible?
                VerticalScrollBar.new(rect.height, line_count: @lines.size, top_line: @top_line)
              end
  (0...rect.height).each do |row|
    line = paintable_line(row + @top_line, row, scrollbar)
    screen.print TTY::Cursor.move_to(rect.left, row + rect.top), line
  end
end

#select_next(query, include_current: false) ⇒ Boolean

Moves the cursor to the next line whose text contains ‘query` (case-insensitive substring match). Search wraps around the end of the list. Only lines reachable by the current #cursor are considered. Matching uses the line’s plain text — span styles do not affect the match.

Parameters:

  • query (String)

    substring to match. Empty query never matches.

  • include_current (Boolean) (defaults to: false)

    when true, the current cursor position is eligible (useful when the query has just changed and the current line may still match); when false, the search starts after the current position (useful for “find next” key bindings that should advance past the current).

Returns:

  • (Boolean)

    true if a match was found.



229
230
231
# File 'lib/tuile/component/list.rb', line 229

def select_next(query, include_current: false)
  search_and_go(query, include_current: include_current, reverse: false)
end

#select_prev(query, include_current: false) ⇒ Boolean

Mirror of #select_next that walks the list backwards.

Parameters:

  • query (String)
  • include_current (Boolean) (defaults to: false)

Returns:

  • (Boolean)

    true if a match was found.



237
238
239
# File 'lib/tuile/component/list.rb', line 237

def select_prev(query, include_current: false)
  search_and_go(query, include_current: include_current, reverse: true)
end

#tab_stop?Boolean

Returns:

  • (Boolean)


188
# File 'lib/tuile/component/list.rb', line 188

def tab_stop? = true