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,105 @@
1
+ # Image widget - displays an image from a file path
2
+ # Port of Castella Image
3
+
4
+ # Image fit constants
5
+ IMAGE_FIT_FILL = 0
6
+ IMAGE_FIT_CONTAIN = 1
7
+ IMAGE_FIT_COVER = 2
8
+
9
+ class ImageWidget < Widget
10
+ def initialize(file_path)
11
+ super()
12
+ @file_path = file_path
13
+ @image_id = 0
14
+ @img_width = 0.0
15
+ @img_height = 0.0
16
+ @fit_mode = IMAGE_FIT_CONTAIN
17
+ @width_policy = CONTENT
18
+ @height_policy = CONTENT
19
+ end
20
+
21
+ def fit(mode)
22
+ @fit_mode = mode
23
+ self
24
+ end
25
+
26
+ def set_path(path)
27
+ @file_path = path
28
+ @image_id = 0
29
+ @img_width = 0.0
30
+ @img_height = 0.0
31
+ mark_dirty
32
+ end
33
+
34
+ def load_if_needed(painter)
35
+ if @image_id == 0
36
+ @image_id = painter.load_image(@file_path)
37
+ if @image_id != 0
38
+ @img_width = painter.get_image_width(@image_id) * 1.0
39
+ @img_height = painter.get_image_height(@image_id) * 1.0
40
+ end
41
+ end
42
+ end
43
+
44
+ def measure(painter)
45
+ load_if_needed(painter)
46
+ if @image_id != 0
47
+ Size.new(@img_width, @img_height)
48
+ else
49
+ Size.new(100.0, 100.0)
50
+ end
51
+ end
52
+
53
+ def redraw(painter, completely)
54
+ load_if_needed(painter)
55
+ if @image_id == 0
56
+ # Draw placeholder
57
+ painter.fill_round_rect(0.0, 0.0, @width, @height, 4.0, 0x40FFFFFF)
58
+ painter.stroke_round_rect(0.0, 0.0, @width, @height, 4.0, 0x80FFFFFF, 1.0)
59
+ return
60
+ end
61
+
62
+ if @fit_mode == IMAGE_FIT_FILL
63
+ painter.draw_image(@image_id, 0.0, 0.0, @width, @height)
64
+ elsif @fit_mode == IMAGE_FIT_CONTAIN
65
+ draw_fitted(painter, true)
66
+ else
67
+ draw_fitted(painter, false)
68
+ end
69
+ end
70
+
71
+ def draw_fitted(painter, contain)
72
+ if @img_width < 1.0 || @img_height < 1.0 || @width < 1.0 || @height < 1.0
73
+ return
74
+ end
75
+ img_aspect = @img_width / @img_height
76
+ widget_aspect = @width / @height
77
+
78
+ if contain
79
+ if img_aspect > widget_aspect
80
+ new_w = @width
81
+ new_h = @width / img_aspect
82
+ else
83
+ new_h = @height
84
+ new_w = @height * img_aspect
85
+ end
86
+ else
87
+ if img_aspect > widget_aspect
88
+ new_h = @height
89
+ new_w = @height * img_aspect
90
+ else
91
+ new_w = @width
92
+ new_h = @width / img_aspect
93
+ end
94
+ end
95
+
96
+ dx = (@width - new_w) / 2.0
97
+ dy = (@height - new_h) / 2.0
98
+ painter.draw_image(@image_id, dx, dy, new_w, new_h)
99
+ end
100
+ end
101
+
102
+ # Top-level helper
103
+ def Image(file_path)
104
+ ImageWidget.new(file_path)
105
+ end
@@ -0,0 +1,485 @@
1
+ # Input widget - single-line text input with IME, selection, and clipboard support
2
+ # Port of Castella Input
3
+ #
4
+ # State is held in InputState (defined in core.rb) which persists across
5
+ # Component rebuilds. The widget delegates all text/cursor/selection/IME
6
+ # operations to InputState and handles rendering + event dispatch.
7
+ #
8
+ # JWM Key ordinals used in input_key:
9
+ # ENTER=11, BACKSPACE=12, ESCAPE=17, END=21, HOME=22, LEFT=23, RIGHT=25, DELETE=75
10
+ # A=43, C=45, V=64, X=66
11
+
12
+ class Input < Widget
13
+ def initialize(state)
14
+ super()
15
+ @state = state
16
+ @focused = false
17
+ @font_size_val = 14.0
18
+ @bg_color = 0
19
+ @text_color = 0
20
+ @placeholder_color = 0
21
+ @border_color = 0
22
+ @focus_border = 0
23
+ @use_theme = true
24
+ @radius = 4.0
25
+ @focusable = true
26
+ @width_policy = EXPANDING
27
+ @height_policy = CONTENT
28
+ @pad_top = 8.0
29
+ @pad_right = 12.0
30
+ @pad_bottom = 8.0
31
+ @pad_left = 12.0
32
+ # Character position cache for click-to-position
33
+ @char_positions = []
34
+ @text_start_x = 0.0
35
+ # on_change callback
36
+ @on_change_cb = nil
37
+ end
38
+
39
+ def get_text
40
+ @state.value
41
+ end
42
+
43
+ def set_text(t)
44
+ @state.set(t)
45
+ mark_dirty
46
+ end
47
+
48
+ def font_size(s)
49
+ @font_size_val = s
50
+ self
51
+ end
52
+
53
+ def on_change(&block)
54
+ @on_change_cb = block
55
+ self
56
+ end
57
+
58
+ def measure(painter)
59
+ th = painter.measure_text_height($theme.font_family, @font_size_val)
60
+ Size.new(@width, th + @pad_top + @pad_bottom)
61
+ end
62
+
63
+ # --- Rendering ---
64
+
65
+ def redraw(painter, completely)
66
+ # Resolve colors from theme
67
+ bg_c = @use_theme ? $theme.bg_primary : @bg_color
68
+ tc = @use_theme ? $theme.text_primary : @text_color
69
+ pc = @use_theme ? $theme.text_secondary : @placeholder_color
70
+ brd_c = @use_theme ? $theme.border : @border_color
71
+ fbc = @use_theme ? $theme.border_focus : @focus_border
72
+
73
+ bc = @focused ? fbc : brd_c
74
+ painter.fill_round_rect(0.0, 0.0, @width, @height, @radius, bg_c)
75
+ painter.stroke_round_rect(0.0, 0.0, @width, @height, @radius, bc, 1.0)
76
+
77
+ ascent = painter.get_text_ascent($theme.font_family, @font_size_val)
78
+ display_text = @state.get_display_text
79
+ @text_start_x = @pad_left
80
+
81
+ if display_text.length > 0
82
+ # Draw selection highlight first (behind text)
83
+ if @state.has_selection
84
+ draw_selection_highlight(painter, display_text, ascent)
85
+ end
86
+
87
+ painter.draw_text(display_text, @text_start_x, @pad_top + ascent, $theme.font_family, @font_size_val, tc)
88
+
89
+ # Build character position cache for click-to-position
90
+ @char_positions = [0.0]
91
+ i = 0
92
+ while i < display_text.length
93
+ sub = display_text[0, i + 1]
94
+ w = painter.measure_text_width(sub, $theme.font_family, @font_size_val)
95
+ @char_positions.push(w)
96
+ i = i + 1
97
+ end
98
+
99
+ # Draw preedit underline
100
+ if @state.has_preedit && @focused
101
+ draw_preedit_underline(painter, ascent, tc)
102
+ end
103
+ else
104
+ painter.draw_text(@state.get_placeholder, @text_start_x, @pad_top + ascent, $theme.font_family, @font_size_val, pc)
105
+ @char_positions = [0.0]
106
+ end
107
+
108
+ # Cursor (when focused)
109
+ if @focused
110
+ draw_cursor(painter, tc)
111
+ end
112
+ end
113
+
114
+ def draw_preedit_underline(painter, ascent, tc)
115
+ text_before_preedit = ""
116
+ cursor = @state.get_cursor
117
+ if cursor > 0
118
+ text_before_preedit = @state.value[0, cursor]
119
+ end
120
+ preedit_start_x = @pad_left + painter.measure_text_width(text_before_preedit, $theme.font_family, @font_size_val)
121
+ preedit_width = painter.measure_text_width(@state.get_preedit_text, $theme.font_family, @font_size_val)
122
+ underline_y = @pad_top + ascent + 2.0
123
+ painter.fill_rect(preedit_start_x, underline_y, preedit_width, 2.0, tc)
124
+ end
125
+
126
+ def draw_cursor(painter, tc)
127
+ text_before_caret = compute_text_before_caret
128
+ cursor_x = @pad_left + painter.measure_text_width(text_before_caret, $theme.font_family, @font_size_val)
129
+ painter.draw_line(cursor_x, @pad_top, cursor_x, @height - @pad_bottom, tc, 1.0)
130
+
131
+ # Notify IME of cursor position
132
+ notify_ime_cursor_rect(cursor_x)
133
+ end
134
+
135
+ def compute_text_before_caret
136
+ result = ""
137
+ cursor = @state.get_cursor
138
+ if @state.has_preedit
139
+ if cursor > 0
140
+ result = @state.value[0, cursor]
141
+ end
142
+ result = result + @state.get_preedit_text[0, @state.get_preedit_cursor]
143
+ else
144
+ if cursor > 0
145
+ result = @state.value[0, cursor]
146
+ end
147
+ end
148
+ result
149
+ end
150
+
151
+ def draw_selection_highlight(painter, display_text, ascent)
152
+ if @state.has_selection
153
+ range = @state.get_selection_range
154
+ s = range[0]
155
+ e = range[1]
156
+ # Clamp to display text
157
+ if s > display_text.length
158
+ s = display_text.length
159
+ end
160
+ if e > display_text.length
161
+ e = display_text.length
162
+ end
163
+ if s < e
164
+ x_start = @pad_left
165
+ if s > 0
166
+ x_start = @pad_left + painter.measure_text_width(display_text[0, s], $theme.font_family, @font_size_val)
167
+ end
168
+ x_end = @pad_left + painter.measure_text_width(display_text[0, e], $theme.font_family, @font_size_val)
169
+
170
+ sel_color = $theme.bg_selected
171
+ painter.fill_rect(x_start, @pad_top, x_end - x_start, @height - @pad_top - @pad_bottom, sel_color)
172
+ end
173
+ end
174
+ end
175
+
176
+ def notify_ime_cursor_rect(cursor_x)
177
+ app = App.current
178
+ if app != nil
179
+ app.set_ime_cursor_rect(
180
+ (@x + cursor_x).to_i,
181
+ @y.to_i,
182
+ 1,
183
+ @height.to_i
184
+ )
185
+ end
186
+ end
187
+
188
+ # --- Focus ---
189
+
190
+ def focused
191
+ @focused = true
192
+ @state.start_editing
193
+ app = App.current
194
+ if app != nil
195
+ app.enable_text_input
196
+ end
197
+ mark_dirty
198
+ update
199
+ end
200
+
201
+ def restore_focus
202
+ @focused = true
203
+ app = App.current
204
+ if app != nil
205
+ app.enable_text_input
206
+ end
207
+ mark_dirty
208
+ end
209
+
210
+ def unfocused
211
+ @focused = false
212
+ @state.finish_editing
213
+ app = App.current
214
+ if app != nil
215
+ app.disable_text_input
216
+ end
217
+ mark_dirty
218
+ update
219
+ end
220
+
221
+ # --- Mouse events ---
222
+
223
+ def mouse_down(ev)
224
+ @focused = true
225
+ # Click-to-position
226
+ click_x = ev.pos.x
227
+ rel_x = click_x - @text_start_x
228
+ char_pos = pos_from_click(rel_x)
229
+
230
+ # Clear preedit on click
231
+ if @state.has_preedit
232
+ @state.clear_preedit
233
+ end
234
+
235
+ # Start selection
236
+ @state.start_selection(char_pos)
237
+
238
+ mark_dirty
239
+ update
240
+ end
241
+
242
+ def mouse_drag(ev)
243
+ if @state.is_selecting
244
+ rel_x = ev.pos.x - @text_start_x
245
+ char_pos = pos_from_click(rel_x)
246
+ @state.update_selection(char_pos)
247
+ mark_dirty
248
+ update
249
+ end
250
+ end
251
+
252
+ def mouse_up(ev)
253
+ @state.end_selection
254
+ end
255
+
256
+ def pos_from_click(rel_x)
257
+ result = @state.value.length
258
+ if rel_x <= 0.0
259
+ result = 0
260
+ else
261
+ found = false
262
+ i = 1
263
+ while i < @char_positions.length && !found
264
+ pos = @char_positions[i]
265
+ if pos > rel_x
266
+ prev_pos = @char_positions[i - 1]
267
+ if (rel_x - prev_pos) < (pos - rel_x)
268
+ result = i - 1
269
+ else
270
+ result = i
271
+ end
272
+ found = true
273
+ end
274
+ i = i + 1
275
+ end
276
+ end
277
+ result
278
+ end
279
+
280
+ # --- IME ---
281
+
282
+ def ime_preedit(text, sel_start, sel_end)
283
+ if text != nil && text.length > 0
284
+ @state.set_preedit(text, sel_start)
285
+ else
286
+ @state.clear_preedit
287
+ end
288
+ mark_dirty
289
+ update
290
+ end
291
+
292
+ # --- Text input ---
293
+
294
+ def input_char(text)
295
+ # Clear preedit when text is committed
296
+ if @state.has_preedit
297
+ @state.clear_preedit
298
+ end
299
+ # Delete selection if any
300
+ if @state.has_selection
301
+ @state.delete_selection
302
+ end
303
+ @state.insert(text)
304
+ @on_change_cb.call(@state.value) if @on_change_cb
305
+ mark_dirty
306
+ update
307
+ end
308
+
309
+ # --- Key input ---
310
+
311
+ def input_key(key_code, modifiers)
312
+ # During IME preedit, let IME handle key events
313
+ if @state.has_preedit
314
+ handle_preedit_key(key_code)
315
+ return
316
+ end
317
+
318
+ # Check for Cmd (bit 3 = MAC_COMMAND) or Ctrl (bit 1) modifier
319
+ is_cmd = (modifiers & 8) != 0 || (modifiers & 2) != 0
320
+
321
+ if is_cmd
322
+ handle_cmd_key(key_code)
323
+ return
324
+ end
325
+
326
+ # Clear selection on navigation keys
327
+ if key_code == 23 || key_code == 25
328
+ @state.clear_selection
329
+ end
330
+
331
+ # Delete selection on content-modifying keys
332
+ if (key_code == 12 || key_code == 75) && @state.has_selection
333
+ @state.delete_selection
334
+ @on_change_cb.call(@state.value) if @on_change_cb
335
+ mark_dirty
336
+ update
337
+ return
338
+ end
339
+
340
+ handle_navigation_key(key_code)
341
+ end
342
+
343
+ def handle_preedit_key(key_code)
344
+ # Workaround: single-char preedit + backspace/escape
345
+ if @state.get_preedit_text.length == 1
346
+ if key_code == 12 || key_code == 17
347
+ @state.clear_preedit
348
+ mark_dirty
349
+ update
350
+ end
351
+ end
352
+ end
353
+
354
+ def handle_cmd_key(key_code)
355
+ # Cmd+C (Copy) - C ordinal = 45
356
+ if key_code == 45
357
+ handle_copy
358
+ # Cmd+X (Cut) - X ordinal = 66
359
+ elsif key_code == 66
360
+ handle_cut
361
+ # Cmd+V (Paste) - V ordinal = 64
362
+ elsif key_code == 64
363
+ handle_paste
364
+ # Cmd+A (Select All) - A ordinal = 43
365
+ elsif key_code == 43
366
+ @state.select_all
367
+ mark_dirty
368
+ update
369
+ end
370
+ end
371
+
372
+ def handle_navigation_key(key_code)
373
+ # Backspace (JWM ordinal 12)
374
+ if key_code == 12
375
+ if @state.delete_prev
376
+ @on_change_cb.call(@state.value) if @on_change_cb
377
+ mark_dirty
378
+ update
379
+ end
380
+ # Delete (JWM ordinal 75)
381
+ elsif key_code == 75
382
+ if @state.delete_next
383
+ @on_change_cb.call(@state.value) if @on_change_cb
384
+ mark_dirty
385
+ update
386
+ end
387
+ # Left arrow (JWM ordinal 23)
388
+ elsif key_code == 23
389
+ if @state.move_prev
390
+ mark_dirty
391
+ update
392
+ end
393
+ # Right arrow (JWM ordinal 25)
394
+ elsif key_code == 25
395
+ if @state.move_next
396
+ mark_dirty
397
+ update
398
+ end
399
+ # Home (JWM ordinal 22) - move to beginning
400
+ elsif key_code == 22
401
+ if @state.move_home
402
+ mark_dirty
403
+ update
404
+ end
405
+ # End (JWM ordinal 21) - move to end
406
+ elsif key_code == 21
407
+ if @state.move_end
408
+ mark_dirty
409
+ update
410
+ end
411
+ end
412
+ end
413
+
414
+ # --- Clipboard ---
415
+
416
+ def handle_copy
417
+ text = @state.get_selected_text
418
+ if text.length > 0
419
+ app = App.current
420
+ if app != nil
421
+ app.set_clipboard_text(text)
422
+ end
423
+ end
424
+ end
425
+
426
+ def handle_cut
427
+ text = @state.get_selected_text
428
+ if text.length > 0
429
+ @state.delete_selection
430
+ app = App.current
431
+ if app != nil
432
+ app.set_clipboard_text(text)
433
+ end
434
+ @on_change_cb.call(@state.value) if @on_change_cb
435
+ mark_dirty
436
+ update
437
+ end
438
+ end
439
+
440
+ def handle_paste
441
+ app = App.current
442
+ if app == nil
443
+ return
444
+ end
445
+ text = app.get_clipboard_text
446
+ if text == nil
447
+ return
448
+ end
449
+ if text.length == 0
450
+ return
451
+ end
452
+ if @state.has_selection
453
+ @state.delete_selection
454
+ end
455
+ paste_text(text)
456
+ end
457
+
458
+ def paste_text(text)
459
+ # Single line: take first line only
460
+ first_line = find_first_line(text)
461
+ @state.insert(first_line)
462
+ @on_change_cb.call(@state.value) if @on_change_cb
463
+ mark_dirty
464
+ update
465
+ end
466
+
467
+ def find_first_line(text)
468
+ result = text
469
+ found = false
470
+ newline_idx = 0
471
+ while newline_idx < text.length && !found
472
+ if text[newline_idx] == "\n"
473
+ result = text[0, newline_idx]
474
+ found = true
475
+ end
476
+ newline_idx = newline_idx + 1
477
+ end
478
+ result
479
+ end
480
+ end
481
+
482
+ # Top-level helper — accepts placeholder string for backward compatibility
483
+ def Input(placeholder)
484
+ Input.new(InputState.new(placeholder))
485
+ end
@@ -0,0 +1,57 @@
1
+ # Markdown widget - renders markdown text
2
+ # Combines MarkdownParser + MarkdownRenderer
3
+
4
+ class Markdown < Widget
5
+ def initialize(text)
6
+ super()
7
+ @source = text
8
+ @md_theme = MarkdownTheme.new
9
+ @parser = MarkdownParser.new
10
+ @renderer = MarkdownRenderer.new(@md_theme)
11
+ @ast = nil
12
+ @content_height = 0.0
13
+ @padding_val = 12.0
14
+ @width_policy = EXPANDING
15
+ @height_policy = CONTENT
16
+ end
17
+
18
+ def padding(p)
19
+ @padding_val = p
20
+ self
21
+ end
22
+
23
+ def set_text(t)
24
+ @source = t
25
+ @ast = nil
26
+ mark_dirty
27
+ end
28
+
29
+ def get_text
30
+ @source
31
+ end
32
+
33
+ def measure(painter)
34
+ ensure_parsed
35
+ h = @renderer.measure_height(painter, @ast, @width, @padding_val)
36
+ Size.new(@width, h)
37
+ end
38
+
39
+ def redraw(painter, completely)
40
+ ensure_parsed
41
+ # Background
42
+ painter.fill_rect(0.0, 0.0, @width, @height, $theme.bg_canvas)
43
+ # Render markdown
44
+ @content_height = @renderer.render(painter, @ast, @width, @padding_val)
45
+ end
46
+
47
+ def ensure_parsed
48
+ if @ast == nil
49
+ @ast = @parser.parse(@source)
50
+ end
51
+ end
52
+ end
53
+
54
+ # Top-level helper
55
+ def Markdown(text)
56
+ Markdown.new(text)
57
+ end