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
@@ -22,7 +22,7 @@ module Tuile
22
22
  # - an **index** counts characters into {#text} — {#caret},
23
23
  # {#max_text_length}, `text[i]`, every edit;
24
24
  # - a **column** counts terminal cells — {#rect}, {#cursor_position}, a
25
- # {MouseEvent}, and the private horizontal scroll offset `left_column`.
25
+ # {Mouse::DownEvent}, and the private horizontal scroll offset `left_column`.
26
26
  #
27
27
  # They coincide only while every glyph is one column wide. A fullwidth CJK
28
28
  # char is two columns and a combining mark zero, so index 3 of `"日本語"` is
@@ -103,15 +103,15 @@ module Tuile
103
103
  Point.new(rect.left + offset, rect.top)
104
104
  end
105
105
 
106
- # Places the caret at the clicked column. A click on the right half of a
106
+ # Places the caret at the pressed column. A press on the right half of a
107
107
  # wide glyph lands *after* it, as in any editor.
108
- # @param event [MouseEvent]
109
- # @return [void]
110
- def handle_mouse(event)
111
- super
112
- return unless event.button == :left && rect.contains?(event.point)
108
+ # @param event [Mouse::DownEvent]
109
+ # @return [Boolean]
110
+ def handle_mouse_down?(event)
111
+ return false unless event.button == :left
113
112
 
114
113
  self.caret = index_at(event.x - rect.left + @left_column)
114
+ true
115
115
  end
116
116
 
117
117
  # @return [void]
@@ -129,7 +129,7 @@ module Tuile
129
129
  # "clear what I typed".
130
130
  # @param key [String]
131
131
  # @return [Boolean]
132
- def handle_text_input_key(key)
132
+ def handle_text_input_key?(key)
133
133
  case key
134
134
  when *Keys::HOMES then self.caret = 0
135
135
  when *Keys::ENDS_ then self.caret = @text.length
@@ -159,7 +159,7 @@ module Tuile
159
159
  # Keeps the paste's **first line** and drops the rest, then trims what's
160
160
  # left to what {#max_text_length} still allows:
161
161
  #
162
- # f.handle_paste("name\nstreet\ncity") # => true
162
+ # f.handle_paste("name\nstreet\ncity")
163
163
  # f.text # => "name"
164
164
  #
165
165
  # Overshooting the cap trims rather than rejects, which is what typing the
@@ -175,17 +175,19 @@ module Tuile
175
175
  end
176
176
 
177
177
  # @return [void]
178
- def on_text_mutated
178
+ def handle_text_mutated
179
+ super
179
180
  adjust_left_column
180
181
  end
181
182
 
182
183
  # @return [void]
183
- def on_caret_mutated
184
+ def handle_caret_mutated
185
+ super
184
186
  adjust_left_column
185
187
  end
186
188
 
187
189
  # @return [void]
188
- def on_width_changed
190
+ def handle_width_changed
189
191
  super
190
192
  adjust_left_column
191
193
  end
@@ -20,8 +20,8 @@ module Tuile
20
20
  # splice a range in place. Turn on {#auto_scroll} to keep the latest content
21
21
  # in view.
22
22
  #
23
- # Meant to be the content of a {Window} — focus indication and keyboard-hint
24
- # surfacing rely on the surrounding window chrome.
23
+ # Meant to be the content of a {Window} — focus indication relies on the
24
+ # surrounding window chrome.
25
25
  class TextView < Component
26
26
  def initialize
27
27
  super
@@ -354,7 +354,7 @@ module Tuile
354
354
  # an unfocused view scrolls it (dispatch gates on focus, this doesn't).
355
355
  # @param key [String]
356
356
  # @return [Boolean]
357
- def handle_key(key)
357
+ def handle_key?(key)
358
358
  case key
359
359
  when *Keys::DOWN_ARROWS then move_scroll_top_row_by(1)
360
360
  when *Keys::UP_ARROWS then move_scroll_top_row_by(-1)
@@ -369,14 +369,18 @@ module Tuile
369
369
  true
370
370
  end
371
371
 
372
- # @param event [MouseEvent]
373
- # @return [void]
374
- def handle_mouse(event)
375
- super
376
- case event.button
377
- when :scroll_down then move_scroll_top_row_by(4)
378
- when :scroll_up then move_scroll_top_row_by(-4)
372
+ # Scrolls four rows a notch, and declines — so the notch bubbles to an
373
+ # ancestor scroller — once this view is at that end of its text.
374
+ # @param event [Mouse::ScrollEvent]
375
+ # @return [Boolean]
376
+ def handle_mouse_scroll?(event)
377
+ before = scroll_top_row
378
+ case event.direction
379
+ when :down then move_scroll_top_row_by(4)
380
+ when :up then move_scroll_top_row_by(-4)
381
+ else return false
379
382
  end
383
+ scroll_top_row != before
380
384
  end
381
385
 
382
386
  # Paints the text into {#rect}.
@@ -405,7 +409,7 @@ module Tuile
405
409
  # minus {#scrollbar_columns}, and the latter varies with the width too.
406
410
  # A {#scrollbar_visibility=} flip rewraps from its own setter instead.
407
411
  # @return [void]
408
- def on_width_changed
412
+ def handle_width_changed
409
413
  super
410
414
  rewrap
411
415
  end
@@ -58,6 +58,19 @@ module Tuile
58
58
  # the field (or pressing ENTER) reddens what did not parse, and the next
59
59
  # edit clears it again.
60
60
  #
61
+ # == The value notice waits for the same gesture
62
+ # A prefix of a time can also parse *cleanly*: typing `13:45` passes
63
+ # through `13:4`, a perfectly good four minutes past one. So
64
+ # {HasValue#on_value_change} does not fire per keystroke, but when the user
65
+ # leaves the field or presses ENTER — and a form recalculating from it
66
+ # never sees those intermediate readings.
67
+ #
68
+ # {#value} does *not* wait: it is a live parse of the buffer at every
69
+ # moment, so a save gate reached without leaving the field reads the time
70
+ # on screen. Nor does a change nobody had to type — a {#value=}, a
71
+ # {#set_to}, an arrow-key step, a {#clear} and a reparse under a new
72
+ # {#step} all fire as they happen.
73
+ #
61
74
  # == Implementation details
62
75
  # - **The buffer is the single source of truth.** {#value} is a parse of it,
63
76
  # recomputed on read — so {#step=} and a {Screen#locale=} can change the
@@ -182,6 +195,9 @@ module Tuile
182
195
  def value=(new_value)
183
196
  editor.text = new_value.nil? ? "" : coerce(new_value).strftime(formats.first)
184
197
  editor.caret = editor.text.length
198
+ # The edit above announced nothing ({#notify_on_edit?}); a time written
199
+ # rather than typed has no prefix to be mistaken for a value.
200
+ fire_if_changed
185
201
  end
186
202
 
187
203
  # Sets the value from its parts, so nothing assembles a `Time` on the
@@ -234,7 +250,7 @@ module Tuile
234
250
  #
235
251
  # Why precision rides the stride rather than a knob of its own, and what
236
252
  # that costs — seconds with a minute stride is unsayable — is
237
- # `DECISIONS.md` `D_time_field`.
253
+ # `design/decisions.md` `D_time_field`.
238
254
  #
239
255
  # @param seconds [Integer] 1 up to (not including) a full day.
240
256
  # @return [void]
@@ -309,7 +325,7 @@ module Tuile
309
325
  # @param key [String]
310
326
  # @return [Boolean] `true` for the two page keys, else whatever `super`
311
327
  # returns.
312
- def handle_key(key)
328
+ def handle_key?(key)
313
329
  case key
314
330
  when Keys::PAGE_UP then step_by(SECONDS_PER_HOUR)
315
331
  when Keys::PAGE_DOWN then step_by(-SECONDS_PER_HOUR)
@@ -330,6 +346,12 @@ module Tuile
330
346
  settle(true)
331
347
  end
332
348
 
349
+ # `false`: a prefix of a time can parse cleanly (`13:4` for `13:45`), so
350
+ # the notice settles onto the commit gestures, exactly as the ink does.
351
+ # The class docs carry the case.
352
+ # @return [Boolean]
353
+ def notify_on_edit? = false
354
+
333
355
  # Every prefix of a time is bad input, so the well is latched to the
334
356
  # commit gestures instead of painted per keystroke: `1`, `13`, `13:` on
335
357
  # the way to `13:45` never redden, and a time the field cannot parse
@@ -341,10 +363,13 @@ module Tuile
341
363
  # An edit is the user having another go, so the well goes quiet again
342
364
  # until the next commit gesture.
343
365
  # @return [void]
344
- def on_editor_change = settle(false)
366
+ def handle_editor_change
367
+ super
368
+ settle(false)
369
+ end
345
370
 
346
371
  # @return [void]
347
- def on_locale_changed
372
+ def handle_locale_changed
348
373
  super
349
374
  reformat
350
375
  end