kumiki 0.1.1

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