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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +121 -80
- data/README.md +28 -12
- data/book/04-event-loop.md +12 -12
- data/book/05-focus.md +95 -26
- data/book/06-theming.md +58 -26
- data/book/07-components.md +61 -6
- data/book/08-testing.md +24 -22
- data/book/10-locale.md +2 -2
- data/book/README.md +6 -5
- data/examples/file_commander.rb +14 -5
- data/examples/hello_world.rb +17 -4
- data/examples/sampler.rb +392 -18
- data/lib/tuile/component/abstract_string_field.rb +16 -18
- data/lib/tuile/component/abstract_wrapping_field.rb +47 -11
- data/lib/tuile/component/button.rb +8 -8
- data/lib/tuile/component/checkbox.rb +9 -9
- data/lib/tuile/component/checkbox_group.rb +6 -5
- data/lib/tuile/component/combo_box.rb +50 -35
- data/lib/tuile/component/confirm_window.rb +7 -5
- data/lib/tuile/component/date_field.rb +28 -3
- data/lib/tuile/component/date_time_field.rb +275 -0
- data/lib/tuile/component/has_bad_input.rb +2 -2
- data/lib/tuile/component/has_content.rb +3 -3
- data/lib/tuile/component/has_placeholder.rb +1 -1
- data/lib/tuile/component/has_validation.rb +2 -2
- data/lib/tuile/component/has_value.rb +1 -1
- data/lib/tuile/component/label.rb +1 -1
- data/lib/tuile/component/layout/box.rb +4 -1
- data/lib/tuile/component/layout.rb +3 -3
- data/lib/tuile/component/list.rb +42 -32
- data/lib/tuile/component/list_dropdown.rb +3 -3
- data/lib/tuile/component/menu_bar/cascade.rb +5 -5
- data/lib/tuile/component/menu_bar.rb +18 -18
- data/lib/tuile/component/notification.rb +32 -18
- data/lib/tuile/component/overlay.rb +9 -8
- data/lib/tuile/component/picker_window.rb +27 -8
- data/lib/tuile/component/popup.rb +2 -2
- data/lib/tuile/component/progress_bar.rb +10 -4
- data/lib/tuile/component/radio_group.rb +6 -5
- data/lib/tuile/component/select.rb +11 -12
- data/lib/tuile/component/slot.rb +3 -3
- data/lib/tuile/component/tab_sheet.rb +6 -6
- data/lib/tuile/component/tabs.rb +11 -11
- data/lib/tuile/component/text_area.rb +12 -10
- data/lib/tuile/component/text_field.rb +14 -12
- data/lib/tuile/component/text_view.rb +15 -11
- data/lib/tuile/component/time_field.rb +29 -4
- data/lib/tuile/component.rb +201 -93
- data/lib/tuile/event_queue.rb +4 -4
- data/lib/tuile/fake_event_queue.rb +1 -1
- data/lib/tuile/fake_screen.rb +84 -2
- data/lib/tuile/mouse/router.rb +217 -0
- data/lib/tuile/mouse.rb +177 -0
- data/lib/tuile/screen.rb +98 -61
- data/lib/tuile/screen_pane.rb +41 -36
- data/lib/tuile/styled_string.rb +5 -5
- data/lib/tuile/testing.rb +8 -8
- data/lib/tuile/theme.rb +22 -34
- data/lib/tuile/version.rb +1 -1
- data/lib/tuile/vertical_scroll_bar.rb +1 -1
- data/sig/tuile.rbs +1211 -427
- metadata +4 -16
- data/COMPARISON.md +0 -101
- data/DECISIONS.md +0 -8562
- data/TERMINOLOGY.md +0 -85
- data/ideas/arrow-key-navigation.md +0 -221
- data/ideas/binder.md +0 -177
- data/ideas/composite-field.md +0 -77
- data/ideas/focus-accent.md +0 -116
- data/ideas/form-layout.md +0 -151
- data/ideas/hover/probe.rb +0 -241
- data/ideas/hover/probe_spec.rb +0 -82
- data/ideas/hover.md +0 -909
- data/ideas/modal-backdrop.md +0 -24
- data/ideas/new-components.md +0 -144
- data/ideas/per-component-buffers.md +0 -55
- 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#
|
|
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 `
|
|
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.
|
|
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
|
-
#
|
|
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.
|
|
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.
|
|
183
|
+
scope.walk_shown_tree { shown << _1 }
|
|
184
184
|
hidden = []
|
|
185
|
-
scope.
|
|
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
|
-
#
|
|
12
|
-
#
|
|
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 ]")
|
|
15
|
-
# screen.theme.
|
|
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#
|
|
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,
|
|
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:,
|
|
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:,
|
|
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:,
|
|
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#
|
|
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 {#
|
|
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
|
|
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
|
-
# (`
|
|
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)
|
|
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
|
@@ -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 (`
|
|
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
|