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.
- checksums.yaml +7 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +75 -0
- data/CONTRIBUTING.md +123 -0
- data/LICENSE +21 -0
- data/README.md +257 -0
- data/Rakefile +11 -0
- data/bin/konpeito +6 -0
- data/konpeito.gemspec +43 -0
- data/lib/konpeito/ast/typed_ast.rb +620 -0
- data/lib/konpeito/ast/visitor.rb +78 -0
- data/lib/konpeito/cache/cache_manager.rb +230 -0
- data/lib/konpeito/cache/dependency_graph.rb +192 -0
- data/lib/konpeito/cache.rb +8 -0
- data/lib/konpeito/cli/base_command.rb +187 -0
- data/lib/konpeito/cli/build_command.rb +220 -0
- data/lib/konpeito/cli/check_command.rb +104 -0
- data/lib/konpeito/cli/config.rb +231 -0
- data/lib/konpeito/cli/deps_command.rb +128 -0
- data/lib/konpeito/cli/doctor_command.rb +340 -0
- data/lib/konpeito/cli/fmt_command.rb +199 -0
- data/lib/konpeito/cli/init_command.rb +312 -0
- data/lib/konpeito/cli/lsp_command.rb +40 -0
- data/lib/konpeito/cli/run_command.rb +150 -0
- data/lib/konpeito/cli/test_command.rb +248 -0
- data/lib/konpeito/cli/watch_command.rb +212 -0
- data/lib/konpeito/cli.rb +301 -0
- data/lib/konpeito/codegen/builtin_methods.rb +229 -0
- data/lib/konpeito/codegen/cruby_backend.rb +1090 -0
- data/lib/konpeito/codegen/debug_info.rb +352 -0
- data/lib/konpeito/codegen/inliner.rb +486 -0
- data/lib/konpeito/codegen/jvm_backend.rb +197 -0
- data/lib/konpeito/codegen/jvm_generator.rb +13412 -0
- data/lib/konpeito/codegen/llvm_generator.rb +13191 -0
- data/lib/konpeito/codegen/loop_optimizer.rb +363 -0
- data/lib/konpeito/codegen/monomorphizer.rb +359 -0
- data/lib/konpeito/codegen/profile_runtime.c +341 -0
- data/lib/konpeito/codegen/profiler.rb +99 -0
- data/lib/konpeito/compiler.rb +592 -0
- data/lib/konpeito/dependency_resolver.rb +296 -0
- data/lib/konpeito/diagnostics/collector.rb +127 -0
- data/lib/konpeito/diagnostics/diagnostic.rb +237 -0
- data/lib/konpeito/diagnostics/renderer.rb +144 -0
- data/lib/konpeito/formatter/formatter.rb +1214 -0
- data/lib/konpeito/hir/builder.rb +7167 -0
- data/lib/konpeito/hir/nodes.rb +2465 -0
- data/lib/konpeito/lsp/document_manager.rb +820 -0
- data/lib/konpeito/lsp/server.rb +183 -0
- data/lib/konpeito/lsp/transport.rb +38 -0
- data/lib/konpeito/parser/prism_adapter.rb +65 -0
- data/lib/konpeito/platform.rb +103 -0
- data/lib/konpeito/profile/report.rb +136 -0
- data/lib/konpeito/rbs_inline/preprocessor.rb +199 -0
- data/lib/konpeito/stdlib/compression/compression.rb +72 -0
- data/lib/konpeito/stdlib/compression/compression.rbs +60 -0
- data/lib/konpeito/stdlib/compression/compression_native.c +415 -0
- data/lib/konpeito/stdlib/compression/extconf.rb +19 -0
- data/lib/konpeito/stdlib/crypto/crypto.rb +85 -0
- data/lib/konpeito/stdlib/crypto/crypto.rbs +74 -0
- data/lib/konpeito/stdlib/crypto/crypto_native.c +312 -0
- data/lib/konpeito/stdlib/crypto/extconf.rb +40 -0
- data/lib/konpeito/stdlib/http/extconf.rb +19 -0
- data/lib/konpeito/stdlib/http/http.rb +125 -0
- data/lib/konpeito/stdlib/http/http.rbs +57 -0
- data/lib/konpeito/stdlib/http/http_native.c +440 -0
- data/lib/konpeito/stdlib/json/extconf.rb +17 -0
- data/lib/konpeito/stdlib/json/json.rb +44 -0
- data/lib/konpeito/stdlib/json/json.rbs +33 -0
- data/lib/konpeito/stdlib/json/json_native.c +286 -0
- data/lib/konpeito/stdlib/ui/extconf.rb +216 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.cpp +1625 -0
- data/lib/konpeito/stdlib/ui/konpeito_ui_native.h +162 -0
- data/lib/konpeito/stdlib/ui/ui.rb +318 -0
- data/lib/konpeito/stdlib/ui/ui.rbs +247 -0
- data/lib/konpeito/type_checker/annotation_parser.rb +67 -0
- data/lib/konpeito/type_checker/hm_inferrer.rb +2565 -0
- data/lib/konpeito/type_checker/inferrer.rb +565 -0
- data/lib/konpeito/type_checker/rbs_loader.rb +1621 -0
- data/lib/konpeito/type_checker/type_resolver.rb +276 -0
- data/lib/konpeito/type_checker/types.rb +1434 -0
- data/lib/konpeito/type_checker/unification.rb +323 -0
- data/lib/konpeito/ui/animation/animated_state.rb +80 -0
- data/lib/konpeito/ui/animation/easing.rb +59 -0
- data/lib/konpeito/ui/animation/value_tween.rb +66 -0
- data/lib/konpeito/ui/app.rb +379 -0
- data/lib/konpeito/ui/box.rb +38 -0
- data/lib/konpeito/ui/castella.rb +70 -0
- data/lib/konpeito/ui/castella_native.rb +76 -0
- data/lib/konpeito/ui/chart/area_chart.rb +305 -0
- data/lib/konpeito/ui/chart/bar_chart.rb +288 -0
- data/lib/konpeito/ui/chart/base_chart.rb +210 -0
- data/lib/konpeito/ui/chart/chart_helpers.rb +79 -0
- data/lib/konpeito/ui/chart/gauge_chart.rb +171 -0
- data/lib/konpeito/ui/chart/heatmap_chart.rb +222 -0
- data/lib/konpeito/ui/chart/line_chart.rb +289 -0
- data/lib/konpeito/ui/chart/pie_chart.rb +219 -0
- data/lib/konpeito/ui/chart/scales.rb +77 -0
- data/lib/konpeito/ui/chart/scatter_chart.rb +303 -0
- data/lib/konpeito/ui/chart/stacked_bar_chart.rb +276 -0
- data/lib/konpeito/ui/column.rb +271 -0
- data/lib/konpeito/ui/core.rb +2199 -0
- data/lib/konpeito/ui/dsl.rb +443 -0
- data/lib/konpeito/ui/frame.rb +171 -0
- data/lib/konpeito/ui/frame_native.rb +494 -0
- data/lib/konpeito/ui/markdown/ast.rb +124 -0
- data/lib/konpeito/ui/markdown/mermaid/layout.rb +387 -0
- data/lib/konpeito/ui/markdown/mermaid/models.rb +232 -0
- data/lib/konpeito/ui/markdown/mermaid/parser.rb +519 -0
- data/lib/konpeito/ui/markdown/mermaid/renderer.rb +336 -0
- data/lib/konpeito/ui/markdown/parser.rb +805 -0
- data/lib/konpeito/ui/markdown/renderer.rb +639 -0
- data/lib/konpeito/ui/markdown/theme.rb +165 -0
- data/lib/konpeito/ui/render_node.rb +260 -0
- data/lib/konpeito/ui/row.rb +207 -0
- data/lib/konpeito/ui/spacer.rb +18 -0
- data/lib/konpeito/ui/style.rb +799 -0
- data/lib/konpeito/ui/theme.rb +563 -0
- data/lib/konpeito/ui/themes/material.rb +35 -0
- data/lib/konpeito/ui/themes/tokyo_night.rb +6 -0
- data/lib/konpeito/ui/widgets/button.rb +103 -0
- data/lib/konpeito/ui/widgets/calendar.rb +1034 -0
- data/lib/konpeito/ui/widgets/checkbox.rb +119 -0
- data/lib/konpeito/ui/widgets/container.rb +91 -0
- data/lib/konpeito/ui/widgets/data_table.rb +667 -0
- data/lib/konpeito/ui/widgets/divider.rb +29 -0
- data/lib/konpeito/ui/widgets/image.rb +105 -0
- data/lib/konpeito/ui/widgets/input.rb +485 -0
- data/lib/konpeito/ui/widgets/markdown.rb +57 -0
- data/lib/konpeito/ui/widgets/modal.rb +163 -0
- data/lib/konpeito/ui/widgets/multiline_input.rb +968 -0
- data/lib/konpeito/ui/widgets/multiline_text.rb +180 -0
- data/lib/konpeito/ui/widgets/net_image.rb +100 -0
- data/lib/konpeito/ui/widgets/progress_bar.rb +70 -0
- data/lib/konpeito/ui/widgets/radio_buttons.rb +93 -0
- data/lib/konpeito/ui/widgets/slider.rb +133 -0
- data/lib/konpeito/ui/widgets/switch.rb +84 -0
- data/lib/konpeito/ui/widgets/tabs.rb +157 -0
- data/lib/konpeito/ui/widgets/text.rb +110 -0
- data/lib/konpeito/ui/widgets/tree.rb +426 -0
- data/lib/konpeito/version.rb +5 -0
- data/lib/konpeito.rb +109 -0
- data/test_native_array.rb +172 -0
- data/test_native_array_class.rb +197 -0
- data/test_native_class.rb +151 -0
- data/tools/konpeito-asm/build.sh +65 -0
- data/tools/konpeito-asm/lib/asm-9.7.1.jar +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KArray.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCompression.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KConditionVariable.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KCrypto.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KFile.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHTTP.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KHash.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON$Parser.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KJSON.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KMath.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactor.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KRactorPort.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KSizedQueue.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KThread.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/KTime.class +0 -0
- data/tools/konpeito-asm/runtime-classes/konpeito/runtime/RubyDispatch.class +0 -0
- data/tools/konpeito-asm/src/ClassIntrospector.java +312 -0
- data/tools/konpeito-asm/src/KonpeitoAssembler.java +659 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KArray.java +390 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCompression.java +168 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KConditionVariable.java +48 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KCrypto.java +151 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KFile.java +100 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHTTP.java +113 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KHash.java +228 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KJSON.java +405 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KMath.java +54 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactor.java +244 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KRactorPort.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KSizedQueue.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KThread.java +49 -0
- data/tools/konpeito-asm/src/konpeito/runtime/KTime.java +53 -0
- data/tools/konpeito-asm/src/konpeito/runtime/RubyDispatch.java +416 -0
- 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
|