tuile 0.16.0 → 0.17.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +108 -0
- data/README.md +21 -12
- data/book/02-repaint.md +47 -19
- data/book/03-layout.md +98 -49
- data/book/04-event-loop.md +5 -4
- data/book/05-focus.md +12 -9
- data/book/06-theming.md +55 -17
- data/book/07-components.md +188 -40
- data/book/08-testing.md +115 -15
- data/book/10-locale.md +1 -1
- data/book/README.md +5 -5
- data/examples/file_commander.rb +38 -27
- data/examples/hello_world.rb +1 -1
- data/examples/sampler.rb +225 -169
- data/lib/tuile/buffer.rb +12 -1
- data/lib/tuile/canvas/backend.rb +46 -0
- data/lib/tuile/canvas.rb +212 -0
- data/lib/tuile/color.rb +38 -9
- data/lib/tuile/component/abstract_string_field.rb +81 -80
- data/lib/tuile/component/abstract_wrapping_field.rb +59 -54
- data/lib/tuile/component/big_decimal_field.rb +7 -6
- data/lib/tuile/component/button.rb +19 -11
- data/lib/tuile/component/checkbox.rb +12 -10
- data/lib/tuile/component/checkbox_group.rb +11 -13
- data/lib/tuile/component/combo_box.rb +30 -40
- data/lib/tuile/component/confirm_window.rb +27 -22
- data/lib/tuile/component/date_field.rb +27 -20
- data/lib/tuile/component/date_time_field.rb +75 -31
- data/lib/tuile/component/fill.rb +93 -0
- data/lib/tuile/component/float_field.rb +7 -6
- data/lib/tuile/component/form_item.rb +250 -0
- data/lib/tuile/component/form_layout.rb +206 -0
- data/lib/tuile/component/has_bad_input.rb +98 -27
- data/lib/tuile/component/has_caption.rb +14 -5
- data/lib/tuile/component/has_content.rb +5 -12
- data/lib/tuile/component/has_validation.rb +39 -13
- data/lib/tuile/component/has_value.rb +70 -16
- data/lib/tuile/component/integer_field.rb +7 -6
- data/lib/tuile/component/label.rb +8 -15
- data/lib/tuile/component/layout/absolute.rb +86 -0
- data/lib/tuile/component/layout/box.rb +38 -63
- data/lib/tuile/component/layout.rb +124 -10
- data/lib/tuile/component/list.rb +197 -94
- data/lib/tuile/component/list_dropdown.rb +148 -88
- data/lib/tuile/component/menu_bar/cascade.rb +97 -27
- data/lib/tuile/component/menu_bar.rb +84 -64
- data/lib/tuile/component/notification.rb +44 -31
- data/lib/tuile/component/overlay.rb +210 -52
- data/lib/tuile/component/password_field.rb +1 -8
- data/lib/tuile/component/picker_window.rb +15 -10
- data/lib/tuile/component/popup.rb +13 -24
- data/lib/tuile/component/progress_bar.rb +7 -7
- data/lib/tuile/component/radio_group.rb +10 -12
- data/lib/tuile/component/scroller.rb +266 -0
- data/lib/tuile/component/select.rb +15 -31
- data/lib/tuile/component/slot.rb +1 -2
- data/lib/tuile/component/tab_sheet.rb +21 -28
- data/lib/tuile/component/tabs.rb +39 -24
- data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
- data/lib/tuile/component/text_area.rb +21 -19
- data/lib/tuile/component/text_field.rb +55 -39
- data/lib/tuile/component/text_view.rb +143 -79
- data/lib/tuile/component/time_field.rb +26 -21
- data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
- data/lib/tuile/component/window.rb +27 -26
- data/lib/tuile/component.rb +481 -259
- data/lib/tuile/component_background.rb +177 -0
- data/lib/tuile/component_util.rb +43 -0
- data/lib/tuile/event.rb +29 -0
- data/lib/tuile/event_queue.rb +14 -0
- data/lib/tuile/fake_screen.rb +41 -10
- data/lib/tuile/keys.rb +15 -6
- data/lib/tuile/layout_pass.rb +180 -0
- data/lib/tuile/listeners.rb +219 -0
- data/lib/tuile/mouse/router.rb +51 -35
- data/lib/tuile/mouse.rb +96 -29
- data/lib/tuile/point.rb +6 -0
- data/lib/tuile/rect.rb +33 -0
- data/lib/tuile/screen.rb +419 -84
- data/lib/tuile/screen_pane.rb +144 -31
- data/lib/tuile/strict_layout.rb +127 -0
- data/lib/tuile/styled_string.rb +139 -9
- data/lib/tuile/testing/gestures.rb +35 -0
- data/lib/tuile/testing.rb +310 -36
- data/lib/tuile/theme.rb +170 -19
- data/lib/tuile/theme_def.rb +4 -0
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile.rb +53 -0
- data/sig/tuile.rbs +4951 -1158
- metadata +16 -2
- data/lib/tuile/vertical_scroll_bar.rb +0 -122
data/lib/tuile/component/list.rb
CHANGED
|
@@ -6,14 +6,26 @@ module Tuile
|
|
|
6
6
|
#
|
|
7
7
|
# list = Component::List.new
|
|
8
8
|
# list.items = people
|
|
9
|
-
# list.renderer = ->(p) { StyledString.plain(p.name) + screen.theme.fg(:muted, " #{p.email}") }
|
|
9
|
+
# list.renderer = ->(p, _w) { StyledString.plain(p.name) + screen.theme.fg(:muted, " #{p.email}") }
|
|
10
10
|
# list.cursor = List::Cursor.new # a bare list has none
|
|
11
|
-
# list.on_item_chosen
|
|
11
|
+
# list.on_item_chosen { |e| open(e.item) }
|
|
12
12
|
#
|
|
13
|
-
# The {#renderer} turns an item into one row; the
|
|
14
|
-
# as itself, so a list of
|
|
15
|
-
#
|
|
16
|
-
# rendering (split on `\n`,
|
|
13
|
+
# The {#renderer} turns an item and the columns it gets into one row; the
|
|
14
|
+
# default renders an item as itself and ignores the width, so a list of
|
|
15
|
+
# `String`s or {StyledString}s needs none — which is what {#lines=} and
|
|
16
|
+
# {#build_lines} are, items that are their own rendering (split on `\n`,
|
|
17
|
+
# one row per line).
|
|
18
|
+
#
|
|
19
|
+
# A renderer that *uses* the width lays out against it — an aligned
|
|
20
|
+
# right-hand column, a path elided from the left — neither of which a
|
|
21
|
+
# trailing ellipsis can express:
|
|
22
|
+
#
|
|
23
|
+
# counts_w = files.map { _1.counts.length }.max # per snapshot, not per row
|
|
24
|
+
# list.renderer = lambda do |file, w|
|
|
25
|
+
# name = StyledString.plain(file.path).ellipsize(w - counts_w - 1, at: :start)
|
|
26
|
+
# gap = " " * (w - counts_w - name.display_width)
|
|
27
|
+
# name + StyledString.plain(gap + file.counts)
|
|
28
|
+
# end
|
|
17
29
|
#
|
|
18
30
|
# There are no appenders. The items are always assigned whole — an app that
|
|
19
31
|
# grows a list keeps its own array and re-assigns it (`list.items = mine`) —
|
|
@@ -27,26 +39,38 @@ module Tuile
|
|
|
27
39
|
# automatically; its highlight overlays {Theme#active_bg_color} while
|
|
28
40
|
# preserving each span's foreground color.
|
|
29
41
|
#
|
|
42
|
+
# {#scrollbar_visibility} turns on a {VerticalScrollBar} in the rightmost
|
|
43
|
+
# column: a child component, so its handle drags (wanting
|
|
44
|
+
# `run_event_loop(capture_mouse: :drag)`) and a press on its track pages.
|
|
45
|
+
# Under `:visible` the column stays reserved whether anything scrolls or
|
|
46
|
+
# not, so a growing list re-flows nothing; under `:auto` the bar shows only
|
|
47
|
+
# while the items outnumber the rows, and hands its column back to the
|
|
48
|
+
# rows while they fit (`D_scrollbar_ink`).
|
|
49
|
+
#
|
|
30
50
|
# == Implementation details
|
|
31
51
|
# Rendering is lazy: only the rows in the viewport are rendered, each
|
|
32
|
-
# memoized until {#items=}
|
|
52
|
+
# memoized until {#items=} or {#renderer=} drops the cache, or the row
|
|
53
|
+
# width moves off the one it was padded at.
|
|
33
54
|
# So a renderer runs *at paint time*, on any frame — keep it pure and
|
|
34
55
|
# cheap; work that reaches a service belongs in the item, not in the
|
|
35
|
-
# renderer
|
|
36
|
-
#
|
|
56
|
+
# renderer, and a measurement over the whole snapshot (the example's widest
|
|
57
|
+
# counts column) is computed where {#items=} is assigned, or it is an
|
|
58
|
+
# O(items²) pass on every resize. {#select_next} deliberately renders
|
|
59
|
+
# without memoizing: one failed scan would otherwise cache a row per item.
|
|
37
60
|
class List < Component
|
|
38
|
-
# The default {#renderer}: an item renders as itself
|
|
39
|
-
# output is coerced the same way — a {StyledString}
|
|
40
|
-
# `String` is parsed (so embedded ANSI is honored),
|
|
41
|
-
# `#to_s`'d first.
|
|
61
|
+
# The default {#renderer}: an item renders as itself, whatever the width.
|
|
62
|
+
# Every renderer's output is coerced the same way — a {StyledString}
|
|
63
|
+
# passes through, a `String` is parsed (so embedded ANSI is honored),
|
|
64
|
+
# anything else is `#to_s`'d first.
|
|
42
65
|
# @return [Proc]
|
|
43
|
-
DEFAULT_RENDERER =
|
|
66
|
+
DEFAULT_RENDERER = ->(item, _text_width) { item }
|
|
44
67
|
|
|
45
68
|
def initialize
|
|
46
69
|
super
|
|
47
70
|
@items = []
|
|
48
71
|
@renderer = DEFAULT_RENDERER
|
|
49
72
|
@row_cache = {}
|
|
73
|
+
@row_cache_width = nil
|
|
50
74
|
@blank_row = nil
|
|
51
75
|
@auto_scroll = false
|
|
52
76
|
@follow = true
|
|
@@ -54,27 +78,52 @@ module Tuile
|
|
|
54
78
|
@cursor = Cursor::None.new
|
|
55
79
|
@scrollbar_visibility = :gone
|
|
56
80
|
@show_cursor_when_inactive = false
|
|
57
|
-
@
|
|
58
|
-
@on_cursor_changed = nil
|
|
81
|
+
@interactive = true
|
|
59
82
|
@last_cursor_state = cursor_state
|
|
83
|
+
@scrollbar = VerticalScrollBar.new
|
|
84
|
+
@scrollbar.on_scroll_request { self.scroll_top_row = _1.scroll_top_row }
|
|
85
|
+
add_child(@scrollbar) # chrome, appended: a List has no content children
|
|
60
86
|
end
|
|
61
87
|
|
|
62
|
-
#
|
|
63
|
-
#
|
|
64
|
-
#
|
|
65
|
-
#
|
|
66
|
-
#
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
#
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
#
|
|
73
|
-
#
|
|
74
|
-
#
|
|
75
|
-
#
|
|
76
|
-
#
|
|
77
|
-
|
|
88
|
+
# What {#on_item_chosen} fires.
|
|
89
|
+
#
|
|
90
|
+
# @!attribute [r] source
|
|
91
|
+
# @return [List] the list the item was chosen in.
|
|
92
|
+
# @!attribute [r] position
|
|
93
|
+
# @return [Integer] the chosen item's 0-based index.
|
|
94
|
+
# @!attribute [r] item
|
|
95
|
+
# @return [Object] the chosen item itself.
|
|
96
|
+
ItemChosenEvent = Data.define(:source, :position, :item) { include Tuile::Event }
|
|
97
|
+
|
|
98
|
+
# What {#on_cursor_changed} fires.
|
|
99
|
+
#
|
|
100
|
+
# @!attribute [r] source
|
|
101
|
+
# @return [List] the list whose cursor moved.
|
|
102
|
+
# @!attribute [r] position
|
|
103
|
+
# @return [Integer] the cursor's 0-based index.
|
|
104
|
+
# @!attribute [r] item
|
|
105
|
+
# @return [Object, nil] the item there, `nil` when the cursor is
|
|
106
|
+
# off-content ({Cursor::None}, empty list, or `position` past the last
|
|
107
|
+
# item).
|
|
108
|
+
CursorChangedEvent = Data.define(:source, :position, :item) { include Tuile::Event }
|
|
109
|
+
|
|
110
|
+
# @!method on_item_chosen
|
|
111
|
+
# Fired with an {ItemChosenEvent} when an item is chosen — by pressing
|
|
112
|
+
# Enter on the cursor's item, or by left-clicking it. Never fires when
|
|
113
|
+
# the cursor's position is outside the content (e.g. {Cursor::None}, or
|
|
114
|
+
# empty content).
|
|
115
|
+
# @return [Listeners]
|
|
116
|
+
listener :on_item_chosen
|
|
117
|
+
|
|
118
|
+
# @!method on_cursor_changed
|
|
119
|
+
# Fired with a {CursorChangedEvent} when the `(position, item)` tuple
|
|
120
|
+
# under the cursor changes (items compared with `==`). Fires on cursor
|
|
121
|
+
# moves (key, mouse, search), on {#cursor=}, and on {#items=} when the
|
|
122
|
+
# item at the cursor's index changes (or its in-range/out-of-range status
|
|
123
|
+
# flips). Useful for keeping a details pane in sync with the highlighted
|
|
124
|
+
# row.
|
|
125
|
+
# @return [Listeners]
|
|
126
|
+
listener :on_cursor_changed
|
|
78
127
|
|
|
79
128
|
# @return [Boolean] if true and new content is set, auto-scrolls to the
|
|
80
129
|
# bottom — but only while the viewport is already pinned to the last
|
|
@@ -95,7 +144,9 @@ module Tuile
|
|
|
95
144
|
# @return [Cursor] the list's cursor.
|
|
96
145
|
attr_reader :cursor
|
|
97
146
|
|
|
98
|
-
# @return [Symbol] scrollbar visibility: `:gone
|
|
147
|
+
# @return [Symbol] scrollbar visibility: `:gone`, `:visible`, or `:auto`
|
|
148
|
+
# — shown only while there are more {#items} than rows. `:gone` by
|
|
149
|
+
# default.
|
|
99
150
|
attr_reader :scrollbar_visibility
|
|
100
151
|
|
|
101
152
|
# @return [Boolean] when true, the cursor highlight is painted even while
|
|
@@ -112,15 +163,42 @@ module Tuile
|
|
|
112
163
|
invalidate
|
|
113
164
|
end
|
|
114
165
|
|
|
166
|
+
# Whether the list is a control: focusable, a tab stop, and a press on a
|
|
167
|
+
# row moves the cursor and fires {#on_item_chosen}. `false` makes it a
|
|
168
|
+
# display-only pane — a sidebar summary beside the field the user types
|
|
169
|
+
# in — which Tab skips, a click doesn't focus, and a press on a row
|
|
170
|
+
# bubbles past, so no capability is left reachable only by the mouse.
|
|
171
|
+
# The wheel and the scrollbar still scroll it, and the app still drives
|
|
172
|
+
# {#cursor} and {#scroll_top_row}. `true` by default.
|
|
173
|
+
# @return [Boolean]
|
|
174
|
+
def interactive? = @interactive
|
|
175
|
+
|
|
176
|
+
# Turning it off while focus is inside the list hands focus to the
|
|
177
|
+
# parent, as hiding a focused component does.
|
|
178
|
+
# @param value [Boolean]
|
|
179
|
+
# @return [void]
|
|
180
|
+
def interactive=(value)
|
|
181
|
+
value = value ? true : false
|
|
182
|
+
return if @interactive == value
|
|
183
|
+
|
|
184
|
+
@interactive = value
|
|
185
|
+
repair_focus_after_hiding unless value
|
|
186
|
+
invalidate
|
|
187
|
+
end
|
|
188
|
+
|
|
115
189
|
# Sets the scrollbar visibility.
|
|
116
|
-
# @param value [Symbol] `:gone` or `:
|
|
190
|
+
# @param value [Symbol] `:gone`, `:visible` or `:auto`.
|
|
191
|
+
# @raise [ArgumentError] on any other value.
|
|
192
|
+
# @return [void]
|
|
117
193
|
def scrollbar_visibility=(value)
|
|
118
|
-
|
|
194
|
+
unless %i[gone visible auto].include?(value)
|
|
195
|
+
raise ArgumentError, "expected :gone, :visible or :auto, got #{value.inspect}"
|
|
196
|
+
end
|
|
119
197
|
return if @scrollbar_visibility == value
|
|
120
198
|
|
|
121
199
|
@scrollbar_visibility = value
|
|
122
|
-
drop_row_cache
|
|
123
200
|
invalidate
|
|
201
|
+
invalidate_layout
|
|
124
202
|
end
|
|
125
203
|
|
|
126
204
|
# Sets the new auto_scroll. If true, re-engages tailing and immediately
|
|
@@ -153,14 +231,21 @@ module Tuile
|
|
|
153
231
|
@scroll_top_row = new_row
|
|
154
232
|
@follow = at_bottom?
|
|
155
233
|
invalidate
|
|
234
|
+
invalidate_layout
|
|
156
235
|
end
|
|
157
236
|
|
|
158
237
|
# @return [Array] the items, one row each.
|
|
159
238
|
attr_reader :items
|
|
160
239
|
|
|
161
|
-
#
|
|
162
|
-
#
|
|
163
|
-
#
|
|
240
|
+
# The width is passed whether or not a renderer wants it, so a
|
|
241
|
+
# one-argument callable raises when a row is rendered.
|
|
242
|
+
# @return [Proc, Method] `(item, text_width) -> row`: a {StyledString}, a
|
|
243
|
+
# `String` (parsed, so embedded ANSI is honored), or anything with
|
|
244
|
+
# `#to_s`. Only the first line of a multi-line rendering is kept — one
|
|
245
|
+
# item is one row. `text_width` is the display columns the row body
|
|
246
|
+
# gets, gutters and scrollbar column already deducted; it is `0` before
|
|
247
|
+
# the list has a rect, and every change to it drops the row cache, so a
|
|
248
|
+
# width-dependent row is never left over from the old width.
|
|
164
249
|
attr_reader :renderer
|
|
165
250
|
|
|
166
251
|
# Replaces the items, leaving the cursor where it is — a cursor left past
|
|
@@ -178,9 +263,10 @@ module Tuile
|
|
|
178
263
|
update_scroll_top_row_if_auto_scroll
|
|
179
264
|
notify_cursor_changed
|
|
180
265
|
invalidate
|
|
266
|
+
invalidate_layout # the bar's row_count follows @items.size
|
|
181
267
|
end
|
|
182
268
|
|
|
183
|
-
# @param proc [Proc, Method] item -> row
|
|
269
|
+
# @param proc [Proc, Method] `(item, text_width) -> row`; see {#renderer}.
|
|
184
270
|
# @return [void]
|
|
185
271
|
def renderer=(proc)
|
|
186
272
|
@renderer = proc
|
|
@@ -192,9 +278,9 @@ module Tuile
|
|
|
192
278
|
# it and {#items} stayed the same, e.g. one prefixing a marker read from
|
|
193
279
|
# a selection it closes over:
|
|
194
280
|
#
|
|
195
|
-
# def
|
|
281
|
+
# def set_value(new_value, from_user:) # RadioGroup: the marked row moved
|
|
196
282
|
# super
|
|
197
|
-
#
|
|
283
|
+
# list.refresh_rows
|
|
198
284
|
# end
|
|
199
285
|
#
|
|
200
286
|
# @return [void]
|
|
@@ -243,9 +329,11 @@ module Tuile
|
|
|
243
329
|
self.lines = buffer
|
|
244
330
|
end
|
|
245
331
|
|
|
246
|
-
|
|
332
|
+
# @return [Boolean] {#interactive?}.
|
|
333
|
+
def focusable? = @interactive
|
|
247
334
|
|
|
248
|
-
|
|
335
|
+
# @return [Boolean] {#interactive?}.
|
|
336
|
+
def tab_stop? = @interactive
|
|
249
337
|
|
|
250
338
|
# @param key [String] a key.
|
|
251
339
|
# @return [Boolean] true if the key was handled.
|
|
@@ -317,13 +405,14 @@ module Tuile
|
|
|
317
405
|
end
|
|
318
406
|
|
|
319
407
|
# Moves the cursor to the pressed row and fires {#on_item_chosen}; what
|
|
320
|
-
# each {Cursor} does with a press is its own.
|
|
408
|
+
# each {Cursor} does with a press is its own. Declines every press while
|
|
409
|
+
# not {#interactive?}.
|
|
321
410
|
# @param event [Mouse::DownEvent]
|
|
322
411
|
# @return [Boolean]
|
|
323
412
|
def handle_mouse_down?(event)
|
|
324
|
-
return false unless event.button == :left
|
|
413
|
+
return false unless @interactive && event.button == :left
|
|
325
414
|
|
|
326
|
-
item_index = event.y
|
|
415
|
+
item_index = event.y + scroll_top_row
|
|
327
416
|
if @cursor.handle_mouse_down?(item_index, event, @items.size)
|
|
328
417
|
move_viewport_to_cursor
|
|
329
418
|
notify_cursor_changed
|
|
@@ -348,24 +437,27 @@ module Tuile
|
|
|
348
437
|
end
|
|
349
438
|
|
|
350
439
|
# Paints the visible items into {#rect}, rendering the ones not already
|
|
351
|
-
# cached.
|
|
440
|
+
# cached. The scrollbar's column is the child {VerticalScrollBar}'s,
|
|
441
|
+
# reserved out of {#content_width}.
|
|
352
442
|
#
|
|
353
443
|
# Skips the {Component#repaint} default's auto-clear: every row of
|
|
354
444
|
# {#rect} is painted below (with blank padding past the last item),
|
|
355
445
|
# so the parent contract — "fully draw over your rect" — is met
|
|
356
|
-
# without an upfront wipe
|
|
357
|
-
#
|
|
446
|
+
# without an upfront wipe — but not {Component#invalidate_children}, or
|
|
447
|
+
# the scrollbar goes stale under an ancestor's clear. Rows go through
|
|
448
|
+
# {Canvas#set_text}, so
|
|
449
|
+
# content *and* blank filler inherit {ComponentBackground#effective}
|
|
358
450
|
# (a {#bg_color} set here or on an ancestor); the cursor row's
|
|
359
451
|
# {Theme#active_bg_color} highlight composes on top of it.
|
|
452
|
+
# @param canvas [Canvas] see {Component#repaint}.
|
|
360
453
|
# @return [void]
|
|
361
|
-
def repaint
|
|
454
|
+
def repaint(canvas)
|
|
362
455
|
return if rect.empty?
|
|
363
456
|
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
end
|
|
457
|
+
invalidate_children
|
|
458
|
+
sync_row_cache_width
|
|
367
459
|
(0...rect.height).each do |row|
|
|
368
|
-
|
|
460
|
+
canvas.set_text(0, row, paintable_row(row + @scroll_top_row))
|
|
369
461
|
end
|
|
370
462
|
end
|
|
371
463
|
|
|
@@ -573,18 +665,16 @@ module Tuile
|
|
|
573
665
|
|
|
574
666
|
protected
|
|
575
667
|
|
|
576
|
-
#
|
|
577
|
-
#
|
|
578
|
-
#
|
|
579
|
-
#
|
|
580
|
-
# populated before the popup was opened), the auto-scroll update
|
|
581
|
-
# was skipped because there was no viewport — re-run it now that there
|
|
582
|
-
# is one, so the list snaps to the bottom on first paint.
|
|
668
|
+
# The bar's whole state, pushed in one place: its column, and the two
|
|
669
|
+
# numbers it paints a handle from. {#auto_scroll} re-pins first, since a
|
|
670
|
+
# taller viewport moves the bottom, and a rect that was empty skipped the
|
|
671
|
+
# pin altogether.
|
|
583
672
|
# @return [void]
|
|
584
|
-
def
|
|
585
|
-
super
|
|
586
|
-
drop_row_cache
|
|
673
|
+
def relayout
|
|
587
674
|
update_scroll_top_row_if_auto_scroll
|
|
675
|
+
@scrollbar.rect = scrollbar_visible? ? Rect.new(rect.width - 1, 0, 1, rect.height) : Rect.new(0, 0, 0, 0)
|
|
676
|
+
@scrollbar.row_count = @items.size
|
|
677
|
+
@scrollbar.scroll_top_row = @scroll_top_row
|
|
588
678
|
end
|
|
589
679
|
|
|
590
680
|
private
|
|
@@ -637,7 +727,7 @@ module Tuile
|
|
|
637
727
|
# @return [void]
|
|
638
728
|
def fire_item_chosen
|
|
639
729
|
pos = @cursor.position
|
|
640
|
-
|
|
730
|
+
on_item_chosen.fire(ItemChosenEvent.new(source: self, position: pos, item: @items[pos]))
|
|
641
731
|
end
|
|
642
732
|
|
|
643
733
|
# @return [Array((Integer, Object, nil))]
|
|
@@ -657,7 +747,7 @@ module Tuile
|
|
|
657
747
|
return if state == @last_cursor_state
|
|
658
748
|
|
|
659
749
|
@last_cursor_state = state
|
|
660
|
-
|
|
750
|
+
on_cursor_changed.fire(CursorChangedEvent.new(source: self, position: state[0], item: state[1]))
|
|
661
751
|
end
|
|
662
752
|
|
|
663
753
|
# @param query [String]
|
|
@@ -731,15 +821,13 @@ module Tuile
|
|
|
731
821
|
# @return [Integer] the number of visible rows.
|
|
732
822
|
def viewport_rows = rect.height
|
|
733
823
|
|
|
734
|
-
# Scrolls the list.
|
|
824
|
+
# Scrolls the list, clamped at both ends. Goes through
|
|
825
|
+
# {#scroll_top_row=}, so a wheel notch or a PgUp re-evaluates
|
|
826
|
+
# {#following?} exactly as an assignment does.
|
|
735
827
|
# @param delta [Integer] negative scrolls up, positive scrolls down.
|
|
736
828
|
# @return [void]
|
|
737
829
|
def move_scroll_top_row_by(delta)
|
|
738
|
-
|
|
739
|
-
return if @scroll_top_row == new_scroll_top_row
|
|
740
|
-
|
|
741
|
-
@scroll_top_row = new_scroll_top_row
|
|
742
|
-
invalidate
|
|
830
|
+
self.scroll_top_row = (@scroll_top_row + delta).clamp(0, scroll_top_row_max)
|
|
743
831
|
end
|
|
744
832
|
|
|
745
833
|
# If auto-scrolling, recalculate the top row and snap the cursor to the
|
|
@@ -748,8 +836,7 @@ module Tuile
|
|
|
748
836
|
# negating the auto-scroll. Skipped when {#rect} is empty: without a
|
|
749
837
|
# viewport the "items minus viewport" formula yields `@items.size`,
|
|
750
838
|
# which would leave `scroll_top_row` past the last item once a real rect
|
|
751
|
-
# arrives. {#
|
|
752
|
-
# the snap-to-bottom intent is preserved.
|
|
839
|
+
# arrives. {#relayout} re-runs it once one does.
|
|
753
840
|
#
|
|
754
841
|
# Gated on {#following?}: once the user scrolls up off the bottom the
|
|
755
842
|
# cursor snap and viewport pin are both skipped, so reading older
|
|
@@ -768,11 +855,17 @@ module Tuile
|
|
|
768
855
|
self.scroll_top_row = new_scroll_top_row
|
|
769
856
|
end
|
|
770
857
|
|
|
858
|
+
# Derived on every read, never stored, so {#content_width} can't disagree
|
|
859
|
+
# with it.
|
|
771
860
|
# @return [Boolean] whether the scrollbar should be drawn right now.
|
|
772
861
|
def scrollbar_visible?
|
|
773
862
|
return false if rect.empty?
|
|
774
863
|
|
|
775
|
-
@scrollbar_visibility
|
|
864
|
+
case @scrollbar_visibility
|
|
865
|
+
when :visible then true
|
|
866
|
+
when :auto then @items.size > rect.height
|
|
867
|
+
else false
|
|
868
|
+
end
|
|
776
869
|
end
|
|
777
870
|
|
|
778
871
|
# @return [Integer] column width available for row content (rect width
|
|
@@ -784,14 +877,35 @@ module Tuile
|
|
|
784
877
|
rect.width - (scrollbar_visible? ? 1 : 0)
|
|
785
878
|
end
|
|
786
879
|
|
|
880
|
+
# The budget handed to {#renderer} and the width {#pad_to_row} fits a row
|
|
881
|
+
# into: {#content_width} less the gutter either side. Clamped at `0` — a
|
|
882
|
+
# list with no rect still renders when {#search_and_go} scans it, and a
|
|
883
|
+
# renderer subtracting from a negative width would blow up at paint time.
|
|
884
|
+
# @return [Integer] display columns available to the row body.
|
|
885
|
+
def text_width
|
|
886
|
+
[content_width - 2, 0].max
|
|
887
|
+
end
|
|
888
|
+
|
|
787
889
|
# Discards every rendered row, so the next paint re-renders the viewport
|
|
788
|
-
# against the current items
|
|
890
|
+
# against the current items and renderer.
|
|
789
891
|
# @return [void]
|
|
790
892
|
def drop_row_cache
|
|
791
893
|
@row_cache.clear
|
|
792
894
|
@blank_row = nil
|
|
793
895
|
end
|
|
794
896
|
|
|
897
|
+
# Drops the row cache when {#content_width} has moved off the width it
|
|
898
|
+
# was padded at. The width is compared rather than announced because an
|
|
899
|
+
# `:auto` bar moves it on a *height* change too — a list shrinking below
|
|
900
|
+
# its item count takes the bar and the column with it.
|
|
901
|
+
# @return [void]
|
|
902
|
+
def sync_row_cache_width
|
|
903
|
+
return if @row_cache_width == content_width
|
|
904
|
+
|
|
905
|
+
drop_row_cache
|
|
906
|
+
@row_cache_width = content_width
|
|
907
|
+
end
|
|
908
|
+
|
|
795
909
|
# @param index [Integer] 0-based index into {#items}.
|
|
796
910
|
# @return [StyledString] the item's padded row, rendered on first use and
|
|
797
911
|
# memoized until {#drop_row_cache}.
|
|
@@ -812,7 +926,7 @@ module Tuile
|
|
|
812
926
|
# {StyledString}, cut to its first line since a `\n` reaching the buffer
|
|
813
927
|
# would corrupt the frame.
|
|
814
928
|
def render(item)
|
|
815
|
-
rendered = @renderer.call(item)
|
|
929
|
+
rendered = @renderer.call(item, text_width)
|
|
816
930
|
rendered = StyledString.parse(rendered.to_s) unless rendered.is_a?(StyledString)
|
|
817
931
|
return rendered unless rendered.spans.any? { _1.text.include?("\n") }
|
|
818
932
|
|
|
@@ -833,27 +947,16 @@ module Tuile
|
|
|
833
947
|
return StyledString::EMPTY if cw <= 0
|
|
834
948
|
return StyledString.plain(" " * cw) if cw < 2
|
|
835
949
|
|
|
836
|
-
|
|
837
|
-
body = row.ellipsize(text_width)
|
|
838
|
-
fill = cw - 2 - body.display_width
|
|
839
|
-
StyledString.plain(" ") + body + StyledString.plain(" " * (fill + 1))
|
|
950
|
+
StyledString.plain(" ") + row.ellipsize(text_width).ljust(cw - 1)
|
|
840
951
|
end
|
|
841
952
|
|
|
842
953
|
# @param index [Integer] 0-based index into {#items}.
|
|
843
|
-
# @
|
|
844
|
-
#
|
|
845
|
-
|
|
846
|
-
# @return [StyledString] paintable row exactly `rect.width` columns wide;
|
|
847
|
-
# highlighted if cursor is here.
|
|
848
|
-
def paintable_row(index, row_in_viewport, scrollbar)
|
|
954
|
+
# @return [StyledString] paintable row exactly {#content_width} columns
|
|
955
|
+
# wide — the bar's column is the child's; highlighted if cursor is here.
|
|
956
|
+
def paintable_row(index)
|
|
849
957
|
base = index < @items.size ? padded_row(index) : blank_row
|
|
850
958
|
is_cursor = (active? || @show_cursor_when_inactive) && index < @items.size && @cursor.position == index
|
|
851
|
-
|
|
852
|
-
if scrollbar
|
|
853
|
-
styled += StyledString.styled(scrollbar.scrollbar_char(row_in_viewport),
|
|
854
|
-
fg: screen.theme.scrollbar_color)
|
|
855
|
-
end
|
|
856
|
-
styled
|
|
959
|
+
is_cursor ? base.with_bg(screen.theme.active_bg_color) : base
|
|
857
960
|
end
|
|
858
961
|
end
|
|
859
962
|
end
|