Class: Tuile::Component::List

Inherits:
Component
  • Object
show all
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

Tuile::Component::ListDropdown::Menu

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 String is parsed (so embedded ANSI is honored), anything else is #to_s'd first.

Returns:

  • (Proc)
:itself.to_proc

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initializeList

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_scrollBoolean

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

Returns:

  • (Boolean)


84
85
86
# File 'lib/tuile/component/list.rb', line 84

def auto_scroll
  @auto_scroll
end

#cursorCursor

@return — the list's cursor.

Returns:



96
97
98
# File 'lib/tuile/component/list.rb', line 96

def cursor
  @cursor
end

#items::Array[untyped]

@return — the items, one row each.

Returns:

  • (::Array[untyped])


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

def items
  @items
end

#on_cursor_changedProc?

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

Returns:

  • (Proc, nil)


77
78
79
# File 'lib/tuile/component/list.rb', line 77

def on_cursor_changed
  @on_cursor_changed
end

#on_item_chosenProc?

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

Returns:

  • (Proc, nil)


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

def on_item_chosen
  @on_item_chosen
end

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

Returns:

  • (Proc, Method)


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

def renderer
  @renderer
end

#scroll_top_rowInteger

@return — top row of the viewport. 0 or positive.

Returns:

  • (Integer)


93
94
95
# File 'lib/tuile/component/list.rb', line 93

def scroll_top_row
  @scroll_top_row
end

#scrollbar_visibilitySymbol

@return — scrollbar visibility: :gone or :visible.

Returns:

  • (Symbol)


99
100
101
# File 'lib/tuile/component/list.rb', line 99

def scrollbar_visibility
  @scrollbar_visibility
end

#show_cursor_when_inactiveBoolean

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

Returns:

  • (Boolean)


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

Returns:

  • (Boolean)


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

def at_bottom? = @scroll_top_row == scroll_top_row_max

#blank_rowStyledString

@return — the blank row painted past the last item.

Returns:



793
794
795
# File 'lib/tuile/component/list.rb', line 793

def blank_row
  @blank_row ||= pad_to_row(StyledString::EMPTY)
end

#build_linesvoid

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_widthInteger

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

Returns:

  • (Integer)


771
772
773
774
775
# File 'lib/tuile/component/list.rb', line 771

def content_width
  return 0 if rect.width <= 0

  rect.width - (scrollbar_visible? ? 1 : 0)
end

#cursor_on_item?Boolean

@return — true if the cursor sits on a real item.

Returns:

  • (Boolean)


620
621
622
623
# File 'lib/tuile/component/list.rb', line 620

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.

Returns:

  • ([[Integer, Object, NilClass]])


636
637
638
639
640
# File 'lib/tuile/component/list.rb', line 636

def cursor_state
  pos = @cursor.position
  item = pos >= 0 && pos < @items.size ? @items[pos] : nil
  [pos, item]
end

#drop_row_cachevoid

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.



780
781
782
783
# File 'lib/tuile/component/list.rb', line 780

def drop_row_cache
  @row_cache.clear
  @blank_row = nil
end

#fire_item_chosenvoid

This method returns an undefined value.

Calls #on_item_chosen with the cursor's current (index, item). Caller must ensure #cursor_on_item?.



628
629
630
631
# File 'lib/tuile/component/list.rb', line 628

def fire_item_chosen
  pos = @cursor.position
  @on_item_chosen&.call(pos, @items[pos])
end

#focusable?Boolean

Returns:

  • (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.

Returns:

  • (Boolean)


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.

Parameters:

  • key (String)

Returns:

  • (Boolean)


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(-viewport_rows)
    true
  elsif key == Keys::PAGE_DOWN
    move_scroll_top_row_by(viewport_rows)
    true
  elsif key == Keys::ENTER && cursor_on_item?
    fire_item_chosen
    true
  elsif @cursor.handle_key(key, @items.size, viewport_rows)
    move_viewport_to_cursor
    notify_cursor_changed
    invalidate
    true
  else
    false
  end
end

#handle_mouse(event) ⇒ void

This method returns an undefined value.

@param event

Parameters:



321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
# File 'lib/tuile/component/list.rb', line 321

def handle_mouse(event)
  super
  if event.button == :scroll_down
    move_scroll_top_row_by(4)
  elsif event.button == :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)
      move_viewport_to_cursor
      notify_cursor_changed
      invalidate
    end
    fire_item_chosen if event.button == :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.

Parameters:

  • lines (::Array[untyped])


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.

Parameters:

  • delta (Integer)


727
728
729
730
731
732
733
# File 'lib/tuile/component/list.rb', line 727

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_cursorvoid

This method returns an undefined value.

Scrolls the viewport so the cursor is visible.



703
704
705
706
707
708
709
710
711
712
# File 'lib/tuile/component/list.rb', line 703

def move_viewport_to_cursor
  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_changedvoid

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.



645
646
647
648
649
650
651
# File 'lib/tuile/component/list.rb', line 645

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_changedvoid

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.



574
575
576
577
578
# File 'lib/tuile/component/list.rb', line 574

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



683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
# File 'lib/tuile/component/list.rb', line 683

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

Parameters:

Returns:



819
820
821
822
823
824
825
826
827
828
# File 'lib/tuile/component/list.rb', line 819

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.

Parameters:

  • index (Integer)

Returns:



788
789
790
# File 'lib/tuile/component/list.rb', line 788

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.

Parameters:

Returns:



836
837
838
839
840
841
842
# File 'lib/tuile/component/list.rb', line 836

def paintable_row(index, row_in_viewport, scrollbar)
  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(scrollbar.scrollbar_char(row_in_viewport)) if scrollbar
  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

Parameters:

  • entries (::Array[untyped])

Returns:



591
592
593
# File 'lib/tuile/component/list.rb', line 591

def parse_input_lines(entries)
  entries.flat_map { |entry| split_to_lines(entry) }
end

#refresh_rowsvoid

This method returns an undefined value.

Re-renders every row — for a #renderer whose inputs changed while it and #items stayed the same, e.g. one prefixing a marker read from a selection it closes over:

def value=(new_value)   # RadioGroup: the marked row moved
super
content.refresh_rows
end


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.

Parameters:

  • item (Object)

Returns:



804
805
806
807
808
809
810
# File 'lib/tuile/component/list.rb', line 804

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

#repaintvoid

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.



351
352
353
354
355
356
357
358
359
360
# File 'lib/tuile/component/list.rb', line 351

def repaint
  return if rect.empty?

  scrollbar = if scrollbar_visible?
                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, scrollbar))
  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

Parameters:

Returns:



610
611
612
613
614
615
616
617
# File 'lib/tuile/component/list.rb', line 610

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_maxInteger

@return — the max value of #scroll_top_row.

Returns:

  • (Integer)


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

def scroll_top_row_max = (@items.size - rect.height).clamp(0, nil)

#scrollbar_visible?Boolean

@return — whether the scrollbar should be drawn right now.

Returns:

  • (Boolean)


762
763
764
765
766
# File 'lib/tuile/component/list.rb', line 762

def scrollbar_visible?
  return false if rect.empty?

  @scrollbar_visibility == :visible
end

#search_and_go(query, include_current:, reverse:) ⇒ Boolean

@param query

@param include_current

@param reverse

Parameters:

  • query (String)
  • include_current: (Boolean)
  • reverse: (Boolean)

Returns:

  • (Boolean)


657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
# File 'lib/tuile/component/list.rb', line 657

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)
  move_viewport_to_cursor
  notify_cursor_changed
  invalidate
  true
end

#select(index) ⇒ Boolean

Moves the cursor to the item at index, scrolling it into view and firing #on_cursor_changed — the positional member of the #select_next / #select_prev family, for a caller that already knows which item it wants:

list.select(items.index(chosen))

Refuses an index the current #cursor can't reach — out of range, or anything at all under Tuile::Component::List::Cursor::None — rather than stranding the cursor off-content.

@param index

@return — whether the cursor moved there.

Parameters:

  • index (Integer)

Returns:

  • (Boolean)


309
310
311
312
313
314
315
316
317
# File 'lib/tuile/component/list.rb', line 309

def select(index)
  return false unless @cursor.candidate_positions(@items.size).include?(index)

  @cursor.go(index)
  move_viewport_to_cursor
  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.

Parameters:

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

Returns:

  • (Boolean)


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.

Parameters:

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

Returns:

  • (Boolean)


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

Parameters:

  • entry (Object)

Returns:



597
598
599
600
601
602
# File 'lib/tuile/component/list.rb', line 597

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

Returns:

  • (Boolean)


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

def tab_stop? = true

#update_scroll_top_row_if_auto_scrollvoid

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.



749
750
751
752
753
754
755
756
757
758
759
# File 'lib/tuile/component/list.rb', line 749

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 - viewport_rows).clamp(0, nil)
  return unless @scroll_top_row != new_scroll_top_row

  self.scroll_top_row = new_scroll_top_row
end

#viewport_rowsInteger

@return — the number of visible rows.

Returns:

  • (Integer)


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

def viewport_rows = rect.height