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,968 @@
1
+ # MultilineInput widget - multi-line editable text area with IME, selection, clipboard
2
+ # Port of Castella MultilineInput
3
+ #
4
+ # State is held in MultilineInputState (defined in core.rb) which persists across
5
+ # Component rebuilds. The widget delegates all text/cursor/selection/IME
6
+ # operations to MultilineInputState and handles rendering + event dispatch.
7
+ #
8
+ # JWM Key ordinals:
9
+ # ENTER=11, BACKSPACE=12, ESCAPE=17, END=21, HOME=22, LEFT=23, UP=24, RIGHT=25, DOWN=26, DELETE=75
10
+ # A=43, C=45, V=64, X=66
11
+
12
+ SCROLLBAR_WIDTH = 10.0
13
+
14
+ class MultilineInput < Widget
15
+ def initialize(state)
16
+ super()
17
+ @state = state
18
+ @focused_flag = false
19
+ @font_size_val = 14.0
20
+ @line_spacing = 4.0
21
+ @wrap = true
22
+ @bg_color = 0
23
+ @text_color = 0
24
+ @border_color = 0
25
+ @focus_border = 0
26
+ @use_theme = true
27
+ @radius = 4.0
28
+ @focusable = true
29
+ @width_policy = EXPANDING
30
+ @height_policy = FIXED
31
+ @height = 200.0
32
+ @pad_top = 8.0
33
+ @pad_right = 8.0
34
+ @pad_bottom = 8.0
35
+ @pad_left = 8.0
36
+ @border_width = 1.0
37
+ # Scroll state (view-level, not in MultilineInputState for content_height dependency)
38
+ @content_height = 0.0
39
+ # Scrollbar drag state
40
+ @scroll_box_y = nil # [x, y, w, h] or nil
41
+ @under_dragging_y = false
42
+ @last_drag_y = 0.0
43
+ # Display lines cache (for click handling)
44
+ @last_display_lines = nil
45
+ # Character position cache
46
+ @char_positions_cache = nil
47
+ # on_change callback
48
+ @on_change_cb = nil
49
+ end
50
+
51
+ # --- API / method chaining ---
52
+
53
+ def font_size(s)
54
+ @font_size_val = s
55
+ self
56
+ end
57
+
58
+ def wrap_text(flag)
59
+ @wrap = flag
60
+ self
61
+ end
62
+
63
+ def line_spacing(s)
64
+ @line_spacing = s
65
+ self
66
+ end
67
+
68
+ def on_change(&block)
69
+ @on_change_cb = block
70
+ self
71
+ end
72
+
73
+ def get_text
74
+ @state.value
75
+ end
76
+
77
+ def set_text(t)
78
+ @state.set_text(t)
79
+ mark_dirty
80
+ update
81
+ end
82
+
83
+ # --- Measure ---
84
+
85
+ def measure(painter)
86
+ Size.new(@width, @height)
87
+ end
88
+
89
+ # --- Word wrapping ---
90
+
91
+ def get_wrapped_lines(painter, line_width)
92
+ display_lines = [] # Each entry: [logical_row, display_text, start_col]
93
+ lines = @state.get_lines
94
+
95
+ i = 0
96
+ while i < lines.length
97
+ line = lines[i]
98
+ # Insert preedit at cursor position
99
+ if @state.has_preedit && i == @state.get_row
100
+ line = build_preedit_line(line)
101
+ end
102
+
103
+ if !@wrap || line_width <= 0
104
+ display_lines << [i, line, 0]
105
+ elsif line.length == 0
106
+ display_lines << [i, "", 0]
107
+ else
108
+ wrap_single_line(painter, line_width, i, line, display_lines)
109
+ end
110
+
111
+ i = i + 1
112
+ end
113
+
114
+ display_lines
115
+ end
116
+
117
+ def build_preedit_line(line)
118
+ col = @state.get_col
119
+ before = ""
120
+ if col > 0
121
+ before = line[0, col]
122
+ end
123
+ after_len = line.length - col
124
+ after = ""
125
+ if after_len > 0
126
+ after = line[col, after_len]
127
+ end
128
+ before + @state.get_preedit_text + after
129
+ end
130
+
131
+ def wrap_single_line(painter, line_width, logical_row, line, display_lines)
132
+ col_offset = 0
133
+ remaining = line
134
+
135
+ while remaining.length > 0
136
+ text_w = painter.measure_text_width(remaining, $theme.font_family, @font_size_val)
137
+ if text_w <= line_width
138
+ display_lines << [logical_row, remaining, col_offset]
139
+ remaining = ""
140
+ else
141
+ break_idx = find_break_index(painter, remaining, line_width)
142
+ break_idx = try_word_boundary_break(remaining, break_idx)
143
+
144
+ display_lines << [logical_row, remaining[0, break_idx], col_offset]
145
+ col_offset = col_offset + break_idx
146
+ remaining = remaining[break_idx, remaining.length - break_idx]
147
+ end
148
+ end
149
+ end
150
+
151
+ def find_break_index(painter, text, line_width)
152
+ break_idx = text.length
153
+ found = false
154
+ j = 1
155
+ while j <= text.length && !found
156
+ sub_w = painter.measure_text_width(text[0, j], $theme.font_family, @font_size_val)
157
+ if sub_w > line_width
158
+ break_idx = j - 1
159
+ if break_idx < 1
160
+ break_idx = 1
161
+ end
162
+ found = true
163
+ end
164
+ j = j + 1
165
+ end
166
+ break_idx
167
+ end
168
+
169
+ def try_word_boundary_break(text, break_idx)
170
+ result = break_idx
171
+ space_idx = -1
172
+ k = break_idx
173
+ while k >= 0 && space_idx == -1
174
+ if text[k] == " "
175
+ space_idx = k
176
+ end
177
+ k = k - 1
178
+ end
179
+ if space_idx > 0
180
+ result = space_idx + 1
181
+ end
182
+ result
183
+ end
184
+
185
+ def find_cursor_display_pos(painter, display_lines)
186
+ cursor_row = @state.get_row
187
+ cursor_col = @state.get_col
188
+ display_cursor_col = cursor_col
189
+ if @state.has_preedit
190
+ display_cursor_col = cursor_col + @state.get_preedit_cursor
191
+ end
192
+
193
+ result_idx = 0
194
+ result_x = 0.0
195
+ found = false
196
+
197
+ # Forward search for matching display line
198
+ i = 0
199
+ while i < display_lines.length && !found
200
+ dl = display_lines[i]
201
+ logical_row = dl[0]
202
+ text = dl[1]
203
+ start_col = dl[2]
204
+ if logical_row == cursor_row
205
+ end_col = start_col + text.length
206
+ if start_col <= display_cursor_col && display_cursor_col <= end_col
207
+ text_before = ""
208
+ offset = display_cursor_col - start_col
209
+ if offset > 0
210
+ text_before = text[0, offset]
211
+ end
212
+ result_idx = i
213
+ result_x = painter.measure_text_width(text_before, $theme.font_family, @font_size_val)
214
+ found = true
215
+ end
216
+ end
217
+ i = i + 1
218
+ end
219
+
220
+ # Fallback: last line of cursor row
221
+ if !found
222
+ i = display_lines.length - 1
223
+ while i >= 0 && !found
224
+ dl = display_lines[i]
225
+ if dl[0] == cursor_row
226
+ result_idx = i
227
+ result_x = painter.measure_text_width(dl[1], $theme.font_family, @font_size_val)
228
+ found = true
229
+ end
230
+ i = i - 1
231
+ end
232
+ end
233
+
234
+ [result_idx, result_x]
235
+ end
236
+
237
+ # --- Rendering ---
238
+
239
+ def redraw(painter, completely)
240
+ bg_c = @use_theme ? $theme.bg_primary : @bg_color
241
+ tc = @use_theme ? $theme.text_primary : @text_color
242
+ brd_c = @use_theme ? $theme.border : @border_color
243
+ fbc = @use_theme ? $theme.border_focus : @focus_border
244
+ sel_color = $theme.bg_selected
245
+
246
+ bc = @focused_flag ? fbc : brd_c
247
+ painter.fill_round_rect(0.0, 0.0, @width, @height, @radius, bg_c)
248
+ painter.stroke_round_rect(0.0, 0.0, @width, @height, @radius, bc, 1.0)
249
+
250
+ ascent = painter.get_text_ascent($theme.font_family, @font_size_val)
251
+ font_size = @font_size_val
252
+ padding = @pad_left
253
+ border_width = @border_width
254
+
255
+ # Calculate line width with scrollbar space reserved
256
+ scrollbar_width = SCROLLBAR_WIDTH
257
+ line_width = @width - (padding + border_width) * 2.0 - scrollbar_width
258
+ display_lines = get_wrapped_lines(painter, line_width)
259
+
260
+ # Calculate content height
261
+ num_lines = display_lines.length
262
+ spacing_total = compute_spacing_total(num_lines)
263
+ @content_height = font_size * num_lines + spacing_total + padding * 2.0
264
+
265
+ visible_height = @height - border_width * 2.0
266
+ needs_scrollbar = @content_height > @height
267
+
268
+ if !needs_scrollbar
269
+ line_width = @width - (padding + border_width) * 2.0
270
+ display_lines = get_wrapped_lines(painter, line_width)
271
+ num_lines = display_lines.length
272
+ spacing_total = compute_spacing_total(num_lines)
273
+ @content_height = font_size * num_lines + spacing_total + padding * 2.0
274
+ @scroll_box_y = nil
275
+ scrollbar_width = 0.0
276
+ end
277
+
278
+ @last_display_lines = display_lines
279
+
280
+ # Build character position cache
281
+ build_char_positions_cache(painter, display_lines, font_size)
282
+
283
+ # Auto-scroll to keep cursor visible when editing
284
+ scroll_y = @state.get_scroll_y
285
+ if @focused_flag && !@state.is_manual_scroll
286
+ scroll_y = auto_scroll_to_cursor(painter, display_lines, font_size, padding, visible_height, scroll_y)
287
+ @state.set_scroll_y(scroll_y)
288
+ end
289
+
290
+ # Clamp scroll
291
+ scroll_y = clamp_scroll(visible_height, scroll_y)
292
+ @state.set_scroll_y(scroll_y)
293
+
294
+ # Clip content area
295
+ content_width = @width - border_width * 2.0 - scrollbar_width
296
+ if content_width < 0.0
297
+ content_width = 0.0
298
+ end
299
+ painter.save
300
+ painter.clip_rect(border_width, border_width, content_width, visible_height)
301
+ painter.translate(0.0, 0.0 - scroll_y)
302
+
303
+ # Draw selection highlight
304
+ if @state.has_selection
305
+ draw_selection_highlight(painter, display_lines, sel_color)
306
+ end
307
+
308
+ # Draw text lines
309
+ draw_text_lines(painter, display_lines, padding, ascent, font_size, tc)
310
+
311
+ # Draw preedit underline
312
+ if @state.has_preedit && @focused_flag
313
+ draw_preedit_underline(painter, display_lines, padding, ascent, font_size, tc)
314
+ end
315
+
316
+ # Draw cursor
317
+ if @focused_flag
318
+ draw_cursor(painter, display_lines, padding, font_size, tc)
319
+ end
320
+
321
+ painter.restore
322
+
323
+ # Draw scrollbar
324
+ if needs_scrollbar
325
+ draw_scrollbar(painter, border_width, scrollbar_width, visible_height)
326
+ end
327
+ end
328
+
329
+ def compute_spacing_total(num_lines)
330
+ result = 0.0
331
+ if num_lines > 1
332
+ result = @line_spacing * (num_lines - 1)
333
+ end
334
+ result
335
+ end
336
+
337
+ def build_char_positions_cache(painter, display_lines, font_size)
338
+ @char_positions_cache = []
339
+ i = 0
340
+ while i < display_lines.length
341
+ dl = display_lines[i]
342
+ text = dl[1]
343
+ positions = [0.0]
344
+ cumulative = 0.0
345
+ j = 0
346
+ while j < text.length
347
+ ch_w = painter.measure_text_width(text[j], $theme.font_family, font_size)
348
+ cumulative = cumulative + ch_w
349
+ positions << cumulative
350
+ j = j + 1
351
+ end
352
+ @char_positions_cache << positions
353
+ i = i + 1
354
+ end
355
+ end
356
+
357
+ def auto_scroll_to_cursor(painter, display_lines, font_size, padding, visible_height, scroll_y)
358
+ cursor_info = find_cursor_display_pos(painter, display_lines)
359
+ display_idx = cursor_info[0]
360
+ cursor_top = padding + display_idx * (font_size + @line_spacing)
361
+ cursor_bottom = cursor_top + font_size
362
+ if cursor_top - scroll_y < 0.0
363
+ scroll_y = cursor_top
364
+ if scroll_y < 0.0
365
+ scroll_y = 0.0
366
+ end
367
+ elsif cursor_bottom - scroll_y > visible_height
368
+ scroll_y = cursor_bottom - visible_height
369
+ end
370
+ scroll_y
371
+ end
372
+
373
+ def clamp_scroll(visible_height, scroll_y)
374
+ max_scroll = @content_height - @height
375
+ if max_scroll < 0.0
376
+ max_scroll = 0.0
377
+ end
378
+ if scroll_y < 0.0
379
+ scroll_y = 0.0
380
+ end
381
+ if scroll_y > max_scroll
382
+ scroll_y = max_scroll
383
+ end
384
+ scroll_y
385
+ end
386
+
387
+ def draw_text_lines(painter, display_lines, padding, ascent, font_size, tc)
388
+ i = 0
389
+ while i < display_lines.length
390
+ dl = display_lines[i]
391
+ text = dl[1]
392
+ y = padding + ascent + i * (font_size + @line_spacing)
393
+ if text.length > 0
394
+ painter.draw_text(text, padding, y, $theme.font_family, font_size, tc)
395
+ end
396
+ i = i + 1
397
+ end
398
+ end
399
+
400
+ def draw_preedit_underline(painter, display_lines, padding, ascent, font_size, tc)
401
+ cursor_info = find_cursor_display_pos(painter, display_lines)
402
+ display_idx = cursor_info[0]
403
+ dl = display_lines[display_idx]
404
+ text = dl[1]
405
+ start_col = dl[2]
406
+ preedit_start_offset = @state.get_col - start_col
407
+ if preedit_start_offset < 0
408
+ preedit_start_offset = 0
409
+ end
410
+ text_before = ""
411
+ if preedit_start_offset > 0
412
+ text_before = text[0, preedit_start_offset]
413
+ end
414
+ preedit_start_x = padding + painter.measure_text_width(text_before, $theme.font_family, font_size)
415
+ preedit_w = painter.measure_text_width(@state.get_preedit_text, $theme.font_family, font_size)
416
+ underline_y = padding + ascent + display_idx * (font_size + @line_spacing) + 2.0
417
+ painter.fill_rect(preedit_start_x, underline_y, preedit_w, 2.0, tc)
418
+ end
419
+
420
+ def draw_cursor(painter, display_lines, padding, font_size, tc)
421
+ cursor_info = find_cursor_display_pos(painter, display_lines)
422
+ display_idx = cursor_info[0]
423
+ x_offset = cursor_info[1]
424
+ cursor_x = padding + x_offset
425
+ cursor_y = padding + display_idx * (font_size + @line_spacing)
426
+ painter.draw_line(cursor_x, cursor_y, cursor_x, cursor_y + font_size, tc, 1.0)
427
+
428
+ # Notify IME of cursor position
429
+ scroll_y = @state.get_scroll_y
430
+ abs_cursor_x = @x + cursor_x
431
+ abs_cursor_y = @y + cursor_y - scroll_y
432
+ app = App.current
433
+ if app != nil
434
+ app.set_ime_cursor_rect(abs_cursor_x.to_i, abs_cursor_y.to_i, 1, font_size.to_i)
435
+ end
436
+ end
437
+
438
+ def draw_scrollbar(painter, border_width, scrollbar_width, visible_height)
439
+ scrollbar_x = @width - scrollbar_width - border_width
440
+ # Track
441
+ scrollbar_bg = $theme.scrollbar_bg
442
+ painter.fill_rect(scrollbar_x, border_width, scrollbar_width, visible_height, scrollbar_bg)
443
+ # Thumb
444
+ scroll_y = @state.get_scroll_y
445
+ if @content_height > 0.0
446
+ thumb_height = (visible_height / @content_height) * visible_height
447
+ if thumb_height < 20.0
448
+ thumb_height = 20.0
449
+ end
450
+ scroll_range = @content_height - @height
451
+ thumb_y = 0.0
452
+ if scroll_range > 0.0
453
+ thumb_y = (scroll_y / scroll_range) * (visible_height - thumb_height)
454
+ end
455
+ scrollbar_fg = $theme.scrollbar_fg
456
+ painter.fill_rect(scrollbar_x, border_width + thumb_y, scrollbar_width, thumb_height, scrollbar_fg)
457
+ @scroll_box_y = [scrollbar_x, border_width + thumb_y, scrollbar_width, thumb_height]
458
+ end
459
+ end
460
+
461
+ def draw_selection_highlight(painter, display_lines, sel_color)
462
+ if !@state.has_selection
463
+ return
464
+ end
465
+ range = @state.get_selection_range
466
+ sr = range[0]
467
+ sc = range[1]
468
+ er = range[2]
469
+ ec = range[3]
470
+ font_size = @font_size_val
471
+ padding = @pad_left
472
+
473
+ i = 0
474
+ while i < display_lines.length
475
+ dl = display_lines[i]
476
+ logical_row = dl[0]
477
+ text = dl[1]
478
+ line_start_col = dl[2]
479
+
480
+ # Only process lines within the selection range
481
+ if logical_row >= sr && logical_row <= er
482
+ draw_selection_for_line(painter, i, logical_row, text, line_start_col, sr, sc, er, ec, font_size, padding, sel_color)
483
+ end
484
+
485
+ i = i + 1
486
+ end
487
+ end
488
+
489
+ def draw_selection_for_line(painter, display_idx, logical_row, text, line_start_col, sr, sc, er, ec, font_size, padding, sel_color)
490
+ line_end_col = line_start_col + text.length
491
+
492
+ # Determine selection start column for this display line
493
+ sel_start_in_line = 0
494
+ if logical_row == sr
495
+ sel_start_col = sc
496
+ if sel_start_col < line_start_col
497
+ sel_start_col = line_start_col
498
+ end
499
+ sel_start_in_line = sel_start_col - line_start_col
500
+ end
501
+
502
+ # Determine selection end column for this display line
503
+ sel_end_in_line = text.length
504
+ if logical_row == er
505
+ sel_end_col = ec
506
+ if sel_end_col > line_end_col
507
+ sel_end_col = line_end_col
508
+ end
509
+ sel_end_in_line = sel_end_col - line_start_col
510
+ end
511
+
512
+ if sel_start_in_line < sel_end_in_line
513
+ y = padding + display_idx * (font_size + @line_spacing)
514
+ x_start = padding
515
+ if sel_start_in_line > 0
516
+ x_start = padding + painter.measure_text_width(text[0, sel_start_in_line], $theme.font_family, font_size)
517
+ end
518
+ x_end = padding + painter.measure_text_width(text[0, sel_end_in_line], $theme.font_family, font_size)
519
+ painter.fill_rect(x_start, y, x_end - x_start, font_size, sel_color)
520
+ end
521
+ end
522
+
523
+ # --- Scrollable ---
524
+
525
+ def is_scrollable
526
+ true
527
+ end
528
+
529
+ def dispatch_to_scrollable(p, is_direction_x)
530
+ result_widget = nil
531
+ result_point = nil
532
+ if !is_direction_x && contain(p)
533
+ result_widget = self
534
+ result_point = p
535
+ end
536
+ [result_widget, result_point]
537
+ end
538
+
539
+ # --- Focus ---
540
+
541
+ def focused
542
+ @focused_flag = true
543
+ app = App.current
544
+ if app != nil
545
+ app.enable_text_input
546
+ end
547
+ mark_dirty
548
+ update
549
+ end
550
+
551
+ def restore_focus
552
+ @focused_flag = true
553
+ app = App.current
554
+ if app != nil
555
+ app.enable_text_input
556
+ end
557
+ mark_dirty
558
+ end
559
+
560
+ def unfocused
561
+ @focused_flag = false
562
+ @state.finish_editing
563
+ app = App.current
564
+ if app != nil
565
+ app.disable_text_input
566
+ end
567
+ mark_dirty
568
+ update
569
+ end
570
+
571
+ # --- Mouse events ---
572
+
573
+ def mouse_down(ev)
574
+ # Check if click is on scrollbar thumb
575
+ if @scroll_box_y != nil
576
+ if click_on_scrollbar_thumb(ev)
577
+ @under_dragging_y = true
578
+ @last_drag_y = ev.pos.y
579
+ return
580
+ end
581
+ end
582
+
583
+ # Click in text area
584
+ @focused_flag = true
585
+ if @state.has_preedit
586
+ @state.clear_preedit
587
+ end
588
+
589
+ pos = pos_from_point(ev.pos)
590
+ @state.start_selection(pos[0], pos[1])
591
+ @state.set_manual_scroll(false)
592
+
593
+ mark_dirty
594
+ update
595
+ end
596
+
597
+ def click_on_scrollbar_thumb(ev)
598
+ sx = @scroll_box_y[0]
599
+ sy = @scroll_box_y[1]
600
+ sw = @scroll_box_y[2]
601
+ sh = @scroll_box_y[3]
602
+ ev.pos.x >= sx && ev.pos.x < sx + sw && ev.pos.y >= sy && ev.pos.y < sy + sh
603
+ end
604
+
605
+ def mouse_up(ev)
606
+ @state.end_selection
607
+ @under_dragging_y = false
608
+ end
609
+
610
+ def mouse_drag(ev)
611
+ if @under_dragging_y
612
+ handle_scrollbar_drag(ev)
613
+ return
614
+ end
615
+
616
+ if @state.is_selecting
617
+ pos = pos_from_point(ev.pos)
618
+ @state.update_selection(pos[0], pos[1])
619
+ mark_dirty
620
+ update
621
+ end
622
+ end
623
+
624
+ def handle_scrollbar_drag(ev)
625
+ delta_y = ev.pos.y - @last_drag_y
626
+ @last_drag_y = ev.pos.y
627
+ if delta_y == 0.0
628
+ return
629
+ end
630
+
631
+ visible_height = @height - @border_width * 2.0
632
+ scroll_range = @content_height - @height
633
+ if scroll_range <= 0.0
634
+ return
635
+ end
636
+
637
+ thumb_height = (visible_height / @content_height) * visible_height
638
+ if thumb_height < 20.0
639
+ thumb_height = 20.0
640
+ end
641
+ track_range = visible_height - thumb_height
642
+ if track_range <= 0.0
643
+ return
644
+ end
645
+
646
+ scroll_y = @state.get_scroll_y
647
+ scroll_delta = (delta_y / track_range) * scroll_range
648
+ new_scroll = scroll_y + scroll_delta
649
+ if new_scroll < 0.0
650
+ new_scroll = 0.0
651
+ end
652
+ if new_scroll > scroll_range
653
+ new_scroll = scroll_range
654
+ end
655
+ if new_scroll != scroll_y
656
+ @state.set_scroll_y(new_scroll)
657
+ @state.set_manual_scroll(true)
658
+ mark_dirty
659
+ update
660
+ end
661
+ end
662
+
663
+ def mouse_wheel(ev)
664
+ delta = ev.delta_y
665
+ if delta == 0.0
666
+ return
667
+ end
668
+ max_scroll = @content_height - @height
669
+ if max_scroll <= 0.0
670
+ return
671
+ end
672
+ scroll_y = @state.get_scroll_y
673
+ # Scroll by wheel delta (negative = scroll down on macOS)
674
+ new_scroll = scroll_y - delta * 3.0
675
+ if new_scroll < 0.0
676
+ new_scroll = 0.0
677
+ end
678
+ if new_scroll > max_scroll
679
+ new_scroll = max_scroll
680
+ end
681
+ if new_scroll != scroll_y
682
+ @state.set_scroll_y(new_scroll)
683
+ @state.set_manual_scroll(true)
684
+ mark_dirty
685
+ update
686
+ end
687
+ end
688
+
689
+ def pos_from_point(point)
690
+ font_size = @font_size_val
691
+ padding = @pad_left
692
+ border_width = @border_width
693
+ scroll_y = @state.get_scroll_y
694
+
695
+ click_y = point.y + scroll_y - border_width
696
+ display_line_idx = 0
697
+ if click_y >= padding
698
+ display_line_idx = ((click_y - padding) / (font_size + @line_spacing)).to_i
699
+ end
700
+
701
+ display_lines = @last_display_lines
702
+ if display_lines == nil || display_lines.length == 0
703
+ return [0, 0]
704
+ end
705
+
706
+ if display_line_idx < 0
707
+ display_line_idx = 0
708
+ end
709
+ if display_line_idx >= display_lines.length
710
+ display_line_idx = display_lines.length - 1
711
+ end
712
+
713
+ dl = display_lines[display_line_idx]
714
+ logical_row = dl[0]
715
+ start_col = dl[2]
716
+
717
+ click_x = point.x - padding
718
+ col = start_col
719
+ if click_x > 0.0
720
+ col = find_col_from_click_x(display_line_idx, start_col, click_x)
721
+ end
722
+
723
+ lines = @state.get_lines
724
+ line_len = lines[logical_row].length
725
+ if col > line_len
726
+ col = line_len
727
+ end
728
+ [logical_row, col]
729
+ end
730
+
731
+ def find_col_from_click_x(display_line_idx, start_col, click_x)
732
+ col = start_col
733
+ if @char_positions_cache != nil && display_line_idx < @char_positions_cache.length
734
+ positions = @char_positions_cache[display_line_idx]
735
+ found = false
736
+ k = 0
737
+ while k < positions.length && !found
738
+ if positions[k] > click_x
739
+ if k > 0
740
+ prev_pos = positions[k - 1]
741
+ curr_pos = positions[k]
742
+ if (click_x - prev_pos) < (curr_pos - click_x)
743
+ col = start_col + k - 1
744
+ else
745
+ col = start_col + k
746
+ end
747
+ else
748
+ col = start_col + k
749
+ end
750
+ found = true
751
+ else
752
+ col = start_col + k
753
+ end
754
+ k = k + 1
755
+ end
756
+ end
757
+ col
758
+ end
759
+
760
+ # --- IME ---
761
+
762
+ def ime_preedit(text, sel_start, sel_end)
763
+ if text != nil && text.length > 0
764
+ @state.set_preedit(text, sel_start)
765
+ else
766
+ @state.clear_preedit
767
+ end
768
+ mark_dirty
769
+ update
770
+ end
771
+
772
+ # --- Text input ---
773
+
774
+ def input_char(text)
775
+ # Clear preedit when committed
776
+ if @state.has_preedit
777
+ @state.clear_preedit
778
+ end
779
+ # Delete selection if any
780
+ if @state.has_selection
781
+ @state.delete_selection
782
+ end
783
+ # Insert text at cursor
784
+ @state.insert_char(text)
785
+ @on_change_cb.call(@state.value) if @on_change_cb
786
+ mark_dirty
787
+ update
788
+ end
789
+
790
+ # --- Key input ---
791
+
792
+ def input_key(key_code, modifiers)
793
+ # During IME preedit, let IME handle key events
794
+ if @state.has_preedit
795
+ handle_preedit_key(key_code)
796
+ return
797
+ end
798
+
799
+ @state.set_manual_scroll(false)
800
+ # Cmd (bit 3 = MAC_COMMAND) or Ctrl (bit 1) for Linux/Windows
801
+ is_cmd = (modifiers & 8) != 0 || (modifiers & 2) != 0
802
+
803
+ if is_cmd
804
+ handle_cmd_key(key_code)
805
+ return
806
+ end
807
+
808
+ # Clear selection on navigation keys
809
+ if key_code == 23 || key_code == 25 || key_code == 24 || key_code == 26
810
+ @state.clear_selection
811
+ end
812
+
813
+ # Delete selection on content-modifying keys
814
+ if (key_code == 12 || key_code == 75) && @state.has_selection
815
+ @state.delete_selection
816
+ @on_change_cb.call(@state.value) if @on_change_cb
817
+ mark_dirty
818
+ update
819
+ return
820
+ end
821
+
822
+ handle_navigation_key(key_code)
823
+ end
824
+
825
+ def handle_preedit_key(key_code)
826
+ if @state.get_preedit_text.length == 1
827
+ if key_code == 12 || key_code == 17
828
+ @state.clear_preedit
829
+ mark_dirty
830
+ update
831
+ end
832
+ end
833
+ end
834
+
835
+ def handle_cmd_key(key_code)
836
+ # Cmd+C (Copy) - C ordinal = 45
837
+ if key_code == 45
838
+ handle_copy
839
+ # Cmd+X (Cut) - X ordinal = 66
840
+ elsif key_code == 66
841
+ handle_cut
842
+ # Cmd+V (Paste) - V ordinal = 64
843
+ elsif key_code == 64
844
+ handle_paste
845
+ # Cmd+A (Select All) - A ordinal = 43
846
+ elsif key_code == 43
847
+ @state.select_all
848
+ mark_dirty
849
+ update
850
+ end
851
+ end
852
+
853
+ def handle_navigation_key(key_code)
854
+ # Backspace (12)
855
+ if key_code == 12
856
+ if @state.delete_prev
857
+ @on_change_cb.call(@state.value) if @on_change_cb
858
+ mark_dirty
859
+ update
860
+ end
861
+ # Delete (75)
862
+ elsif key_code == 75
863
+ if @state.delete_next
864
+ @on_change_cb.call(@state.value) if @on_change_cb
865
+ mark_dirty
866
+ update
867
+ end
868
+ # Left (23)
869
+ elsif key_code == 23
870
+ @state.move_left
871
+ mark_dirty
872
+ update
873
+ # Right (25)
874
+ elsif key_code == 25
875
+ @state.move_right
876
+ mark_dirty
877
+ update
878
+ # Up (24)
879
+ elsif key_code == 24
880
+ @state.move_up
881
+ mark_dirty
882
+ update
883
+ # Down (26)
884
+ elsif key_code == 26
885
+ @state.move_down
886
+ mark_dirty
887
+ update
888
+ # Enter (11)
889
+ elsif key_code == 11
890
+ handle_enter
891
+ # Home (22)
892
+ elsif key_code == 22
893
+ if @state.move_home
894
+ mark_dirty
895
+ update
896
+ end
897
+ # End (21)
898
+ elsif key_code == 21
899
+ if @state.move_end
900
+ mark_dirty
901
+ update
902
+ end
903
+ end
904
+ end
905
+
906
+ def handle_enter
907
+ if @state.has_selection
908
+ @state.delete_selection
909
+ end
910
+ @state.insert_newline
911
+ @on_change_cb.call(@state.value) if @on_change_cb
912
+ mark_dirty
913
+ update
914
+ end
915
+
916
+ # --- Clipboard ---
917
+
918
+ def handle_copy
919
+ text = @state.get_selected_text
920
+ if text.length > 0
921
+ app = App.current
922
+ if app != nil
923
+ app.set_clipboard_text(text)
924
+ end
925
+ end
926
+ end
927
+
928
+ def handle_cut
929
+ text = @state.get_selected_text
930
+ if text.length > 0
931
+ @state.delete_selection
932
+ app = App.current
933
+ if app != nil
934
+ app.set_clipboard_text(text)
935
+ end
936
+ @on_change_cb.call(@state.value) if @on_change_cb
937
+ mark_dirty
938
+ update
939
+ end
940
+ end
941
+
942
+ def handle_paste
943
+ app = App.current
944
+ if app == nil
945
+ return
946
+ end
947
+ text = app.get_clipboard_text
948
+ if text == nil
949
+ return
950
+ end
951
+ if text.length == 0
952
+ return
953
+ end
954
+ if @state.has_selection
955
+ @state.delete_selection
956
+ end
957
+ # Handle multi-line paste
958
+ @state.paste_text(text)
959
+ @on_change_cb.call(@state.value) if @on_change_cb
960
+ mark_dirty
961
+ update
962
+ end
963
+ end
964
+
965
+ # Top-level helper — accepts initial text string for backward compatibility
966
+ def MultilineInput(text)
967
+ MultilineInput.new(MultilineInputState.new(text))
968
+ end