tuile 0.11.0 → 0.12.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 +32 -0
- data/DECISIONS.md +680 -8
- data/README.md +12 -13
- data/TERMINOLOGY.md +61 -0
- data/book/02-repaint.md +1 -1
- data/book/03-layout.md +1 -1
- data/book/06-theming.md +1 -1
- data/book/07-components.md +97 -27
- data/examples/file_commander.rb +5 -4
- data/examples/sampler.rb +38 -1
- data/ideas/new-components.md +9 -4
- data/lib/tuile/buffer.rb +7 -7
- 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 +10 -7
- data/lib/tuile/component/info_window.rb +1 -1
- data/lib/tuile/component/label.rb +14 -14
- data/lib/tuile/component/list.rb +291 -216
- data/lib/tuile/component/list_dropdown.rb +14 -7
- data/lib/tuile/component/notification.rb +317 -0
- data/lib/tuile/component/picker_window.rb +3 -3
- data/lib/tuile/component/popup.rb +8 -10
- data/lib/tuile/component/progress_bar.rb +1 -1
- data/lib/tuile/component/radio_group.rb +32 -30
- data/lib/tuile/component/select.rb +7 -7
- data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
- data/lib/tuile/component/text_area.rb +79 -273
- data/lib/tuile/component/text_field.rb +1 -1
- data/lib/tuile/component/text_view.rb +191 -177
- data/lib/tuile/component/window.rb +8 -8
- data/lib/tuile/component.rb +5 -5
- data/lib/tuile/screen.rb +1 -1
- data/lib/tuile/styled_string.rb +12 -12
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +6 -6
- data/sig/tuile.rbs +788 -377
- metadata +4 -1
|
@@ -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,8 @@ module Tuile
|
|
|
52
52
|
self.content = field
|
|
53
53
|
|
|
54
54
|
@overlay = ListDropdown.new
|
|
55
|
-
@overlay.
|
|
55
|
+
@overlay.renderer = ->(item) { @item_label.call(item) }
|
|
56
|
+
@overlay.on_item_chosen = ->(_index, item) { commit(item) }
|
|
56
57
|
end
|
|
57
58
|
|
|
58
59
|
# @return [Array] the candidate items.
|
|
@@ -195,7 +196,7 @@ module Tuile
|
|
|
195
196
|
if @filtered.empty?
|
|
196
197
|
close_menu
|
|
197
198
|
else
|
|
198
|
-
@overlay.
|
|
199
|
+
@overlay.items = @filtered
|
|
199
200
|
@overlay.cursor = List::Cursor.new(position: @filtered.index(value) || 0)
|
|
200
201
|
@overlay.open unless @overlay.open?
|
|
201
202
|
anchor
|
|
@@ -214,12 +215,11 @@ module Tuile
|
|
|
214
215
|
@items.select { |item| @item_label.call(item).to_s.downcase.include?(needle) }
|
|
215
216
|
end
|
|
216
217
|
|
|
217
|
-
# Commits the item
|
|
218
|
-
#
|
|
219
|
-
# @param
|
|
218
|
+
# Commits the item chosen from the menu: closes the dropdown and adopts it
|
|
219
|
+
# as {#value} (which repaints the field with its label).
|
|
220
|
+
# @param item [Object]
|
|
220
221
|
# @return [void]
|
|
221
|
-
def commit(
|
|
222
|
-
item = @filtered[index]
|
|
222
|
+
def commit(item)
|
|
223
223
|
close_menu
|
|
224
224
|
self.value = item
|
|
225
225
|
end
|
|
@@ -235,6 +235,9 @@ module Tuile
|
|
|
235
235
|
|
|
236
236
|
# Sets the field's text without triggering a refilter — for programmatic
|
|
237
237
|
# value changes and query reverts, which must not spring the dropdown.
|
|
238
|
+
# Every programmatic write to the field goes through here; a direct
|
|
239
|
+
# `content.text =` reaches the field's `on_change` and pops the dropdown
|
|
240
|
+
# open on a {#value=} the user never asked to browse.
|
|
238
241
|
# Parks the caret at the end: `text=` only *clamps* the caret, so a
|
|
239
242
|
# shorter query replaced by a longer label would otherwise strand it
|
|
240
243
|
# 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]
|