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 a dark background while preserving each span’s foreground color.

Defined Under Namespace

Classes: Cursor

Constant Summary collapse

CURSOR_BG =

256-color SGR index for the cursor-row background highlight. Matches what ‘Rainbow(…).bg(:darkslategray)` emits.

Returns:

  • (Integer)
59

Instance Attribute Summary collapse

Attributes inherited from Tuile::Component

#key_shortcut, #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_removed, #on_focus, #on_tree, #root, #screen

Constructor Details

#initializeList

Returns a new instance of List.



25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/tuile/component/list.rb', line 25

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.



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

def auto_scroll
  @auto_scroll
end

#cursorCursor

Returns the list’s cursor.

Returns:

  • (Cursor)

    the list’s cursor.



65
66
67
# File 'lib/tuile/component/list.rb', line 65

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.



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

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



45
46
47
# File 'lib/tuile/component/list.rb', line 45

def on_item_chosen
  @on_item_chosen
end

#scrollbar_visibilitySymbol

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

Returns:

  • (Symbol)

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



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

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.



73
74
75
# File 'lib/tuile/component/list.rb', line 73

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.



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

def top_line
  @top_line
end

Instance Method Details

#add_line(line) ⇒ void

This method returns an undefined value.

Adds a line.

Parameters:



169
170
171
# File 'lib/tuile/component/list.rb', line 169

def add_line(line)
  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`.



179
180
181
182
183
184
185
186
187
188
# File 'lib/tuile/component/list.rb', line 179

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

#content_sizeSize

Returns:



191
192
193
194
195
196
197
# File 'lib/tuile/component/list.rb', line 191

def content_size
  @content_size ||= begin
    content_w = @lines.map(&:display_width).max || 0
    width = @lines.empty? ? 0 : content_w + 2
    Size.new(width, @lines.size)
  end
end

#focusable?Boolean

Returns:

  • (Boolean)


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

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.



205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
# File 'lib/tuile/component/list.rb', line 205

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:



256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
# File 'lib/tuile/component/list.rb', line 256

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



158
159
160
161
162
163
164
# File 'lib/tuile/component/list.rb', line 158

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)


134
135
136
137
138
139
140
141
142
143
# File 'lib/tuile/component/list.rb', line 134

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

  @lines = parse_input_lines(lines)
  @content_size = nil
  rebuild_padded_lines
  update_top_line_if_auto_scroll
  notify_cursor_changed
  invalidate
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.



282
283
284
285
286
287
288
289
290
291
292
# File 'lib/tuile/component/list.rb', line 282

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.



242
243
244
# File 'lib/tuile/component/list.rb', line 242

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.



250
251
252
# File 'lib/tuile/component/list.rb', line 250

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

#tab_stop?Boolean

Returns:

  • (Boolean)


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

def tab_stop? = true