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,157 @@
1
+ # Tabs widget - tabbed container with header buttons
2
+ # Port of Castella Tabs
3
+
4
+ class Tabs < Layout
5
+ def initialize(labels, contents)
6
+ super()
7
+ @tab_labels = labels
8
+ @tab_contents = contents
9
+ @selected = 0
10
+ @tab_height = 36.0
11
+ @font_size_val = 13.0
12
+ @width_policy = EXPANDING
13
+ @height_policy = EXPANDING
14
+ @tab_widths = []
15
+ # Add initial content
16
+ if @tab_contents.length > 0
17
+ add(@tab_contents[0])
18
+ end
19
+ end
20
+
21
+ def select_tab(index)
22
+ if index >= 0
23
+ if index < @tab_contents.length
24
+ if index != @selected
25
+ @selected = index
26
+ clear_children
27
+ add(@tab_contents[@selected])
28
+ mark_dirty
29
+ mark_layout_dirty
30
+ update
31
+ end
32
+ end
33
+ end
34
+ self
35
+ end
36
+
37
+ def relocate_children(painter)
38
+ # Content area starts below tab header
39
+ content_y = @y + @tab_height
40
+ content_h = @height - @tab_height
41
+ if content_h < 0.0
42
+ content_h = 0.0
43
+ end
44
+ if @children.length > 0
45
+ c = @children[0]
46
+ c.move_xy(@x, content_y)
47
+ c.resize_wh(@width, content_h)
48
+ end
49
+ end
50
+
51
+ def measure(painter)
52
+ Size.new(@width, @height)
53
+ end
54
+
55
+ def redraw(painter, completely)
56
+ # 1) Layout + draw children first (redraw_children may clear the entire area)
57
+ relocate_children(painter)
58
+ redraw_children(painter, completely)
59
+
60
+ # 2) Draw tab bar ON TOP so it is not overwritten by background clear
61
+ draw_tab_bar(painter)
62
+ end
63
+
64
+ def draw_tab_bar(painter)
65
+ tab_bg = $theme.bg_canvas
66
+ tab_active_bg = $theme.bg_primary
67
+ tab_text_c = $theme.text_secondary
68
+ tab_active_text = $theme.text_primary
69
+ tab_border_c = $theme.border
70
+ tab_indicator_c = $theme.accent
71
+
72
+ # Tab bar background
73
+ painter.fill_rect(0.0, 0.0, @width, @tab_height, tab_bg)
74
+
75
+ # Calculate tab widths based on labels
76
+ @tab_widths = []
77
+ pad_h = 16.0
78
+ i = 0
79
+ while i < @tab_labels.length
80
+ tw = painter.measure_text_width(@tab_labels[i], $theme.font_family, @font_size_val)
81
+ @tab_widths.push(tw + pad_h * 2.0)
82
+ i = i + 1
83
+ end
84
+
85
+ # Draw each tab header
86
+ ascent = painter.get_text_ascent($theme.font_family, @font_size_val)
87
+ draw_tab_headers(painter, ascent, tab_active_bg, tab_active_text, tab_text_c, tab_indicator_c)
88
+
89
+ # Border line below tabs
90
+ painter.draw_line(0.0, @tab_height, @width, @tab_height, tab_border_c, 1.0)
91
+ end
92
+
93
+ def draw_tab_headers(painter, ascent, active_bg, active_tc, inactive_tc, indicator_c)
94
+ tab_x = 0.0
95
+ i = 0
96
+ while i < @tab_labels.length
97
+ tw = @tab_widths[i]
98
+ draw_one_tab(painter, i, tab_x, tw, ascent, active_bg, active_tc, inactive_tc, indicator_c)
99
+ tab_x = tab_x + tw
100
+ i = i + 1
101
+ end
102
+ end
103
+
104
+ def draw_one_tab(painter, i, tab_x, tw, ascent, active_bg, active_tc, inactive_tc, indicator_c)
105
+ is_selected = (i == @selected)
106
+
107
+ # Tab background
108
+ if is_selected
109
+ painter.fill_rect(tab_x, 0.0, tw, @tab_height, active_bg)
110
+ end
111
+
112
+ # Tab label
113
+ if is_selected
114
+ tc = active_tc
115
+ else
116
+ tc = inactive_tc
117
+ end
118
+ label_w = painter.measure_text_width(@tab_labels[i], $theme.font_family, @font_size_val)
119
+ text_x = tab_x + (tw - label_w) / 2.0
120
+ text_y = (@tab_height - painter.measure_text_height($theme.font_family, @font_size_val)) / 2.0 + ascent
121
+ painter.draw_text(@tab_labels[i], text_x, text_y, $theme.font_family, @font_size_val, tc)
122
+
123
+ # Active indicator line at bottom
124
+ if is_selected
125
+ painter.fill_rect(tab_x, @tab_height - 2.0, tw, 2.0, indicator_c)
126
+ end
127
+ end
128
+
129
+ def mouse_down(ev)
130
+ # Check if click is in the tab header area
131
+ click_y = ev.pos.y
132
+ if click_y < @tab_height
133
+ find_clicked_tab(ev.pos.x)
134
+ end
135
+ end
136
+
137
+ def find_clicked_tab(click_x)
138
+ tab_x = 0.0
139
+ i = 0
140
+ while i < @tab_widths.length
141
+ tw = @tab_widths[i]
142
+ if click_x >= tab_x
143
+ if click_x < tab_x + tw
144
+ select_tab(i)
145
+ return
146
+ end
147
+ end
148
+ tab_x = tab_x + tw
149
+ i = i + 1
150
+ end
151
+ end
152
+ end
153
+
154
+ # Top-level helper
155
+ def Tabs(labels, contents)
156
+ Tabs.new(labels, contents)
157
+ end
@@ -0,0 +1,110 @@
1
+ # Text widget - displays text
2
+ # Port of Castella Text
3
+
4
+ # Text alignment constants
5
+ TEXT_ALIGN_LEFT = 0
6
+ TEXT_ALIGN_CENTER = 1
7
+ TEXT_ALIGN_RIGHT = 2
8
+
9
+ class Text < Widget
10
+ def initialize(text)
11
+ super()
12
+ @text = text
13
+ @font_family_val = nil
14
+ @font_size_val = 14.0
15
+ @color_val = 0xFFC0CAF5
16
+ @custom_color = false
17
+ @kind_val = 0
18
+ @text_align = TEXT_ALIGN_LEFT
19
+ @font_weight = 0
20
+ @font_slant = 0
21
+ @width_policy = CONTENT
22
+ @height_policy = CONTENT
23
+ end
24
+
25
+ def font_size(s)
26
+ @font_size_val = s
27
+ self
28
+ end
29
+
30
+ def font_family(f)
31
+ @font_family_val = f
32
+ self
33
+ end
34
+
35
+ def resolved_font_family
36
+ if @font_family_val != nil
37
+ @font_family_val
38
+ else
39
+ $theme.font_family
40
+ end
41
+ end
42
+
43
+ def bold
44
+ @font_weight = 1
45
+ self
46
+ end
47
+
48
+ def italic
49
+ @font_slant = 1
50
+ self
51
+ end
52
+
53
+ def color(c)
54
+ @color_val = c
55
+ @custom_color = true
56
+ self
57
+ end
58
+
59
+ def kind(k)
60
+ @kind_val = k
61
+ self
62
+ end
63
+
64
+ def align(a)
65
+ @text_align = a
66
+ self
67
+ end
68
+
69
+ def set_text(t)
70
+ @text = t
71
+ mark_dirty
72
+ end
73
+
74
+ def get_text
75
+ @text
76
+ end
77
+
78
+ def measure(painter)
79
+ ff = resolved_font_family
80
+ w = painter.measure_text_width(@text, ff, @font_size_val)
81
+ h = painter.measure_text_height(ff, @font_size_val)
82
+ Size.new(w, h)
83
+ end
84
+
85
+ def redraw(painter, completely)
86
+ ff = resolved_font_family
87
+ ascent = painter.get_text_ascent(ff, @font_size_val)
88
+ x_offset = 0.0
89
+ if @text_align == TEXT_ALIGN_CENTER
90
+ text_w = painter.measure_text_width(@text, ff, @font_size_val)
91
+ x_offset = (@width - text_w) / 2.0
92
+ if x_offset < 0.0
93
+ x_offset = 0.0
94
+ end
95
+ elsif @text_align == TEXT_ALIGN_RIGHT
96
+ text_w = painter.measure_text_width(@text, ff, @font_size_val)
97
+ x_offset = @width - text_w
98
+ if x_offset < 0.0
99
+ x_offset = 0.0
100
+ end
101
+ end
102
+ c = @custom_color ? @color_val : $theme.text_color_for_kind(@kind_val)
103
+ painter.draw_text(@text, x_offset, ascent, ff, @font_size_val, c, @font_weight, @font_slant)
104
+ end
105
+ end
106
+
107
+ # Top-level helper
108
+ def Text(text)
109
+ Text.new(text)
110
+ end
@@ -0,0 +1,426 @@
1
+ # Tree - expandable tree view widget with virtual scroll
2
+ # Features: expand/collapse, selection, hover highlight, icons,
3
+ # virtual scroll (only visible rows rendered)
4
+
5
+ TREE_ROW_HEIGHT = 26.0
6
+ TREE_INDENT = 20.0
7
+ TREE_ICON_SIZE = 16.0
8
+ TREE_TOGGLE_SIZE = 16.0
9
+ TREE_SCROLLBAR_WIDTH = 8.0
10
+
11
+ # TreeNode - data model for tree items
12
+ class TreeNode
13
+ def initialize(id, label)
14
+ @id = id
15
+ @label = label
16
+ @children = []
17
+ @icon = nil
18
+ @data = nil
19
+ end
20
+
21
+ def id
22
+ @id
23
+ end
24
+
25
+ def label
26
+ @label
27
+ end
28
+
29
+ def children
30
+ @children
31
+ end
32
+
33
+ def icon
34
+ @icon
35
+ end
36
+
37
+ def data
38
+ @data
39
+ end
40
+
41
+ def set_icon(i)
42
+ @icon = i
43
+ self
44
+ end
45
+
46
+ def set_data(d)
47
+ @data = d
48
+ self
49
+ end
50
+
51
+ def add_child(child)
52
+ @children << child
53
+ self
54
+ end
55
+
56
+ def has_children
57
+ @children.length > 0
58
+ end
59
+ end
60
+
61
+ # TreeState - reactive state for tree
62
+ class TreeState < ObservableBase
63
+ def initialize(nodes)
64
+ super()
65
+ @nodes = nodes # Array of TreeNode (root nodes)
66
+ @expanded_ids = [] # Array of expanded node IDs
67
+ @selected_id = "none"
68
+ end
69
+
70
+ def nodes
71
+ @nodes
72
+ end
73
+
74
+ def selected_id
75
+ @selected_id
76
+ end
77
+
78
+ def is_expanded(id)
79
+ i = 0
80
+ while i < @expanded_ids.length
81
+ if @expanded_ids[i] == id
82
+ return true
83
+ end
84
+ i = i + 1
85
+ end
86
+ false
87
+ end
88
+
89
+ def toggle_expanded(id)
90
+ if is_expanded(id)
91
+ collapse(id)
92
+ else
93
+ expand(id)
94
+ end
95
+ end
96
+
97
+ def expand(id)
98
+ if is_expanded(id)
99
+ return
100
+ end
101
+ @expanded_ids << id
102
+ notify_observers
103
+ end
104
+
105
+ def collapse(id)
106
+ new_ids = []
107
+ i = 0
108
+ while i < @expanded_ids.length
109
+ if @expanded_ids[i] != id
110
+ new_ids << @expanded_ids[i]
111
+ end
112
+ i = i + 1
113
+ end
114
+ @expanded_ids = new_ids
115
+ notify_observers
116
+ end
117
+
118
+ def expand_all
119
+ collect_all_ids(@nodes)
120
+ notify_observers
121
+ end
122
+
123
+ def collapse_all
124
+ @expanded_ids = []
125
+ notify_observers
126
+ end
127
+
128
+ def select(id)
129
+ @selected_id = id
130
+ notify_observers
131
+ end
132
+
133
+ def set_nodes(n)
134
+ @nodes = n
135
+ @expanded_ids = []
136
+ @selected_id = "none"
137
+ notify_observers
138
+ end
139
+
140
+ private
141
+
142
+ def collect_all_ids(nodes)
143
+ i = 0
144
+ while i < nodes.length
145
+ node = nodes[i]
146
+ if node.has_children
147
+ @expanded_ids << node.id
148
+ collect_all_ids(node.children)
149
+ end
150
+ i = i + 1
151
+ end
152
+ end
153
+ end
154
+
155
+ # Tree widget - custom drawing with virtual scroll
156
+ class Tree < Widget
157
+ def initialize(state)
158
+ super()
159
+ @state = state
160
+ @scroll_y = 0.0
161
+ @max_scroll = 0.0
162
+ @scrollable_flag = true
163
+ @hover_row = -1
164
+ @visible_nodes = [] # Flat list of [node, depth] pairs for rendering
165
+ @width_policy = EXPANDING
166
+ @height_policy = EXPANDING
167
+ @state.attach(self)
168
+ end
169
+
170
+ def on_attach(observable)
171
+ end
172
+
173
+ def on_detach(observable)
174
+ end
175
+
176
+ def on_notify
177
+ rebuild_visible
178
+ mark_dirty
179
+ update
180
+ end
181
+
182
+ def get_scrollable
183
+ @scrollable_flag
184
+ end
185
+
186
+ def rebuild_visible
187
+ @visible_nodes = []
188
+ collect_visible(@state.nodes, 0)
189
+ end
190
+
191
+ def collect_visible(nodes, depth)
192
+ i = 0
193
+ while i < nodes.length
194
+ node = nodes[i]
195
+ @visible_nodes << [node, depth]
196
+ if node.has_children
197
+ if @state.is_expanded(node.id)
198
+ collect_visible(node.children, depth + 1)
199
+ end
200
+ end
201
+ i = i + 1
202
+ end
203
+ end
204
+
205
+ def redraw(painter, completely)
206
+ rebuild_visible
207
+ visible_h = @height
208
+ compute_tree_scroll(visible_h)
209
+
210
+ # Background
211
+ painter.fill_rect(0.0, 0.0, @width, @height, $theme.bg_primary)
212
+
213
+ # Clip and draw visible rows
214
+ painter.save
215
+ painter.clip_rect(0.0, 0.0, @width, @height)
216
+
217
+ first_row = tree_float_to_row(@scroll_y / TREE_ROW_HEIGHT)
218
+ if first_row < 0
219
+ first_row = 0
220
+ end
221
+ last_row = tree_float_to_row((@scroll_y + visible_h) / TREE_ROW_HEIGHT) + 1
222
+ if last_row > @visible_nodes.length
223
+ last_row = @visible_nodes.length
224
+ end
225
+
226
+ ri = first_row
227
+ while ri < last_row
228
+ draw_tree_row(painter, ri)
229
+ ri = ri + 1
230
+ end
231
+
232
+ painter.restore
233
+
234
+ # Scrollbar
235
+ draw_tree_scrollbar(painter, visible_h)
236
+ end
237
+
238
+ def draw_tree_row(painter, ri)
239
+ entry = @visible_nodes[ri]
240
+ node = entry[0]
241
+ depth = entry[1]
242
+ ri_f = ri * 1.0
243
+ row_y = ri_f * TREE_ROW_HEIGHT - @scroll_y
244
+
245
+ # Background: selection > hover > alternating
246
+ bg = compute_tree_row_bg(painter, ri, node)
247
+ painter.fill_rect(0.0, row_y, @width, TREE_ROW_HEIGHT, bg)
248
+
249
+ # Indent
250
+ indent = depth * 1.0 * TREE_INDENT + 8.0
251
+
252
+ # Toggle icon (expand/collapse arrow)
253
+ if node.has_children
254
+ draw_toggle(painter, indent, row_y, node)
255
+ end
256
+
257
+ # Label
258
+ label_x = indent + TREE_TOGGLE_SIZE + 4.0
259
+ ascent = painter.get_text_ascent($theme.font_family, 13.0)
260
+ mh = painter.measure_text_height($theme.font_family, 13.0)
261
+ label_y = row_y + (TREE_ROW_HEIGHT - mh) / 2.0 + ascent
262
+ tc = $theme.text_primary
263
+ painter.draw_text(node.label, label_x, label_y, $theme.font_family, 13.0, tc)
264
+
265
+ # Bottom border
266
+ bc = painter.with_alpha($theme.border, 30)
267
+ painter.draw_line(0.0, row_y + TREE_ROW_HEIGHT, @width, row_y + TREE_ROW_HEIGHT, bc, 1.0)
268
+ end
269
+
270
+ def draw_toggle(painter, indent, row_y, node)
271
+ # Draw a small triangle: > for collapsed, v for expanded
272
+ tx = indent + TREE_TOGGLE_SIZE / 2.0
273
+ ty = row_y + TREE_ROW_HEIGHT / 2.0
274
+ tc = $theme.text_secondary
275
+ s = 5.0
276
+ if @state.is_expanded(node.id)
277
+ # Down arrow (v)
278
+ painter.fill_triangle(tx - s, ty - s / 2.0,
279
+ tx + s, ty - s / 2.0,
280
+ tx, ty + s / 2.0, tc)
281
+ else
282
+ # Right arrow (>)
283
+ painter.fill_triangle(tx - s / 2.0, ty - s,
284
+ tx + s / 2.0, ty,
285
+ tx - s / 2.0, ty + s, tc)
286
+ end
287
+ end
288
+
289
+ def compute_tree_row_bg(painter, ri, node)
290
+ bg = $theme.bg_primary
291
+ if node.id == @state.selected_id
292
+ ac = $theme.accent
293
+ bg = painter.with_alpha(ac, 50)
294
+ elsif ri == @hover_row
295
+ bg = painter.lighten_color(bg, 0.08)
296
+ end
297
+ bg
298
+ end
299
+
300
+ def draw_tree_scrollbar(painter, visible_h)
301
+ if @max_scroll <= 0.0
302
+ return
303
+ end
304
+ vn_len = @visible_nodes.length * 1.0
305
+ content_h = vn_len * TREE_ROW_HEIGHT
306
+ sb_x = @width - TREE_SCROLLBAR_WIDTH
307
+ sb_ratio = visible_h / content_h
308
+ sb_h = visible_h * sb_ratio
309
+ if sb_h < 20.0
310
+ sb_h = 20.0
311
+ end
312
+ sb_travel = visible_h - sb_h
313
+ sb_pos = 0.0
314
+ if @max_scroll > 0.0
315
+ sb_pos = (@scroll_y / @max_scroll) * sb_travel
316
+ end
317
+ painter.fill_rect(sb_x, 0.0, TREE_SCROLLBAR_WIDTH, visible_h, $theme.scrollbar_bg)
318
+ painter.fill_round_rect(sb_x + 1.0, sb_pos, TREE_SCROLLBAR_WIDTH - 2.0, sb_h, 3.0, $theme.scrollbar_fg)
319
+ end
320
+
321
+ # --- Event Handlers ---
322
+
323
+ def mouse_up(ev)
324
+ mx = ev.pos.x
325
+ my = ev.pos.y
326
+ row_idx = tree_row_at_y(my)
327
+ if row_idx < 0
328
+ return
329
+ end
330
+ if row_idx >= @visible_nodes.length
331
+ return
332
+ end
333
+ entry = @visible_nodes[row_idx]
334
+ node = entry[0]
335
+ depth = entry[1]
336
+ indent = depth * 1.0 * TREE_INDENT + 8.0
337
+ toggle_end = indent + TREE_TOGGLE_SIZE
338
+ if mx < toggle_end
339
+ if node.has_children
340
+ @state.toggle_expanded(node.id)
341
+ return
342
+ end
343
+ end
344
+ @state.select(node.id)
345
+ end
346
+
347
+ def cursor_pos(ev)
348
+ my = ev.pos.y
349
+ old_hr = @hover_row
350
+ @hover_row = tree_row_at_y(my)
351
+ if @hover_row != old_hr
352
+ mark_dirty
353
+ update
354
+ end
355
+ end
356
+
357
+ def mouse_out
358
+ if @hover_row != -1
359
+ @hover_row = -1
360
+ mark_dirty
361
+ update
362
+ end
363
+ end
364
+
365
+ def dispatch_to_scrollable(p, is_direction_x)
366
+ if contain(p)
367
+ [self, p]
368
+ else
369
+ [nil, nil]
370
+ end
371
+ end
372
+
373
+ def mouse_wheel(ev)
374
+ @scroll_y = @scroll_y - ev.delta_y * 30.0
375
+ if @scroll_y < 0.0
376
+ @scroll_y = 0.0
377
+ end
378
+ if @scroll_y > @max_scroll
379
+ @scroll_y = @max_scroll
380
+ end
381
+ mark_dirty
382
+ update
383
+ end
384
+
385
+ private
386
+
387
+ def compute_tree_scroll(visible_h)
388
+ vn_len = @visible_nodes.length * 1.0
389
+ content_h = vn_len * TREE_ROW_HEIGHT
390
+ @max_scroll = content_h - visible_h
391
+ if @max_scroll < 0.0
392
+ @max_scroll = 0.0
393
+ end
394
+ if @scroll_y > @max_scroll
395
+ @scroll_y = @max_scroll
396
+ end
397
+ if @scroll_y < 0.0
398
+ @scroll_y = 0.0
399
+ end
400
+ end
401
+
402
+ def tree_row_at_y(y)
403
+ row = tree_float_to_row((y + @scroll_y) / TREE_ROW_HEIGHT)
404
+ if row < 0
405
+ row = -1
406
+ end
407
+ if row >= @visible_nodes.length
408
+ row = -1
409
+ end
410
+ row
411
+ end
412
+
413
+ def tree_float_to_row(f)
414
+ # Convert float to integer row index without .to_i (JVM limitation)
415
+ r = 0
416
+ while r * 1.0 + 1.0 <= f
417
+ r = r + 1
418
+ end
419
+ r
420
+ end
421
+ end
422
+
423
+ # Top-level helper
424
+ def Tree(state)
425
+ Tree.new(state)
426
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Konpeito
4
+ VERSION = "0.1.0"
5
+ end