Class: Tuile::Component::Scroller
- Inherits:
-
Component
- Object
- Component
- Tuile::Component::Scroller
- Includes:
- HasContent
- Defined in:
- lib/tuile/component/scroller.rb,
sig/tuile.rbs
Overview
A viewport onto a taller pile of components: one content child, scrolled vertically, with a VerticalScrollBar in the rightmost column.
form = Component::FormLayout.new
# …nine fields, 27 rows of them…
window.content = Component::Scroller.new(form, content_rows: 27)
You say how tall the content is. Nothing in Tuile measures
(D_declared_size), so #content_rows is the number of rows the content
wants, and the child is handed that many or the viewport's height,
whichever is more — a rect taller than this component's, whose top goes
negative as you scroll. Keep it current as the content grows.
Four things scroll it: the wheel, the bar's handle (dragging it wants
run_event_loop(capture_mouse: :drag); pressing the track pages either
way), #scroll_half_page_up /
#scroll_half_page_down from app code, and #scroll_to_visible,
which Screen#focused= makes on every focus change — so Tab into a field
below the fold brings that field into view. It claims no keys:
PgUp/PgDn and the arrows belong to the focused field.
A scrolled-away child is not hidden. It keeps its rect, its keys and its
tab stop, and merely paints into cells the clip drops (D_clip), which
is what lets Tab reach it in the first place.
Why a component you compose rather than a capability every container
grows, and what the app-supplied row count costs: D_scroller.
Instance Attribute Summary collapse
-
#content_rows ⇒ Integer
@return — how tall the content says it is, in rows.
-
#scroll_top_row ⇒ Integer
@return — the content row painted at the top of the viewport.
-
#scrollbar_visibility ⇒ Symbol
@return —
:visible(the default),:gone, or:auto— shown only while #content_rows exceeds #viewport_rows.
Attributes included from HasContent
Instance Method Summary collapse
-
#content_height ⇒ Integer
@return — the rows the content child is given: what it declared, or the viewport, whichever is taller.
-
#focusable? ⇒ Boolean
A container, so focus lands here only to be forwarded into the content by HasContent#handle_focus; it is no tab stop of its own.
-
#half_page_rows ⇒ Integer
@return — half a viewport, at least one row.
- #handle_focus ⇒ void
-
#handle_mouse_scroll?(event) ⇒ Boolean
Four rows a notch, and declines — so the notch bubbles to an outer scroller — once this one is at that end.
-
#initialize(content = nil, content_rows: 0) ⇒ Scroller
constructor
@param
content— the content child, also settable later through HasContent#content=. -
#inner_width ⇒ Integer
@return — columns left for the content once the bar has its own.
- #inspect_details ⇒ Array<String>
-
#move_scroll_top_row_by(delta) ⇒ void
@param
delta— negative scrolls up, positive down. -
#place_scrollbar ⇒ void
The bar's whole state, pushed in one place: its column, and the two numbers it paints a handle from.
-
#relayout ⇒ void
The content is as wide as the viewport minus the bar's columns, and as tall as it says it is — lifted by #scroll_top_row, which is what puts a negative
topon a Tuile rect. -
#scroll_delta_for(rect) ⇒ Integer
Rows to scroll — positive down — to bring
rect(in this component's coordinates) fully into view. -
#scroll_half_page_down ⇒ void
#scroll_half_page_up's twin, clamped at the last row.
-
#scroll_half_page_up ⇒ void
Scrolls up half a viewport, clamped at the top — the verb to bind a key to, a scroller claiming none of its own.
-
#scroll_to_visible(rect = local_extent_rect) ⇒ void
Scrolls the minimum that brings
rectfully into the viewport, then passes the request on to whatever scrolls above this — see #scroll_to_visible. -
#scroll_top_row_max ⇒ Integer
@return — the largest #scroll_top_row that still shows content in every viewport row;
0when the content fits. -
#scrollbar_columns ⇒ Integer
The bar's column plus the blank one beside it, so content never runs into the glyph —
TextView's reserve, including its drop below width 3 (D_scrollbar_reserve). -
#scrollbar_visible? ⇒ Boolean
Derived on every read, so #relayout places the bar and sizes the content from one answer.
-
#validate_rows(rows, name) ⇒ Integer
@param
rows. -
#viewport_rows ⇒ Integer
@return — rows of content on screen at once — #rect's height, the bar taking a column rather than a row.
Constructor Details
#initialize(content = nil, content_rows: 0) ⇒ Scroller
@param content — the content child, also settable later through HasContent#content=.
@param content_rows — see #content_rows=.
38 39 40 41 42 43 44 45 46 47 48 |
# File 'lib/tuile/component/scroller.rb', line 38 def initialize(content = nil, content_rows: 0) super() @content = nil @scroll_top_row = 0 @content_rows = validate_rows(content_rows, :content_rows) @scrollbar_visibility = :visible @scrollbar = VerticalScrollBar.new(row_count: @content_rows) @scrollbar.on_scroll_request { self.scroll_top_row = _1.scroll_top_row } add_child(@scrollbar) # chrome, appended: the content goes in at 0 self.content = content unless content.nil? end |
Instance Attribute Details
#content_rows ⇒ Integer
@return — how tall the content says it is, in rows.
54 55 56 |
# File 'lib/tuile/component/scroller.rb', line 54 def content_rows @content_rows end |
#scroll_top_row ⇒ Integer
@return — the content row painted at the top of the viewport.
51 52 53 |
# File 'lib/tuile/component/scroller.rb', line 51 def scroll_top_row @scroll_top_row end |
#scrollbar_visibility ⇒ Symbol
@return — :visible (the default), :gone, or :auto — shown
only while #content_rows exceeds #viewport_rows.
58 59 60 |
# File 'lib/tuile/component/scroller.rb', line 58 def @scrollbar_visibility end |
Instance Method Details
#content_height ⇒ Integer
@return — the rows the content child is given: what it declared, or the viewport, whichever is taller. An unset #content_rows makes a scroller that behaves like a plain one-child container.
211 |
# File 'lib/tuile/component/scroller.rb', line 211 def content_height = [@content_rows, ].max |
#focusable? ⇒ Boolean
A container, so focus lands here only to be forwarded into the content by HasContent#handle_focus; it is no tab stop of its own.
164 |
# File 'lib/tuile/component/scroller.rb', line 164 def focusable? = true |
#half_page_rows ⇒ Integer
@return — half a viewport, at least one row.
244 |
# File 'lib/tuile/component/scroller.rb', line 244 def half_page_rows = [ / 2, 1].max |
#handle_focus ⇒ void
This method returns an undefined value.
8230 |
# File 'sig/tuile.rbs', line 8230
def handle_focus: () -> void
|
#handle_mouse_scroll?(event) ⇒ Boolean
Four rows a notch, and declines — so the notch bubbles to an outer scroller — once this one is at that end.
@param event
151 152 153 154 155 156 157 158 159 |
# File 'lib/tuile/component/scroller.rb', line 151 def handle_mouse_scroll?(event) before = @scroll_top_row case event.direction when :down then move_scroll_top_row_by(4) when :up then move_scroll_top_row_by(-4) else return false end @scroll_top_row != before end |
#inner_width ⇒ Integer
@return — columns left for the content once the bar has its own.
214 |
# File 'lib/tuile/component/scroller.rb', line 214 def inner_width = [rect.width - , 0].max |
#inspect_details ⇒ Array<String>
167 |
# File 'lib/tuile/component/scroller.rb', line 167 def inspect_details = super + ["#{scroll_top_row}/#{content_rows} rows"] |
#move_scroll_top_row_by(delta) ⇒ void
This method returns an undefined value.
@param delta — negative scrolls up, positive down.
248 249 250 251 |
# File 'lib/tuile/component/scroller.rb', line 248 def move_scroll_top_row_by(delta) clamped = (@scroll_top_row + delta).clamp(0, scroll_top_row_max) self.scroll_top_row = clamped unless @scroll_top_row == clamped end |
#place_scrollbar ⇒ void
This method returns an undefined value.
The bar's whole state, pushed in one place: its column, and the two
numbers it paints a handle from. It holds no authority over either —
it asks through on_scroll_request and is told here.
187 188 189 190 191 |
# File 'lib/tuile/component/scroller.rb', line 187 def @scrollbar.rect = ? Rect.new(rect.width - 1, 0, 1, rect.height) : Rect.new(0, 0, 0, 0) @scrollbar.row_count = @content_rows @scrollbar.scroll_top_row = @scroll_top_row end |
#relayout ⇒ void
This method returns an undefined value.
The content is as wide as the viewport minus the bar's columns, and as
tall as it says it is — lifted by #scroll_top_row, which is what puts
a negative top on a Tuile rect. Where all three knobs end: each moves
the content's rect, the bar's handle, or both.
176 177 178 179 |
# File 'lib/tuile/component/scroller.rb', line 176 def relayout content&.rect = Rect.new(0, -@scroll_top_row, inner_width, content_height) end |
#scroll_delta_for(rect) ⇒ Integer
Rows to scroll — positive down — to bring rect (in this component's
coordinates) fully into view. The four cases are JViewport's: fully
visible and covering the viewport both answer zero, and a rect too tall
to fit aligns its top.
@param rect
199 200 201 202 203 204 205 206 |
# File 'lib/tuile/component/scroller.rb', line 199 def scroll_delta_for(rect) top = rect.top bottom = top + rect.height return 0 if (top >= 0 && bottom <= ) || (top <= 0 && bottom >= ) return top if top.negative? [bottom - , top].min end |
#scroll_half_page_down ⇒ void
This method returns an undefined value.
#scroll_half_page_up's twin, clamped at the last row.
124 |
# File 'lib/tuile/component/scroller.rb', line 124 def scroll_half_page_down = move_scroll_top_row_by(half_page_rows) |
#scroll_half_page_up ⇒ void
This method returns an undefined value.
Scrolls up half a viewport, clamped at the top — the verb to bind a key to, a scroller claiming none of its own.
120 |
# File 'lib/tuile/component/scroller.rb', line 120 def scroll_half_page_up = move_scroll_top_row_by(-half_page_rows) |
#scroll_to_visible(rect = local_extent_rect) ⇒ void
This method returns an undefined value.
Scrolls the minimum that brings rect fully into the viewport, then
passes the request on to whatever scrolls above this — see
Tuile::Component#scroll_to_visible.
Nothing moves when rect is already visible, and nothing moves when it
covers the viewport, so a focused child taller than the viewport
doesn't jump to its own top every time it is re-focused. When it must
move and still cannot fit, the top edge wins.
@param rect — in this component's coordinates.
136 137 138 139 140 141 142 143 144 145 |
# File 'lib/tuile/component/scroller.rb', line 136 def scroll_to_visible(rect = local_extent_rect) before = @scroll_top_row move_scroll_top_row_by(scroll_delta_for(rect)) # The scroll just latched moved every rect beneath this scroller, and # layout is deferred — so settle before the request climbs on, or a # second request (a {FormItem} makes two) measures the content where it # used to be and scrolls again to undo this one. flush_layout unless before == @scroll_top_row super(rect.moved_by(Point.new(0, before - @scroll_top_row))) end |
#scroll_top_row_max ⇒ Integer
@return — the largest #scroll_top_row that still shows content
in every viewport row; 0 when the content fits.
241 |
# File 'lib/tuile/component/scroller.rb', line 241 def scroll_top_row_max = [@content_rows - , 0].max |
#scrollbar_columns ⇒ Integer
The bar's column plus the blank one beside it, so content never runs
into the glyph — TextView's reserve, including its drop below width 3
(D_scrollbar_reserve).
@return — 0, 1 or 2.
220 221 222 223 224 |
# File 'lib/tuile/component/scroller.rb', line 220 def return 0 unless rect.width >= 3 ? 2 : 1 end |
#scrollbar_visible? ⇒ Boolean
Derived on every read, so #relayout places the bar and sizes the content from one answer.
229 230 231 232 233 234 235 236 237 |
# File 'lib/tuile/component/scroller.rb', line 229 def return false if rect.empty? case @scrollbar_visibility when :visible then true when :auto then @content_rows > else false end end |
#validate_rows(rows, name) ⇒ Integer
@param rows
@param name — the accessor, for the message.
@return — rows.
257 258 259 260 261 262 263 |
# File 'lib/tuile/component/scroller.rb', line 257 def validate_rows(rows, name) unless rows.is_a?(Integer) && !rows.negative? raise ArgumentError, "#{name} expects a non-negative Integer, got #{rows.inspect}" end rows end |
#viewport_rows ⇒ Integer
@return — rows of content on screen at once — Tuile::Component#rect's height, the bar taking a column rather than a row.
62 |
# File 'lib/tuile/component/scroller.rb', line 62 def = rect.height |