hokusai-zero 0.2.6 → 0.2.7

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +0 -1
  3. data/Gemfile.lock +0 -2
  4. data/README.md +1 -1
  5. data/ast/src/core/ast.c +3 -11
  6. data/ast/src/core/hml.c +212 -40
  7. data/ast/src/core/hml.h +1 -0
  8. data/ast/src/core/input.h +0 -1
  9. data/ast/src/core/log.c +87 -0
  10. data/ast/src/core/log.h +41 -0
  11. data/ast/src/core/util.c +23 -23
  12. data/ast/src/core/util.h +7 -7
  13. data/ast/test/parser.c +1 -0
  14. data/ext/extconf.rb +6 -6
  15. data/hokusai.gemspec +1 -2
  16. data/ui/examples/drag.rb +154 -0
  17. data/ui/examples/embedded.rb +58 -0
  18. data/ui/examples/forum/file.rb +1 -1
  19. data/ui/examples/forum/post.rb +0 -1
  20. data/ui/examples/forum.rb +7 -7
  21. data/ui/examples/game.rb +143 -0
  22. data/ui/examples/keyboard.rb +47 -0
  23. data/ui/examples/overlay.rb +233 -0
  24. data/ui/examples/provider.rb +56 -0
  25. data/ui/examples/shader/test.rb +155 -0
  26. data/ui/examples/shader.rb +100 -0
  27. data/ui/examples/spreadsheet.rb +12 -11
  28. data/ui/examples/wiki.rb +82 -0
  29. data/ui/lib/lib_hokusai.rb +43 -24
  30. data/ui/spec/hokusai/e2e/keyboard_spec.rb +52 -0
  31. data/ui/spec/spec_helper.rb +1 -1
  32. data/ui/src/hokusai/assets/arrow-down-line.png +0 -0
  33. data/ui/src/hokusai/assets/arrow-down-wide-line.png +0 -0
  34. data/ui/src/hokusai/assets/icons/outline/arrow-big-up.svg +19 -0
  35. data/ui/src/hokusai/assets/icons/outline/backspace.svg +20 -0
  36. data/ui/src/hokusai/automation/driver_commands/base.rb +2 -8
  37. data/ui/src/hokusai/automation/driver_commands/trigger_keyboard.rb +3 -6
  38. data/ui/src/hokusai/automation/driver_commands/trigger_mouse.rb +12 -5
  39. data/ui/src/hokusai/automation/server.rb +2 -3
  40. data/ui/src/hokusai/backends/raylib/config.rb +2 -1
  41. data/ui/src/hokusai/backends/raylib/font.rb +24 -3
  42. data/ui/src/hokusai/backends/raylib.rb +167 -36
  43. data/ui/src/hokusai/backends/sdl2/config.rb +9 -6
  44. data/ui/src/hokusai/backends/sdl2/font.rb +3 -1
  45. data/ui/src/hokusai/backends/sdl2.rb +188 -93
  46. data/ui/src/hokusai/blocks/color_picker.rb +1080 -0
  47. data/ui/src/hokusai/blocks/input.rb +2 -2
  48. data/ui/src/hokusai/blocks/keyboard.rb +249 -0
  49. data/ui/src/hokusai/blocks/shader_begin.rb +22 -0
  50. data/ui/src/hokusai/blocks/shader_end.rb +12 -0
  51. data/ui/src/hokusai/blocks/slider.rb +139 -0
  52. data/ui/src/hokusai/blocks/texture.rb +23 -0
  53. data/ui/src/hokusai/commands/rect.rb +12 -3
  54. data/ui/src/hokusai/commands/shader.rb +33 -0
  55. data/ui/src/hokusai/commands/texture.rb +26 -0
  56. data/ui/src/hokusai/commands.rb +22 -0
  57. data/ui/src/hokusai/event.rb +2 -1
  58. data/ui/src/hokusai/events/keyboard.rb +11 -18
  59. data/ui/src/hokusai/events/mouse.rb +10 -8
  60. data/ui/src/hokusai/events/touch.rb +62 -0
  61. data/ui/src/hokusai/meta.rb +9 -4
  62. data/ui/src/hokusai/mounting/loop_entry.rb +1 -1
  63. data/ui/src/hokusai/mounting/update_entry.rb +7 -6
  64. data/ui/src/hokusai/painter.rb +31 -8
  65. data/ui/src/hokusai/types/display.rb +151 -0
  66. data/ui/src/hokusai/types/keyboard.rb +168 -0
  67. data/ui/src/hokusai/types/mouse.rb +36 -0
  68. data/ui/src/hokusai/types/primitives.rb +56 -0
  69. data/ui/src/hokusai/types/touch.rb +181 -0
  70. data/ui/src/hokusai/types.rb +20 -244
  71. data/ui/src/hokusai/util/wrap_stream.rb +193 -0
  72. data/ui/src/hokusai.rb +61 -35
  73. data/xmake.lua +2 -1
  74. metadata +34 -22
  75. data/ui/src/hokusai/assets/chevron-down.svg +0 -1
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hokusai
4
+ class Vec2
5
+ attr_accessor :x, :y
6
+ def initialize(x, y)
7
+ @x = x
8
+ @y = y
9
+ end
10
+ end
11
+
12
+ class Rect
13
+ attr_accessor :x, :y, :width, :height
14
+
15
+ def initialize(x, y, width, height)
16
+ @x = x
17
+ @y = y
18
+ @width = width
19
+ @height = height
20
+ end
21
+ def to_hoku_rect
22
+ @hoku_rect ||= LibHokusai::HmlRect.create(x, y, width, height)
23
+ end
24
+
25
+ def includes_y?(y)
26
+ LibHokusai.hoku_rect_includes_y(to_hoku_rect, y)
27
+ end
28
+
29
+ def includes_x?(x)
30
+ LibHokusai.hoku_rect_includes_x(to_hoku_rect, x)
31
+ end
32
+
33
+ def move_x_left(times = 1)
34
+ LibHokusai.hoku_rect_x_left(to_hoku_rect, times)
35
+ end
36
+
37
+ def move_x_right(times = 1)
38
+ LibHokusai.hoku_rect_x_right(to_hoku_rect, times)
39
+ end
40
+
41
+ def move_y_up(times = 1)
42
+ LibHokusai.hoku_rect_y_up(to_hoku_rect, times)
43
+ end
44
+
45
+ def move_y_down(times = 1)
46
+ LibHokusai.hoku_rect_y_down(to_hoku_rect, times)
47
+ end
48
+
49
+ def self.from_hoku_rect(rect)
50
+ self.x = rect[:x]
51
+ self.y = rect[:y]
52
+ self.width = rect[:w]
53
+ self.height = rect[:h]
54
+ end
55
+ end
56
+ end
@@ -0,0 +1,181 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Hokusai
4
+ class Touch
5
+ attr_accessor :stack, :archive
6
+
7
+ def initialize
8
+ @stack = []
9
+ @archive = []
10
+ @tapped = false
11
+ @swiped = false
12
+ @pinched = false
13
+ # @file = File.open("touch.log", "w")
14
+ end
15
+
16
+ def tapped?
17
+ @tapped
18
+ end
19
+
20
+ def swiped?
21
+ @swiped
22
+ end
23
+
24
+ def pinched?
25
+ @pinched
26
+ end
27
+
28
+ def longtapping?(stuff = "ok")
29
+ log("#{touching?} - #{elapsed(token)} - #{stuff}") if touching?
30
+ touching? && elapsed(token) > 5
31
+ end
32
+
33
+ def longtapped?
34
+ @longtapped
35
+ end
36
+
37
+ def touching?
38
+ type == :down || type == :move
39
+ end
40
+
41
+ def duration
42
+ if longtapping?
43
+ return elapsed(token)
44
+ end
45
+
46
+ first, last = archive[-2..-1]
47
+
48
+ last[:start] - first[:start]
49
+ end
50
+
51
+ def distance
52
+ raise Hokusai::Error.new("Archive is empty") if archive.empty?
53
+ first, last = archive[-2..-1]
54
+
55
+ x = last[:x] - first[:x]
56
+ y = last[:y] - first[:y]
57
+
58
+ [x, y]
59
+ end
60
+
61
+ def direction
62
+ raise Hokusai::Error.new("Archive is empty") if archive.empty?
63
+
64
+ first, last = archive[-2..-1]
65
+
66
+ x = last[:x] - first[:x]
67
+ y = last[:y] - first[:y]
68
+
69
+ if x.abs > y.abs
70
+ # swiping left/right
71
+ last[:x] > first[:x] ? :right : :left
72
+ else
73
+ # swiping up/down
74
+ last[:y] > first[:y] ? :down : :up
75
+ end
76
+ end
77
+
78
+ def angle
79
+ raise Hokusai::Error.new("Archive is empty") if archive.empty?
80
+
81
+ last, first = archive[-2..-1]
82
+
83
+ x = last[:x] - first[:x]
84
+ y = last[:y] - first[:y]
85
+
86
+ (Math.atan2(x, y) * (-180 / Math::PI)).round(0).to_i
87
+ end
88
+
89
+ def log(str)
90
+ # Thread.new do
91
+ # @file.write_nonblock("#{str}\n")
92
+ # end
93
+ end
94
+
95
+ def record(finger, x, y)
96
+ log("recording #{token}")
97
+ if type == :down
98
+ push(:move, finger, x, y)
99
+ log("state is move")
100
+ elsif type == :move
101
+ stack.last[:x] = x
102
+ stack.last[:y] = y
103
+
104
+ log("updated state move")
105
+ else
106
+ @longtapped = false
107
+ @swiped = false
108
+ @tapped = false
109
+ push(:down, finger, x, y)
110
+ log("state is down")
111
+ end
112
+ end
113
+
114
+ def clear
115
+ # log("clearing")
116
+ if type == :move
117
+ log("elapsed: #{elapsed(token)}")
118
+ if elapsed(token) > 300 && within(10.0)
119
+ @longtapped = true
120
+ log('longtap')
121
+ elsif within(10.0)
122
+ @tapped = true
123
+ else
124
+ @swiped = true
125
+ log('swipe')
126
+ end
127
+ elsif type == :down
128
+ @tapped = true
129
+ log('tap')
130
+ else
131
+ @longtapped = false
132
+ @swiped = false
133
+ @tapped = false
134
+ end
135
+
136
+ self.archive = stack.dup
137
+ stack.clear
138
+ end
139
+
140
+ def elapsed(token)
141
+ Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond) - token[:start]
142
+ end
143
+
144
+ def within(threshold)
145
+ move = stack.last
146
+ down = stack[-2]
147
+
148
+ t1 = (move[:x] - down[:x]).abs
149
+ t2 = (move[:y] - down[:y]).abs
150
+
151
+ t1 < threshold && t2 < threshold
152
+ end
153
+
154
+ def pop
155
+ stack.pop
156
+ end
157
+
158
+ def push(type, finger, x, y)
159
+ log("push: #{type}")
160
+ stack << {
161
+ type: type,
162
+ i: finger,
163
+ x: x,
164
+ y: y,
165
+ start: Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
166
+ }
167
+ end
168
+
169
+ def index
170
+ token&.[](:finger)
171
+ end
172
+
173
+ def type
174
+ token&.[](:type)
175
+ end
176
+
177
+ def token
178
+ @stack.last
179
+ end
180
+ end
181
+ end
@@ -1,266 +1,42 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- module Hokusai
4
- # Rect = Struct.new(:x, :y, :width, :height) do
5
- class Rect
6
- attr_accessor :x, :y, :width, :height
7
-
8
- def initialize(x, y, width, height)
9
- @x = x
10
- @y = y
11
- @width = width
12
- @height = height
13
- end
14
- def to_hoku_rect
15
- @hoku_rect ||= LibHokusai::HmlRect.create(x, y, width, height)
16
- end
17
-
18
- def includes_y?(y)
19
- LibHokusai.hoku_rect_includes_y(to_hoku_rect, y)
20
- end
21
-
22
- def includes_x?(x)
23
- LibHokusai.hoku_rect_includes_x(to_hoku_rect, x)
24
- end
25
-
26
- def move_x_left(times = 1)
27
- LibHokusai.hoku_rect_x_left(to_hoku_rect, times)
28
- end
29
-
30
- def move_x_right(times = 1)
31
- LibHokusai.hoku_rect_x_right(to_hoku_rect, times)
32
- end
33
-
34
- def move_y_up(times = 1)
35
- LibHokusai.hoku_rect_y_up(to_hoku_rect, times)
36
- end
37
-
38
- def move_y_down(times = 1)
39
- LibHokusai.hoku_rect_y_down(to_hoku_rect, times)
40
- end
41
-
42
- def self.from_hoku_rect(rect)
43
- self.x = rect[:x]
44
- self.y = rect[:y]
45
- self.width = rect[:w]
46
- self.height = rect[:h]
47
- end
48
- end
49
-
50
- Outline = Struct.new(:top, :right, :bottom, :left) do
51
- def self.default
52
- new(0.0, 0.0, 0.0, 0.0)
53
- end
3
+ require_relative "./types/primitives"
4
+ require_relative "./types/display"
5
+ require_relative "./types/touch"
6
+ require_relative "./types/mouse"
7
+ require_relative "./types/keyboard"
54
8
 
55
- def hash
56
- [self.class, top, right, bottom, left].hash
57
- end
58
-
59
- def self.convert(value)
60
- case value
61
- when String
62
- if value =~ /,/
63
- convert(value.split(",").map(&:to_f))
64
- else
65
- convert(value.to_f)
66
- end
67
- when Float
68
- new(value, value, value, value)
69
- when Array
70
- new(value[0] || 0.0, value[1] || 0.0, value[2] || 0.0, value[3] || 0.0)
71
- when Outline
72
- value
73
- end
74
- end
75
-
76
- def present?
77
- top > 0.0 || right > 0.0 || bottom > 0.0 || left > 0.0
78
- end
79
-
80
- def uniform?
81
- top == right && top == bottom && top == left
82
- end
83
- end
84
-
85
- class Padding
86
- attr_reader :top, :left, :right, :bottom
87
- def initialize(top, right, bottom, left)
88
- @top = top
89
- @left = left
90
- @right = right
91
- @bottom = bottom
92
- end
93
-
94
- alias_method :t, :top
95
- alias_method :l, :left
96
- alias_method :r, :right
97
- alias_method :b, :bottom
98
-
99
- def self.convert(value)
100
- case value
101
- when String
102
- if value =~ /,/
103
- convert(value.split(",").map(&:to_f))
104
- else
105
- convert(value.to_i)
106
- end
107
- when Integer
108
- new(value, value, value, value)
109
- when Array
110
- new(value[0], value[1], value[2], value[3])
111
- when Padding
112
- value
113
- else
114
- raise Hokusai::Error.new("Unsupported conversion type #{value.class} for Hokusai::Padding")
115
- end
116
- end
117
-
118
- def hash
119
- [self.class, top, right, bottom, left].hash
120
- end
121
- end
122
-
123
- class Canvas
124
- attr_accessor :width, :height, :x, :y, :vertical, :reverse
125
- def initialize(width, height, x = 0.0, y = 0.0, vertical = true, reverse = false)
126
- @width = width
127
- @height = height
128
- @x = x
129
- @y = y
130
- @vertical = vertical
131
- @reverse = reverse
132
- end
133
-
134
- def reset(x, y, width, height, vertical: true, reverse: false)
135
- self.x = x
136
- self.y = y
137
- self.width = width
138
- self.height = height
139
- self.vertical = vertical
140
- self.reverse = reverse
141
- end
142
-
143
- def to_bounds
144
- Hokusai::Rect.new(x, y, width, height)
145
- end
146
-
147
- def reverse?
148
- reverse
149
- end
150
-
151
- def to_hoku_rect
152
- LibHokusai::HmlRect.create(x, y, width, height)
153
- end
154
- end
155
-
156
- # Color = Struct.new(:red, :green, :blue, :alpha) do
157
- class Color
158
- attr_reader :red, :green, :blue, :alpha
159
- def initialize(red, green, blue, alpha = 255)
160
- @red = red.freeze
161
- @green = green.freeze
162
- @blue = blue.freeze
163
- @alpha = alpha.freeze
164
- end
165
-
166
- alias_method :r, :red
167
- alias_method :b, :blue
168
- alias_method :g, :green
169
- alias_method :a, :alpha
170
-
171
- def self.convert(value)
172
- case value
173
- when String
174
- value = value.split(",").map(&:to_i)
175
- when Array
176
- when Color
177
- return value
178
- else
179
- raise Hokusai::Error.new("Unsupported conversion type #{value.class} for Hokusai::Color")
180
- end
181
-
182
- new(value[0], value[1], value[2], value[3] || 255)
183
- end
9
+ module Hokusai
10
+ class Input
11
+ attr_accessor :keyboard_override
12
+ attr_reader :raw, :touch
184
13
 
185
14
  def hash
186
- [self.class, r, g, b, a].hash
187
- end
188
- end
189
-
190
- class Keyboard
191
- attr_reader :raw
192
-
193
- [
194
- :shift, :super, :ctrl,
195
- :alt, :pressed, :pressed_len, :released,
196
- :released_len
197
- ].each do |name|
198
- define_method(name) do
199
- raw[name]
200
- end
201
- end
202
-
203
- def keyboard_key
204
- pressed.read_array_of_type(TYPE_UINT8, :read_uint8, pressed_len).first
205
- end
206
-
207
- def key
208
- keyboard_key[:key][:key]
209
- end
210
-
211
- def initialize(raw)
212
- @raw = raw
213
- end
214
- end
215
-
216
- class Mouse
217
- attr_reader :raw
218
-
219
- def initialize(raw)
220
- @raw = raw
15
+ [self.class, mouse.pos.x, mouse.pos.y, mouse.scroll, mouse.left.clicked, mouse.left.down, mouse.left.up].hash
221
16
  end
222
17
 
223
- [
224
- :pos, :delta, :scroll,
225
- :scroll_delta, :selection,
226
- :selection_type, :left,
227
- :middle, :right
228
- ].each do |name|
229
- define_method(name) do
230
- # instance_variable_set("@#{name}", raw[name]) if instance_variable_get("@#{name}").nil?
231
- #
232
- # instance_variable_get("@#{name}")
233
-
234
- raw[name]
235
- end
236
-
237
- define_method("#{name}=") do |val|
238
- raw[name] = val
239
- end
18
+ def initialize
19
+ @touch = nil
20
+ @keyboard_override = false
240
21
  end
241
- end
242
22
 
243
- class Input
244
- attr_reader :raw
245
-
246
- def hash
247
- [self.class, mouse.pos.x, mouse.pos.y, mouse.scroll, mouse.left.clicked, mouse.left.down, mouse.left.up].hash
248
- end
23
+ def support_touch!
24
+ @touch ||= Touch.new
249
25
 
250
- def initialize(raw)
251
- @raw = raw
26
+ self
252
27
  end
253
28
 
254
29
  def keyboard
255
- Keyboard.new(@raw[:keyboard])
30
+ @keyboard ||= Keyboard.new
256
31
  end
257
32
 
258
33
  def mouse
259
- @mouse ||= Mouse.new(@raw[:mouse])
34
+ @mouse ||= Mouse.new
260
35
  end
261
36
 
262
37
  def hovered?(canvas)
263
- LibHokusai.hoku_input_is_hovered(raw, canvas.to_hoku_rect)
38
+ pos = mouse.pos
39
+ pos.x >= canvas.x && pos.x <= canvas.x + canvas.width && pos.y >= canvas.y && pos.y <= canvas.y + canvas.height
264
40
  end
265
41
  end
266
42
  end
@@ -0,0 +1,193 @@
1
+ module Hokusai::Util
2
+ class Wrapped
3
+ attr_accessor :text, :x, :y, :extra
4
+
5
+ def initialize(text, x, y, extra)
6
+ @text = text
7
+ @x = x
8
+ @y = y
9
+ @extra = extra
10
+ end
11
+ end
12
+
13
+ class SelectionWrapper
14
+ attr_accessor :x, :y, :width, :height
15
+
16
+ def initialize(x, y, w, h)
17
+ @x = x
18
+ @y = y
19
+ @width = w
20
+ @height = h
21
+ end
22
+
23
+ def <<(w)
24
+ @width += w
25
+ end
26
+ end
27
+
28
+ class WrapStream
29
+ attr_accessor :width, :current_width, :x, :y,
30
+ :origin_x, :origin_y, :buffer, :stack
31
+ attr_reader :on_text_cb, :on_text_selection_cb, :on_advancex_cb, :selector, :padding
32
+
33
+ def initialize(width, &measure)
34
+ @width = width
35
+ @current_width = 0.0
36
+
37
+ @origin_x = 0.0
38
+ @origin_y = 0.0
39
+ @x = @origin_x
40
+ @y = @origin_y
41
+
42
+ @stack = []
43
+ @buffer = ""
44
+
45
+ @measure_cb = measure
46
+ end
47
+
48
+ def on_advancex(&block)
49
+ @on_advancex_cb = block
50
+ end
51
+
52
+ def on_text_selection(selector, padding, &block)
53
+ @selector = selector
54
+ @padding = padding || Hokusai::Padding.new(0.0, 0.0, 0.0, 0.0)
55
+ @on_text_selection_cb = block
56
+ end
57
+
58
+ def on_text(&block)
59
+ @on_text_cb = block
60
+ end
61
+
62
+ def measure(string, extra)
63
+ @measure_cb.call(string, extra)
64
+ end
65
+
66
+ def flush
67
+ sx = x + padding.left
68
+ wrapper = nil
69
+ stack.each do |(range, extra)|
70
+ str = buffer[range]
71
+ if selector
72
+ str.split("").each do |char|
73
+ nw, nh = measure(char, extra)
74
+ ox = on_advancex_cb.call(char.codepoints.first)
75
+
76
+ if selector.selected(sx, y + padding.top - selector.offset_y, nw, nh)
77
+ wrapper ||= SelectionWrapper.new(sx, y + padding.top, 0.0, nh)
78
+ wrapper << ox
79
+ end
80
+
81
+ sx += ox
82
+ end
83
+
84
+ on_text_selection_cb.call(wrapper) if wrapper
85
+ wrapper = nil
86
+ end
87
+
88
+ nw, _ = measure(str, extra)
89
+
90
+ # hard break on the buffer, split on character
91
+ wrap_and_call(str, extra)
92
+
93
+ self.x += nw
94
+ end
95
+
96
+ self.current_width = 0.0
97
+ self.buffer = ""
98
+ stack.clear
99
+ self.x = origin_x
100
+ end
101
+
102
+ def wrap(text, extra)
103
+ offset = 0
104
+ size = text.size
105
+
106
+ stack << [((buffer.size)..(text.size + buffer.size - 1)), extra]
107
+
108
+ while offset < size
109
+ char = text[offset]
110
+ w, h = measure(char, extra)
111
+
112
+ # if it's a newline we want to break
113
+ if char =~ /\n|\r\n/
114
+ flush
115
+
116
+ stack << [(0...(text.size - offset - 1)), extra]
117
+ self.y += h
118
+ self.x = origin_x
119
+ offset += 1
120
+
121
+ next
122
+ end
123
+
124
+ # if adding this char extends beyond the boundary
125
+ if current_width + w > width
126
+ # find the last space
127
+ idx = buffer.rindex(" ")
128
+
129
+ # if there is a break in this line split the buffer
130
+ # and render the current line
131
+ unless idx.nil? || idx < (buffer.size / 2)
132
+ cur = []
133
+ nex = []
134
+ found = false
135
+
136
+ # We need to split up both the buffer, and the ranges
137
+ while payload = stack.shift
138
+ range, xtra = payload
139
+ if range.include?(idx)
140
+ # putting the space on this line
141
+ cur << [(range.begin..idx), xtra]
142
+ # pp [range, idx]
143
+ nex << [(0..(range.end - idx - 1)), xtra] unless idx == range.end
144
+
145
+ found = true
146
+ elsif !found
147
+ cur << payload
148
+ else
149
+ nex << [((range.begin - idx - 1)..(range.end - idx - 1)), xtra]
150
+ end
151
+ end
152
+
153
+ scur = buffer[0..idx]
154
+ snex = buffer[(idx + 1)..-1]
155
+
156
+ cur.each do |(range, xtra)|
157
+ str = scur[range]
158
+
159
+ nw, _ = measure(str, xtra)
160
+ wrap_and_call(str, xtra)
161
+
162
+ self.x += nw
163
+ end
164
+
165
+ self.buffer = snex + char
166
+ self.stack = nex
167
+ self.y += h
168
+ self.current_width = measure(buffer, extra).first
169
+ self.x = origin_x
170
+ else
171
+ # break on this word
172
+ flush
173
+
174
+ self.y += h
175
+ self.current_width = measure(char, extra).first
176
+ self.buffer = char
177
+ stack << [(0...(text.size - offset)), extra]
178
+ end
179
+ else
180
+ self.current_width += w
181
+ buffer << char
182
+ end
183
+
184
+ offset += 1
185
+ end
186
+
187
+ end
188
+
189
+ private def wrap_and_call(text, extra)
190
+ on_text_cb.call Wrapped.new(text, x, y, extra)
191
+ end
192
+ end
193
+ end