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,180 @@
1
+ module Kumiki
2
+ # MultilineText widget - displays multiline text with optional word wrapping
3
+
4
+ class MultilineText < Widget
5
+ def initialize(text)
6
+ super()
7
+ @text = text
8
+ @font_family = nil
9
+ @font_size_val = 14.0
10
+ @color_val = 0xFFC0CAF5
11
+ @custom_color = false
12
+ @kind_val = 0
13
+ @padding_val = 8.0
14
+ @line_spacing = 4.0
15
+ @wrap_enabled = false
16
+ @border_width_val = 1.0
17
+ @cached_lines = []
18
+ end
19
+
20
+ def font_family(f)
21
+ @font_family = f
22
+ self
23
+ end
24
+
25
+ def resolved_font_family
26
+ if @font_family != nil
27
+ @font_family
28
+ else
29
+ Kumiki.theme.font_family
30
+ end
31
+ end
32
+
33
+ def font_size(s)
34
+ @font_size_val = s
35
+ self
36
+ end
37
+
38
+ def color(c)
39
+ @color_val = c
40
+ @custom_color = true
41
+ self
42
+ end
43
+
44
+ def kind(k)
45
+ @kind_val = k
46
+ self
47
+ end
48
+
49
+ def padding(p)
50
+ @padding_val = p
51
+ self
52
+ end
53
+
54
+ def line_spacing(s)
55
+ @line_spacing = s
56
+ self
57
+ end
58
+
59
+ def wrap_text(w)
60
+ @wrap_enabled = w
61
+ self
62
+ end
63
+
64
+ def set_text(t)
65
+ @text = t
66
+ @cached_lines = []
67
+ mark_dirty
68
+ end
69
+
70
+ def get_text
71
+ @text
72
+ end
73
+
74
+ def measure(painter)
75
+ lines = split_lines(painter)
76
+ @cached_lines = lines
77
+ if lines.length == 0
78
+ return Size.new(@padding_val * 2.0, @padding_val * 2.0)
79
+ end
80
+ ff = resolved_font_family
81
+ max_w = 0.0
82
+ i = 0
83
+ while i < lines.length
84
+ lw = painter.measure_text_width(lines[i], ff, @font_size_val)
85
+ if lw > max_w
86
+ max_w = lw
87
+ end
88
+ i = i + 1
89
+ end
90
+ w = max_w + (@padding_val + @border_width_val) * 2.0
91
+ h = @font_size_val * lines.length + @line_spacing * (lines.length - 1) + @padding_val * 2.0 + @border_width_val * 2.0
92
+ Size.new(w, h)
93
+ end
94
+
95
+ def redraw(painter, completely)
96
+ lines = split_lines(painter)
97
+ @cached_lines = lines
98
+
99
+ # Background
100
+ bg_c = Kumiki.theme.bg_primary
101
+ brd_c = Kumiki.theme.border
102
+ painter.fill_rect(0.0, 0.0, @width, @height, bg_c)
103
+ painter.stroke_rect(0.0, 0.0, @width, @height, brd_c, @border_width_val)
104
+
105
+ # Text
106
+ ff = resolved_font_family
107
+ c = @custom_color ? @color_val : Kumiki.theme.text_color_for_kind(@kind_val)
108
+ ascent = painter.get_text_ascent(ff, @font_size_val)
109
+ y = @padding_val + @border_width_val + ascent
110
+ i = 0
111
+ while i < lines.length
112
+ painter.draw_text(lines[i], @padding_val + @border_width_val, y, ff, @font_size_val, c)
113
+ y = y + @font_size_val + @line_spacing
114
+ i = i + 1
115
+ end
116
+ end
117
+
118
+ def split_lines(painter)
119
+ raw_lines = @text.split("\n")
120
+ if !@wrap_enabled
121
+ return raw_lines
122
+ end
123
+ # Word wrapping
124
+ ff = resolved_font_family
125
+ line_width = @width - (@padding_val + @border_width_val) * 2.0
126
+ if line_width <= 0.0
127
+ return raw_lines
128
+ end
129
+ result = []
130
+ ri = 0
131
+ while ri < raw_lines.length
132
+ line = raw_lines[ri]
133
+ words = line.split(" ")
134
+ if words.length == 0
135
+ result.push("")
136
+ ri = ri + 1
137
+ next
138
+ end
139
+ current_words = []
140
+ current_width = 0.0
141
+ wi = 0
142
+ while wi < words.length
143
+ word = words[wi]
144
+ word_w = painter.measure_text_width(word, ff, @font_size_val)
145
+ space_w = 0.0
146
+ if current_words.length > 0
147
+ space_w = painter.measure_text_width(" ", ff, @font_size_val)
148
+ end
149
+ if current_width + space_w + word_w > line_width
150
+ if current_words.length > 0
151
+ result.push(current_words.join(" "))
152
+ current_words = [word]
153
+ current_width = word_w
154
+ else
155
+ # Single word wider than line - just include it
156
+ result.push(word)
157
+ current_words = []
158
+ current_width = 0.0
159
+ end
160
+ else
161
+ current_words.push(word)
162
+ current_width = current_width + space_w + word_w
163
+ end
164
+ wi = wi + 1
165
+ end
166
+ if current_words.length > 0
167
+ result.push(current_words.join(" "))
168
+ end
169
+ ri = ri + 1
170
+ end
171
+ result
172
+ end
173
+ end
174
+
175
+ # Top-level helper
176
+ def MultilineText(text)
177
+ MultilineText.new(text)
178
+ end
179
+
180
+ end
@@ -0,0 +1,100 @@
1
+ module Kumiki
2
+ # NetImage widget - displays an image from a network URL
3
+
4
+ class NetImageWidget < Widget
5
+ def initialize(url)
6
+ super()
7
+ @url = url
8
+ @image_id = 0
9
+ @img_width = 0.0
10
+ @img_height = 0.0
11
+ @fit_mode = IMAGE_FIT_CONTAIN
12
+ end
13
+
14
+ def fit(mode)
15
+ @fit_mode = mode
16
+ self
17
+ end
18
+
19
+ def set_url(url)
20
+ @url = url
21
+ @image_id = 0
22
+ @img_width = 0.0
23
+ @img_height = 0.0
24
+ mark_dirty
25
+ end
26
+
27
+ def load_if_needed(painter)
28
+ if @image_id == 0
29
+ @image_id = painter.load_net_image(@url)
30
+ if @image_id != 0
31
+ @img_width = painter.get_image_width(@image_id) * 1.0
32
+ @img_height = painter.get_image_height(@image_id) * 1.0
33
+ end
34
+ end
35
+ end
36
+
37
+ def measure(painter)
38
+ load_if_needed(painter)
39
+ if @image_id != 0
40
+ Size.new(@img_width, @img_height)
41
+ else
42
+ Size.new(100.0, 100.0)
43
+ end
44
+ end
45
+
46
+ def redraw(painter, completely)
47
+ load_if_needed(painter)
48
+ if @image_id == 0
49
+ # Draw placeholder while loading or on error
50
+ painter.fill_round_rect(0.0, 0.0, @width, @height, 4.0, 0x40FFFFFF)
51
+ painter.stroke_round_rect(0.0, 0.0, @width, @height, 4.0, 0x80FFFFFF, 1.0)
52
+ return
53
+ end
54
+
55
+ if @fit_mode == IMAGE_FIT_FILL
56
+ painter.draw_image(@image_id, 0.0, 0.0, @width, @height)
57
+ elsif @fit_mode == IMAGE_FIT_CONTAIN
58
+ draw_fitted(painter, true)
59
+ else
60
+ draw_fitted(painter, false)
61
+ end
62
+ end
63
+
64
+ def draw_fitted(painter, contain)
65
+ if @img_width < 1.0 || @img_height < 1.0 || @width < 1.0 || @height < 1.0
66
+ return
67
+ end
68
+ img_aspect = @img_width / @img_height
69
+ widget_aspect = @width / @height
70
+
71
+ if contain
72
+ if img_aspect > widget_aspect
73
+ new_w = @width
74
+ new_h = @width / img_aspect
75
+ else
76
+ new_h = @height
77
+ new_w = @height * img_aspect
78
+ end
79
+ else
80
+ if img_aspect > widget_aspect
81
+ new_h = @height
82
+ new_w = @height * img_aspect
83
+ else
84
+ new_w = @width
85
+ new_h = @width / img_aspect
86
+ end
87
+ end
88
+
89
+ dx = (@width - new_w) / 2.0
90
+ dy = (@height - new_h) / 2.0
91
+ painter.draw_image(@image_id, dx, dy, new_w, new_h)
92
+ end
93
+ end
94
+
95
+ # Top-level helper
96
+ def NetImage(url)
97
+ NetImageWidget.new(url)
98
+ end
99
+
100
+ end
@@ -0,0 +1,72 @@
1
+ module Kumiki
2
+ # ProgressBar widget - progress indicator
3
+
4
+ class ProgressBar < Widget
5
+ def initialize
6
+ super()
7
+ @value = 0.0
8
+ @width_policy = EXPANDING
9
+ @height_policy = FIXED
10
+ @height = 8.0
11
+ # Colors (Tokyo Night)
12
+ @track_color = 0xFF414868
13
+ @fill_color = 0xFF7AA2F7
14
+ @radius = 4.0
15
+ end
16
+
17
+ def with_value(v)
18
+ if v < 0.0
19
+ @value = 0.0
20
+ elsif v > 1.0
21
+ @value = 1.0
22
+ else
23
+ @value = v
24
+ end
25
+ self
26
+ end
27
+
28
+ def set_value(v)
29
+ if v < 0.0
30
+ v = 0.0
31
+ end
32
+ if v > 1.0
33
+ v = 1.0
34
+ end
35
+ if v != @value
36
+ @value = v
37
+ mark_dirty
38
+ update
39
+ end
40
+ end
41
+
42
+ def get_value
43
+ @value
44
+ end
45
+
46
+ def fill_color(c)
47
+ @fill_color = c
48
+ self
49
+ end
50
+
51
+ def measure(painter)
52
+ Size.new(200.0, 8.0)
53
+ end
54
+
55
+ def redraw(painter, completely)
56
+ # Track background
57
+ painter.fill_round_rect(0.0, 0.0, @width, @height, @radius, @track_color)
58
+
59
+ # Fill
60
+ fill_w = @width * @value
61
+ if fill_w > 0.0
62
+ painter.fill_round_rect(0.0, 0.0, fill_w, @height, @radius, @fill_color)
63
+ end
64
+ end
65
+ end
66
+
67
+ # Top-level helper
68
+ def ProgressBar()
69
+ ProgressBar.new
70
+ end
71
+
72
+ end
@@ -0,0 +1,93 @@
1
+ module Kumiki
2
+ # RadioButtons widget - exclusive selection from a list
3
+ # Single widget that renders all options (avoids cross-class method calls)
4
+
5
+ class RadioButtons < Widget
6
+ def initialize(options)
7
+ super()
8
+ @options = options
9
+ @option_count = options.length
10
+ @selected = 0
11
+ @change_handler = nil
12
+ @hovered_index = -1
13
+ # Colors (Tokyo Night)
14
+ @text_color = 0xFFC0CAF5
15
+ @ring_color = 0xFF565F89
16
+ @selected_color = 0xFF7AA2F7
17
+ @font_size_val = 14.0
18
+ @item_height = 32.0
19
+ end
20
+
21
+ def with_selected(index)
22
+ @selected = index
23
+ self
24
+ end
25
+
26
+ def on_change(&block)
27
+ @change_handler = block
28
+ self
29
+ end
30
+
31
+ def get_selected
32
+ @selected
33
+ end
34
+
35
+ def measure(painter)
36
+ max_w = 0.0
37
+ i = 0
38
+ while i < @option_count
39
+ tw = painter.measure_text_width(@options[i], Kumiki.theme.font_family, @font_size_val)
40
+ w = tw + 32.0
41
+ if w > max_w
42
+ max_w = w
43
+ end
44
+ i = i + 1
45
+ end
46
+ Size.new(max_w, @item_height * @option_count)
47
+ end
48
+
49
+ def redraw(painter, completely)
50
+ circle_r = 7.0
51
+ i = 0
52
+ while i < @option_count
53
+ iy = @item_height * i
54
+ cx = circle_r + 4.0
55
+ cy = iy + @item_height / 2.0
56
+
57
+ # Outer ring
58
+ painter.fill_circle(cx, cy, circle_r, @ring_color)
59
+ painter.fill_circle(cx, cy, circle_r - 1.5, 0xFF1A1B26)
60
+
61
+ # Selected dot
62
+ if @selected == i
63
+ painter.fill_circle(cx, cy, 4.0, @selected_color)
64
+ end
65
+
66
+ # Label
67
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, @font_size_val)
68
+ text_x = cx + circle_r + 8.0
69
+ th = painter.measure_text_height(Kumiki.theme.font_family, @font_size_val)
70
+ text_y = iy + (@item_height - th) / 2.0 + ascent
71
+ painter.draw_text(@options[i], text_x, text_y, Kumiki.theme.font_family, @font_size_val, @text_color)
72
+
73
+ i = i + 1
74
+ end
75
+ end
76
+
77
+ def mouse_down(ev)
78
+ index = (ev.pos.y / @item_height).to_i
79
+ if index >= 0 && index < @option_count && index != @selected
80
+ @selected = index
81
+ @change_handler.call(@selected) if @change_handler
82
+ mark_dirty
83
+ update
84
+ end
85
+ end
86
+ end
87
+
88
+ # Top-level helper
89
+ def RadioButtons(options)
90
+ RadioButtons.new(options)
91
+ end
92
+
93
+ end
@@ -0,0 +1,135 @@
1
+ module Kumiki
2
+ # Slider widget - draggable range input
3
+
4
+ class Slider < Widget
5
+ def initialize(min_val, max_val)
6
+ super()
7
+ @min_val = min_val
8
+ @max_val = max_val
9
+ @value = min_val
10
+ @dragging = false
11
+ @change_handler = nil
12
+ @width_policy = EXPANDING
13
+ @height_policy = FIXED
14
+ @height = 30.0
15
+ # Colors (Tokyo Night)
16
+ @track_color = 0xFF414868
17
+ @fill_color = 0xFF7AA2F7
18
+ @thumb_color = 0xFFC0CAF5
19
+ @thumb_hover = 0xFFFFFFFF
20
+ @hovered = false
21
+ end
22
+
23
+ def with_value(v)
24
+ if v < @min_val
25
+ @value = @min_val
26
+ elsif v > @max_val
27
+ @value = @max_val
28
+ else
29
+ @value = v
30
+ end
31
+ self
32
+ end
33
+
34
+ def on_change(&block)
35
+ @change_handler = block
36
+ self
37
+ end
38
+
39
+ def get_value
40
+ @value
41
+ end
42
+
43
+ def measure(painter)
44
+ Size.new(200.0, 30.0)
45
+ end
46
+
47
+ def redraw(painter, completely)
48
+ thumb_r = 8.0
49
+ track_h = 4.0
50
+ track_y = (@height - track_h) / 2.0
51
+ track_x = thumb_r
52
+ track_w = @width - thumb_r * 2.0
53
+ if track_w < 0.0
54
+ track_w = 0.0
55
+ end
56
+
57
+ # Track background
58
+ painter.fill_round_rect(track_x, track_y, track_w, track_h, 2.0, @track_color)
59
+
60
+ # Fill (progress)
61
+ ratio = 0.0
62
+ range = @max_val - @min_val
63
+ if range > 0.0
64
+ ratio = (@value - @min_val) / range
65
+ end
66
+ fill_w = track_w * ratio
67
+ if fill_w > 0.0
68
+ painter.fill_round_rect(track_x, track_y, fill_w, track_h, 2.0, @fill_color)
69
+ end
70
+
71
+ # Thumb circle
72
+ thumb_x = track_x + fill_w
73
+ thumb_y = @height / 2.0
74
+ tc = @hovered ? @thumb_hover : @thumb_color
75
+ painter.fill_circle(thumb_x, thumb_y, thumb_r, tc)
76
+ end
77
+
78
+ def mouse_down(ev)
79
+ @dragging = true
80
+ update_from_pos(ev.pos.x)
81
+ end
82
+
83
+ def mouse_drag(ev)
84
+ if @dragging
85
+ update_from_pos(ev.pos.x)
86
+ end
87
+ end
88
+
89
+ def mouse_up(ev)
90
+ @dragging = false
91
+ end
92
+
93
+ def mouse_over
94
+ @hovered = true
95
+ mark_dirty
96
+ update
97
+ end
98
+
99
+ def mouse_out
100
+ @hovered = false
101
+ mark_dirty
102
+ update
103
+ end
104
+
105
+ def update_from_pos(x)
106
+ thumb_r = 8.0
107
+ track_x = thumb_r
108
+ track_w = @width - thumb_r * 2.0
109
+ if track_w <= 0.0
110
+ return
111
+ end
112
+ ratio = (x - track_x) / track_w
113
+ if ratio < 0.0
114
+ ratio = 0.0
115
+ end
116
+ if ratio > 1.0
117
+ ratio = 1.0
118
+ end
119
+ range = @max_val - @min_val
120
+ new_val = @min_val + ratio * range
121
+ if new_val != @value
122
+ @value = new_val
123
+ @change_handler.call(@value) if @change_handler
124
+ mark_dirty
125
+ update
126
+ end
127
+ end
128
+ end
129
+
130
+ # Top-level helper
131
+ def Slider(min_val, max_val)
132
+ Slider.new(min_val, max_val)
133
+ end
134
+
135
+ end
@@ -0,0 +1,84 @@
1
+ module Kumiki
2
+ # Switch widget - ON/OFF toggle
3
+
4
+ class Switch < Widget
5
+ def initialize
6
+ super()
7
+ @on = false
8
+ @change_handler = nil
9
+ # Colors (Tokyo Night)
10
+ @on_color = 0xFF7AA2F7
11
+ @off_color = 0xFF414868
12
+ @knob_color = 0xFFFFFFFF
13
+ @hovered = false
14
+ end
15
+
16
+ def with_on(v)
17
+ @on = v
18
+ self
19
+ end
20
+
21
+ def on_change(&block)
22
+ @change_handler = block
23
+ self
24
+ end
25
+
26
+ def is_on
27
+ @on
28
+ end
29
+
30
+ def measure(painter)
31
+ Size.new(44.0, 24.0)
32
+ end
33
+
34
+ def redraw(painter, completely)
35
+ w = 44.0
36
+ h = 24.0
37
+ r = h / 2.0
38
+
39
+ # Track
40
+ track_color = @on ? @on_color : @off_color
41
+ painter.fill_round_rect(0.0, 0.0, w, h, r, track_color)
42
+
43
+ # Knob
44
+ knob_r = 9.0
45
+ knob_y = h / 2.0
46
+ knob_x = 0.0
47
+ if @on
48
+ knob_x = w - r
49
+ else
50
+ knob_x = r
51
+ end
52
+ painter.fill_circle(knob_x, knob_y, knob_r, @knob_color)
53
+ end
54
+
55
+ def mouse_up(ev)
56
+ if @on
57
+ @on = false
58
+ else
59
+ @on = true
60
+ end
61
+ @change_handler.call(@on) if @change_handler
62
+ mark_dirty
63
+ update
64
+ end
65
+
66
+ def mouse_over
67
+ @hovered = true
68
+ mark_dirty
69
+ update
70
+ end
71
+
72
+ def mouse_out
73
+ @hovered = false
74
+ mark_dirty
75
+ update
76
+ end
77
+ end
78
+
79
+ # Top-level helper
80
+ def Switch()
81
+ Switch.new
82
+ end
83
+
84
+ end