konpeito 0.1.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 (180) hide show
  1. checksums.yaml +7 -0
  2. data/.ruby-version +1 -0
  3. data/CHANGELOG.md +75 -0
  4. data/CONTRIBUTING.md +123 -0
  5. data/LICENSE +21 -0
  6. data/README.md +257 -0
  7. data/Rakefile +11 -0
  8. data/bin/konpeito +6 -0
  9. data/konpeito.gemspec +43 -0
  10. data/lib/konpeito/ast/typed_ast.rb +620 -0
  11. data/lib/konpeito/ast/visitor.rb +78 -0
  12. data/lib/konpeito/cache/cache_manager.rb +230 -0
  13. data/lib/konpeito/cache/dependency_graph.rb +192 -0
  14. data/lib/konpeito/cache.rb +8 -0
  15. data/lib/konpeito/cli/base_command.rb +187 -0
  16. data/lib/konpeito/cli/build_command.rb +220 -0
  17. data/lib/konpeito/cli/check_command.rb +104 -0
  18. data/lib/konpeito/cli/config.rb +231 -0
  19. data/lib/konpeito/cli/deps_command.rb +128 -0
  20. data/lib/konpeito/cli/doctor_command.rb +340 -0
  21. data/lib/konpeito/cli/fmt_command.rb +199 -0
  22. data/lib/konpeito/cli/init_command.rb +312 -0
  23. data/lib/konpeito/cli/lsp_command.rb +40 -0
  24. data/lib/konpeito/cli/run_command.rb +150 -0
  25. data/lib/konpeito/cli/test_command.rb +248 -0
  26. data/lib/konpeito/cli/watch_command.rb +212 -0
  27. data/lib/konpeito/cli.rb +301 -0
  28. data/lib/konpeito/codegen/builtin_methods.rb +229 -0
  29. data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
  30. data/lib/konpeito/codegen/debug_info.rb +352 -0
  31. data/lib/konpeito/codegen/inliner.rb +486 -0
  32. data/lib/konpeito/codegen/jvm_backend.rb +197 -0
  33. data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
  34. data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
  35. data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
  36. data/lib/konpeito/codegen/monomorphizer.rb +359 -0
  37. data/lib/konpeito/codegen/profile_runtime.c +341 -0
  38. data/lib/konpeito/codegen/profiler.rb +99 -0
  39. data/lib/konpeito/compiler.rb +592 -0
  40. data/lib/konpeito/dependency_resolver.rb +296 -0
  41. data/lib/konpeito/diagnostics/collector.rb +127 -0
  42. data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
  43. data/lib/konpeito/diagnostics/renderer.rb +144 -0
  44. data/lib/konpeito/formatter/formatter.rb +1214 -0
  45. data/lib/konpeito/hir/builder.rb +7167 -0
  46. data/lib/konpeito/hir/nodes.rb +2465 -0
  47. data/lib/konpeito/lsp/document_manager.rb +820 -0
  48. data/lib/konpeito/lsp/server.rb +183 -0
  49. data/lib/konpeito/lsp/transport.rb +38 -0
  50. data/lib/konpeito/parser/prism_adapter.rb +65 -0
  51. data/lib/konpeito/platform.rb +103 -0
  52. data/lib/konpeito/profile/report.rb +136 -0
  53. data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
  54. data/lib/konpeito/stdlib/compression/compression.rb +72 -0
  55. data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
  56. data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
  57. data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
  58. data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
  59. data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
  60. data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
  61. data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
  62. data/lib/konpeito/stdlib/http/extconf.rb +19 -0
  63. data/lib/konpeito/stdlib/http/http.rb +125 -0
  64. data/lib/konpeito/stdlib/http/http.rbs +57 -0
  65. data/lib/konpeito/stdlib/http/http_native.c +440 -0
  66. data/lib/konpeito/stdlib/json/extconf.rb +17 -0
  67. data/lib/konpeito/stdlib/json/json.rb +44 -0
  68. data/lib/konpeito/stdlib/json/json.rbs +33 -0
  69. data/lib/konpeito/stdlib/json/json_native.c +286 -0
  70. data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
  71. data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
  72. data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
  73. data/lib/konpeito/stdlib/ui/ui.rb +318 -0
  74. data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
  75. data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
  76. data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
  77. data/lib/konpeito/type_checker/inferrer.rb +565 -0
  78. data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
  79. data/lib/konpeito/type_checker/type_resolver.rb +276 -0
  80. data/lib/konpeito/type_checker/types.rb +1434 -0
  81. data/lib/konpeito/type_checker/unification.rb +323 -0
  82. data/lib/konpeito/ui/animation/animated_state.rb +80 -0
  83. data/lib/konpeito/ui/animation/easing.rb +59 -0
  84. data/lib/konpeito/ui/animation/value_tween.rb +66 -0
  85. data/lib/konpeito/ui/app.rb +379 -0
  86. data/lib/konpeito/ui/box.rb +38 -0
  87. data/lib/konpeito/ui/castella.rb +70 -0
  88. data/lib/konpeito/ui/castella_native.rb +76 -0
  89. data/lib/konpeito/ui/chart/area_chart.rb +305 -0
  90. data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
  91. data/lib/konpeito/ui/chart/base_chart.rb +210 -0
  92. data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
  93. data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
  94. data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
  95. data/lib/konpeito/ui/chart/line_chart.rb +289 -0
  96. data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
  97. data/lib/konpeito/ui/chart/scales.rb +77 -0
  98. data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
  99. data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
  100. data/lib/konpeito/ui/column.rb +271 -0
  101. data/lib/konpeito/ui/core.rb +2199 -0
  102. data/lib/konpeito/ui/dsl.rb +443 -0
  103. data/lib/konpeito/ui/frame.rb +171 -0
  104. data/lib/konpeito/ui/frame_native.rb +494 -0
  105. data/lib/konpeito/ui/markdown/ast.rb +124 -0
  106. data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
  107. data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
  108. data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
  109. data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
  110. data/lib/konpeito/ui/markdown/parser.rb +805 -0
  111. data/lib/konpeito/ui/markdown/renderer.rb +639 -0
  112. data/lib/konpeito/ui/markdown/theme.rb +165 -0
  113. data/lib/konpeito/ui/render_node.rb +260 -0
  114. data/lib/konpeito/ui/row.rb +207 -0
  115. data/lib/konpeito/ui/spacer.rb +18 -0
  116. data/lib/konpeito/ui/style.rb +799 -0
  117. data/lib/konpeito/ui/theme.rb +563 -0
  118. data/lib/konpeito/ui/themes/material.rb +35 -0
  119. data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
  120. data/lib/konpeito/ui/widgets/button.rb +103 -0
  121. data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
  122. data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
  123. data/lib/konpeito/ui/widgets/container.rb +91 -0
  124. data/lib/konpeito/ui/widgets/data_table.rb +667 -0
  125. data/lib/konpeito/ui/widgets/divider.rb +29 -0
  126. data/lib/konpeito/ui/widgets/image.rb +105 -0
  127. data/lib/konpeito/ui/widgets/input.rb +485 -0
  128. data/lib/konpeito/ui/widgets/markdown.rb +57 -0
  129. data/lib/konpeito/ui/widgets/modal.rb +163 -0
  130. data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
  131. data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
  132. data/lib/konpeito/ui/widgets/net_image.rb +100 -0
  133. data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
  134. data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
  135. data/lib/konpeito/ui/widgets/slider.rb +133 -0
  136. data/lib/konpeito/ui/widgets/switch.rb +84 -0
  137. data/lib/konpeito/ui/widgets/tabs.rb +157 -0
  138. data/lib/konpeito/ui/widgets/text.rb +110 -0
  139. data/lib/konpeito/ui/widgets/tree.rb +426 -0
  140. data/lib/konpeito/version.rb +5 -0
  141. data/lib/konpeito.rb +109 -0
  142. data/test_native_array.rb +172 -0
  143. data/test_native_array_class.rb +197 -0
  144. data/test_native_class.rb +151 -0
  145. data/tools/konpeito-asm/build.sh +65 -0
  146. data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
  147. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
  148. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
  149. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
  150. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
  151. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
  152. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
  153. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
  154. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
  155. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
  156. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
  157. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
  158. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
  159. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
  160. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
  161. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
  162. data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
  163. data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
  164. data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
  165. data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
  166. data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
  167. data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
  168. data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
  169. data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
  170. data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
  171. data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
  172. data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
  173. data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
  174. data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
  175. data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
  176. data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
  177. data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
  178. data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
  179. data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
  180. metadata +267 -0
@@ -0,0 +1,2199 @@
1
+ # rbs_inline: enabled
2
+
3
+ # Castella UI Core - Widget / State / Layout / Component
4
+ #
5
+ # Port of ~/castella (Python) to Konpeito/JVM.
6
+ # Uses JWM + Skija via KUIRuntime for rendering.
7
+
8
+ # ===== Size Policy Constants =====
9
+ FIXED = 0
10
+ EXPANDING = 1
11
+ CONTENT = 2
12
+
13
+ # Propagated clear color from Container to child layouts during rendering.
14
+ # 0 = not set (use $theme.bg_canvas). Set by Container.redraw, read by Layout.redraw_children.
15
+ $__bg_clear_color = 0
16
+
17
+ # ===== Geometry =====
18
+
19
+ class Point
20
+ #: (Float x, Float y) -> void
21
+ def initialize(x, y)
22
+ @x = x
23
+ @y = y
24
+ end
25
+
26
+ #: () -> Float
27
+ def x
28
+ @x
29
+ end
30
+
31
+ #: () -> Float
32
+ def y
33
+ @y
34
+ end
35
+
36
+ #: (Float v) -> Float
37
+ def x=(v)
38
+ @x = v
39
+ end
40
+
41
+ #: (Float v) -> Float
42
+ def y=(v)
43
+ @y = v
44
+ end
45
+ end
46
+
47
+ class Size
48
+ #: (Float width, Float height) -> void
49
+ def initialize(width, height)
50
+ @width = width
51
+ @height = height
52
+ end
53
+
54
+ #: () -> Float
55
+ def width
56
+ @width
57
+ end
58
+
59
+ #: () -> Float
60
+ def height
61
+ @height
62
+ end
63
+
64
+ #: (Float v) -> Float
65
+ def width=(v)
66
+ @width = v
67
+ end
68
+
69
+ #: (Float v) -> Float
70
+ def height=(v)
71
+ @height = v
72
+ end
73
+ end
74
+
75
+ class Rect
76
+ #: (Float x, Float y, Float width, Float height) -> void
77
+ def initialize(x, y, width, height)
78
+ @x = x
79
+ @y = y
80
+ @width = width
81
+ @height = height
82
+ end
83
+
84
+ #: () -> Float
85
+ def x
86
+ @x
87
+ end
88
+
89
+ #: () -> Float
90
+ def y
91
+ @y
92
+ end
93
+
94
+ #: () -> Float
95
+ def width
96
+ @width
97
+ end
98
+
99
+ #: () -> Float
100
+ def height
101
+ @height
102
+ end
103
+ end
104
+
105
+ # ===== Mouse Event =====
106
+
107
+ class MouseEvent
108
+ #: (Point pos, Integer button) -> void
109
+ def initialize(pos, button)
110
+ @pos = pos
111
+ @button = button
112
+ end
113
+
114
+ #: () -> Point
115
+ def pos
116
+ @pos
117
+ end
118
+
119
+ #: (Point v) -> Point
120
+ def pos=(v)
121
+ @pos = v
122
+ end
123
+
124
+ #: () -> Integer
125
+ def button
126
+ @button
127
+ end
128
+ end
129
+
130
+ # ===== Wheel Event =====
131
+
132
+ class WheelEvent
133
+ #: (Point pos, Float delta_y) -> void
134
+ def initialize(pos, delta_y)
135
+ @pos = pos
136
+ @delta_y = delta_y
137
+ end
138
+
139
+ #: () -> Point
140
+ def pos
141
+ @pos
142
+ end
143
+
144
+ #: () -> Float
145
+ def delta_y
146
+ @delta_y
147
+ end
148
+ end
149
+
150
+ # ===== Observer/Observable Pattern =====
151
+ # Port of ~/castella/castella/core.py ObservableBase/Observer
152
+
153
+ class ObservableBase
154
+ def initialize
155
+ @observers = []
156
+ end
157
+
158
+ #: (untyped observer) -> void
159
+ def attach(observer)
160
+ @observers << observer
161
+ observer.on_attach(self)
162
+ end
163
+
164
+ #: (untyped observer) -> void
165
+ def detach(observer)
166
+ i = 0
167
+ while i < @observers.length
168
+ if @observers[i] == observer
169
+ @observers.delete_at(i)
170
+ observer.on_detach(self)
171
+ return
172
+ end
173
+ i = i + 1
174
+ end
175
+ end
176
+
177
+ #: () -> void
178
+ def notify_observers
179
+ # Iterate over a copy to avoid issues if observers are modified during notification
180
+ copy = []
181
+ i = 0
182
+ while i < @observers.length
183
+ copy << @observers[i]
184
+ i = i + 1
185
+ end
186
+ i = 0
187
+ while i < copy.length
188
+ # Check observer is still attached before notifying
189
+ j = 0
190
+ still_attached = false
191
+ while j < @observers.length
192
+ if @observers[j] == copy[i]
193
+ still_attached = true
194
+ break
195
+ end
196
+ j = j + 1
197
+ end
198
+ copy[i].on_notify if still_attached
199
+ i = i + 1
200
+ end
201
+ end
202
+ end
203
+
204
+ # ===== State =====
205
+ # Port of ~/castella/castella/core.py State[T]
206
+
207
+ class State < ObservableBase
208
+ #: (untyped value) -> void
209
+ def initialize(value)
210
+ super()
211
+ @value = value
212
+ end
213
+
214
+ #: () -> untyped
215
+ def value
216
+ @value
217
+ end
218
+
219
+ #: (untyped v) -> void
220
+ def set(v)
221
+ @value = v
222
+ notify_observers
223
+ end
224
+
225
+ # In-place mutation operators (for @count += 1 pattern)
226
+ # Ruby has no __iadd__, so += expands to @count = @count.+(1)
227
+ # These mutate the value, notify observers, and return self.
228
+ #: (untyped other) -> State
229
+ def +(other)
230
+ @value = @value + other
231
+ notify_observers
232
+ self
233
+ end
234
+
235
+ #: (untyped other) -> State
236
+ def -(other)
237
+ @value = @value - other
238
+ notify_observers
239
+ self
240
+ end
241
+
242
+ #: (untyped other) -> State
243
+ def *(other)
244
+ @value = @value * other
245
+ notify_observers
246
+ self
247
+ end
248
+
249
+ #: (untyped other) -> State
250
+ def /(other)
251
+ @value = @value / other
252
+ notify_observers
253
+ self
254
+ end
255
+
256
+ #: () -> String
257
+ def to_s
258
+ @value.to_s
259
+ end
260
+
261
+ #: () -> Integer
262
+ def to_i
263
+ @value.to_i
264
+ end
265
+
266
+ #: () -> Float
267
+ def to_f
268
+ @value.to_f
269
+ end
270
+ end
271
+
272
+ # ===== ListState =====
273
+ # Port of ~/castella/castella/core.py ListState
274
+ # Reactive list that notifies observers on mutation
275
+
276
+ class ListState < ObservableBase
277
+ #: (Array items) -> void
278
+ def initialize(items)
279
+ super()
280
+ @items = []
281
+ i = 0
282
+ while i < items.length
283
+ @items << items[i]
284
+ i = i + 1
285
+ end
286
+ end
287
+
288
+ #: () -> Integer
289
+ def length
290
+ @items.length
291
+ end
292
+
293
+ #: (Integer index) -> untyped
294
+ def [](index)
295
+ @items[index]
296
+ end
297
+
298
+ #: (Integer index, untyped value) -> untyped
299
+ def []=(index, value)
300
+ @items[index] = value
301
+ notify_observers
302
+ end
303
+
304
+ #: (untyped value) -> void
305
+ def push(value)
306
+ @items << value
307
+ notify_observers
308
+ end
309
+
310
+ #: () -> untyped
311
+ def pop
312
+ result = @items.pop
313
+ notify_observers
314
+ result
315
+ end
316
+
317
+ #: (Integer index) -> untyped
318
+ def delete_at(index)
319
+ result = @items.delete_at(index)
320
+ notify_observers
321
+ result
322
+ end
323
+
324
+ #: () -> void
325
+ def clear
326
+ @items = []
327
+ notify_observers
328
+ end
329
+
330
+ #: (Array items) -> void
331
+ def set(items)
332
+ @items = []
333
+ i = 0
334
+ while i < items.length
335
+ @items << items[i]
336
+ i = i + 1
337
+ end
338
+ notify_observers
339
+ end
340
+
341
+ #: () { (untyped) -> void } -> void
342
+ def each(&block)
343
+ i = 0
344
+ while i < @items.length
345
+ block.call(@items[i])
346
+ i = i + 1
347
+ end
348
+ end
349
+ end
350
+
351
+ # ===== ScrollState =====
352
+ # Port of ~/castella/castella/core.py ScrollState
353
+ # Observable scroll position that persists across view rebuilds
354
+
355
+ class ScrollState < ObservableBase
356
+ def initialize
357
+ super()
358
+ @x = 0.0
359
+ @y = 0.0
360
+ end
361
+
362
+ #: () -> Float
363
+ def x
364
+ @x
365
+ end
366
+
367
+ #: (Float v) -> void
368
+ def set_x(v)
369
+ if @x != v
370
+ @x = v
371
+ notify_observers
372
+ end
373
+ end
374
+
375
+ #: () -> Float
376
+ def y
377
+ @y
378
+ end
379
+
380
+ #: (Float v) -> void
381
+ def set_y(v)
382
+ if @y != v
383
+ @y = v
384
+ notify_observers
385
+ end
386
+ end
387
+
388
+ #: (Float x, Float y) -> void
389
+ def set(x, y)
390
+ changed = false
391
+ if @x != x
392
+ @x = x
393
+ changed = true
394
+ end
395
+ if @y != y
396
+ @y = y
397
+ changed = true
398
+ end
399
+ notify_observers if changed
400
+ end
401
+ end
402
+
403
+ # ===== InputState =====
404
+ # Port of ~/castella/castella/input.py InputState
405
+ # Holds single-line input state (text, cursor, selection, IME preedit).
406
+ # Persists across Component rebuilds when stored in Component#initialize.
407
+
408
+ class InputState
409
+ #: (String placeholder) -> void
410
+ def initialize(placeholder)
411
+ @text = ""
412
+ @placeholder = placeholder
413
+ @cursor = 0
414
+ @selection_start = -1
415
+ @selection_end = -1
416
+ @is_selecting = false
417
+ @preedit_text = ""
418
+ @preedit_cursor = 0
419
+ end
420
+
421
+ # --- Getters ---
422
+
423
+ #: () -> String
424
+ def value
425
+ @text
426
+ end
427
+
428
+ #: () -> Integer
429
+ def get_cursor
430
+ @cursor
431
+ end
432
+
433
+ #: () -> String
434
+ def get_placeholder
435
+ @placeholder
436
+ end
437
+
438
+ # --- Text operations ---
439
+
440
+ #: (String v) -> void
441
+ def set(v)
442
+ @text = v
443
+ @cursor = v.length
444
+ end
445
+
446
+ #: (String text) -> void
447
+ def insert(text)
448
+ before = ""
449
+ if @cursor > 0
450
+ before = @text[0, @cursor]
451
+ end
452
+ rest_len = @text.length - @cursor
453
+ after = @text[@cursor, rest_len]
454
+ @text = before + text + after
455
+ @cursor = @cursor + text.length
456
+ end
457
+
458
+ #: () -> bool
459
+ def delete_prev
460
+ if @cursor > 0
461
+ before = ""
462
+ if @cursor > 1
463
+ before = @text[0, @cursor - 1]
464
+ end
465
+ rest_len = @text.length - @cursor
466
+ after = @text[@cursor, rest_len]
467
+ @text = before + after
468
+ @cursor = @cursor - 1
469
+ return true
470
+ end
471
+ false
472
+ end
473
+
474
+ #: () -> bool
475
+ def delete_next
476
+ if @cursor < @text.length
477
+ before = ""
478
+ if @cursor > 0
479
+ before = @text[0, @cursor]
480
+ end
481
+ rest_start = @cursor + 1
482
+ rest_len = @text.length - rest_start
483
+ after = ""
484
+ if rest_len > 0
485
+ after = @text[rest_start, rest_len]
486
+ end
487
+ @text = before + after
488
+ return true
489
+ end
490
+ false
491
+ end
492
+
493
+ # --- Cursor movement ---
494
+
495
+ #: () -> bool
496
+ def move_prev
497
+ if @cursor > 0
498
+ @cursor = @cursor - 1
499
+ return true
500
+ end
501
+ false
502
+ end
503
+
504
+ #: () -> bool
505
+ def move_next
506
+ if @cursor < @text.length
507
+ @cursor = @cursor + 1
508
+ return true
509
+ end
510
+ false
511
+ end
512
+
513
+ #: () -> bool
514
+ def move_home
515
+ if @cursor > 0
516
+ @cursor = 0
517
+ return true
518
+ end
519
+ false
520
+ end
521
+
522
+ #: () -> bool
523
+ def move_end
524
+ if @cursor < @text.length
525
+ @cursor = @text.length
526
+ return true
527
+ end
528
+ false
529
+ end
530
+
531
+ # --- Selection ---
532
+
533
+ #: () -> bool
534
+ def has_selection
535
+ if @selection_start < 0
536
+ return false
537
+ end
538
+ if @selection_end < 0
539
+ return false
540
+ end
541
+ if @selection_start == @selection_end
542
+ return false
543
+ end
544
+ true
545
+ end
546
+
547
+ #: () -> Array
548
+ def get_selection_range
549
+ result_s = 0
550
+ result_e = 0
551
+ if has_selection
552
+ s = @selection_start
553
+ e = @selection_end
554
+ if s > e
555
+ result_s = e
556
+ result_e = s
557
+ else
558
+ result_s = s
559
+ result_e = e
560
+ end
561
+ end
562
+ [result_s, result_e]
563
+ end
564
+
565
+ #: () -> String
566
+ def get_selected_text
567
+ result = ""
568
+ if has_selection
569
+ range = get_selection_range
570
+ s = range[0]
571
+ e = range[1]
572
+ len = e - s
573
+ result = @text[s, len]
574
+ end
575
+ result
576
+ end
577
+
578
+ #: () -> void
579
+ def delete_selection
580
+ if has_selection
581
+ range = get_selection_range
582
+ s = range[0]
583
+ e = range[1]
584
+ before = ""
585
+ if s > 0
586
+ before = @text[0, s]
587
+ end
588
+ rest_start = e
589
+ rest_len = @text.length - e
590
+ after = ""
591
+ if rest_len > 0
592
+ after = @text[rest_start, rest_len]
593
+ end
594
+ @text = before + after
595
+ @cursor = s
596
+ @selection_start = -1
597
+ @selection_end = -1
598
+ @is_selecting = false
599
+ end
600
+ end
601
+
602
+ #: () -> void
603
+ def clear_selection
604
+ @selection_start = -1
605
+ @selection_end = -1
606
+ @is_selecting = false
607
+ end
608
+
609
+ #: () -> void
610
+ def select_all
611
+ if @text.length > 0
612
+ @selection_start = 0
613
+ @selection_end = @text.length
614
+ @is_selecting = false
615
+ end
616
+ end
617
+
618
+ #: (Integer pos) -> void
619
+ def start_selection(pos)
620
+ clear_selection
621
+ @selection_start = pos
622
+ @selection_end = pos
623
+ @is_selecting = true
624
+ @cursor = pos
625
+ end
626
+
627
+ #: (Integer pos) -> void
628
+ def update_selection(pos)
629
+ @selection_end = pos
630
+ @cursor = pos
631
+ end
632
+
633
+ #: () -> void
634
+ def end_selection
635
+ @is_selecting = false
636
+ end
637
+
638
+ #: () -> bool
639
+ def is_selecting
640
+ @is_selecting
641
+ end
642
+
643
+ #: (Integer pos) -> void
644
+ def set_cursor_by_click(pos)
645
+ @cursor = pos
646
+ end
647
+
648
+ # --- IME ---
649
+
650
+ #: () -> bool
651
+ def has_preedit
652
+ @preedit_text.length > 0
653
+ end
654
+
655
+ #: () -> String
656
+ def get_display_text
657
+ result = @text
658
+ if has_preedit
659
+ before = ""
660
+ if @cursor > 0
661
+ before = @text[0, @cursor]
662
+ end
663
+ rest_len = @text.length - @cursor
664
+ after = @text[@cursor, rest_len]
665
+ result = before + @preedit_text + after
666
+ end
667
+ result
668
+ end
669
+
670
+ #: (String text, Integer cursor) -> void
671
+ def set_preedit(text, cursor)
672
+ @preedit_text = text
673
+ @preedit_cursor = cursor
674
+ end
675
+
676
+ #: () -> void
677
+ def clear_preedit
678
+ @preedit_text = ""
679
+ @preedit_cursor = 0
680
+ end
681
+
682
+ #: () -> String
683
+ def get_preedit_text
684
+ @preedit_text
685
+ end
686
+
687
+ #: () -> Integer
688
+ def get_preedit_cursor
689
+ @preedit_cursor
690
+ end
691
+
692
+ # --- Focus lifecycle ---
693
+
694
+ #: () -> void
695
+ def start_editing
696
+ end
697
+
698
+ #: () -> void
699
+ def finish_editing
700
+ clear_preedit
701
+ clear_selection
702
+ end
703
+ end
704
+
705
+ # ===== MultilineInputState =====
706
+ # Holds multi-line input state (lines, cursor, selection, scroll, IME preedit).
707
+ # Persists across Component rebuilds when stored in Component#initialize.
708
+
709
+ class MultilineInputState
710
+ #: (String text) -> void
711
+ def initialize(text)
712
+ @lines = [""]
713
+ if text != nil && text.length > 0
714
+ @lines = split_lines(text)
715
+ end
716
+ @row = @lines.length - 1
717
+ @col = @lines[@row].length
718
+ @target_col = -1
719
+ @scroll_y = 0.0
720
+ @manual_scroll = false
721
+ @selection_start = [-1, -1]
722
+ @selection_end = [-1, -1]
723
+ @is_selecting = false
724
+ @preedit_text = ""
725
+ @preedit_cursor = 0
726
+ end
727
+
728
+ #: (String text) -> Array
729
+ def split_lines(text)
730
+ result = []
731
+ current = ""
732
+ i = 0
733
+ while i < text.length
734
+ ch = text[i]
735
+ if ch == "\n"
736
+ result << current
737
+ current = ""
738
+ else
739
+ current = current + ch
740
+ end
741
+ i = i + 1
742
+ end
743
+ result << current
744
+ result
745
+ end
746
+
747
+ # --- Getters ---
748
+
749
+ #: () -> String
750
+ def value
751
+ get_text
752
+ end
753
+
754
+ #: () -> String
755
+ def get_text
756
+ result = ""
757
+ i = 0
758
+ while i < @lines.length
759
+ if i > 0
760
+ result = result + "\n"
761
+ end
762
+ result = result + @lines[i]
763
+ i = i + 1
764
+ end
765
+ result
766
+ end
767
+
768
+ #: () -> Array
769
+ def get_lines
770
+ @lines
771
+ end
772
+
773
+ #: () -> Integer
774
+ def get_row
775
+ @row
776
+ end
777
+
778
+ #: () -> Integer
779
+ def get_col
780
+ @col
781
+ end
782
+
783
+ #: () -> Integer
784
+ def get_target_col
785
+ @target_col
786
+ end
787
+
788
+ #: () -> Float
789
+ def get_scroll_y
790
+ @scroll_y
791
+ end
792
+
793
+ #: (Float v) -> void
794
+ def set_scroll_y(v)
795
+ @scroll_y = v
796
+ end
797
+
798
+ #: () -> bool
799
+ def is_manual_scroll
800
+ @manual_scroll
801
+ end
802
+
803
+ #: (bool v) -> void
804
+ def set_manual_scroll(v)
805
+ @manual_scroll = v
806
+ end
807
+
808
+ #: (String t) -> void
809
+ def set_text(t)
810
+ @lines = split_lines(t)
811
+ @row = @lines.length - 1
812
+ @col = @lines[@row].length
813
+ @target_col = -1
814
+ end
815
+
816
+ # --- Text operations ---
817
+
818
+ #: (String text) -> void
819
+ def insert_char(text)
820
+ line = @lines[@row]
821
+ before = ""
822
+ if @col > 0
823
+ before = line[0, @col]
824
+ end
825
+ after_len = line.length - @col
826
+ after = ""
827
+ if after_len > 0
828
+ after = line[@col, after_len]
829
+ end
830
+ @lines[@row] = before + text + after
831
+ @col = @col + text.length
832
+ @target_col = -1
833
+ @manual_scroll = false
834
+ end
835
+
836
+ #: () -> void
837
+ def insert_newline
838
+ line = @lines[@row]
839
+ before = ""
840
+ if @col > 0
841
+ before = line[0, @col]
842
+ end
843
+ after_len = line.length - @col
844
+ after = ""
845
+ if after_len > 0
846
+ after = line[@col, after_len]
847
+ end
848
+ @lines[@row] = before
849
+ insert_line_after_row(@row, after)
850
+ @row = @row + 1
851
+ @col = 0
852
+ @target_col = -1
853
+ end
854
+
855
+ #: (Integer row, String line_text) -> void
856
+ def insert_line_after_row(row, line_text)
857
+ new_lines = []
858
+ j = 0
859
+ while j <= row
860
+ new_lines << @lines[j]
861
+ j = j + 1
862
+ end
863
+ new_lines << line_text
864
+ j = row + 1
865
+ while j < @lines.length
866
+ new_lines << @lines[j]
867
+ j = j + 1
868
+ end
869
+ @lines = new_lines
870
+ end
871
+
872
+ #: () -> bool
873
+ def delete_prev
874
+ if @col > 0
875
+ line = @lines[@row]
876
+ before = ""
877
+ if @col > 1
878
+ before = line[0, @col - 1]
879
+ end
880
+ after_len = line.length - @col
881
+ after = ""
882
+ if after_len > 0
883
+ after = line[@col, after_len]
884
+ end
885
+ @lines[@row] = before + after
886
+ @col = @col - 1
887
+ @target_col = -1
888
+ return true
889
+ elsif @row > 0
890
+ prev_line = @lines[@row - 1]
891
+ curr_line = @lines[@row]
892
+ @lines[@row - 1] = prev_line + curr_line
893
+ @lines.delete_at(@row)
894
+ @row = @row - 1
895
+ @col = prev_line.length
896
+ @target_col = -1
897
+ return true
898
+ end
899
+ false
900
+ end
901
+
902
+ #: () -> bool
903
+ def delete_next
904
+ line = @lines[@row]
905
+ if @col < line.length
906
+ before = ""
907
+ if @col > 0
908
+ before = line[0, @col]
909
+ end
910
+ rest_start = @col + 1
911
+ rest_len = line.length - rest_start
912
+ after = ""
913
+ if rest_len > 0
914
+ after = line[rest_start, rest_len]
915
+ end
916
+ @lines[@row] = before + after
917
+ @target_col = -1
918
+ return true
919
+ elsif @row < @lines.length - 1
920
+ next_line = @lines[@row + 1]
921
+ @lines[@row] = line + next_line
922
+ @lines.delete_at(@row + 1)
923
+ @target_col = -1
924
+ return true
925
+ end
926
+ false
927
+ end
928
+
929
+ # --- Cursor movement ---
930
+
931
+ #: () -> void
932
+ def move_left
933
+ if @col > 0
934
+ @col = @col - 1
935
+ elsif @row > 0
936
+ @row = @row - 1
937
+ @col = @lines[@row].length
938
+ end
939
+ @target_col = -1
940
+ end
941
+
942
+ #: () -> void
943
+ def move_right
944
+ line = @lines[@row]
945
+ if @col < line.length
946
+ @col = @col + 1
947
+ elsif @row < @lines.length - 1
948
+ @row = @row + 1
949
+ @col = 0
950
+ end
951
+ @target_col = -1
952
+ end
953
+
954
+ #: () -> bool
955
+ def move_up
956
+ if @row > 0
957
+ if @target_col < 0
958
+ @target_col = @col
959
+ end
960
+ @row = @row - 1
961
+ line_len = @lines[@row].length
962
+ @col = @target_col
963
+ if @col > line_len
964
+ @col = line_len
965
+ end
966
+ return true
967
+ end
968
+ false
969
+ end
970
+
971
+ #: () -> bool
972
+ def move_down
973
+ if @row < @lines.length - 1
974
+ if @target_col < 0
975
+ @target_col = @col
976
+ end
977
+ @row = @row + 1
978
+ line_len = @lines[@row].length
979
+ @col = @target_col
980
+ if @col > line_len
981
+ @col = line_len
982
+ end
983
+ return true
984
+ end
985
+ false
986
+ end
987
+
988
+ #: () -> bool
989
+ def move_home
990
+ if @col > 0
991
+ @col = 0
992
+ @target_col = -1
993
+ return true
994
+ end
995
+ false
996
+ end
997
+
998
+ #: () -> bool
999
+ def move_end
1000
+ line_len = @lines[@row].length
1001
+ if @col < line_len
1002
+ @col = line_len
1003
+ @target_col = -1
1004
+ return true
1005
+ end
1006
+ false
1007
+ end
1008
+
1009
+ # --- Selection ---
1010
+
1011
+ #: () -> bool
1012
+ def has_selection
1013
+ if @selection_start[0] < 0
1014
+ return false
1015
+ end
1016
+ if @selection_end[0] < 0
1017
+ return false
1018
+ end
1019
+ if @selection_start[0] == @selection_end[0] && @selection_start[1] == @selection_end[1]
1020
+ return false
1021
+ end
1022
+ true
1023
+ end
1024
+
1025
+ #: () -> Array
1026
+ def get_selection_range
1027
+ result_sr = 0
1028
+ result_sc = 0
1029
+ result_er = 0
1030
+ result_ec = 0
1031
+ if has_selection
1032
+ sr = @selection_start[0]
1033
+ sc = @selection_start[1]
1034
+ er = @selection_end[0]
1035
+ ec = @selection_end[1]
1036
+ if sr > er
1037
+ result_sr = er
1038
+ result_sc = ec
1039
+ result_er = sr
1040
+ result_ec = sc
1041
+ elsif sr == er && sc > ec
1042
+ result_sr = sr
1043
+ result_sc = ec
1044
+ result_er = er
1045
+ result_ec = sc
1046
+ else
1047
+ result_sr = sr
1048
+ result_sc = sc
1049
+ result_er = er
1050
+ result_ec = ec
1051
+ end
1052
+ end
1053
+ [result_sr, result_sc, result_er, result_ec]
1054
+ end
1055
+
1056
+ #: () -> String
1057
+ def get_selected_text
1058
+ result = ""
1059
+ if has_selection
1060
+ range = get_selection_range
1061
+ sr = range[0]
1062
+ sc = range[1]
1063
+ er = range[2]
1064
+ ec = range[3]
1065
+ if sr == er
1066
+ line = @lines[sr]
1067
+ len = ec - sc
1068
+ result = line[sc, len]
1069
+ else
1070
+ first_line = @lines[sr]
1071
+ first_len = first_line.length - sc
1072
+ result = first_line[sc, first_len]
1073
+ r = sr + 1
1074
+ while r < er
1075
+ result = result + "\n" + @lines[r]
1076
+ r = r + 1
1077
+ end
1078
+ last_line = @lines[er]
1079
+ result = result + "\n" + last_line[0, ec]
1080
+ end
1081
+ end
1082
+ result
1083
+ end
1084
+
1085
+ #: () -> void
1086
+ def delete_selection
1087
+ if !has_selection
1088
+ return
1089
+ end
1090
+ range = get_selection_range
1091
+ sr = range[0]
1092
+ sc = range[1]
1093
+ er = range[2]
1094
+ ec = range[3]
1095
+ if sr == er
1096
+ delete_selection_single_line(sr, sc, ec)
1097
+ else
1098
+ delete_selection_multi_line(sr, sc, er, ec)
1099
+ end
1100
+ @row = sr
1101
+ @col = sc
1102
+ @selection_start = [-1, -1]
1103
+ @selection_end = [-1, -1]
1104
+ @is_selecting = false
1105
+ end
1106
+
1107
+ #: (Integer row, Integer sc, Integer ec) -> void
1108
+ def delete_selection_single_line(row, sc, ec)
1109
+ line = @lines[row]
1110
+ before = ""
1111
+ if sc > 0
1112
+ before = line[0, sc]
1113
+ end
1114
+ after_len = line.length - ec
1115
+ after = ""
1116
+ if after_len > 0
1117
+ after = line[ec, after_len]
1118
+ end
1119
+ @lines[row] = before + after
1120
+ end
1121
+
1122
+ #: (Integer sr, Integer sc, Integer er, Integer ec) -> void
1123
+ def delete_selection_multi_line(sr, sc, er, ec)
1124
+ first_part = ""
1125
+ if sc > 0
1126
+ first_line = @lines[sr]
1127
+ first_part = first_line[0, sc]
1128
+ end
1129
+ last_line = @lines[er]
1130
+ last_part = ""
1131
+ after_len = last_line.length - ec
1132
+ if after_len > 0
1133
+ last_part = last_line[ec, after_len]
1134
+ end
1135
+ @lines[sr] = first_part + last_part
1136
+ count = er - sr
1137
+ while count > 0
1138
+ @lines.delete_at(sr + 1)
1139
+ count = count - 1
1140
+ end
1141
+ end
1142
+
1143
+ #: () -> void
1144
+ def clear_selection
1145
+ @selection_start = [-1, -1]
1146
+ @selection_end = [-1, -1]
1147
+ @is_selecting = false
1148
+ end
1149
+
1150
+ #: () -> void
1151
+ def select_all
1152
+ if @lines.length > 0
1153
+ @selection_start = [0, 0]
1154
+ last_row = @lines.length - 1
1155
+ @selection_end = [last_row, @lines[last_row].length]
1156
+ @is_selecting = false
1157
+ end
1158
+ end
1159
+
1160
+ #: (Integer row, Integer col) -> void
1161
+ def start_selection(row, col)
1162
+ clear_selection
1163
+ @selection_start = [row, col]
1164
+ @selection_end = [row, col]
1165
+ @is_selecting = true
1166
+ @row = row
1167
+ @col = col
1168
+ end
1169
+
1170
+ #: (Integer row, Integer col) -> void
1171
+ def update_selection(row, col)
1172
+ @selection_end = [row, col]
1173
+ @row = row
1174
+ @col = col
1175
+ end
1176
+
1177
+ #: () -> void
1178
+ def end_selection
1179
+ @is_selecting = false
1180
+ end
1181
+
1182
+ #: () -> bool
1183
+ def is_selecting
1184
+ @is_selecting
1185
+ end
1186
+
1187
+ # --- IME ---
1188
+
1189
+ #: () -> bool
1190
+ def has_preedit
1191
+ @preedit_text.length > 0
1192
+ end
1193
+
1194
+ #: (String text, Integer cursor) -> void
1195
+ def set_preedit(text, cursor)
1196
+ @preedit_text = text
1197
+ @preedit_cursor = cursor
1198
+ end
1199
+
1200
+ #: () -> void
1201
+ def clear_preedit
1202
+ @preedit_text = ""
1203
+ @preedit_cursor = 0
1204
+ end
1205
+
1206
+ #: () -> String
1207
+ def get_preedit_text
1208
+ @preedit_text
1209
+ end
1210
+
1211
+ #: () -> Integer
1212
+ def get_preedit_cursor
1213
+ @preedit_cursor
1214
+ end
1215
+
1216
+ # --- Focus lifecycle ---
1217
+
1218
+ #: () -> void
1219
+ def finish_editing
1220
+ clear_preedit
1221
+ clear_selection
1222
+ end
1223
+
1224
+ # --- Paste ---
1225
+
1226
+ #: (String text) -> void
1227
+ def paste_text(text)
1228
+ i = 0
1229
+ while i < text.length
1230
+ ch = text[i]
1231
+ if ch == "\n"
1232
+ insert_newline
1233
+ else
1234
+ paste_single_char(ch)
1235
+ end
1236
+ i = i + 1
1237
+ end
1238
+ @target_col = -1
1239
+ end
1240
+
1241
+ #: (String ch) -> void
1242
+ def paste_single_char(ch)
1243
+ line = @lines[@row]
1244
+ before = ""
1245
+ if @col > 0
1246
+ before = line[0, @col]
1247
+ end
1248
+ after_len = line.length - @col
1249
+ after = ""
1250
+ if after_len > 0
1251
+ after = line[@col, after_len]
1252
+ end
1253
+ @lines[@row] = before + ch + after
1254
+ @col = @col + 1
1255
+ end
1256
+ end
1257
+
1258
+ # ===== Widget =====
1259
+ # Port of ~/castella/castella/core.py Widget
1260
+ # Now with RenderNode, lifecycle hooks, z-order, dirty tracking
1261
+
1262
+ class Widget
1263
+ def initialize
1264
+ @x = 0.0
1265
+ @y = 0.0
1266
+ @width = 0.0
1267
+ @height = 0.0
1268
+ @visible = true
1269
+ @dirty = true
1270
+ @parent = nil
1271
+ @width_policy = EXPANDING
1272
+ @height_policy = EXPANDING
1273
+ @flex = 1
1274
+ @z_index = 1
1275
+ @tab_index = 0
1276
+ @focusable = false
1277
+ @mounted = false
1278
+ @cached = false
1279
+ @depth = 0
1280
+ @enable_to_detach = true
1281
+ @render_node = nil
1282
+ @observables = []
1283
+ @pad_top = 0.0
1284
+ @pad_right = 0.0
1285
+ @pad_bottom = 0.0
1286
+ @pad_left = 0.0
1287
+ end
1288
+
1289
+ # --- Size Policy / Style (method chaining) ---
1290
+
1291
+ #: (Float w) -> Widget
1292
+ def fixed_width(w)
1293
+ @width_policy = FIXED
1294
+ @width = w
1295
+ self
1296
+ end
1297
+
1298
+ #: (Float h) -> Widget
1299
+ def fixed_height(h)
1300
+ @height_policy = FIXED
1301
+ @height = h
1302
+ self
1303
+ end
1304
+
1305
+ #: (Float w, Float h) -> Widget
1306
+ def fixed_size(w, h)
1307
+ fixed_width(w)
1308
+ fixed_height(h)
1309
+ end
1310
+
1311
+ #: () -> Widget
1312
+ def fit_content
1313
+ @width_policy = CONTENT
1314
+ @height_policy = CONTENT
1315
+ self
1316
+ end
1317
+
1318
+ #: (Integer f) -> Widget
1319
+ def flex(f)
1320
+ @flex = f
1321
+ self
1322
+ end
1323
+
1324
+ #: (Integer p) -> Widget
1325
+ def set_width_policy(p)
1326
+ @width_policy = p
1327
+ self
1328
+ end
1329
+
1330
+ #: (Integer p) -> Widget
1331
+ def set_height_policy(p)
1332
+ @height_policy = p
1333
+ self
1334
+ end
1335
+
1336
+ #: (Float t, Float r, Float b, Float l) -> Widget
1337
+ def padding(t, r, b, l)
1338
+ @pad_top = t
1339
+ @pad_right = r
1340
+ @pad_bottom = b
1341
+ @pad_left = l
1342
+ self
1343
+ end
1344
+
1345
+ #: (Integer z) -> Widget
1346
+ def z_index(z)
1347
+ @z_index = z
1348
+ # Invalidate parent's z-order cache
1349
+ if @parent != nil
1350
+ rn = @parent.get_render_node
1351
+ if rn != nil
1352
+ rn.invalidate_z_order
1353
+ end
1354
+ end
1355
+ self
1356
+ end
1357
+
1358
+ #: () -> Integer
1359
+ def get_z_index
1360
+ @z_index
1361
+ end
1362
+
1363
+ #: (Integer value) -> Widget
1364
+ def tab_index(value)
1365
+ @tab_index = value
1366
+ self
1367
+ end
1368
+
1369
+ #: () -> Integer
1370
+ def get_tab_index
1371
+ @tab_index
1372
+ end
1373
+
1374
+ #: (bool value) -> Widget
1375
+ def focusable(value)
1376
+ @focusable = value
1377
+ self
1378
+ end
1379
+
1380
+ #: () -> bool
1381
+ def is_focusable
1382
+ @focusable
1383
+ end
1384
+
1385
+ # --- Children (overridden by Layout) ---
1386
+
1387
+ #: () -> Array
1388
+ def get_children
1389
+ []
1390
+ end
1391
+
1392
+ # --- Layout Protocol ---
1393
+
1394
+ #: (untyped painter) -> Size
1395
+ def measure(painter)
1396
+ Size.new(@width, @height)
1397
+ end
1398
+
1399
+ #: (untyped painter) -> void
1400
+ def relocate(painter)
1401
+ end
1402
+
1403
+ #: (untyped painter, bool completely) -> void
1404
+ def redraw(painter, completely)
1405
+ end
1406
+
1407
+ # --- Position / Size ---
1408
+
1409
+ #: () -> Point
1410
+ def get_pos
1411
+ Point.new(@x, @y)
1412
+ end
1413
+
1414
+ #: () -> Size
1415
+ def get_size
1416
+ Size.new(@width, @height)
1417
+ end
1418
+
1419
+ #: () -> Float
1420
+ def get_x
1421
+ @x
1422
+ end
1423
+
1424
+ #: () -> Float
1425
+ def get_y
1426
+ @y
1427
+ end
1428
+
1429
+ #: () -> Float
1430
+ def get_width
1431
+ @width
1432
+ end
1433
+
1434
+ #: () -> Float
1435
+ def get_height
1436
+ @height
1437
+ end
1438
+
1439
+ #: () -> Integer
1440
+ def get_width_policy
1441
+ @width_policy
1442
+ end
1443
+
1444
+ #: () -> Integer
1445
+ def get_height_policy
1446
+ @height_policy
1447
+ end
1448
+
1449
+ #: () -> Integer
1450
+ def get_flex
1451
+ @flex
1452
+ end
1453
+
1454
+ #: (Point p) -> Widget
1455
+ def move(p)
1456
+ new_x = p.x
1457
+ new_y = p.y
1458
+ if new_x != @x || new_y != @y
1459
+ @x = new_x
1460
+ @y = new_y
1461
+ mark_layout_dirty
1462
+ end
1463
+ self
1464
+ end
1465
+
1466
+ #: (Float x, Float y) -> Widget
1467
+ def move_xy(x, y)
1468
+ if x != @x || y != @y
1469
+ @x = x
1470
+ @y = y
1471
+ mark_layout_dirty
1472
+ end
1473
+ self
1474
+ end
1475
+
1476
+ #: (Size s) -> Widget
1477
+ def resize(s)
1478
+ new_w = s.width
1479
+ new_h = s.height
1480
+ if new_w != @width || new_h != @height
1481
+ @width = new_w
1482
+ @height = new_h
1483
+ mark_layout_dirty
1484
+ end
1485
+ self
1486
+ end
1487
+
1488
+ #: (Float w, Float h) -> Widget
1489
+ def resize_wh(w, h)
1490
+ if w != @width || h != @height
1491
+ @width = w
1492
+ @height = h
1493
+ mark_layout_dirty
1494
+ end
1495
+ self
1496
+ end
1497
+
1498
+ # --- Parent / Tree ---
1499
+
1500
+ #: (untyped p) -> void
1501
+ def set_parent(p)
1502
+ do_mount(p)
1503
+ end
1504
+
1505
+ #: () -> untyped
1506
+ def get_parent
1507
+ @parent
1508
+ end
1509
+
1510
+ #: () -> Integer
1511
+ def get_depth
1512
+ @depth
1513
+ end
1514
+
1515
+ # --- RenderNode ---
1516
+
1517
+ #: () -> untyped
1518
+ def get_render_node
1519
+ @render_node
1520
+ end
1521
+
1522
+ #: () -> untyped
1523
+ def ensure_render_node
1524
+ if @render_node == nil
1525
+ @render_node = create_render_node
1526
+ end
1527
+ @render_node
1528
+ end
1529
+
1530
+ #: () -> RenderNodeBase
1531
+ def create_render_node
1532
+ RenderNodeBase.new(self)
1533
+ end
1534
+
1535
+ # --- Dirty Tracking ---
1536
+ # Delegated to RenderNode when available, with fallback to @dirty flag
1537
+
1538
+ #: () -> bool
1539
+ def is_dirty
1540
+ if @render_node != nil
1541
+ return @render_node.is_paint_dirty
1542
+ end
1543
+ @dirty
1544
+ end
1545
+
1546
+ #: () -> bool
1547
+ def is_layout_dirty
1548
+ if @render_node != nil
1549
+ return @render_node.is_layout_dirty
1550
+ end
1551
+ @dirty
1552
+ end
1553
+
1554
+ #: () -> bool
1555
+ def is_subtree_dirty
1556
+ if @render_node != nil
1557
+ return @render_node.is_subtree_dirty
1558
+ end
1559
+ false
1560
+ end
1561
+
1562
+ #: (bool flag) -> void
1563
+ def set_dirty(flag)
1564
+ @dirty = flag
1565
+ if @render_node != nil
1566
+ if flag
1567
+ @render_node.mark_paint_dirty
1568
+ else
1569
+ @render_node.clear_dirty
1570
+ end
1571
+ end
1572
+ end
1573
+
1574
+ #: () -> void
1575
+ def mark_dirty
1576
+ @dirty = true
1577
+ if @render_node != nil
1578
+ @render_node.mark_paint_dirty
1579
+ end
1580
+ propagate_subtree_dirty
1581
+ end
1582
+
1583
+ #: () -> void
1584
+ def mark_layout_dirty
1585
+ @dirty = true
1586
+ if @render_node != nil
1587
+ @render_node.mark_layout_dirty
1588
+ end
1589
+ propagate_subtree_dirty
1590
+ end
1591
+
1592
+ #: () -> void
1593
+ def mark_paint_dirty
1594
+ @dirty = true
1595
+ if @render_node != nil
1596
+ @render_node.mark_paint_dirty
1597
+ end
1598
+ propagate_subtree_dirty
1599
+ end
1600
+
1601
+ # Propagate subtree_dirty up the parent chain
1602
+ #: () -> void
1603
+ def propagate_subtree_dirty
1604
+ p = @parent
1605
+ while p != nil
1606
+ rn = p.get_render_node
1607
+ if rn != nil
1608
+ break if rn.is_subtree_dirty
1609
+ rn.mark_subtree_dirty
1610
+ end
1611
+ p = p.get_parent
1612
+ end
1613
+ end
1614
+
1615
+ # --- Lifecycle ---
1616
+
1617
+ #: () -> void
1618
+ def on_mount
1619
+ end
1620
+
1621
+ #: () -> void
1622
+ def on_unmount
1623
+ end
1624
+
1625
+ #: (untyped parent) -> void
1626
+ def do_mount(parent)
1627
+ if !@mounted
1628
+ @mounted = true
1629
+ @parent = parent
1630
+ @depth = parent != nil ? parent.get_depth + 1 : 0
1631
+ on_mount
1632
+ else
1633
+ # Already mounted - update parent (for cached widgets being re-parented)
1634
+ @parent = parent
1635
+ @depth = parent != nil ? parent.get_depth + 1 : 0
1636
+ end
1637
+ end
1638
+
1639
+ #: () -> void
1640
+ def do_unmount
1641
+ # Skip unmount for cached widgets (they're being reused)
1642
+ if @cached
1643
+ return
1644
+ end
1645
+ if @mounted
1646
+ on_unmount
1647
+ @mounted = false
1648
+ end
1649
+ end
1650
+
1651
+ #: () -> bool
1652
+ def is_mounted
1653
+ @mounted
1654
+ end
1655
+
1656
+ #: () -> void
1657
+ def freeze_widget
1658
+ @enable_to_detach = false
1659
+ end
1660
+
1661
+ # --- Observer Protocol ---
1662
+
1663
+ #: (untyped o) -> void
1664
+ def on_attach(o)
1665
+ @observables << o
1666
+ end
1667
+
1668
+ #: (untyped o) -> void
1669
+ def on_detach(o)
1670
+ i = 0
1671
+ while i < @observables.length
1672
+ if @observables[i] == o
1673
+ @observables.delete_at(i)
1674
+ return
1675
+ end
1676
+ i = i + 1
1677
+ end
1678
+ end
1679
+
1680
+ #: () -> void
1681
+ def on_notify
1682
+ mark_paint_dirty
1683
+ end
1684
+
1685
+ # --- Detach ---
1686
+ # Port of ~/castella/castella/core.py Widget.detach
1687
+
1688
+ #: () -> void
1689
+ def detach
1690
+ do_unmount
1691
+ if @enable_to_detach
1692
+ # Detach from all observables (copy list for safe iteration)
1693
+ copy = []
1694
+ i = 0
1695
+ while i < @observables.length
1696
+ copy << @observables[i]
1697
+ i = i + 1
1698
+ end
1699
+ i = 0
1700
+ while i < copy.length
1701
+ copy[i].detach(self)
1702
+ i = i + 1
1703
+ end
1704
+ end
1705
+ # Clear App-level references to prevent ghost redraws
1706
+ app = App.current
1707
+ if app != nil
1708
+ app.clear_widget_refs(self)
1709
+ end
1710
+ end
1711
+
1712
+ #: (untyped state) -> void
1713
+ def model(state)
1714
+ # Detach from old state if any
1715
+ if @observables.length > 0
1716
+ copy = []
1717
+ i = 0
1718
+ while i < @observables.length
1719
+ copy << @observables[i]
1720
+ i = i + 1
1721
+ end
1722
+ i = 0
1723
+ while i < copy.length
1724
+ copy[i].detach(self)
1725
+ i = i + 1
1726
+ end
1727
+ end
1728
+ state.attach(self)
1729
+ end
1730
+
1731
+ # --- Hit Test ---
1732
+
1733
+ #: (Point p) -> bool
1734
+ def contain(p)
1735
+ p.x >= @x && p.x < @x + @width && p.y >= @y && p.y < @y + @height
1736
+ end
1737
+
1738
+ #: (Point p) -> Array
1739
+ def dispatch(p)
1740
+ if contain(p)
1741
+ local_p = Point.new(p.x - @x, p.y - @y)
1742
+ [self, local_p]
1743
+ else
1744
+ [nil, nil]
1745
+ end
1746
+ end
1747
+
1748
+ #: (Point p, bool is_direction_x) -> Array
1749
+ def dispatch_to_scrollable(p, is_direction_x)
1750
+ [nil, nil]
1751
+ end
1752
+
1753
+ #: () -> bool
1754
+ def is_scrollable
1755
+ false
1756
+ end
1757
+
1758
+ # --- Events ---
1759
+
1760
+ #: (MouseEvent ev) -> void
1761
+ def mouse_down(ev)
1762
+ end
1763
+
1764
+ #: (MouseEvent ev) -> void
1765
+ def mouse_up(ev)
1766
+ end
1767
+
1768
+ #: (MouseEvent ev) -> void
1769
+ def mouse_drag(ev)
1770
+ end
1771
+
1772
+ #: () -> void
1773
+ def mouse_over
1774
+ end
1775
+
1776
+ #: () -> void
1777
+ def mouse_out
1778
+ end
1779
+
1780
+ #: (WheelEvent ev) -> void
1781
+ def mouse_wheel(ev)
1782
+ end
1783
+
1784
+ #: (MouseEvent ev) -> void
1785
+ def cursor_pos(ev)
1786
+ end
1787
+
1788
+ #: (String text) -> void
1789
+ def input_char(text)
1790
+ end
1791
+
1792
+ #: (Integer key_code, Integer modifiers) -> void
1793
+ def input_key(key_code, modifiers)
1794
+ end
1795
+
1796
+ #: (String text, Integer sel_start, Integer sel_end) -> void
1797
+ def ime_preedit(text, sel_start, sel_end)
1798
+ end
1799
+
1800
+ # Text state for focus preservation across Component rebuilds
1801
+ # Override in Input/MultilineInput
1802
+ #: () -> String
1803
+ def get_text
1804
+ ""
1805
+ end
1806
+
1807
+ #: (String t) -> void
1808
+ def set_text(t)
1809
+ end
1810
+
1811
+ # Restore text without triggering update/requestFrame
1812
+ #: (String t) -> void
1813
+ def restore_text(t)
1814
+ set_text(t)
1815
+ end
1816
+
1817
+ #: () -> void
1818
+ def focused
1819
+ end
1820
+
1821
+ # Restore focus state without triggering update/requestFrame
1822
+ # Used during Component rebuild to avoid infinite rendering loop
1823
+ #: () -> void
1824
+ def restore_focus
1825
+ focused
1826
+ end
1827
+
1828
+ #: () -> void
1829
+ def unfocused
1830
+ end
1831
+
1832
+ # --- Update ---
1833
+ # Walk up the tree to find scrollable/component parent for targeted update
1834
+
1835
+ #: () -> void
1836
+ def update
1837
+ parent = @parent
1838
+ root = nil
1839
+ while parent != nil
1840
+ if parent.is_scrollable
1841
+ root = parent
1842
+ end
1843
+ parent = parent.get_parent
1844
+ end
1845
+
1846
+ app = App.current
1847
+ if app == nil
1848
+ return
1849
+ end
1850
+
1851
+ if root == nil
1852
+ app.post_update(self)
1853
+ else
1854
+ app.post_update(root)
1855
+ end
1856
+ end
1857
+ end
1858
+
1859
+ # ===== Layout =====
1860
+ # Port of ~/castella/castella/core.py Layout
1861
+ # Now with LayoutRenderNode, z-order dispatch, child lifecycle
1862
+
1863
+ class Layout < Widget
1864
+ def initialize
1865
+ super
1866
+ @children = []
1867
+ end
1868
+
1869
+ #: () -> Array
1870
+ def get_children
1871
+ @children
1872
+ end
1873
+
1874
+ #: () -> LayoutRenderNode
1875
+ def create_render_node
1876
+ LayoutRenderNode.new(self)
1877
+ end
1878
+
1879
+ #: (untyped w) -> Layout
1880
+ def add(w)
1881
+ # Remove from old parent if needed
1882
+ old_parent = w.get_parent
1883
+ if old_parent != nil && old_parent != self
1884
+ old_parent.remove_child_widget(w)
1885
+ end
1886
+
1887
+ @children << w
1888
+ w.set_parent(self)
1889
+
1890
+ # Sync with render node for z-order caching
1891
+ rn = ensure_render_node
1892
+ rn.add_child(w)
1893
+ self
1894
+ end
1895
+
1896
+ #: (untyped w) -> void
1897
+ def remove_child_widget(w)
1898
+ i = 0
1899
+ while i < @children.length
1900
+ if @children[i] == w
1901
+ @children.delete_at(i)
1902
+ break
1903
+ end
1904
+ i = i + 1
1905
+ end
1906
+ rn = get_render_node
1907
+ if rn != nil
1908
+ rn.remove_child(w)
1909
+ end
1910
+ end
1911
+
1912
+ #: (untyped w) -> void
1913
+ def remove(w)
1914
+ remove_child_widget(w)
1915
+ w.do_unmount
1916
+ end
1917
+
1918
+ #: () -> void
1919
+ def clear_children
1920
+ @children = []
1921
+ rn = get_render_node
1922
+ if rn != nil
1923
+ rn.clear_children
1924
+ end
1925
+ end
1926
+
1927
+ #: () -> void
1928
+ def detach
1929
+ super
1930
+ if @enable_to_detach
1931
+ i = 0
1932
+ while i < @children.length
1933
+ @children[i].detach
1934
+ i = i + 1
1935
+ end
1936
+ end
1937
+ end
1938
+
1939
+ # --- Hit Test with z-order ---
1940
+ # Port of ~/castella/castella/core.py Layout.dispatch
1941
+
1942
+ #: (Point p) -> Array
1943
+ def dispatch(p)
1944
+ if contain(p)
1945
+ # Use z-order: higher z-index receives events first
1946
+ rn = ensure_render_node
1947
+ hit_order = rn.iter_hit_test_order
1948
+ i = 0
1949
+ while i < hit_order.length
1950
+ result = hit_order[i].dispatch(p)
1951
+ target = result[0]
1952
+ adjusted = result[1]
1953
+ if target != nil
1954
+ return [target, adjusted]
1955
+ end
1956
+ i = i + 1
1957
+ end
1958
+ local_p = Point.new(p.x - @x, p.y - @y)
1959
+ [self, local_p]
1960
+ else
1961
+ [nil, nil]
1962
+ end
1963
+ end
1964
+
1965
+ #: (Point p, bool is_direction_x) -> Array
1966
+ def dispatch_to_scrollable(p, is_direction_x)
1967
+ if contain(p)
1968
+ rn = ensure_render_node
1969
+ hit_order = rn.iter_hit_test_order
1970
+ i = 0
1971
+ while i < hit_order.length
1972
+ result = hit_order[i].dispatch_to_scrollable(p, is_direction_x)
1973
+ target = result[0]
1974
+ adjusted = result[1]
1975
+ if target != nil
1976
+ return [target, adjusted]
1977
+ end
1978
+ i = i + 1
1979
+ end
1980
+ if has_scrollbar(is_direction_x)
1981
+ return [self, p]
1982
+ end
1983
+ [nil, nil]
1984
+ else
1985
+ [nil, nil]
1986
+ end
1987
+ end
1988
+
1989
+ #: (bool is_direction_x) -> bool
1990
+ def has_scrollbar(is_direction_x)
1991
+ false
1992
+ end
1993
+
1994
+ # --- Redraw with z-order ---
1995
+ # Port of ~/castella/castella/core.py Layout.redraw
1996
+ # Separated into _relocate_children and _redraw_children
1997
+
1998
+ #: (untyped painter, bool completely) -> void
1999
+ def redraw(painter, completely)
2000
+ relocate_children(painter)
2001
+ redraw_children(painter, completely)
2002
+ end
2003
+
2004
+ #: (untyped painter) -> void
2005
+ def relocate_children(painter)
2006
+ # Subclasses override this (Column, Row, Box)
2007
+ relocate(painter)
2008
+ end
2009
+
2010
+ #: (untyped painter, bool completely) -> void
2011
+ def redraw_children(painter, completely)
2012
+ # Determine effective clear color with 3-level fallback:
2013
+ # own @bg_clear_color > propagated $__bg_clear_color > $theme.bg_canvas
2014
+ has_own_bg = false
2015
+ if @bg_clear_color != nil
2016
+ has_own_bg = true
2017
+ end
2018
+ has_parent_bg = false
2019
+ if !has_own_bg && $__bg_clear_color != 0
2020
+ has_parent_bg = true
2021
+ end
2022
+ effective_clear = 0
2023
+ if has_own_bg
2024
+ effective_clear = @bg_clear_color
2025
+ else
2026
+ if has_parent_bg
2027
+ effective_clear = $__bg_clear_color
2028
+ else
2029
+ effective_clear = $theme.bg_canvas
2030
+ end
2031
+ end
2032
+ # When this layout itself is dirty (scroll, resize, etc.), force full child repaint.
2033
+ # This is in redraw_children (not redraw) because Column/Row override redraw without calling super.
2034
+ if is_dirty
2035
+ completely = true
2036
+ painter.fill_rect(0.0, 0.0, @width, @height, effective_clear)
2037
+ end
2038
+ # Use z-order: lower z-index drawn first (background to foreground)
2039
+ rn = ensure_render_node
2040
+ paint_order = rn.iter_paint_order
2041
+ i = 0
2042
+ while i < paint_order.length
2043
+ c = paint_order[i]
2044
+ if completely || c.is_dirty || c.is_subtree_dirty
2045
+ painter.save
2046
+ painter.translate(c.get_x - @x, c.get_y - @y)
2047
+ painter.clip_rect(0.0, 0.0, c.get_width, c.get_height)
2048
+ # Clear dirty widget's area before redrawing (off-screen surface retains old pixels)
2049
+ if !completely && c.is_dirty
2050
+ painter.fill_rect(0.0, 0.0, c.get_width, c.get_height, effective_clear)
2051
+ end
2052
+ c.redraw(painter, completely)
2053
+ painter.restore
2054
+ c.set_dirty(false)
2055
+ end
2056
+ i = i + 1
2057
+ end
2058
+ end
2059
+ end
2060
+
2061
+ # ===== Component =====
2062
+ # Port of ~/castella/castella/core.py Component
2063
+ # Now with pending_rebuild flag and proper detach on rebuild
2064
+
2065
+ class Component < Layout
2066
+ def initialize
2067
+ super
2068
+ @width_policy = EXPANDING
2069
+ @height_policy = EXPANDING
2070
+ @child = nil
2071
+ @pending_rebuild = false
2072
+ end
2073
+
2074
+ # Helper: create State + auto-attach
2075
+ #: (untyped initial) -> State
2076
+ def state(initial)
2077
+ s = State.new(initial)
2078
+ s.attach(self)
2079
+ s
2080
+ end
2081
+
2082
+ # Subclass overrides: returns widget tree
2083
+ #: () -> untyped
2084
+ def view
2085
+ nil
2086
+ end
2087
+
2088
+ # State change notification -> schedule rebuild
2089
+ #: () -> void
2090
+ def on_notify
2091
+ @pending_rebuild = true
2092
+ mark_paint_dirty
2093
+ app = App.current
2094
+ if app != nil
2095
+ app.post_update(self)
2096
+ end
2097
+ end
2098
+
2099
+ #: (untyped painter, bool completely) -> void
2100
+ def redraw(painter, completely)
2101
+ # Handle pending rebuild
2102
+ if @pending_rebuild
2103
+ @pending_rebuild = false
2104
+ # Save focused widget's tab_index
2105
+ saved_focus_tab = -1
2106
+ app = App.current
2107
+ if app != nil
2108
+ focused = app.get_focused
2109
+ if focused != nil
2110
+ saved_focus_tab = focused.get_tab_index
2111
+ end
2112
+ end
2113
+
2114
+ # Destroy old tree, build new tree
2115
+ if @child != nil
2116
+ remove(@child)
2117
+ @child.detach
2118
+ @child = nil
2119
+ end
2120
+ @child = view
2121
+ if @child != nil
2122
+ add(@child)
2123
+ completely = true
2124
+ end
2125
+
2126
+ # Restore focus (text restoration not needed — InputState persists)
2127
+ if saved_focus_tab > 0 && app != nil
2128
+ focus_target = find_focusable_by_tab_index(@child, saved_focus_tab)
2129
+ if focus_target != nil
2130
+ app.set_focused(focus_target)
2131
+ focus_target.restore_focus
2132
+ end
2133
+ end
2134
+ end
2135
+
2136
+ # Build view if needed
2137
+ if @child == nil
2138
+ @child = view
2139
+ if @child != nil
2140
+ add(@child)
2141
+ completely = true
2142
+ end
2143
+ end
2144
+
2145
+ # Relocate + redraw
2146
+ if @children.length > 0
2147
+ relocate_children(painter)
2148
+ redraw_children(painter, completely)
2149
+ end
2150
+ end
2151
+
2152
+ # Resize and position child to fill this Component (matching Python Castella)
2153
+ #: (untyped painter) -> void
2154
+ def relocate_children(painter)
2155
+ if @children.length > 0
2156
+ c = @children[0]
2157
+ c.resize_wh(@width, @height)
2158
+ c.move_xy(@x, @y)
2159
+ end
2160
+ end
2161
+
2162
+ #: (untyped widget, Integer tab_index) -> untyped
2163
+ def find_focusable_by_tab_index(widget, tab_index)
2164
+ return nil if widget == nil
2165
+ if widget.is_focusable && widget.get_tab_index == tab_index
2166
+ return widget
2167
+ end
2168
+ children = widget.get_children
2169
+ i = 0
2170
+ while i < children.length
2171
+ result = find_focusable_by_tab_index(children[i], tab_index)
2172
+ if result != nil
2173
+ return result
2174
+ end
2175
+ i = i + 1
2176
+ end
2177
+ nil
2178
+ end
2179
+
2180
+ #: (untyped painter) -> Size
2181
+ def measure(painter)
2182
+ if @child == nil
2183
+ Size.new(0.0, 0.0)
2184
+ else
2185
+ @child.measure(painter)
2186
+ end
2187
+ end
2188
+ end
2189
+
2190
+ # ===== StatefulComponent =====
2191
+ # Shorthand: Component that auto-attaches to a State
2192
+
2193
+ class StatefulComponent < Component
2194
+ #: (State state) -> void
2195
+ def initialize(state)
2196
+ super()
2197
+ model(state)
2198
+ end
2199
+ end