tuile 0.11.0 → 0.13.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 +77 -0
- data/DECISIONS.md +1970 -14
- data/README.md +136 -491
- data/TERMINOLOGY.md +70 -0
- data/book/01-first-app.md +22 -17
- data/book/02-repaint.md +19 -6
- data/book/03-layout.md +12 -11
- data/book/05-focus.md +133 -18
- data/book/06-theming.md +6 -3
- data/book/07-components.md +498 -38
- data/book/08-testing.md +18 -4
- data/book/README.md +7 -5
- data/examples/file_commander.rb +27 -20
- data/examples/hello_world.rb +17 -5
- data/examples/sampler.rb +422 -66
- data/ideas/arrow-key-navigation.md +16 -0
- data/ideas/new-components.md +16 -10
- data/lib/tuile/ansi.rb +10 -0
- data/lib/tuile/buffer.rb +7 -7
- data/lib/tuile/component/abstract_string_field.rb +36 -0
- data/lib/tuile/component/button.rb +1 -1
- data/lib/tuile/component/checkbox.rb +1 -1
- data/lib/tuile/component/checkbox_group.rb +31 -26
- data/lib/tuile/component/combo_box.rb +13 -8
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/list.rb +313 -216
- data/lib/tuile/component/list_dropdown.rb +100 -10
- data/lib/tuile/component/menu_bar/cascade.rb +255 -0
- data/lib/tuile/component/menu_bar.rb +582 -0
- data/lib/tuile/component/notification.rb +320 -0
- data/lib/tuile/component/picker_window.rb +3 -8
- data/lib/tuile/component/popup.rb +83 -19
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +10 -8
- data/lib/tuile/component/tab_sheet.rb +242 -0
- data/lib/tuile/component/tabs.rb +528 -0
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +84 -277
- data/lib/tuile/component/text_field.rb +24 -7
- data/lib/tuile/component/text_view.rb +197 -180
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +43 -18
- data/lib/tuile/event_queue.rb +25 -1
- data/lib/tuile/fake_screen.rb +14 -0
- data/lib/tuile/keys.rb +65 -0
- data/lib/tuile/screen.rb +95 -78
- data/lib/tuile/screen_pane.rb +109 -27
- data/lib/tuile/styled_string.rb +52 -12
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/sig/tuile.rbs +2307 -516
- metadata +9 -3
- data/mise.toml +0 -2
data/lib/tuile/ansi.rb
CHANGED
|
@@ -12,6 +12,16 @@ module Tuile
|
|
|
12
12
|
# @return [String]
|
|
13
13
|
RESET = "\e[0m"
|
|
14
14
|
|
|
15
|
+
# The bell (`BEL`, `\a`, 0x07) — the "that keystroke went nowhere" signal.
|
|
16
|
+
# Ring it with {Screen#beep} rather than printing it: the bell is terminal
|
|
17
|
+
# IO, which is {Screen}'s job.
|
|
18
|
+
#
|
|
19
|
+
# What the user gets is the *terminal's* business — an audible beep, a
|
|
20
|
+
# visual flash, or nothing at all — and Tuile keeps no preference of its
|
|
21
|
+
# own about that.
|
|
22
|
+
# @return [String]
|
|
23
|
+
BEL = "\a"
|
|
24
|
+
|
|
15
25
|
# Begin Synchronized Update (DEC private mode 2026, "Synchronized
|
|
16
26
|
# Output"). The terminal stops refreshing its display and buffers every
|
|
17
27
|
# subsequent write until {SYNC_END}, then composites the whole batch
|
data/lib/tuile/buffer.rb
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
module Tuile
|
|
4
4
|
# An in-memory grid of styled cells mirroring the terminal screen. This is
|
|
5
5
|
# the back buffer behind flicker-free rendering: components paint into it
|
|
6
|
-
# (via {#
|
|
6
|
+
# (via {#set_text} / {#set_char} / {#fill}) instead of writing escape
|
|
7
7
|
# sequences straight to the terminal, and {#flush} emits the minimal escape
|
|
8
8
|
# string needed to bring a terminal — one that already matches the buffer's
|
|
9
9
|
# state as of the previous flush — up to date. Only cells that actually
|
|
@@ -133,7 +133,7 @@ module Tuile
|
|
|
133
133
|
# @param x [Integer] column.
|
|
134
134
|
# @param y [Integer] row.
|
|
135
135
|
# @return [Cell, nil] the live cell at `(x, y)` (do not mutate — paint via
|
|
136
|
-
# {#set_char} / {#
|
|
136
|
+
# {#set_char} / {#set_text} so dirty tracking stays correct), or nil when
|
|
137
137
|
# out of bounds.
|
|
138
138
|
def cell(x, y)
|
|
139
139
|
return nil unless in_bounds?(x, y)
|
|
@@ -160,12 +160,12 @@ module Tuile
|
|
|
160
160
|
|
|
161
161
|
# Writes a {StyledString} starting at `(x, y)`, advancing by each grapheme's
|
|
162
162
|
# display width and clipping at the right edge. Newlines are not handled —
|
|
163
|
-
# pass one
|
|
163
|
+
# pass the text of one row.
|
|
164
164
|
# @param x [Integer] starting column.
|
|
165
165
|
# @param y [Integer] row.
|
|
166
166
|
# @param styled [StyledString]
|
|
167
167
|
# @return [void]
|
|
168
|
-
def
|
|
168
|
+
def set_text(x, y, styled)
|
|
169
169
|
col = x
|
|
170
170
|
styled.spans.each do |span|
|
|
171
171
|
span.text.grapheme_clusters.each do |g|
|
|
@@ -281,7 +281,7 @@ module Tuile
|
|
|
281
281
|
# @param y [Integer] row.
|
|
282
282
|
# @return [String] row `y` rendered to ANSI across its full width — the
|
|
283
283
|
# minimal-SGR encoding of its cells, equivalent to what a component's
|
|
284
|
-
# `
|
|
284
|
+
# `set_text` of the whole row would have printed. Intended for tests that
|
|
285
285
|
# assert on styled output (see {FakeScreen}); empty for an out-of-range row.
|
|
286
286
|
def row_ansi(y)
|
|
287
287
|
return "" unless y >= 0 && y < @height
|
|
@@ -304,7 +304,7 @@ module Tuile
|
|
|
304
304
|
|
|
305
305
|
# @param rect [Rect]
|
|
306
306
|
# @return [Array<String>] each row within `rect` rendered to ANSI, top to
|
|
307
|
-
# bottom — byte-identical to what a component's per-row `
|
|
307
|
+
# bottom — byte-identical to what a component's per-row `set_text` over
|
|
308
308
|
# that rect emitted. The region equivalent of {#row_ansi}. Intended for
|
|
309
309
|
# tests asserting styled output.
|
|
310
310
|
def region_ansi(rect)
|
|
@@ -316,7 +316,7 @@ module Tuile
|
|
|
316
316
|
private
|
|
317
317
|
|
|
318
318
|
# Core of {#set_char} with the grapheme's display width already known.
|
|
319
|
-
# {#
|
|
319
|
+
# {#set_text} computes each width once while advancing the column and passes
|
|
320
320
|
# it straight through, so the paint hot path measures every grapheme exactly
|
|
321
321
|
# once (and that once is a {.display_width} memo read). See {#set_char} for
|
|
322
322
|
# the wide-glyph / clipping / out-of-bounds contract.
|
|
@@ -42,6 +42,8 @@ module Tuile
|
|
|
42
42
|
#
|
|
43
43
|
# - {#preprocess_text} — input filter (e.g. {TextField} truncates to
|
|
44
44
|
# fit `rect.width - 1`).
|
|
45
|
+
# - {#preprocess_paste} — the same for {#handle_paste}, which lands a
|
|
46
|
+
# whole clipboard at the caret in one mutation.
|
|
45
47
|
# - {#on_text_mutated} / {#on_caret_mutated} — post-mutation side
|
|
46
48
|
# effects (e.g. {TextArea} invalidates its wrap cache and scrolls to
|
|
47
49
|
# keep the caret visible).
|
|
@@ -155,8 +157,42 @@ module Tuile
|
|
|
155
157
|
handle_text_input_key(key)
|
|
156
158
|
end
|
|
157
159
|
|
|
160
|
+
# Inserts pasted text at the caret as **one** mutation, so {#on_change}
|
|
161
|
+
# fires once for the whole paste rather than once per character.
|
|
162
|
+
# {#preprocess_paste} filters it first.
|
|
163
|
+
# @param text [String]
|
|
164
|
+
# @return [Boolean] always true — a field consumes every paste, an empty
|
|
165
|
+
# one included.
|
|
166
|
+
def handle_paste(text)
|
|
167
|
+
insert_text(preprocess_paste(text))
|
|
168
|
+
true
|
|
169
|
+
end
|
|
170
|
+
|
|
158
171
|
protected
|
|
159
172
|
|
|
173
|
+
# Input filter for {#handle_paste}, the paste-side counterpart of
|
|
174
|
+
# {#preprocess_text}. Strips the C0 control characters a text buffer
|
|
175
|
+
# cannot hold — a raw `\e` or `\t` reaching {Buffer} would move the real
|
|
176
|
+
# terminal cursor mid-frame — keeping `\n`, and turning a tab into a
|
|
177
|
+
# single space so pasted code keeps its word gaps. {TextField} narrows it
|
|
178
|
+
# further; an app wanting tab *expansion* overrides {#handle_paste}.
|
|
179
|
+
# @param text [String]
|
|
180
|
+
# @return [String]
|
|
181
|
+
def preprocess_paste(text) = text.tr("\t", " ").gsub(/[\x00-\x09\x0b-\x1f\x7f]/, "")
|
|
182
|
+
|
|
183
|
+
# Inserts `str` at the caret, leaving the caret behind it. The bulk
|
|
184
|
+
# counterpart of a subclass's per-key insert.
|
|
185
|
+
# @param str [String]
|
|
186
|
+
# @return [Boolean] true if the text changed.
|
|
187
|
+
def insert_text(str)
|
|
188
|
+
return false if str.empty?
|
|
189
|
+
|
|
190
|
+
new_text = @text.dup.insert(@caret, str)
|
|
191
|
+
@caret += str.length
|
|
192
|
+
self.text = new_text
|
|
193
|
+
true
|
|
194
|
+
end
|
|
195
|
+
|
|
160
196
|
# Renders `text` on the field's background well, looked up from the
|
|
161
197
|
# current {Screen#theme} at paint time: {Theme#active_bg_color} when this
|
|
162
198
|
# input is on the active (focus) chain, {Theme#input_bg_color} otherwise —
|
|
@@ -76,7 +76,7 @@ module Tuile
|
|
|
76
76
|
|
|
77
77
|
label = (StyledString.plain("[ ") + caption + StyledString.plain(" ]")).ellipsize(rect.width)
|
|
78
78
|
label = label.with_bg(screen.theme.active_bg_color) if active?
|
|
79
|
-
|
|
79
|
+
draw_text(rect.left, rect.top, label)
|
|
80
80
|
end
|
|
81
81
|
end
|
|
82
82
|
end
|
|
@@ -127,7 +127,7 @@ module Tuile
|
|
|
127
127
|
|
|
128
128
|
label = (StyledString.plain(value ? "[x] " : "[ ] ") + caption).ellipsize(rect.width)
|
|
129
129
|
label = label.with_bg(screen.theme.active_bg_color) if active?
|
|
130
|
-
|
|
130
|
+
draw_text(rect.left, rect.top, label)
|
|
131
131
|
end
|
|
132
132
|
end
|
|
133
133
|
end
|
|
@@ -23,12 +23,13 @@ module Tuile
|
|
|
23
23
|
# Treat it as *unordered*: it iterates in toggle order, so use
|
|
24
24
|
# `cg.items & cg.value.to_a` when you need {#items} order.
|
|
25
25
|
#
|
|
26
|
-
# Composes rather than subclasses, like {ComboBox}: a {List}
|
|
27
|
-
# {HasContent} child, which is where the cursor, scrolling, the
|
|
28
|
-
# per-row mouse hit-testing come from
|
|
29
|
-
#
|
|
30
|
-
#
|
|
31
|
-
#
|
|
26
|
+
# Composes rather than subclasses, like {ComboBox}: a {List} of the items is
|
|
27
|
+
# its single {HasContent} child, which is where the cursor, scrolling, the
|
|
28
|
+
# scrollbar and per-row mouse hit-testing come from — the group only supplies
|
|
29
|
+
# the {List#renderer} that puts the box in front of the label. `content` is
|
|
30
|
+
# that list, so an app can tune it (`scrollbar_visibility`,
|
|
31
|
+
# `show_cursor_when_inactive`, …). Rows beyond {#rect}'s height scroll; the
|
|
32
|
+
# inner list is the tab stop, not the group.
|
|
32
33
|
#
|
|
33
34
|
# == +items+ is chrome; +value+ is authoritative
|
|
34
35
|
# {#items=} changes only what is *presented*. It never touches {#value} and
|
|
@@ -46,7 +47,7 @@ module Tuile
|
|
|
46
47
|
# mutated after being selected becomes unfindable. Two `==`-equal items also
|
|
47
48
|
# share one selection — their rows check and uncheck together — whereas two
|
|
48
49
|
# *distinct* items that merely render the same label toggle independently,
|
|
49
|
-
# because a row resolves to
|
|
50
|
+
# because a row resolves to its own item, never to its label.
|
|
50
51
|
#
|
|
51
52
|
# Rows repeat {Checkbox}'s `[x] `/`[ ] ` glyph convention rather than
|
|
52
53
|
# importing a constant from it.
|
|
@@ -67,7 +68,6 @@ module Tuile
|
|
|
67
68
|
# matter to a form helper.
|
|
68
69
|
def initialize(items: [], value: nil)
|
|
69
70
|
super()
|
|
70
|
-
@items = items.to_a
|
|
71
71
|
@item_label = :to_s.to_proc
|
|
72
72
|
@value = coerce(value)
|
|
73
73
|
@on_value_change = nil
|
|
@@ -75,13 +75,14 @@ module Tuile
|
|
|
75
75
|
list = List.new
|
|
76
76
|
# A List has no cursor at all by default (Cursor::None, position -1).
|
|
77
77
|
list.cursor = List::Cursor.new
|
|
78
|
-
list.
|
|
78
|
+
list.renderer = method(:render_row)
|
|
79
|
+
list.on_item_chosen = ->(_index, item) { toggle(item) }
|
|
80
|
+
list.items = items.to_a
|
|
79
81
|
self.content = list
|
|
80
|
-
rebuild_rows
|
|
81
82
|
end
|
|
82
83
|
|
|
83
84
|
# @return [Array] the presented items.
|
|
84
|
-
|
|
85
|
+
def items = content.items
|
|
85
86
|
|
|
86
87
|
# @return [Proc, Method] item -> row label (a `String`, {StyledString}, or
|
|
87
88
|
# anything with `#to_s`); `:to_s` by default.
|
|
@@ -92,17 +93,14 @@ module Tuile
|
|
|
92
93
|
# @raise [TypeError] unless `new_items` is an `Array`.
|
|
93
94
|
# @return [void]
|
|
94
95
|
def items=(new_items)
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
@items = new_items
|
|
98
|
-
rebuild_rows
|
|
96
|
+
content.items = new_items
|
|
99
97
|
end
|
|
100
98
|
|
|
101
99
|
# @param proc [Proc, Method] item -> row label.
|
|
102
100
|
# @return [void]
|
|
103
101
|
def item_label=(proc)
|
|
104
102
|
@item_label = proc
|
|
105
|
-
|
|
103
|
+
content.refresh_rows
|
|
106
104
|
end
|
|
107
105
|
|
|
108
106
|
# @return [Set] the frozen empty set — {HasValue#empty?} means nothing is
|
|
@@ -122,7 +120,7 @@ module Tuile
|
|
|
122
120
|
return if value == selected
|
|
123
121
|
|
|
124
122
|
super(selected)
|
|
125
|
-
|
|
123
|
+
content.refresh_rows
|
|
126
124
|
end
|
|
127
125
|
|
|
128
126
|
# Toggles the cursor row on Space. Nothing else is claimed: the composed
|
|
@@ -148,22 +146,29 @@ module Tuile
|
|
|
148
146
|
private
|
|
149
147
|
|
|
150
148
|
# Flips membership of the item on row `index`; an index outside {#items} is
|
|
151
|
-
# ignored
|
|
149
|
+
# ignored — {List::Cursor::None}'s `-1` would otherwise toggle the *last*
|
|
150
|
+
# item.
|
|
152
151
|
# @param index [Integer]
|
|
153
152
|
# @return [void]
|
|
154
153
|
def toggle_at(index)
|
|
155
|
-
return unless index.between?(0,
|
|
154
|
+
return unless index.between?(0, items.size - 1)
|
|
156
155
|
|
|
157
|
-
|
|
158
|
-
self.value = value.include?(item) ? value - [item] : value + [item]
|
|
156
|
+
toggle(items[index])
|
|
159
157
|
end
|
|
160
158
|
|
|
161
|
-
#
|
|
159
|
+
# Flips `item`'s membership of {#value}.
|
|
160
|
+
# @param item [Object]
|
|
162
161
|
# @return [void]
|
|
163
|
-
def
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
162
|
+
def toggle(item)
|
|
163
|
+
self.value = value.include?(item) ? value - [item] : value + [item]
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
# @param item [Object]
|
|
167
|
+
# @return [StyledString] the item's row: its label behind a checkmark box.
|
|
168
|
+
# The {List} calls this at paint time, so the boxes track {#value}
|
|
169
|
+
# without re-rendering anything but the visible rows.
|
|
170
|
+
def render_row(item)
|
|
171
|
+
StyledString.plain(value.include?(item) ? "[x] " : "[ ] ") + label_for(item)
|
|
167
172
|
end
|
|
168
173
|
|
|
169
174
|
# @param new_value [Enumerable, nil]
|
|
@@ -52,7 +52,11 @@ module Tuile
|
|
|
52
52
|
self.content = field
|
|
53
53
|
|
|
54
54
|
@overlay = ListDropdown.new
|
|
55
|
-
|
|
55
|
+
# Outside-click dismissal spans the owner chain, so a click on this
|
|
56
|
+
# combo's dropdown must not dismiss a dialog the combo sits in.
|
|
57
|
+
@overlay.owner = self
|
|
58
|
+
@overlay.renderer = ->(item) { @item_label.call(item) }
|
|
59
|
+
@overlay.on_item_chosen = ->(_index, item) { commit(item) }
|
|
56
60
|
end
|
|
57
61
|
|
|
58
62
|
# @return [Array] the candidate items.
|
|
@@ -97,7 +101,6 @@ module Tuile
|
|
|
97
101
|
def cursor_position = content.cursor_position
|
|
98
102
|
|
|
99
103
|
# @return [String]
|
|
100
|
-
def keyboard_hint = "↑↓ #{screen.theme.hint("select")} ⏎ #{screen.theme.hint("accept")}"
|
|
101
104
|
|
|
102
105
|
# Re-anchors the (open) dropdown after {HasContent#rect=} has resized the
|
|
103
106
|
# field via {#layout}.
|
|
@@ -195,7 +198,7 @@ module Tuile
|
|
|
195
198
|
if @filtered.empty?
|
|
196
199
|
close_menu
|
|
197
200
|
else
|
|
198
|
-
@overlay.
|
|
201
|
+
@overlay.items = @filtered
|
|
199
202
|
@overlay.cursor = List::Cursor.new(position: @filtered.index(value) || 0)
|
|
200
203
|
@overlay.open unless @overlay.open?
|
|
201
204
|
anchor
|
|
@@ -214,12 +217,11 @@ module Tuile
|
|
|
214
217
|
@items.select { |item| @item_label.call(item).to_s.downcase.include?(needle) }
|
|
215
218
|
end
|
|
216
219
|
|
|
217
|
-
# Commits the item
|
|
218
|
-
#
|
|
219
|
-
# @param
|
|
220
|
+
# Commits the item chosen from the menu: closes the dropdown and adopts it
|
|
221
|
+
# as {#value} (which repaints the field with its label).
|
|
222
|
+
# @param item [Object]
|
|
220
223
|
# @return [void]
|
|
221
|
-
def commit(
|
|
222
|
-
item = @filtered[index]
|
|
224
|
+
def commit(item)
|
|
223
225
|
close_menu
|
|
224
226
|
self.value = item
|
|
225
227
|
end
|
|
@@ -235,6 +237,9 @@ module Tuile
|
|
|
235
237
|
|
|
236
238
|
# Sets the field's text without triggering a refilter — for programmatic
|
|
237
239
|
# value changes and query reverts, which must not spring the dropdown.
|
|
240
|
+
# Every programmatic write to the field goes through here; a direct
|
|
241
|
+
# `content.text =` reaches the field's `on_change` and pops the dropdown
|
|
242
|
+
# open on a {#value=} the user never asked to browse.
|
|
238
243
|
# Parks the caret at the end: `text=` only *clamps* the caret, so a
|
|
239
244
|
# shorter query replaced by a longer label would otherwise strand it
|
|
240
245
|
# mid-word (commit "Go", then pick "Kotlin" → caret after "Ko").
|
|
@@ -25,7 +25,7 @@ module Tuile
|
|
|
25
25
|
# list wraps and scrolls within it. Defaults to {Fraction::HALF}.
|
|
26
26
|
# @return [Popup] the opened popup.
|
|
27
27
|
def self.open(caption, lines, size: Fraction::HALF)
|
|
28
|
-
Popup.
|
|
28
|
+
Popup.new(content: InfoWindow.new(caption, lines), size: size).open
|
|
29
29
|
end
|
|
30
30
|
end
|
|
31
31
|
end
|
|
@@ -16,8 +16,8 @@ module Tuile
|
|
|
16
16
|
super()
|
|
17
17
|
@text = StyledString::EMPTY
|
|
18
18
|
@bg = nil
|
|
19
|
-
@
|
|
20
|
-
@
|
|
19
|
+
@rows = []
|
|
20
|
+
@blank_row = ""
|
|
21
21
|
self.text = text unless text.nil?
|
|
22
22
|
end
|
|
23
23
|
|
|
@@ -43,7 +43,7 @@ module Tuile
|
|
|
43
43
|
return if @text == new_text
|
|
44
44
|
|
|
45
45
|
@text = new_text
|
|
46
|
-
|
|
46
|
+
update_rows
|
|
47
47
|
invalidate
|
|
48
48
|
end
|
|
49
49
|
|
|
@@ -60,7 +60,7 @@ module Tuile
|
|
|
60
60
|
return if @bg == new_bg
|
|
61
61
|
|
|
62
62
|
@bg = new_bg
|
|
63
|
-
|
|
63
|
+
update_rows
|
|
64
64
|
invalidate
|
|
65
65
|
end
|
|
66
66
|
|
|
@@ -69,15 +69,15 @@ module Tuile
|
|
|
69
69
|
# Skips the {Component#repaint} default's auto-clear: every row is
|
|
70
70
|
# painted explicitly (with pre-padded blanks past the last line), so
|
|
71
71
|
# the "fully draw over your rect" contract is met without an upfront
|
|
72
|
-
# wipe. Rows go through {Component#
|
|
72
|
+
# wipe. Rows go through {Component#draw_text}, so the padding and blank
|
|
73
73
|
# rows inherit {Component#effective_bg_color} when {#bg} is unset.
|
|
74
74
|
# @return [void]
|
|
75
75
|
def repaint
|
|
76
76
|
return if rect.empty?
|
|
77
77
|
|
|
78
78
|
(0...rect.height).each do |row|
|
|
79
|
-
line = @
|
|
80
|
-
|
|
79
|
+
line = @rows[row] || @blank_row
|
|
80
|
+
draw_text(rect.left, rect.top + row, line)
|
|
81
81
|
end
|
|
82
82
|
end
|
|
83
83
|
|
|
@@ -86,22 +86,22 @@ module Tuile
|
|
|
86
86
|
# @return [void]
|
|
87
87
|
def on_width_changed
|
|
88
88
|
super
|
|
89
|
-
|
|
89
|
+
update_rows
|
|
90
90
|
end
|
|
91
91
|
|
|
92
92
|
private
|
|
93
93
|
|
|
94
|
-
# Recomputes {@
|
|
94
|
+
# Recomputes {@rows} for the current text and rect width.
|
|
95
95
|
# Each line is ellipsized to fit and padded with trailing spaces out to
|
|
96
|
-
# the full width, so {#repaint} is just a lookup + {Buffer#
|
|
97
|
-
# row. {@
|
|
96
|
+
# the full width, so {#repaint} is just a lookup + {Buffer#set_text} per
|
|
97
|
+
# row. {@blank_row} covers rows past the last text line. When {#bg} is
|
|
98
98
|
# set, every produced line (and the blank row) has the bg applied
|
|
99
99
|
# uniformly.
|
|
100
100
|
# @return [void]
|
|
101
|
-
def
|
|
101
|
+
def update_rows
|
|
102
102
|
width = rect.width.clamp(0, nil)
|
|
103
|
-
@
|
|
104
|
-
@
|
|
103
|
+
@blank_row = apply_bg(StyledString.plain(" " * width))
|
|
104
|
+
@rows = @text.lines.map { |line| apply_bg(pad_to(line.ellipsize(width), width)) }
|
|
105
105
|
end
|
|
106
106
|
|
|
107
107
|
# @param line [StyledString]
|