tuile 0.15.0 → 0.16.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 (78) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +121 -80
  3. data/README.md +28 -12
  4. data/book/04-event-loop.md +12 -12
  5. data/book/05-focus.md +95 -26
  6. data/book/06-theming.md +58 -26
  7. data/book/07-components.md +61 -6
  8. data/book/08-testing.md +24 -22
  9. data/book/10-locale.md +2 -2
  10. data/book/README.md +6 -5
  11. data/examples/file_commander.rb +14 -5
  12. data/examples/hello_world.rb +17 -4
  13. data/examples/sampler.rb +392 -18
  14. data/lib/tuile/component/abstract_string_field.rb +16 -18
  15. data/lib/tuile/component/abstract_wrapping_field.rb +47 -11
  16. data/lib/tuile/component/button.rb +8 -8
  17. data/lib/tuile/component/checkbox.rb +9 -9
  18. data/lib/tuile/component/checkbox_group.rb +6 -5
  19. data/lib/tuile/component/combo_box.rb +50 -35
  20. data/lib/tuile/component/confirm_window.rb +7 -5
  21. data/lib/tuile/component/date_field.rb +28 -3
  22. data/lib/tuile/component/date_time_field.rb +275 -0
  23. data/lib/tuile/component/has_bad_input.rb +2 -2
  24. data/lib/tuile/component/has_content.rb +3 -3
  25. data/lib/tuile/component/has_placeholder.rb +1 -1
  26. data/lib/tuile/component/has_validation.rb +2 -2
  27. data/lib/tuile/component/has_value.rb +1 -1
  28. data/lib/tuile/component/label.rb +1 -1
  29. data/lib/tuile/component/layout/box.rb +4 -1
  30. data/lib/tuile/component/layout.rb +3 -3
  31. data/lib/tuile/component/list.rb +42 -32
  32. data/lib/tuile/component/list_dropdown.rb +3 -3
  33. data/lib/tuile/component/menu_bar/cascade.rb +5 -5
  34. data/lib/tuile/component/menu_bar.rb +18 -18
  35. data/lib/tuile/component/notification.rb +32 -18
  36. data/lib/tuile/component/overlay.rb +9 -8
  37. data/lib/tuile/component/picker_window.rb +27 -8
  38. data/lib/tuile/component/popup.rb +2 -2
  39. data/lib/tuile/component/progress_bar.rb +10 -4
  40. data/lib/tuile/component/radio_group.rb +6 -5
  41. data/lib/tuile/component/select.rb +11 -12
  42. data/lib/tuile/component/slot.rb +3 -3
  43. data/lib/tuile/component/tab_sheet.rb +6 -6
  44. data/lib/tuile/component/tabs.rb +11 -11
  45. data/lib/tuile/component/text_area.rb +12 -10
  46. data/lib/tuile/component/text_field.rb +14 -12
  47. data/lib/tuile/component/text_view.rb +15 -11
  48. data/lib/tuile/component/time_field.rb +29 -4
  49. data/lib/tuile/component.rb +201 -93
  50. data/lib/tuile/event_queue.rb +4 -4
  51. data/lib/tuile/fake_event_queue.rb +1 -1
  52. data/lib/tuile/fake_screen.rb +84 -2
  53. data/lib/tuile/mouse/router.rb +217 -0
  54. data/lib/tuile/mouse.rb +177 -0
  55. data/lib/tuile/screen.rb +98 -61
  56. data/lib/tuile/screen_pane.rb +41 -36
  57. data/lib/tuile/styled_string.rb +5 -5
  58. data/lib/tuile/testing.rb +8 -8
  59. data/lib/tuile/theme.rb +22 -34
  60. data/lib/tuile/version.rb +1 -1
  61. data/lib/tuile/vertical_scroll_bar.rb +1 -1
  62. data/sig/tuile.rbs +1211 -427
  63. metadata +4 -16
  64. data/COMPARISON.md +0 -101
  65. data/DECISIONS.md +0 -8562
  66. data/TERMINOLOGY.md +0 -85
  67. data/ideas/arrow-key-navigation.md +0 -221
  68. data/ideas/binder.md +0 -177
  69. data/ideas/composite-field.md +0 -77
  70. data/ideas/focus-accent.md +0 -116
  71. data/ideas/form-layout.md +0 -151
  72. data/ideas/hover/probe.rb +0 -241
  73. data/ideas/hover/probe_spec.rb +0 -82
  74. data/ideas/hover.md +0 -909
  75. data/ideas/modal-backdrop.md +0 -24
  76. data/ideas/new-components.md +0 -144
  77. data/ideas/per-component-buffers.md +0 -55
  78. data/lib/tuile/mouse_event.rb +0 -68
data/lib/tuile/testing.rb CHANGED
@@ -4,7 +4,7 @@ module Tuile
4
4
  # Finds a component in the tree, so a spec can drive the UI it built four
5
5
  # layers down:
6
6
  #
7
- # Testing.get(Component::Button, caption: "Save").handle_key(Keys::ENTER)
7
+ # Testing.get(Component::Button, caption: "Save").handle_key?(Keys::ENTER)
8
8
  # Testing.get(id: :name).value = "Zaphod"
9
9
  # Testing.find(Component::Checkbox, in: pane, count: 3)
10
10
  #
@@ -20,7 +20,7 @@ module Tuile
20
20
  #
21
21
  # **A hidden component is never found**: these simulate a user, and a spec
22
22
  # that drove a hidden {Component::Button} would pass against a form nobody
23
- # can operate. Both walk {Component#on_shown_tree}; a failed lookup says how
23
+ # can operate. Both walk {Component#walk_shown_tree}; a failed lookup says how
24
24
  # many hidden components *would* have matched, and {.dump} shows them.
25
25
  #
26
26
  # Testing.find(Component::TextField, count: 0) # the user can't reach it
@@ -31,7 +31,7 @@ module Tuile
31
31
  #
32
32
  # For *what a component shows*, assert on {Screen#buffer} instead — this
33
33
  # locates and drives, it does not replace that channel. See book ch8 for the
34
- # worked usage and `DECISIONS.md` `D_component_lookup` for the design.
34
+ # worked usage and `design/decisions.md` `D_component_lookup` for the design.
35
35
  module Testing
36
36
  # Raised when the match count is not the one asked for. A {Tuile::Error},
37
37
  # so an app rescuing that still catches it.
@@ -67,7 +67,7 @@ module Tuile
67
67
  scope = binding.local_variable_get(:in) || Screen.instance.pane
68
68
  spec = ->(c) { matches_spec?(c, klass, id, caption, predicate) }
69
69
  matches = []
70
- scope.on_shown_tree { |c| matches << c if spec.call(c) }
70
+ scope.walk_shown_tree { |c| matches << c if spec.call(c) }
71
71
  return matches if count.nil? || spec_match?(count, matches.size)
72
72
 
73
73
  raise LookupError, failure(klass, id, caption, predicate, count, matches, scope, spec)
@@ -110,9 +110,9 @@ module Tuile
110
110
  def dump(scope, marked = [], excluded = [])
111
111
  base = scope.depth
112
112
  rows = []
113
- # on_tree, not on_shown_tree: a reader looks here to find out where
113
+ # walk_tree, not walk_shown_tree: a reader looks here to find out where
114
114
  # their component went, so the ones the search skipped are the point.
115
- scope.on_tree do |c|
115
+ scope.walk_tree do |c|
116
116
  mark = if marked.any? { _1.equal?(c) } then "→"
117
117
  elsif excluded.any? { _1.equal?(c) } then "⊘"
118
118
  else " "
@@ -180,9 +180,9 @@ module Tuile
180
180
  # @return [Array<Component>]
181
181
  def hidden_matches(scope, spec)
182
182
  shown = Set.new
183
- scope.on_shown_tree { shown << _1 }
183
+ scope.walk_shown_tree { shown << _1 }
184
184
  hidden = []
185
- scope.on_tree { |c| hidden << c if !shown.include?(c) && spec.call(c) }
185
+ scope.walk_tree { |c| hidden << c if !shown.include?(c) && spec.call(c) }
186
186
  hidden
187
187
  end
188
188
 
data/lib/tuile/theme.rb CHANGED
@@ -7,12 +7,12 @@ module Tuile
7
7
  # restyles everything via one invalidate-everything pass. Book ch6 is the
8
8
  # concept in full (why accents-only, dark/light, live OS flips).
9
9
  #
10
- # The rendering helpers — {#active_bg}, {#active_border}, {#input_bg},
11
- # {#hint} — wrap a plain string in the token's SGR color (on the channel
12
- # appropriate for the token's role) and reset:
10
+ # The rendering helpers — {#active_bg}, {#active_border}, {#input_bg} — wrap
11
+ # a plain string in the token's SGR color (on the channel appropriate for the
12
+ # token's role) and reset:
13
13
  #
14
- # screen.theme.active_bg("[ Ok ]") # => "\e[48;5;59m[ Ok ]\e[0m"
15
- # screen.theme.hint("quit") # => "\e[38;5;109mquit\e[0m"
14
+ # screen.theme.active_bg("[ Ok ]") # => "\e[48;5;59m[ Ok ]\e[0m"
15
+ # screen.theme.active_border("┌────┐") # => "\e[32m┌────┐\e[0m"
16
16
  #
17
17
  # Content passes through verbatim (so it may carry other escapes). For
18
18
  # span-aware styling — a token applied to a {StyledString} without flattening
@@ -44,7 +44,7 @@ module Tuile
44
44
  #
45
45
  # For a color slot resolved *live* at paint — currently
46
46
  # {Component#bg_color=} — assign a {Ref} instead of reading + rebuilding
47
- # the token in {Component#on_theme_changed}; it tracks theme swaps on its
47
+ # the token in {Component#handle_theme_changed}; it tracks theme swaps on its
48
48
  # own. Baked content colors ({Component::Label} text and friends) can't:
49
49
  # they live in a frozen {StyledString} and still need the hook.
50
50
  #
@@ -64,12 +64,6 @@ module Tuile
64
64
  # {Component::TextArea} when *not* active — visibly a field, but
65
65
  # distinctly subtler than {#active_bg_color}.
66
66
  # @return [Color]
67
- # @!attribute [r] hint_color
68
- # Foreground of subdued *accent* text an app wants noticed — a
69
- # keyboard-shortcut caption, a {Component::PickerWindow} option. An accent,
70
- # not a grey: it pulls the eye, which is why a placeholder uses
71
- # {#placeholder_color} instead. See {#hint}.
72
- # @return [Color]
73
67
  # @!attribute [r] placeholder_color
74
68
  # Foreground of the hint a field paints into its own empty well
75
69
  # ({Component::HasPlaceholder}) — the one token tuned to be *barely*
@@ -103,13 +97,12 @@ module Tuile
103
97
  # lookups (it fail-fasts on typos); read this directly to enumerate
104
98
  # the tokens.
105
99
  # @return [Hash{Symbol => Color}]
106
- class Theme < Data.define(:active_bg_color, :active_border_color, :input_bg_color, :hint_color,
100
+ class Theme < Data.define(:active_bg_color, :active_border_color, :input_bg_color,
107
101
  :placeholder_color, :error_color, :error_bg_color, :error_active_bg_color,
108
102
  :scrollbar_color, :custom)
109
103
  # @param active_bg_color [Color]
110
104
  # @param active_border_color [Color]
111
105
  # @param input_bg_color [Color]
112
- # @param hint_color [Color]
113
106
  # @param placeholder_color [Color]
114
107
  # @param error_color [Color]
115
108
  # @param error_bg_color [Color]
@@ -118,9 +111,9 @@ module Tuile
118
111
  # @param custom [Hash{Symbol => Color}] app-specific tokens, see {#custom}.
119
112
  # @raise [TypeError] when a token is not a {Color}, or `custom` is not a
120
113
  # `Hash{Symbol => Color}`.
121
- def initialize(active_bg_color:, active_border_color:, input_bg_color:, hint_color:, placeholder_color:,
114
+ def initialize(active_bg_color:, active_border_color:, input_bg_color:, placeholder_color:,
122
115
  error_color:, error_bg_color:, error_active_bg_color:, scrollbar_color:, custom: {})
123
- { active_bg_color:, active_border_color:, input_bg_color:, hint_color:, placeholder_color:,
116
+ { active_bg_color:, active_border_color:, input_bg_color:, placeholder_color:,
124
117
  error_color:, error_bg_color:, error_active_bg_color:, scrollbar_color: }.each do |name, value|
125
118
  raise TypeError, "#{name} must be a Tuile::Color, got #{value.inspect}" unless value.is_a?(Color)
126
119
  end
@@ -130,7 +123,7 @@ module Tuile
130
123
  raise TypeError, "custom key must be a Symbol, got #{key.inspect}" unless key.is_a?(Symbol)
131
124
  raise TypeError, "custom[#{key.inspect}] must be a Tuile::Color, got #{value.inspect}" unless value.is_a?(Color)
132
125
  end
133
- super(active_bg_color:, active_border_color:, input_bg_color:, hint_color:, placeholder_color:,
126
+ super(active_bg_color:, active_border_color:, input_bg_color:, placeholder_color:,
134
127
  error_color:, error_bg_color:, error_active_bg_color:, scrollbar_color:, custom: custom.dup.freeze)
135
128
  end
136
129
 
@@ -162,7 +155,7 @@ module Tuile
162
155
  # A live reference to a theme token, resolved against the current theme at
163
156
  # paint time rather than baked to a concrete {Color}. Assign one where a
164
157
  # slot is resolved late — currently {Component#bg_color=} — and it follows
165
- # light/dark flips with no {Component#on_theme_changed} hook:
158
+ # light/dark flips with no {Component#handle_theme_changed} hook:
166
159
  #
167
160
  # panel.bg_color = Tuile::Theme.ref(:panel_bg) # a #custom token
168
161
  # dropdown.bg_color = Tuile::Theme.ref(:input_bg_color) # built-in chrome
@@ -194,7 +187,14 @@ module Tuile
194
187
  end
195
188
 
196
189
  # Renders `text` in the foreground color of the app-specific `token`
197
- # — the generic counterpart of {#hint} for {#custom} tokens.
190
+ # — the generic counterpart of {#active_border} for {#custom} tokens, and
191
+ # the route for an app's own status-line chrome:
192
+ #
193
+ # "q #{screen.theme.fg(:hint, "quit")}" # => "q \e[38;5;245mquit\e[0m"
194
+ #
195
+ # The color is baked into the returned String, so text built this way does
196
+ # *not* restyle on a {Screen#theme=} — rebuild it from
197
+ # {Component#handle_theme_changed} instead.
198
198
  # @param token [Symbol]
199
199
  # @param text [String]
200
200
  # @return [String] ANSI-rendered text, ending with an SGR reset.
@@ -226,18 +226,9 @@ module Tuile
226
226
  # @return [String] ANSI-rendered text, ending with an SGR reset.
227
227
  def input_bg(text) = wrap(text, input_bg_color, :bg)
228
228
 
229
- # Renders `text` in the {#hint_color} foreground, for status-bar hints,
230
- # e.g. `"q #{screen.theme.hint("quit")}"`. The color is baked into the
231
- # returned String, so strings built this way do *not* restyle when the
232
- # theme changes — rebuild them instead (the framework's own call sites
233
- # rebuild on every status-bar refresh).
234
- # @param text [String]
235
- # @return [String] ANSI-rendered text, ending with an SGR reset.
236
- def hint(text) = wrap(text, hint_color, :fg)
237
-
238
229
  # The colors Tuile used before themes existed, tuned for dark terminal
239
230
  # backgrounds. GREY37 (palette 59) is what Rainbow emits for
240
- # `:darkslategray`, LIGHT_SKY_BLUE3 (109) for `:cadetblue`; GREY27
231
+ # `:darkslategray`; GREY27
241
232
  # (238, ~#444444) sits in the grayscale ramp, bright enough to stand
242
233
  # out against non-pure-black dark terminal themes (Gruvbox/Solarized/
243
234
  # OneDark base backgrounds sit in the #1d–#2d range) yet distinctly
@@ -263,12 +254,11 @@ module Tuile
263
254
  # as typed text. It is the *dimmest* grey that still quantizes to `:white` on
264
255
  # a 16-color terminal — everything below 248 lands on `:bright_black`
265
256
  # alongside both wells, where the hint is not subtle but gone
266
- # (`DECISIONS.md` `D_placeholder`).
257
+ # (`design/decisions.md` `D_placeholder`).
267
258
  # @return [Theme]
268
259
  DARK = new(active_bg_color: Color::GREY37,
269
260
  active_border_color: Color::GREEN,
270
261
  input_bg_color: Color::GREY27,
271
- hint_color: Color::LIGHT_SKY_BLUE3,
272
262
  placeholder_color: Color::GREY66,
273
263
  error_color: Color::INDIAN_RED1,
274
264
  error_bg_color: Color.palette(88),
@@ -278,8 +268,7 @@ module Tuile
278
268
  # Counterparts legible on light terminal backgrounds: grayscale-ramp
279
269
  # highlights just below white (GREY82 = 252 ~#d0d0d0, GREY85 = 253
280
270
  # ~#dadada — dark enough to read as a "well" against white, one step
281
- # lighter than the active highlight) and a dark teal (TURQUOISE4 = 30,
282
- # ~#008787) keeping the hint hue. `active_border_color` stays the
271
+ # lighter than the active highlight). `active_border_color` stays the
283
272
  # named green — named ANSI colors are remapped by the terminal's own
284
273
  # palette, so the theme picks a light-appropriate green for us. GREY62
285
274
  # (247, ~#9e9e9e) is the scrollbar: a *foreground* against pale, so it
@@ -305,7 +294,6 @@ module Tuile
305
294
  LIGHT = new(active_bg_color: Color::GREY82,
306
295
  active_border_color: Color::GREEN,
307
296
  input_bg_color: Color::GREY85,
308
- hint_color: Color::TURQUOISE4,
309
297
  placeholder_color: Color::GREY62,
310
298
  error_color: Color::RED3,
311
299
  error_bg_color: Color::MISTY_ROSE1,
data/lib/tuile/version.rb CHANGED
@@ -2,5 +2,5 @@
2
2
 
3
3
  module Tuile
4
4
  # @return [String]
5
- VERSION = "0.15.0"
5
+ VERSION = "0.16.0"
6
6
  end
@@ -18,7 +18,7 @@ module Tuile
18
18
  # track is a solid column carrying no information, so `row_count <= height`
19
19
  # paints track at every row — while {#handle_height} / {#handle_start} /
20
20
  # {#handle_end} still report the covering handle. Ink only: the caller's bar
21
- # keeps its column and its content width (`DECISIONS.md` `D_scrollbar_ink`).
21
+ # keeps its column and its content width (`design/decisions.md` `D_scrollbar_ink`).
22
22
  class VerticalScrollBar
23
23
  class << self
24
24
  # The glyph drawn where the handle covers a row, `█` by default. Set the