Class: Tuile::Component::List
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::List
- Defined in:
- lib/tuile/component/list.rb,
sig/tuile.rbs
Overview
A scrollable list of typed items, one row each, with cursor support.
list = Component::List.new
list.items = people
list.renderer = ->(p) { StyledString.plain(p.name) + screen.theme.hint(" #{p.email}") }
list.cursor = List::Cursor.new # a bare list has none
list.on_item_chosen = ->(index, person) { open(person) }
The #renderer turns an item into one row; the default renders an item
as itself, so a list of Strings or StyledStrings needs none — which
is what #lines= and #build_lines are, items that are their own
rendering (split on \n, one row per line).
There are no appenders. The items are always assigned whole — an app that
grows a list keeps its own array and re-assigns it (list.items = mine) —
so a List stays a snapshot of a collection, which is the only shape a
lazily-sourced provider could ever fill.
Rows wider than the viewport are ellipsized via StyledString#ellipsize
with span styles preserved across the cut. Vertical scrolling is via
#scroll_top_row; enable #auto_scroll to keep the bottom in view. The cursor
responds to arrows, jk, Home/End, Ctrl+U/D and scrolls the list
automatically; its highlight overlays Theme#active_bg_color while
preserving each span's foreground color.
Implementation details
Rendering is lazy: only the rows in the viewport are rendered, each memoized until #items=, #renderer= or a width change drops the cache. So a renderer runs at paint time, on any frame — keep it pure and cheap; work that reaches a service belongs in the item, not in the renderer. #select_next deliberately renders without memoizing: one failed scan would otherwise cache a row per item.
Direct Known Subclasses
Defined Under Namespace
Classes: Cursor
Constant Summary collapse
- DEFAULT_RENDERER =
The default #renderer: an item renders as itself. Every renderer's output is coerced the same way — a StyledString passes through, a
Stringis parsed (so embedded ANSI is honored), anything else is#to_s'd first. :itself.to_proc
Instance Attribute Summary collapse
-
#auto_scroll ⇒ Boolean
@return — if true and new content is set, auto-scrolls to the bottom — but only while the viewport is already pinned to the last row (see #following?).
-
#cursor ⇒ Cursor
@return — the list's cursor.
-
#items ⇒ ::Array[untyped]
@return — the items, one row each.
-
#on_cursor_changed ⇒ Proc?
@return — callback fired when the
(index, item)tuple under the cursor changes (items compared with==). -
#on_item_chosen ⇒ Proc?
@return — callback fired when an item is chosen — by pressing Enter on the cursor's item, or by left-clicking it.
-
#renderer ⇒ Proc, Method
@return — item -> row: a StyledString, a
String(parsed, so embedded ANSI is honored), or anything with#to_s. -
#scroll_top_row ⇒ Integer
@return — top row of the viewport.
-
#scrollbar_visibility ⇒ Symbol
@return — scrollbar visibility:
:goneor:visible. -
#show_cursor_when_inactive ⇒ Boolean
@return — when true, the cursor highlight is painted even while the list is inactive (e.g. when focus is on a sibling search field).
Instance Method Summary collapse
-
#at_bottom? ⇒ Boolean
@return — whether the viewport is pinned to the last row.
-
#blank_row ⇒ StyledString
@return — the blank row painted past the last item.
-
#build_lines ⇒ void
Fully re-populates the list, line-flavored: yields a fresh buffer and assigns it through #lines=, so each entry is coerced, split and rstripped exactly as there.
-
#content_width ⇒ Integer
@return — column width available for row content (rect width minus the scrollbar gutter, when visible).
-
#cursor_on_item? ⇒ Boolean
@return — true if the cursor sits on a real item.
-
#cursor_state ⇒ [[Integer, Object, NilClass]]
@return —
[position, item_at_position], with the item nil when the cursor is off-content. -
#drop_row_cache ⇒ void
Discards every rendered row, so the next paint re-renders the viewport against the current items, renderer and width.
-
#fire_item_chosen ⇒ void
Calls #on_item_chosen with the cursor's current
(index, item). - #focusable? ⇒ Boolean
-
#following? ⇒ Boolean
@return — whether #auto_scroll is currently tailing.
-
#handle_key(key) ⇒ Boolean
@param
key— a key. -
#handle_mouse(event) ⇒ void
@param
event. -
#initialize ⇒ List
constructor
A new instance of List.
-
#lines=(lines) ⇒ void
Sets the items from line-flavored input: each entry is coerced into a StyledString (a
Stringis parsed via StyledString.parse, so embedded ANSI is honored; a StyledString is used as-is; anything else is stringified via#to_sfirst), then split on\ninto separate lines via StyledString#lines, with trailing empty pieces dropped and trailing ASCII whitespace stripped. -
#move_scroll_top_row_by(delta) ⇒ void
Scrolls the list.
-
#move_viewport_to_cursor ⇒ void
Scrolls the viewport so the cursor is visible.
-
#notify_cursor_changed ⇒ void
Fires #on_cursor_changed if #cursor_state differs from the last fired state.
-
#on_width_changed ⇒ void
Drops the rendered-row cache when the wrap width changes.
-
#order_for_search(candidates, current, include_current:, reverse:) ⇒ Object
Rotates
candidates(sorted ascending) so iteration starts from the position appropriate for "find next" / "find prev" with optional inclusion of the current. -
#pad_to_row(row) ⇒ StyledString
Pads
rowto one full row of the viewport (scrollbar gutter excluded). -
#padded_row(index) ⇒ StyledString
@param
index— 0-based index into #items. -
#paintable_row(index, row_in_viewport, scrollbar) ⇒ StyledString
@param
index— 0-based index into #items. -
#parse_input_lines(entries) ⇒ ::Array[StyledString]
Coerces and flattens a list of input entries into trimmed StyledString lines.
- #refresh_rows ⇒ void
-
#render(item) ⇒ StyledString
Renders one item, without populating the row cache — #search_and_go scans with this, and caching a failed scan would grow the cache to one row per item.
-
#repaint ⇒ void
Paints the visible items into #rect, rendering the ones not already cached.
-
#rstrip_styled(line) ⇒ StyledString
Returns
linewith trailing ASCII whitespace (space/tab) dropped, preserving span styles on the surviving prefix. -
#scroll_top_row_max ⇒ Integer
@return — the max value of #scroll_top_row.
-
#scrollbar_visible? ⇒ Boolean
@return — whether the scrollbar should be drawn right now.
-
#search_and_go(query, include_current:, reverse:) ⇒ Boolean
@param
query. -
#select_next(query, include_current: false) ⇒ Boolean
Moves the cursor to the next item whose text contains
query(case-insensitive substring match). -
#select_prev(query, include_current: false) ⇒ Boolean
Mirror of #select_next that walks the list backwards.
-
#split_to_lines(entry) ⇒ ::Array[StyledString]
@param
entry. - #tab_stop? ⇒ Boolean
-
#update_scroll_top_row_if_auto_scroll ⇒ void
If auto-scrolling, recalculate the top row and snap the cursor to the last reachable position.
-
#viewport_rows ⇒ Integer
@return — the number of visible rows.
Constructor Details
#initialize ⇒ List
Returns a new instance of List.
45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 |
# File 'lib/tuile/component/list.rb', line 45 def initialize super @items = [] @renderer = DEFAULT_RENDERER @row_cache = {} @blank_row = nil @auto_scroll = false @follow = true @scroll_top_row = 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_scroll ⇒ Boolean
@return — if true and new content is set, auto-scrolls to the bottom — but only while the viewport is already pinned to the last row (see #following?). Scroll up to read older content and incoming rows stop yanking you back down; scroll back to the bottom and tailing resumes.
84 85 86 |
# File 'lib/tuile/component/list.rb', line 84 def auto_scroll @auto_scroll end |
#cursor ⇒ Cursor
@return — the list's cursor.
96 97 98 |
# File 'lib/tuile/component/list.rb', line 96 def cursor @cursor end |
#items ⇒ ::Array[untyped]
@return — the items, one row each.
159 160 161 |
# File 'lib/tuile/component/list.rb', line 159 def items @items end |
#on_cursor_changed ⇒ Proc?
@return — callback fired when the (index, item) tuple under
the cursor changes (items compared with ==). Called as
proc.call(index, item), with item nil when the cursor is
off-content (Tuile::Component::List::Cursor::None, empty list, or index past the last
item). Fires on cursor moves (key, mouse, search), on #cursor=,
and on #items= when the item 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.
77 78 79 |
# File 'lib/tuile/component/list.rb', line 77 def on_cursor_changed @on_cursor_changed end |
#on_item_chosen ⇒ Proc?
@return — callback fired when an item is chosen — by pressing
Enter on the cursor's item, or by left-clicking it. Called as
proc.call(index, item) with the chosen 0-based index and the item
itself. Never fires when the cursor's position is outside the content
(e.g. Tuile::Component::List::Cursor::None, or empty content).
67 68 69 |
# File 'lib/tuile/component/list.rb', line 67 def on_item_chosen @on_item_chosen end |
#renderer ⇒ Proc, Method
@return — item -> row: a StyledString, a String (parsed,
so embedded ANSI is honored), or anything with #to_s. Only the first
line of a multi-line rendering is kept — one item is one row.
164 165 166 |
# File 'lib/tuile/component/list.rb', line 164 def renderer @renderer end |
#scroll_top_row ⇒ Integer
@return — top row of the viewport. 0 or positive.
93 94 95 |
# File 'lib/tuile/component/list.rb', line 93 def scroll_top_row @scroll_top_row end |
#scrollbar_visibility ⇒ Symbol
@return — scrollbar visibility: :gone or :visible.
99 100 101 |
# File 'lib/tuile/component/list.rb', line 99 def @scrollbar_visibility end |
#show_cursor_when_inactive ⇒ Boolean
@return — 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.
104 105 106 |
# File 'lib/tuile/component/list.rb', line 104 def show_cursor_when_inactive @show_cursor_when_inactive end |
Instance Method Details
#at_bottom? ⇒ Boolean
@return — whether the viewport is pinned to the last row. Drives #following?: re-evaluated on every #scroll_top_row=.
697 |
# File 'lib/tuile/component/list.rb', line 697 def at_bottom? = @scroll_top_row == scroll_top_row_max |
#blank_row ⇒ StyledString
@return — the blank row painted past the last item.
771 772 773 |
# File 'lib/tuile/component/list.rb', line 771 def blank_row @blank_row ||= pad_to_row(StyledString::EMPTY) end |
#build_lines ⇒ void
This method returns an undefined value.
Fully re-populates the list, line-flavored: yields a fresh buffer and
assigns it through #lines=, so each entry is coerced, split and
rstripped exactly as there. The buffer is a plain Array, which a
builder can read mid-build — recording the row a Tuile::Component::List::Cursor::Limited may
land on is the case this exists for:
list.build_lines do |lines|
cursor_positions << lines.size
lines << format_overview(vm)
lines << format_detail(vm)
end
240 241 242 243 244 |
# File 'lib/tuile/component/list.rb', line 240 def build_lines buffer = [] yield buffer self.lines = buffer end |
#content_width ⇒ Integer
@return — column width available for row content (rect width
minus the scrollbar gutter, when visible). 0 when Tuile::Component#rect's width
is non-positive.
749 750 751 752 753 |
# File 'lib/tuile/component/list.rb', line 749 def content_width return 0 if rect.width <= 0 rect.width - ( ? 1 : 0) end |
#cursor_on_item? ⇒ Boolean
@return — true if the cursor sits on a real item.
598 599 600 601 |
# File 'lib/tuile/component/list.rb', line 598 def cursor_on_item? pos = @cursor.position pos >= 0 && pos < @items.size end |
#cursor_state ⇒ [[Integer, Object, NilClass]]
@return — [position, item_at_position], with the item nil when the cursor is
off-content.
614 615 616 617 618 |
# File 'lib/tuile/component/list.rb', line 614 def cursor_state pos = @cursor.position item = pos >= 0 && pos < @items.size ? @items[pos] : nil [pos, item] end |
#drop_row_cache ⇒ void
This method returns an undefined value.
Discards every rendered row, so the next paint re-renders the viewport against the current items, renderer and width.
758 759 760 761 |
# File 'lib/tuile/component/list.rb', line 758 def drop_row_cache @row_cache.clear @blank_row = nil end |
#fire_item_chosen ⇒ void
This method returns an undefined value.
Calls #on_item_chosen with the cursor's current (index, item).
Caller must ensure #cursor_on_item?.
606 607 608 609 |
# File 'lib/tuile/component/list.rb', line 606 def fire_item_chosen pos = @cursor.position @on_item_chosen&.call(pos, @items[pos]) end |
#focusable? ⇒ Boolean
246 |
# File 'lib/tuile/component/list.rb', line 246 def focusable? = true |
#following? ⇒ Boolean
@return — whether #auto_scroll is currently tailing. True while the viewport sits at the last row; flips to false the moment the user scrolls up, and back to true once they scroll to the bottom again. Only consulted when #auto_scroll is enabled.
90 |
# File 'lib/tuile/component/list.rb', line 90 def following? = @follow |
#handle_key(key) ⇒ Boolean
@param key — a key.
@return — true if the key was handled.
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 |
# File 'lib/tuile/component/list.rb', line 252 def handle_key(key) if key == Keys::PAGE_UP move_scroll_top_row_by(-) true elsif key == Keys::PAGE_DOWN move_scroll_top_row_by() true elsif key == Keys::ENTER && cursor_on_item? fire_item_chosen true elsif @cursor.handle_key(key, @items.size, ) notify_cursor_changed invalidate true else false end end |
#handle_mouse(event) ⇒ void
This method returns an undefined value.
@param event
299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 |
# File 'lib/tuile/component/list.rb', line 299 def handle_mouse(event) super if event. == :scroll_down move_scroll_top_row_by(4) elsif event. == :scroll_up move_scroll_top_row_by(-4) else return unless rect.contains?(event.point) item_index = event.y - rect.top + scroll_top_row if @cursor.handle_mouse(item_index, event, @items.size) notify_cursor_changed invalidate end fire_item_chosen if event. == :left && item_index >= 0 && item_index < @items.size && cursor_on_item? end end |
#lines=(lines) ⇒ void
This method returns an undefined value.
Sets the items from line-flavored input: 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. The resulting StyledStrings
*are* the #items, so under the
DEFAULT_RENDERER each is its own row.
@param lines — entries are String, StyledString, or anything that responds to #to_s.
217 218 219 220 221 |
# File 'lib/tuile/component/list.rb', line 217 def lines=(lines) raise TypeError, "expected Array, got #{lines.inspect}" unless lines.is_a? Array self.items = parse_input_lines(lines) end |
#move_scroll_top_row_by(delta) ⇒ void
This method returns an undefined value.
Scrolls the list.
@param delta — negative scrolls up, positive scrolls down.
705 706 707 708 709 710 711 |
# File 'lib/tuile/component/list.rb', line 705 def move_scroll_top_row_by(delta) new_scroll_top_row = (@scroll_top_row + delta).clamp(0, scroll_top_row_max) return if @scroll_top_row == new_scroll_top_row @scroll_top_row = new_scroll_top_row invalidate end |
#move_viewport_to_cursor ⇒ void
This method returns an undefined value.
Scrolls the viewport so the cursor is visible.
681 682 683 684 685 686 687 688 689 690 |
# File 'lib/tuile/component/list.rb', line 681 def pos = @cursor.position return unless pos >= 0 if @scroll_top_row > pos self.scroll_top_row = pos elsif pos > @scroll_top_row + rect.height - 1 self.scroll_top_row = pos - rect.height + 1 end end |
#notify_cursor_changed ⇒ void
This method returns an undefined value.
Fires #on_cursor_changed if #cursor_state differs from the last fired state. Idempotent — safe to call after any mutation.
623 624 625 626 627 628 629 |
# File 'lib/tuile/component/list.rb', line 623 def notify_cursor_changed state = cursor_state return if state == @last_cursor_state @last_cursor_state = state @on_cursor_changed&.call(*state) end |
#on_width_changed ⇒ void
This method returns an undefined value.
Drops the rendered-row cache when the wrap width changes. The wrap
width depends on Tuile::Component#rect.width and the scrollbar gutter, both of
which trigger this hook. Also re-evaluates #auto_scroll: if items were
assigned while the rect was empty (e.g. a Popup-wrapped list was
populated before the popup was opened), the auto-scroll update
was skipped because there was no viewport — re-run it now that there
is one, so the list snaps to the bottom on first paint.
552 553 554 555 556 |
# File 'lib/tuile/component/list.rb', line 552 def on_width_changed super drop_row_cache update_scroll_top_row_if_auto_scroll end |
#order_for_search(candidates, current, include_current:, reverse:) ⇒ Object
Rotates candidates (sorted ascending) so iteration starts from the
position appropriate for "find next" / "find prev" with optional
inclusion of the current.
@param candidates
@param current
@param include_current
@param reverse
661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 |
# File 'lib/tuile/component/list.rb', line 661 def order_for_search(candidates, current, include_current:, reverse:) if reverse before, after = if include_current [candidates.select { _1 <= current }, candidates.select { _1 > current }] else [candidates.select { _1 < current }, candidates.select { _1 >= current }] end before.reverse + after.reverse else after, before = if include_current [candidates.select { _1 >= current }, candidates.select { _1 < current }] else [candidates.select { _1 > current }, candidates.select { _1 <= current }] end after + before end end |
#pad_to_row(row) ⇒ StyledString
Pads row to one full row of the viewport (scrollbar gutter
excluded). Rows wider than the content area are ellipsized via
StyledString#ellipsize (span styles survive the cut); shorter
ones are padded with default-styled spaces.
@param row
@return — exactly #content_width display columns wide (or StyledString::EMPTY when content_width is non-positive).
797 798 799 800 801 802 803 804 805 806 |
# File 'lib/tuile/component/list.rb', line 797 def pad_to_row(row) cw = content_width return StyledString::EMPTY if cw <= 0 return StyledString.plain(" " * cw) if cw < 2 text_width = cw - 2 body = row.ellipsize(text_width) fill = cw - 2 - body.display_width StyledString.plain(" ") + body + StyledString.plain(" " * (fill + 1)) end |
#padded_row(index) ⇒ StyledString
@param index — 0-based index into #items.
@return — the item's padded row, rendered on first use and memoized until #drop_row_cache.
766 767 768 |
# File 'lib/tuile/component/list.rb', line 766 def padded_row(index) @row_cache[index] ||= pad_to_row(render(@items[index])) end |
#paintable_row(index, row_in_viewport, scrollbar) ⇒ StyledString
@param index — 0-based index into #items.
@param row_in_viewport — 0-based row within the viewport.
@param scrollbar — scrollbar instance, or nil if not shown.
@return — paintable row exactly rect.width columns wide;
highlighted if cursor is here.
814 815 816 817 818 819 820 |
# File 'lib/tuile/component/list.rb', line 814 def paintable_row(index, , ) base = index < @items.size ? padded_row(index) : blank_row is_cursor = (active? || @show_cursor_when_inactive) && index < @items.size && @cursor.position == index styled = is_cursor ? base.with_bg(screen.theme.active_bg_color) : base styled += StyledString.plain(.()) if styled end |
#parse_input_lines(entries) ⇒ ::Array[StyledString]
Coerces and flattens a list of input entries into trimmed
StyledString lines. Each entry becomes a StyledString (String
via StyledString.parse, StyledString passed through, anything else
via #to_s), then split on \n via StyledString#lines — with
trailing empty pieces dropped (matching String#split("\n")'s
default behavior, so a lone "" entry adds no row) — and trailing ASCII
whitespace stripped on each resulting line.
@param entries
569 570 571 |
# File 'lib/tuile/component/list.rb', line 569 def parse_input_lines(entries) entries.flat_map { |entry| split_to_lines(entry) } end |
#refresh_rows ⇒ void
201 202 203 204 |
# File 'lib/tuile/component/list.rb', line 201 def refresh_rows drop_row_cache invalidate end |
#render(item) ⇒ StyledString
Renders one item, without populating the row cache — #search_and_go scans with this, and caching a failed scan would grow the cache to one row per item.
@param item
@return — one row: the #renderer's output coerced to a
StyledString, cut to its first line since a \n reaching the buffer
would corrupt the frame.
782 783 784 785 786 787 788 |
# File 'lib/tuile/component/list.rb', line 782 def render(item) rendered = @renderer.call(item) rendered = StyledString.parse(rendered.to_s) unless rendered.is_a?(StyledString) return rendered unless rendered.spans.any? { _1.text.include?("\n") } rendered.lines.first end |
#repaint ⇒ void
This method returns an undefined value.
Paints the visible items into Tuile::Component#rect, rendering the ones not already cached.
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. Rows go through Tuile::Component#draw_text, so content and blank filler inherit Tuile::Component#effective_bg_color (a Tuile::Component#bg_color set here or on an ancestor); the cursor row's Theme#active_bg_color highlight composes on top of it.
329 330 331 332 333 334 335 336 337 338 |
# File 'lib/tuile/component/list.rb', line 329 def repaint return if rect.empty? = if VerticalScrollBar.new(rect.height, row_count: @items.size, scroll_top_row: @scroll_top_row) end (0...rect.height).each do |row| draw_text(rect.left, row + rect.top, paintable_row(row + @scroll_top_row, row, )) end end |
#rstrip_styled(line) ⇒ StyledString
Returns line with trailing ASCII whitespace (space/tab) dropped,
preserving span styles on the surviving prefix. Whitespace chars are
all single-column ASCII, so byte-count delta equals column-count
delta and StyledString#slice can do the cut.
@param line
588 589 590 591 592 593 594 595 |
# File 'lib/tuile/component/list.rb', line 588 def rstrip_styled(line) plain = line.to_s trailing = plain.length - plain.rstrip.length return line if trailing.zero? return StyledString::EMPTY if trailing == plain.length line.slice(0, line.display_width - trailing) end |
#scroll_top_row_max ⇒ Integer
@return — the max value of #scroll_top_row.
693 |
# File 'lib/tuile/component/list.rb', line 693 def scroll_top_row_max = (@items.size - rect.height).clamp(0, nil) |
#scrollbar_visible? ⇒ Boolean
@return — whether the scrollbar should be drawn right now.
740 741 742 743 744 |
# File 'lib/tuile/component/list.rb', line 740 def return false if rect.empty? @scrollbar_visibility == :visible end |
#search_and_go(query, include_current:, reverse:) ⇒ Boolean
@param query
@param include_current
@param reverse
635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 |
# File 'lib/tuile/component/list.rb', line 635 def search_and_go(query, include_current:, reverse:) return false if query.empty? candidates = @cursor.candidate_positions(@items.size) return false if candidates.empty? ordered = order_for_search(candidates, @cursor.position, include_current: include_current, reverse: reverse) query_lc = query.downcase match = ordered.find { |idx| render(@items[idx]).to_s.downcase.include?(query_lc) } return false unless match @cursor.go(match) notify_cursor_changed invalidate true end |
#select_next(query, include_current: false) ⇒ Boolean
Moves the cursor to the next item whose text contains query
(case-insensitive substring match). Search wraps around the end of the
list. Only items reachable by the current #cursor are considered.
Matching uses the rendered row's plain text — span styles do not affect the
match.
@param query — substring to match. Empty query never matches.
@param include_current — when true, the current cursor position is eligible (useful when the query has just changed and the current item may still match); when false, the search starts after the current position (useful for "find next" key bindings that should advance past the current).
@return — true if a match was found.
285 286 287 |
# File 'lib/tuile/component/list.rb', line 285 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.
@param query
@param include_current
@return — true if a match was found.
293 294 295 |
# File 'lib/tuile/component/list.rb', line 293 def select_prev(query, include_current: false) search_and_go(query, include_current: include_current, reverse: true) end |
#split_to_lines(entry) ⇒ ::Array[StyledString]
@param entry
575 576 577 578 579 580 |
# File 'lib/tuile/component/list.rb', line 575 def split_to_lines(entry) styled = entry.is_a?(StyledString) ? entry : StyledString.parse(entry.to_s) parts = styled.lines parts.pop while parts.last && parts.last.empty? parts.map { |line| rstrip_styled(line) } end |
#tab_stop? ⇒ Boolean
248 |
# File 'lib/tuile/component/list.rb', line 248 def tab_stop? = true |
#update_scroll_top_row_if_auto_scroll ⇒ void
This method returns an undefined value.
If auto-scrolling, recalculate the top row and snap the cursor to the
last reachable position. Without the cursor snap the viewport gets
yanked back to wherever the cursor sat on the next arrow press,
negating the auto-scroll. Skipped when Tuile::Component#rect is empty: without a
viewport the "items minus viewport" formula yields @items.size,
which would leave scroll_top_row past the last item once a real rect
arrives. #on_width_changed re-runs this hook when the rect grows so
the snap-to-bottom intent is preserved.
Gated on #following?: once the user scrolls up off the bottom the
cursor snap and viewport pin are both skipped, so reading older
content is not interrupted by incoming items. #scroll_top_row= re-arms
@follow when the viewport returns to the bottom.
727 728 729 730 731 732 733 734 735 736 737 |
# File 'lib/tuile/component/list.rb', line 727 def update_scroll_top_row_if_auto_scroll return unless @auto_scroll && @follow return if rect.empty? notify_cursor_changed if @cursor.go_to_last(@items.size) new_scroll_top_row = (@items.size - ).clamp(0, nil) return unless @scroll_top_row != new_scroll_top_row self.scroll_top_row = new_scroll_top_row end |
#viewport_rows ⇒ Integer
@return — the number of visible rows.
700 |
# File 'lib/tuile/component/list.rb', line 700 def = rect.height |