tuile 0.16.0 → 0.17.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 (92) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +108 -0
  3. data/README.md +21 -12
  4. data/book/02-repaint.md +47 -19
  5. data/book/03-layout.md +98 -49
  6. data/book/04-event-loop.md +5 -4
  7. data/book/05-focus.md +12 -9
  8. data/book/06-theming.md +55 -17
  9. data/book/07-components.md +188 -40
  10. data/book/08-testing.md +115 -15
  11. data/book/10-locale.md +1 -1
  12. data/book/README.md +5 -5
  13. data/examples/file_commander.rb +38 -27
  14. data/examples/hello_world.rb +1 -1
  15. data/examples/sampler.rb +225 -169
  16. data/lib/tuile/buffer.rb +12 -1
  17. data/lib/tuile/canvas/backend.rb +46 -0
  18. data/lib/tuile/canvas.rb +212 -0
  19. data/lib/tuile/color.rb +38 -9
  20. data/lib/tuile/component/abstract_string_field.rb +81 -80
  21. data/lib/tuile/component/abstract_wrapping_field.rb +59 -54
  22. data/lib/tuile/component/big_decimal_field.rb +7 -6
  23. data/lib/tuile/component/button.rb +19 -11
  24. data/lib/tuile/component/checkbox.rb +12 -10
  25. data/lib/tuile/component/checkbox_group.rb +11 -13
  26. data/lib/tuile/component/combo_box.rb +30 -40
  27. data/lib/tuile/component/confirm_window.rb +27 -22
  28. data/lib/tuile/component/date_field.rb +27 -20
  29. data/lib/tuile/component/date_time_field.rb +75 -31
  30. data/lib/tuile/component/fill.rb +93 -0
  31. data/lib/tuile/component/float_field.rb +7 -6
  32. data/lib/tuile/component/form_item.rb +250 -0
  33. data/lib/tuile/component/form_layout.rb +206 -0
  34. data/lib/tuile/component/has_bad_input.rb +98 -27
  35. data/lib/tuile/component/has_caption.rb +14 -5
  36. data/lib/tuile/component/has_content.rb +5 -12
  37. data/lib/tuile/component/has_validation.rb +39 -13
  38. data/lib/tuile/component/has_value.rb +70 -16
  39. data/lib/tuile/component/integer_field.rb +7 -6
  40. data/lib/tuile/component/label.rb +8 -15
  41. data/lib/tuile/component/layout/absolute.rb +86 -0
  42. data/lib/tuile/component/layout/box.rb +38 -63
  43. data/lib/tuile/component/layout.rb +124 -10
  44. data/lib/tuile/component/list.rb +197 -94
  45. data/lib/tuile/component/list_dropdown.rb +148 -88
  46. data/lib/tuile/component/menu_bar/cascade.rb +97 -27
  47. data/lib/tuile/component/menu_bar.rb +84 -64
  48. data/lib/tuile/component/notification.rb +44 -31
  49. data/lib/tuile/component/overlay.rb +210 -52
  50. data/lib/tuile/component/password_field.rb +1 -8
  51. data/lib/tuile/component/picker_window.rb +15 -10
  52. data/lib/tuile/component/popup.rb +13 -24
  53. data/lib/tuile/component/progress_bar.rb +7 -7
  54. data/lib/tuile/component/radio_group.rb +10 -12
  55. data/lib/tuile/component/scroller.rb +266 -0
  56. data/lib/tuile/component/select.rb +15 -31
  57. data/lib/tuile/component/slot.rb +1 -2
  58. data/lib/tuile/component/tab_sheet.rb +21 -28
  59. data/lib/tuile/component/tabs.rb +39 -24
  60. data/lib/tuile/component/text_area/wrapped_text.rb +1 -1
  61. data/lib/tuile/component/text_area.rb +21 -19
  62. data/lib/tuile/component/text_field.rb +55 -39
  63. data/lib/tuile/component/text_view.rb +143 -79
  64. data/lib/tuile/component/time_field.rb +26 -21
  65. data/lib/tuile/component/vertical_scroll_bar.rb +257 -0
  66. data/lib/tuile/component/window.rb +27 -26
  67. data/lib/tuile/component.rb +481 -259
  68. data/lib/tuile/component_background.rb +177 -0
  69. data/lib/tuile/component_util.rb +43 -0
  70. data/lib/tuile/event.rb +29 -0
  71. data/lib/tuile/event_queue.rb +14 -0
  72. data/lib/tuile/fake_screen.rb +41 -10
  73. data/lib/tuile/keys.rb +15 -6
  74. data/lib/tuile/layout_pass.rb +180 -0
  75. data/lib/tuile/listeners.rb +219 -0
  76. data/lib/tuile/mouse/router.rb +51 -35
  77. data/lib/tuile/mouse.rb +96 -29
  78. data/lib/tuile/point.rb +6 -0
  79. data/lib/tuile/rect.rb +33 -0
  80. data/lib/tuile/screen.rb +419 -84
  81. data/lib/tuile/screen_pane.rb +144 -31
  82. data/lib/tuile/strict_layout.rb +127 -0
  83. data/lib/tuile/styled_string.rb +139 -9
  84. data/lib/tuile/testing/gestures.rb +35 -0
  85. data/lib/tuile/testing.rb +310 -36
  86. data/lib/tuile/theme.rb +170 -19
  87. data/lib/tuile/theme_def.rb +4 -0
  88. data/lib/tuile/version.rb +1 -1
  89. data/lib/tuile.rb +53 -0
  90. data/sig/tuile.rbs +4951 -1158
  91. metadata +16 -2
  92. data/lib/tuile/vertical_scroll_bar.rb +0 -122
@@ -3,13 +3,35 @@
3
3
  module Tuile
4
4
  class Component
5
5
  # A layout doesn't paint anything by itself: its job is to position child
6
- # components. Two families, both top-down (see book ch3):
6
+ # components. Three ways to use one, all top-down (see book ch3):
7
7
  #
8
- # - {Absolute} — you override {Component#rect=} and compute every child's
9
- # rectangle yourself. Total control, and the base for anything unusual.
8
+ # - Subclass {Layout} and compute every child's rectangle yourself in a
9
+ # {Component#relayout} override. Total control, and the base for anything
10
+ # unusual:
11
+ #
12
+ # class SplitPane < Component::Layout
13
+ # def initialize
14
+ # super
15
+ # add(@sidebar = Component::List.new)
16
+ # add(@main = Component::Window.new("Main"))
17
+ # end
18
+ #
19
+ # protected
20
+ #
21
+ # def relayout
22
+ # left = width * 4 / 10
23
+ # @sidebar.rect = Rect.new(0, 0, left, height)
24
+ # @main.rect = Rect.new(left, 0, width - left, height)
25
+ # end
26
+ # end
27
+ #
28
+ # The framework runs it after a resize, an `add` or `remove`, and a
29
+ # child's {Component#visible=}.
30
+ # - {Absolute} — you give each child a fixed {Rect} and the layout assigns
31
+ # exactly that.
10
32
  # - {Box} / {Vertical} / {Horizontal} — you declare each child's extent as
11
- # a {Fixed}, {Percent} or {Expand} constraint and the layout does the
12
- # arithmetic. Sugar over the same `rect=` assignment, for the common case.
33
+ # a {Fixed}, {Percent} or {Expand} constraint — a {Percent} optionally
34
+ # bounded with {Percent#clamp} — and the layout does the arithmetic.
13
35
  #
14
36
  # Children that fully tile the layout's rect repaint themselves and
15
37
  # cover everything; children that leave gaps (e.g. a form with widgets
@@ -17,6 +39,18 @@ module Tuile
17
39
  # the background is cleared and children are re-invalidated so they
18
40
  # paint over a clean surface.
19
41
  class Layout < Component
42
+ # What {Fixed}, {Percent}, {Expand} and {Clamp} have in common: each is a
43
+ # value a {Box} resolves against the space it has. The set is closed —
44
+ # {Box#add} accepts these four and nothing else, so an app never
45
+ # implements the protocol.
46
+ module Constraint
47
+ # @api private
48
+ # @param available [Integer] the extent a {Box} divides.
49
+ # @return [Integer, nil] cells wanted, before the box clamps to what is
50
+ # left; `nil` for an {Expand}, which takes a share of the residue instead.
51
+ def resolve(available) = raise(NotImplementedError, "#{self.class} must implement resolve")
52
+ end
53
+
20
54
  # How much space a child gets along one axis of a {Box}: exactly {#cells},
21
55
  # clamped to whatever is still unassigned.
22
56
  #
@@ -35,6 +69,8 @@ module Tuile
35
69
  # @!attribute [r] cells
36
70
  # @return [Integer] cell count along the axis.
37
71
  class Fixed < Data.define(:cells)
72
+ include Constraint
73
+
38
74
  # @param cells [Integer] cell count along the axis; `>= 0`.
39
75
  # @raise [ArgumentError] unless `cells` is a non-negative Integer.
40
76
  def initialize(cells:)
@@ -44,6 +80,11 @@ module Tuile
44
80
 
45
81
  super
46
82
  end
83
+
84
+ # @api private
85
+ # @param _available [Integer]
86
+ # @return [Integer] {#cells}.
87
+ def resolve(_available) = cells
47
88
  end
48
89
 
49
90
  # A percentage of the space *available* along a {Box}'s axis — measured
@@ -56,6 +97,8 @@ module Tuile
56
97
  # @!attribute [r] percent
57
98
  # @return [Numeric] percentage of the available extent, `0..100`.
58
99
  class Percent < Data.define(:percent)
100
+ include Constraint
101
+
59
102
  # @param percent [Numeric] percentage of the available extent, `0..100`.
60
103
  # @raise [ArgumentError] unless `percent` is a Numeric in `0..100`.
61
104
  def initialize(percent:)
@@ -65,6 +108,25 @@ module Tuile
65
108
 
66
109
  super
67
110
  end
111
+
112
+ # Bounds the cells this share resolves to:
113
+ #
114
+ # add(sidebar, Percent[33].clamp(..16)) # a third, never over 16
115
+ # add(list, Percent[33].clamp(20..40)) # a third, within 20..40
116
+ #
117
+ # The floor is best-effort: a {Box} still clamps the result to what is
118
+ # unassigned, so an over-subscribed box starves a clamped child like any
119
+ # other.
120
+ # @param range [Range] inclusive, non-negative Integer endpoints, either
121
+ # one `nil` for unbounded.
122
+ # @raise [ArgumentError] on an unusable `range`.
123
+ # @return [Clamp]
124
+ def clamp(range) = Clamp[self, range]
125
+
126
+ # @api private
127
+ # @param available [Integer]
128
+ # @return [Integer] {#percent} of `available`, rounded.
129
+ def resolve(available) = (available * percent / 100.0).round
68
130
  end
69
131
 
70
132
  # A share of whatever a {Box} has left once its {Fixed} and {Percent}
@@ -81,6 +143,8 @@ module Tuile
81
143
  # @!attribute [r] weight
82
144
  # @return [Integer] relative share of the leftover space.
83
145
  class Expand < Data.define(:weight)
146
+ include Constraint
147
+
84
148
  # @param weight [Integer] relative share; `>= 1`.
85
149
  # @raise [ArgumentError] unless `weight` is a positive Integer.
86
150
  def initialize(weight:)
@@ -90,6 +154,61 @@ module Tuile
90
154
 
91
155
  super
92
156
  end
157
+
158
+ # @api private
159
+ # @param _available [Integer]
160
+ # @return [nil] an Expand has no size of its own — see {Constraint#resolve}.
161
+ def resolve(_available) = nil
162
+ end
163
+
164
+ # A {Percent} whose cells are bounded by {#range} — what {Percent#clamp}
165
+ # builds, and the way to say "half the width, but never more than 60
166
+ # columns":
167
+ #
168
+ # add(system, Percent[50].clamp(..60))
169
+ #
170
+ # Only a {Percent}: a {Fixed} is already exact, one range already carries
171
+ # both bounds, and an {Expand}'s share depends on its siblings, so capping
172
+ # it would have to hand cells back to them.
173
+ #
174
+ # @!attribute [r] percent
175
+ # @return [Percent] the share being bounded.
176
+ # @!attribute [r] range
177
+ # @return [Range] the inclusive bounds, in cells.
178
+ class Clamp < Data.define(:percent, :range)
179
+ include Constraint
180
+
181
+ # @param percent [Percent]
182
+ # @param range [Range] see {Percent#clamp}.
183
+ # @raise [ArgumentError] unless `percent` is a {Percent}; see {Percent#clamp}.
184
+ def initialize(percent:, range:)
185
+ raise ArgumentError, "Clamp expects a Percent, got #{percent.inspect}" unless percent.is_a?(Percent)
186
+
187
+ validate_range(range)
188
+ super
189
+ end
190
+
191
+ # @api private
192
+ # @param available [Integer]
193
+ # @return [Integer] {#percent}'s cells, clamped to {#range}.
194
+ def resolve(available) = percent.resolve(available).clamp(range)
195
+
196
+ private
197
+
198
+ # @param range [Object]
199
+ # @raise [ArgumentError] unless an inclusive Range of non-negative
200
+ # Integers (or `nil`s), ascending, bounded on at least one end.
201
+ # @return [void]
202
+ def validate_range(range)
203
+ valid = range.is_a?(Range) && !range.exclude_end? &&
204
+ [range.begin, range.end].all? { _1.nil? || (_1.is_a?(Integer) && !_1.negative?) } &&
205
+ !(range.begin.nil? && range.end.nil?) &&
206
+ (range.begin.nil? || range.end.nil? || range.begin <= range.end)
207
+ return if valid
208
+
209
+ raise ArgumentError, "clamp expects an inclusive Range of non-negative Integers bounded on " \
210
+ "at least one end, like ..60 or 20..40, got #{range.inspect}"
211
+ end
93
212
  end
94
213
 
95
214
  # Per-edge padding for a {Box}, in cells:
@@ -218,11 +337,6 @@ module Tuile
218
337
  screen.focused = first_focusable unless first_focusable.nil?
219
338
  end
220
339
  end
221
-
222
- # Absolute layout. Extend this class, register any children, and
223
- # override {Component#rect=} to reposition the children.
224
- class Absolute < Layout
225
- end
226
340
  end
227
341
  end
228
342
  end