Class: Tuile::Component::List

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

Overview

A scrollable list of String items with cursor support.

Items are lines painted directly into the component’s #rect. Lines are automatically clipped horizontally. 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.

Defined Under Namespace

Classes: Cursor

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.



16
17
18
19
20
21
22
23
24
25
26
27
# File 'lib/tuile/component/list.rb', line 16

def initialize
  super
  @lines = []
  @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.



48
49
50
# File 'lib/tuile/component/list.rb', line 48

def auto_scroll
  @auto_scroll
end

#cursorCursor

Returns the list’s cursor.

Returns:

  • (Cursor)

    the list’s cursor.



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

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



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

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 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 line. Never fires when the cursor’s position is outside the content (e.g. Tuile::Component::List::Cursor::None, or empty content).



34
35
36
# File 'lib/tuile/component/list.rb', line 34

def on_item_chosen
  @on_item_chosen
end

#scrollbar_visibilitySymbol

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

Returns:

  • (Symbol)

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



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

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.



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

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.



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

def top_line
  @top_line
end

Instance Method Details

#add_line(line) ⇒ void

This method returns an undefined value.

Adds a line.

Parameters:

  • line (String)


149
150
151
# File 'lib/tuile/component/list.rb', line 149

def add_line(line)
  add_lines [line]
end

#add_lines(lines) ⇒ void

This method returns an undefined value.

Appends given lines. Each entry is coerced via ‘#to_s`, split on `n` into separate lines, and trailing whitespace stripped — symmetric with #lines=.

Parameters:

  • lines (Array)

    entries need only respond to ‘#to_s`.



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

def add_lines(lines)
  screen.check_locked
  @lines += lines.flat_map { it.to_s.split("\n") }.map(&:rstrip)
  @content_size = nil
  update_top_line_if_auto_scroll
  notify_cursor_changed
  invalidate
end

#content_sizeSize

Returns:



168
169
170
171
172
173
174
# File 'lib/tuile/component/list.rb', line 168

def content_size
  @content_size ||= begin
    content_width = @lines.map { |line| Unicode::DisplayWidth.of(Rainbow.uncolor(line)) }.max || 0
    width = @lines.empty? ? 0 : content_width + 2
    Size.new(width, @lines.size)
  end
end

#focusable?Boolean

Returns:

  • (Boolean)


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

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.



182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
# File 'lib/tuile/component/list.rb', line 182

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:



231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# File 'lib/tuile/component/list.rb', line 231

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<String>

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

    mutable buffer to push lines into.

Yield Returns:

  • (void)

Returns:

  • (Array<String>)

    current lines (when called without a block).



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

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 via ‘#to_s`, split on `n` into separate lines, and trailing whitespace stripped — symmetric with #add_lines, so the stored `@lines` is always `Array<String>`.

Parameters:

  • lines (Array)

    new lines. Entries need only respond to ‘#to_s`.

Raises:

  • (TypeError)


117
118
119
120
121
122
123
124
125
# File 'lib/tuile/component/list.rb', line 117

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

  @lines = lines.flat_map { it.to_s.split("\n") }.map(&:rstrip)
  @content_size = nil
  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 padded content past the last item), so the parent contract — “fully draw over your rect” — is met without an upfront wipe.



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

def repaint
  return if rect.empty?

  width = rect.width
  scrollbar = if scrollbar_visible?
                VerticalScrollBar.new(rect.height, line_count: @lines.size, top_line: @top_line)
              end
  (0..(rect.height - 1)).each do |line_no|
    line_index = line_no + @top_line
    line = paintable_line(line_index, line_no, width, scrollbar)
    screen.print TTY::Cursor.move_to(rect.left, line_no + 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.

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.



217
218
219
# File 'lib/tuile/component/list.rb', line 217

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.



225
226
227
# File 'lib/tuile/component/list.rb', line 225

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

#tab_stop?Boolean

Returns:

  • (Boolean)


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

def tab_stop? = true