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.
Files changed (39) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +32 -0
  3. data/DECISIONS.md +680 -8
  4. data/README.md +12 -13
  5. data/TERMINOLOGY.md +61 -0
  6. data/book/02-repaint.md +1 -1
  7. data/book/03-layout.md +1 -1
  8. data/book/06-theming.md +1 -1
  9. data/book/07-components.md +97 -27
  10. data/examples/file_commander.rb +5 -4
  11. data/examples/sampler.rb +38 -1
  12. data/ideas/new-components.md +9 -4
  13. data/lib/tuile/buffer.rb +7 -7
  14. data/lib/tuile/component/button.rb +1 -1
  15. data/lib/tuile/component/checkbox.rb +1 -1
  16. data/lib/tuile/component/checkbox_group.rb +31 -26
  17. data/lib/tuile/component/combo_box.rb +10 -7
  18. data/lib/tuile/component/info_window.rb +1 -1
  19. data/lib/tuile/component/label.rb +14 -14
  20. data/lib/tuile/component/list.rb +291 -216
  21. data/lib/tuile/component/list_dropdown.rb +14 -7
  22. data/lib/tuile/component/notification.rb +317 -0
  23. data/lib/tuile/component/picker_window.rb +3 -3
  24. data/lib/tuile/component/popup.rb +8 -10
  25. data/lib/tuile/component/progress_bar.rb +1 -1
  26. data/lib/tuile/component/radio_group.rb +32 -30
  27. data/lib/tuile/component/select.rb +7 -7
  28. data/lib/tuile/component/text_area/wrapped_text.rb +320 -0
  29. data/lib/tuile/component/text_area.rb +79 -273
  30. data/lib/tuile/component/text_field.rb +1 -1
  31. data/lib/tuile/component/text_view.rb +191 -177
  32. data/lib/tuile/component/window.rb +8 -8
  33. data/lib/tuile/component.rb +5 -5
  34. data/lib/tuile/screen.rb +1 -1
  35. data/lib/tuile/styled_string.rb +12 -12
  36. data/lib/tuile/version.rb +1 -1
  37. data/lib/tuile/vertical_scroll_bar.rb +6 -6
  38. data/sig/tuile.rbs +788 -377
  39. 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} is its single
27
- # {HasContent} child, which is where the cursor, scrolling, the scrollbar and
28
- # per-row mouse hit-testing come from. `content` is that list, so an app can
29
- # tune it (`scrollbar_visibility`, `show_cursor_when_inactive`, …). Rows
30
- # beyond {#rect}'s height scroll; the inner list is the tab stop, not the
31
- # group.
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 an item by index.
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.on_item_chosen = ->(index, _line) { toggle_at(index) }
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
- attr_reader :items
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
- raise TypeError, "expected Array, got #{new_items.inspect}" unless new_items.is_a?(Array)
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
- rebuild_rows
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
- rebuild_rows
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, @items.size - 1)
154
+ return unless index.between?(0, items.size - 1)
156
155
 
157
- item = @items[index]
158
- self.value = value.include?(item) ? value - [item] : value + [item]
156
+ toggle(items[index])
159
157
  end
160
158
 
161
- # Re-renders every row from the current items, labels and selection.
159
+ # Flips `item`'s membership of {#value}.
160
+ # @param item [Object]
162
161
  # @return [void]
163
- def rebuild_rows
164
- content.lines = @items.map do |item|
165
- StyledString.plain(value.include?(item) ? "[x] " : "[ ] ") + label_for(item)
166
- end
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.on_item_chosen = ->(index, _line) { commit(index) }
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.lines = @filtered.map { |item| @item_label.call(item) }
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 at the menu's `index`: closes the dropdown and adopts
218
- # it as {#value} (which repaints the field with its label).
219
- # @param index [Integer]
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(index)
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.open(content: InfoWindow.new(caption, lines), size: size)
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
- @clipped_lines = []
20
- @blank_line = ""
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
- update_clipped_lines
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
- update_clipped_lines
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#draw_line}, so the padding and blank
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 = @clipped_lines[row] || @blank_line
80
- draw_line(rect.left, rect.top + row, line)
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
- update_clipped_lines
89
+ update_rows
90
90
  end
91
91
 
92
92
  private
93
93
 
94
- # Recomputes {@clipped_lines} for the current text and rect width.
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#set_line} per
97
- # row. {@blank_line} covers rows past the last text line. When {#bg} is
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 update_clipped_lines
101
+ def update_rows
102
102
  width = rect.width.clamp(0, nil)
103
- @blank_line = apply_bg(StyledString.plain(" " * width))
104
- @clipped_lines = @text.lines.map { |line| apply_bg(pad_to(line.ellipsize(width), width)) }
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]