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



36
37
38
# File 'lib/tuile/component/list.rb', line 36

def auto_scroll
  @auto_scroll
end

#cursorCursor

Returns the list’s cursor.

Returns:

  • (Cursor)

    the list’s cursor.



42
43
44
# File 'lib/tuile/component/list.rb', line 42

def cursor
  @cursor
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).



32
33
34
# File 'lib/tuile/component/list.rb', line 32

def on_item_chosen
  @on_item_chosen
end

#scrollbar_visibilitySymbol

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

Returns:

  • (Symbol)

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



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

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.



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

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.



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

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)


135
136
137
# File 'lib/tuile/component/list.rb', line 135

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



144
145
146
147
148
149
150
# File 'lib/tuile/component/list.rb', line 144

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
  invalidate
end

#content_sizeSize

Returns:



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

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)


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

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.



165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
# File 'lib/tuile/component/list.rb', line 165

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
    invalidate
    true
  else
    false
  end
end

#handle_mouse(event) ⇒ void

This method returns an undefined value.

Parameters:



213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
# File 'lib/tuile/component/list.rb', line 213

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



124
125
126
127
128
129
130
# File 'lib/tuile/component/list.rb', line 124

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)


104
105
106
107
108
109
110
111
# File 'lib/tuile/component/list.rb', line 104

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
  invalidate
end

#repaintvoid

This method returns an undefined value.

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



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

def repaint
  super
  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.



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

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.



207
208
209
# File 'lib/tuile/component/list.rb', line 207

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