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.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +256 -0
- data/lib/kumiki/animation/animated_state.rb +83 -0
- data/lib/kumiki/animation/easing.rb +62 -0
- data/lib/kumiki/animation/value_tween.rb +69 -0
- data/lib/kumiki/app.rb +381 -0
- data/lib/kumiki/box.rb +40 -0
- data/lib/kumiki/chart/area_chart.rb +308 -0
- data/lib/kumiki/chart/bar_chart.rb +291 -0
- data/lib/kumiki/chart/base_chart.rb +213 -0
- data/lib/kumiki/chart/chart_helpers.rb +74 -0
- data/lib/kumiki/chart/gauge_chart.rb +174 -0
- data/lib/kumiki/chart/heatmap_chart.rb +223 -0
- data/lib/kumiki/chart/line_chart.rb +292 -0
- data/lib/kumiki/chart/pie_chart.rb +222 -0
- data/lib/kumiki/chart/scales.rb +79 -0
- data/lib/kumiki/chart/scatter_chart.rb +306 -0
- data/lib/kumiki/chart/stacked_bar_chart.rb +279 -0
- data/lib/kumiki/column.rb +351 -0
- data/lib/kumiki/core.rb +2511 -0
- data/lib/kumiki/dsl.rb +408 -0
- data/lib/kumiki/frame_ranma.rb +570 -0
- data/lib/kumiki/markdown/ast.rb +127 -0
- data/lib/kumiki/markdown/mermaid/layout.rb +389 -0
- data/lib/kumiki/markdown/mermaid/models.rb +235 -0
- data/lib/kumiki/markdown/mermaid/parser.rb +522 -0
- data/lib/kumiki/markdown/mermaid/renderer.rb +339 -0
- data/lib/kumiki/markdown/parser.rb +808 -0
- data/lib/kumiki/markdown/renderer.rb +642 -0
- data/lib/kumiki/markdown/theme.rb +168 -0
- data/lib/kumiki/render_node.rb +262 -0
- data/lib/kumiki/row.rb +288 -0
- data/lib/kumiki/spacer.rb +20 -0
- data/lib/kumiki/style.rb +799 -0
- data/lib/kumiki/theme.rb +567 -0
- data/lib/kumiki/themes/material.rb +40 -0
- data/lib/kumiki/themes/tokyo_night.rb +11 -0
- data/lib/kumiki/version.rb +5 -0
- data/lib/kumiki/widgets/button.rb +105 -0
- data/lib/kumiki/widgets/calendar.rb +1028 -0
- data/lib/kumiki/widgets/checkbox.rb +119 -0
- data/lib/kumiki/widgets/container.rb +111 -0
- data/lib/kumiki/widgets/data_table.rb +670 -0
- data/lib/kumiki/widgets/divider.rb +31 -0
- data/lib/kumiki/widgets/image.rb +105 -0
- data/lib/kumiki/widgets/input.rb +485 -0
- data/lib/kumiki/widgets/markdown.rb +58 -0
- data/lib/kumiki/widgets/modal.rb +165 -0
- data/lib/kumiki/widgets/multiline_input.rb +970 -0
- data/lib/kumiki/widgets/multiline_text.rb +180 -0
- data/lib/kumiki/widgets/net_image.rb +100 -0
- data/lib/kumiki/widgets/progress_bar.rb +72 -0
- data/lib/kumiki/widgets/radio_buttons.rb +93 -0
- data/lib/kumiki/widgets/slider.rb +135 -0
- data/lib/kumiki/widgets/switch.rb +84 -0
- data/lib/kumiki/widgets/tabs.rb +175 -0
- data/lib/kumiki/widgets/text.rb +120 -0
- data/lib/kumiki/widgets/tree.rb +434 -0
- data/lib/kumiki/widgets/webview.rb +87 -0
- data/lib/kumiki.rb +130 -0
- metadata +113 -0
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# Markdown theme - color and size tokens for markdown rendering
|
|
3
|
+
# Integrates with the global Kumiki.theme
|
|
4
|
+
|
|
5
|
+
class MarkdownTheme
|
|
6
|
+
def initialize
|
|
7
|
+
# Text colors - derive from global theme
|
|
8
|
+
@text_color = Kumiki.theme.text_primary
|
|
9
|
+
@heading_color = Kumiki.theme.accent
|
|
10
|
+
@link_color = Kumiki.theme.accent
|
|
11
|
+
@code_color = Kumiki.theme.warning
|
|
12
|
+
@emphasis_color = 0xFF9AA5CE # Slightly muted blue for italic
|
|
13
|
+
@strikethrough_color = Kumiki.theme.text_secondary
|
|
14
|
+
|
|
15
|
+
# Background colors
|
|
16
|
+
@code_bg_color = Kumiki.theme.bg_secondary
|
|
17
|
+
@code_inline_bg = Kumiki.theme.bg_secondary
|
|
18
|
+
@blockquote_bg = Kumiki.theme.accent
|
|
19
|
+
|
|
20
|
+
# Heading sizes
|
|
21
|
+
@h1_size = 28.0
|
|
22
|
+
@h2_size = 24.0
|
|
23
|
+
@h3_size = 20.0
|
|
24
|
+
@h4_size = 18.0
|
|
25
|
+
@h5_size = 16.0
|
|
26
|
+
@h6_size = 14.0
|
|
27
|
+
@base_font_size = 14.0
|
|
28
|
+
|
|
29
|
+
# Table colors
|
|
30
|
+
@table_header_bg = Kumiki.theme.bg_secondary
|
|
31
|
+
@table_border_color = Kumiki.theme.text_secondary
|
|
32
|
+
@checkbox_checked_color = Kumiki.theme.success
|
|
33
|
+
@checkbox_unchecked_color = Kumiki.theme.text_secondary
|
|
34
|
+
|
|
35
|
+
# Mermaid colors
|
|
36
|
+
@mermaid_node_fill = 0xFF4A90D9
|
|
37
|
+
@mermaid_node_stroke = 0xFF2C5F8A
|
|
38
|
+
@mermaid_node_text = 0xFFFFFFFF
|
|
39
|
+
@mermaid_edge_color = Kumiki.theme.text_secondary
|
|
40
|
+
@mermaid_subgraph_bg = 0x20FFFFFF
|
|
41
|
+
@mermaid_subgraph_border = Kumiki.theme.text_secondary
|
|
42
|
+
@mermaid_font_size = 12.0
|
|
43
|
+
|
|
44
|
+
# Spacing
|
|
45
|
+
@paragraph_spacing = 8.0
|
|
46
|
+
@block_spacing = 12.0
|
|
47
|
+
@list_indent = 24.0
|
|
48
|
+
@blockquote_indent = 20.0
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def text_color
|
|
52
|
+
@text_color
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def heading_color
|
|
56
|
+
@heading_color
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def link_color
|
|
60
|
+
@link_color
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
def code_color
|
|
64
|
+
@code_color
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def emphasis_color
|
|
68
|
+
@emphasis_color
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def strikethrough_color
|
|
72
|
+
@strikethrough_color
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def code_bg_color
|
|
76
|
+
@code_bg_color
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
def code_inline_bg
|
|
80
|
+
@code_inline_bg
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def blockquote_bg
|
|
84
|
+
@blockquote_bg
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def base_font_size
|
|
88
|
+
@base_font_size
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def paragraph_spacing
|
|
92
|
+
@paragraph_spacing
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def block_spacing
|
|
96
|
+
@block_spacing
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
def list_indent
|
|
100
|
+
@list_indent
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
def blockquote_indent
|
|
104
|
+
@blockquote_indent
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
def table_header_bg
|
|
108
|
+
@table_header_bg
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def table_border_color
|
|
112
|
+
@table_border_color
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def checkbox_checked_color
|
|
116
|
+
@checkbox_checked_color
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def checkbox_unchecked_color
|
|
120
|
+
@checkbox_unchecked_color
|
|
121
|
+
end
|
|
122
|
+
|
|
123
|
+
def mermaid_node_fill
|
|
124
|
+
@mermaid_node_fill
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def mermaid_node_stroke
|
|
128
|
+
@mermaid_node_stroke
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def mermaid_node_text
|
|
132
|
+
@mermaid_node_text
|
|
133
|
+
end
|
|
134
|
+
|
|
135
|
+
def mermaid_edge_color
|
|
136
|
+
@mermaid_edge_color
|
|
137
|
+
end
|
|
138
|
+
|
|
139
|
+
def mermaid_subgraph_bg
|
|
140
|
+
@mermaid_subgraph_bg
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def mermaid_subgraph_border
|
|
144
|
+
@mermaid_subgraph_border
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def mermaid_font_size
|
|
148
|
+
@mermaid_font_size
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def heading_size(level)
|
|
152
|
+
if level == 1
|
|
153
|
+
@h1_size
|
|
154
|
+
elsif level == 2
|
|
155
|
+
@h2_size
|
|
156
|
+
elsif level == 3
|
|
157
|
+
@h3_size
|
|
158
|
+
elsif level == 4
|
|
159
|
+
@h4_size
|
|
160
|
+
elsif level == 5
|
|
161
|
+
@h5_size
|
|
162
|
+
else
|
|
163
|
+
@h6_size
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
|
|
168
|
+
end
|
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# RenderNode system - layout/paint dirty tracking and z-order caching
|
|
5
|
+
|
|
6
|
+
# Base render node with fine-grained dirty tracking
|
|
7
|
+
class RenderNodeBase
|
|
8
|
+
#: (untyped widget) -> void
|
|
9
|
+
def initialize(widget)
|
|
10
|
+
@widget = widget
|
|
11
|
+
@layout_dirty = true
|
|
12
|
+
@paint_dirty = true
|
|
13
|
+
@subtree_dirty = false
|
|
14
|
+
@measured_size = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: () -> untyped
|
|
18
|
+
def get_widget
|
|
19
|
+
@widget
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
# ===== Dirty Tracking =====
|
|
23
|
+
|
|
24
|
+
#: () -> bool
|
|
25
|
+
def is_layout_dirty
|
|
26
|
+
@layout_dirty
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
#: () -> bool
|
|
30
|
+
def is_paint_dirty
|
|
31
|
+
@paint_dirty
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
#: () -> void
|
|
35
|
+
def mark_layout_dirty
|
|
36
|
+
@layout_dirty = true
|
|
37
|
+
@paint_dirty = true
|
|
38
|
+
@measured_size = nil
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
#: () -> void
|
|
42
|
+
def mark_paint_dirty
|
|
43
|
+
@paint_dirty = true
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
#: () -> bool
|
|
47
|
+
def is_subtree_dirty
|
|
48
|
+
@subtree_dirty
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
#: () -> void
|
|
52
|
+
def mark_subtree_dirty
|
|
53
|
+
@subtree_dirty = true
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
#: () -> void
|
|
57
|
+
def clear_dirty
|
|
58
|
+
@layout_dirty = false
|
|
59
|
+
@paint_dirty = false
|
|
60
|
+
@subtree_dirty = false
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
# ===== Layout =====
|
|
64
|
+
|
|
65
|
+
#: (untyped painter) -> Size
|
|
66
|
+
def cached_measure(painter)
|
|
67
|
+
if @measured_size == nil || @layout_dirty
|
|
68
|
+
@measured_size = @widget.measure(painter)
|
|
69
|
+
end
|
|
70
|
+
@measured_size
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
# ===== Hit Testing =====
|
|
74
|
+
|
|
75
|
+
#: (Point point) -> bool
|
|
76
|
+
def hit_test(point)
|
|
77
|
+
@widget.contain(point)
|
|
78
|
+
end
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
# Layout render node with z-order caching and child management
|
|
82
|
+
class LayoutRenderNode < RenderNodeBase
|
|
83
|
+
#: (untyped widget) -> void
|
|
84
|
+
def initialize(widget)
|
|
85
|
+
super(widget)
|
|
86
|
+
@children = []
|
|
87
|
+
@sorted_children = nil
|
|
88
|
+
@z_order_dirty = true
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# ===== Child Management =====
|
|
92
|
+
|
|
93
|
+
#: (untyped child) -> void
|
|
94
|
+
def add_child(child)
|
|
95
|
+
@children << child
|
|
96
|
+
@z_order_dirty = true
|
|
97
|
+
mark_layout_dirty
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
#: (untyped child) -> void
|
|
101
|
+
def remove_child(child)
|
|
102
|
+
i = 0
|
|
103
|
+
while i < @children.length
|
|
104
|
+
if @children[i] == child
|
|
105
|
+
@children.delete_at(i)
|
|
106
|
+
@z_order_dirty = true
|
|
107
|
+
mark_layout_dirty
|
|
108
|
+
return
|
|
109
|
+
end
|
|
110
|
+
i = i + 1
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
#: () -> void
|
|
115
|
+
def clear_children
|
|
116
|
+
@children = []
|
|
117
|
+
@sorted_children = nil
|
|
118
|
+
@z_order_dirty = true
|
|
119
|
+
mark_layout_dirty
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
#: () -> Integer
|
|
123
|
+
def child_count
|
|
124
|
+
@children.length
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
#: () -> Array
|
|
128
|
+
def get_children
|
|
129
|
+
@children
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
# ===== Z-Order Management =====
|
|
133
|
+
|
|
134
|
+
#: () -> void
|
|
135
|
+
def invalidate_z_order
|
|
136
|
+
@z_order_dirty = true
|
|
137
|
+
@sorted_children = nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
#: () -> Array
|
|
141
|
+
def get_sorted_children
|
|
142
|
+
if @z_order_dirty || @sorted_children == nil
|
|
143
|
+
# Copy children list
|
|
144
|
+
@sorted_children = []
|
|
145
|
+
i = 0
|
|
146
|
+
while i < @children.length
|
|
147
|
+
@sorted_children << @children[i]
|
|
148
|
+
i = i + 1
|
|
149
|
+
end
|
|
150
|
+
# Sort by z_index (bubble sort - children count is typically small)
|
|
151
|
+
changed = true
|
|
152
|
+
while changed
|
|
153
|
+
changed = false
|
|
154
|
+
j = 0
|
|
155
|
+
while j < @sorted_children.length - 1
|
|
156
|
+
if @sorted_children[j].get_z_index > @sorted_children[j + 1].get_z_index
|
|
157
|
+
tmp = @sorted_children[j]
|
|
158
|
+
@sorted_children[j] = @sorted_children[j + 1]
|
|
159
|
+
@sorted_children[j + 1] = tmp
|
|
160
|
+
changed = true
|
|
161
|
+
end
|
|
162
|
+
j = j + 1
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
@z_order_dirty = false
|
|
166
|
+
end
|
|
167
|
+
@sorted_children
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# Paint order: lower z-index first (background to foreground)
|
|
171
|
+
#: () -> Array
|
|
172
|
+
def iter_paint_order
|
|
173
|
+
get_sorted_children
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
# Hit test order: higher z-index first (foreground to background)
|
|
177
|
+
#: () -> Array
|
|
178
|
+
def iter_hit_test_order
|
|
179
|
+
sorted = get_sorted_children
|
|
180
|
+
result = []
|
|
181
|
+
i = sorted.length - 1
|
|
182
|
+
while i >= 0
|
|
183
|
+
result << sorted[i]
|
|
184
|
+
i = i - 1
|
|
185
|
+
end
|
|
186
|
+
result
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
# ===== Dirty Propagation =====
|
|
190
|
+
|
|
191
|
+
#: () -> bool
|
|
192
|
+
def is_any_child_dirty
|
|
193
|
+
i = 0
|
|
194
|
+
while i < @children.length
|
|
195
|
+
if @children[i].is_dirty
|
|
196
|
+
return true
|
|
197
|
+
end
|
|
198
|
+
i = i + 1
|
|
199
|
+
end
|
|
200
|
+
false
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
# Scrollable layout render node with viewport culling
|
|
205
|
+
class ScrollableLayoutRenderNode < LayoutRenderNode
|
|
206
|
+
#: (untyped widget) -> void
|
|
207
|
+
def initialize(widget)
|
|
208
|
+
super(widget)
|
|
209
|
+
@scroll_x = 0.0
|
|
210
|
+
@scroll_y = 0.0
|
|
211
|
+
@viewport_width = 0.0
|
|
212
|
+
@viewport_height = 0.0
|
|
213
|
+
@viewport_set = false
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
#: () -> Float
|
|
217
|
+
def get_scroll_x
|
|
218
|
+
@scroll_x
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
#: (Float v) -> void
|
|
222
|
+
def set_scroll_x(v)
|
|
223
|
+
if @scroll_x != v
|
|
224
|
+
@scroll_x = v
|
|
225
|
+
mark_paint_dirty
|
|
226
|
+
end
|
|
227
|
+
end
|
|
228
|
+
|
|
229
|
+
#: () -> Float
|
|
230
|
+
def get_scroll_y
|
|
231
|
+
@scroll_y
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
#: (Float v) -> void
|
|
235
|
+
def set_scroll_y(v)
|
|
236
|
+
if @scroll_y != v
|
|
237
|
+
@scroll_y = v
|
|
238
|
+
mark_paint_dirty
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
#: (Float w, Float h) -> void
|
|
243
|
+
def set_viewport_size(w, h)
|
|
244
|
+
@viewport_width = w
|
|
245
|
+
@viewport_height = h
|
|
246
|
+
@viewport_set = true
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
#: (untyped child) -> bool
|
|
250
|
+
def is_child_visible(child)
|
|
251
|
+
if !@viewport_set
|
|
252
|
+
return true
|
|
253
|
+
end
|
|
254
|
+
cx = child.get_x - @scroll_x
|
|
255
|
+
cy = child.get_y - @scroll_y
|
|
256
|
+
cw = child.get_width
|
|
257
|
+
ch = child.get_height
|
|
258
|
+
!(cx + cw < 0.0 || cx > @viewport_width || cy + ch < 0.0 || cy > @viewport_height)
|
|
259
|
+
end
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
end
|
data/lib/kumiki/row.rb
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Row layout - horizontal arrangement of children
|
|
5
|
+
|
|
6
|
+
class Row < Layout
|
|
7
|
+
def initialize
|
|
8
|
+
super
|
|
9
|
+
@spacing = 0.0
|
|
10
|
+
@is_scrollable = false
|
|
11
|
+
@scroll_offset = 0.0
|
|
12
|
+
@content_width = 0.0
|
|
13
|
+
@pin_right = false
|
|
14
|
+
@external_scroll_state = nil
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
#: (Float s) -> Row
|
|
18
|
+
def spacing(s)
|
|
19
|
+
@spacing = s
|
|
20
|
+
self
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
#: (ScrollState ss) -> Row
|
|
24
|
+
def scroll_state(ss)
|
|
25
|
+
@external_scroll_state = ss
|
|
26
|
+
@scroll_offset = ss.x
|
|
27
|
+
self
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
#: () -> Row
|
|
31
|
+
def scrollable
|
|
32
|
+
@is_scrollable = true
|
|
33
|
+
# Retroactively downgrade existing EXPANDING children to CONTENT
|
|
34
|
+
i = 0
|
|
35
|
+
while i < @children.length
|
|
36
|
+
if @children[i].get_width_policy == EXPANDING
|
|
37
|
+
@children[i].set_width_policy(CONTENT)
|
|
38
|
+
end
|
|
39
|
+
i = i + 1
|
|
40
|
+
end
|
|
41
|
+
self
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
#: () -> Row
|
|
45
|
+
def pin_to_end
|
|
46
|
+
@pin_right = true
|
|
47
|
+
self
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
#: () -> bool
|
|
51
|
+
def is_scrollable
|
|
52
|
+
@is_scrollable
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
#: (bool is_direction_x) -> bool
|
|
56
|
+
def has_scrollbar(is_direction_x)
|
|
57
|
+
if is_direction_x
|
|
58
|
+
@is_scrollable
|
|
59
|
+
else
|
|
60
|
+
false
|
|
61
|
+
end
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
#: () -> Float
|
|
65
|
+
def get_scroll_offset
|
|
66
|
+
@scroll_offset
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
#: (Float v) -> void
|
|
70
|
+
def set_scroll_offset(v)
|
|
71
|
+
@scroll_offset = v
|
|
72
|
+
@external_scroll_state&.set_x(v)
|
|
73
|
+
mark_dirty
|
|
74
|
+
update
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
# Override add: auto-downgrade EXPANDING width to CONTENT in scrollable Row
|
|
78
|
+
#: (untyped w) -> Row
|
|
79
|
+
def add(w)
|
|
80
|
+
if w == nil
|
|
81
|
+
return self
|
|
82
|
+
end
|
|
83
|
+
if @is_scrollable && w.get_width_policy == EXPANDING
|
|
84
|
+
w.set_width_policy(CONTENT)
|
|
85
|
+
end
|
|
86
|
+
super(w)
|
|
87
|
+
self
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
#: (untyped painter) -> Size
|
|
91
|
+
def measure(painter)
|
|
92
|
+
total_w = 0.0
|
|
93
|
+
max_h = 0.0
|
|
94
|
+
i = 0
|
|
95
|
+
while i < @children.length
|
|
96
|
+
c = @children[i]
|
|
97
|
+
cs = c.measure(painter)
|
|
98
|
+
if c.get_width_policy == FIXED
|
|
99
|
+
child_w = c.get_width
|
|
100
|
+
else
|
|
101
|
+
child_w = cs.width
|
|
102
|
+
end
|
|
103
|
+
total_w = total_w + child_w
|
|
104
|
+
total_w = total_w + @spacing if i > 0
|
|
105
|
+
if c.get_height_policy == FIXED
|
|
106
|
+
child_h = c.get_height
|
|
107
|
+
else
|
|
108
|
+
child_h = cs.height
|
|
109
|
+
end
|
|
110
|
+
max_h = child_h if child_h > max_h
|
|
111
|
+
i = i + 1
|
|
112
|
+
end
|
|
113
|
+
Size.new(total_w + @pad_left + @pad_right, max_h + @pad_top + @pad_bottom)
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
# Unified layout: two-pass flex distribution + scroll offset.
|
|
117
|
+
# With approach C (auto-downgrade), scrollable containers have no EXPANDING
|
|
118
|
+
# width children, so flex distribution is a no-op and content stacks sequentially.
|
|
119
|
+
#: (untyped painter) -> void
|
|
120
|
+
def relocate_children(painter)
|
|
121
|
+
# Account for padding
|
|
122
|
+
inner_w = @width - @pad_left - @pad_right
|
|
123
|
+
inner_h = @height - @pad_top - @pad_bottom
|
|
124
|
+
if inner_w < 0.0
|
|
125
|
+
inner_w = 0.0
|
|
126
|
+
end
|
|
127
|
+
if inner_h < 0.0
|
|
128
|
+
inner_h = 0.0
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
remaining = inner_w
|
|
132
|
+
expanding_total_flex = 0
|
|
133
|
+
|
|
134
|
+
# First pass: measure CONTENT/FIXED children, collect EXPANDING flex totals
|
|
135
|
+
i = 0
|
|
136
|
+
while i < @children.length
|
|
137
|
+
c = @children[i]
|
|
138
|
+
if c.get_width_policy != EXPANDING
|
|
139
|
+
# Set height before measure so height-dependent layouts work
|
|
140
|
+
if c.get_height_policy != FIXED
|
|
141
|
+
c.resize_wh(c.get_width, inner_h)
|
|
142
|
+
end
|
|
143
|
+
cs = c.measure(painter)
|
|
144
|
+
# Use explicit width for FIXED, measured width for CONTENT
|
|
145
|
+
if c.get_width_policy == FIXED
|
|
146
|
+
child_w = c.get_width
|
|
147
|
+
else
|
|
148
|
+
child_w = cs.width
|
|
149
|
+
end
|
|
150
|
+
if c.get_height_policy == FIXED
|
|
151
|
+
c.resize_wh(child_w, c.get_height)
|
|
152
|
+
else
|
|
153
|
+
c.resize_wh(child_w, inner_h)
|
|
154
|
+
end
|
|
155
|
+
remaining = remaining - child_w
|
|
156
|
+
else
|
|
157
|
+
expanding_total_flex = expanding_total_flex + c.get_flex
|
|
158
|
+
end
|
|
159
|
+
remaining = remaining - @spacing if i > 0
|
|
160
|
+
i = i + 1
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
if remaining < 0.0
|
|
164
|
+
remaining = 0.0
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
# Second pass: distribute remaining space to EXPANDING, position all
|
|
168
|
+
cx = @x + @pad_left
|
|
169
|
+
if @is_scrollable
|
|
170
|
+
cx = cx - @scroll_offset
|
|
171
|
+
end
|
|
172
|
+
total_content_w = 0.0
|
|
173
|
+
i = 0
|
|
174
|
+
while i < @children.length
|
|
175
|
+
c = @children[i]
|
|
176
|
+
if c.get_width_policy == EXPANDING
|
|
177
|
+
w = 0.0
|
|
178
|
+
if expanding_total_flex > 0 && remaining > 0.0
|
|
179
|
+
w = remaining * c.get_flex / expanding_total_flex
|
|
180
|
+
end
|
|
181
|
+
c.resize_wh(w, inner_h)
|
|
182
|
+
else
|
|
183
|
+
# In a Row, non-FIXED height children fill the row height
|
|
184
|
+
if c.get_height_policy != FIXED
|
|
185
|
+
c.resize_wh(c.get_width, inner_h)
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
c.move_xy(cx, @y + @pad_top)
|
|
189
|
+
cx = cx + c.get_width + @spacing
|
|
190
|
+
total_content_w = total_content_w + c.get_width
|
|
191
|
+
total_content_w = total_content_w + @spacing if i > 0
|
|
192
|
+
i = i + 1
|
|
193
|
+
end
|
|
194
|
+
@content_width = total_content_w
|
|
195
|
+
|
|
196
|
+
# Auto-scroll to end when pinned
|
|
197
|
+
if @pin_right && @is_scrollable
|
|
198
|
+
max_scroll = @content_width - inner_w
|
|
199
|
+
if max_scroll > 0.0
|
|
200
|
+
@scroll_offset = max_scroll
|
|
201
|
+
@external_scroll_state&.set_x(@scroll_offset)
|
|
202
|
+
end
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
#: (untyped painter, bool completely) -> void
|
|
207
|
+
def redraw(painter, completely)
|
|
208
|
+
saved_bg = Kumiki._bg_clear_color
|
|
209
|
+
if @custom_bg && is_dirty
|
|
210
|
+
parent_bg = saved_bg
|
|
211
|
+
if parent_bg == nil || parent_bg == 0
|
|
212
|
+
parent_bg = Kumiki.theme.bg_canvas
|
|
213
|
+
end
|
|
214
|
+
painter.fill_rect(0.0, 0.0, @width, @height, parent_bg)
|
|
215
|
+
set_dirty(false)
|
|
216
|
+
completely = true
|
|
217
|
+
end
|
|
218
|
+
draw_visual_background(painter)
|
|
219
|
+
relocate_children(painter)
|
|
220
|
+
redraw_children(painter, completely)
|
|
221
|
+
draw_scrollbar(painter) if @is_scrollable
|
|
222
|
+
Kumiki._bg_clear_color = saved_bg
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
#: (untyped painter) -> void
|
|
226
|
+
def draw_scrollbar(painter)
|
|
227
|
+
viewport_w = @width
|
|
228
|
+
content_w = @content_width
|
|
229
|
+
return if content_w <= viewport_w
|
|
230
|
+
|
|
231
|
+
bar_height = 8.0
|
|
232
|
+
thumb_color = 0xC0AAAAAA
|
|
233
|
+
|
|
234
|
+
# Thumb
|
|
235
|
+
thumb_w = viewport_w * viewport_w / content_w
|
|
236
|
+
if thumb_w < 20.0
|
|
237
|
+
thumb_w = 20.0
|
|
238
|
+
end
|
|
239
|
+
thumb_x = (@scroll_offset / content_w) * viewport_w
|
|
240
|
+
if thumb_x + thumb_w > viewport_w
|
|
241
|
+
thumb_x = viewport_w - thumb_w
|
|
242
|
+
end
|
|
243
|
+
painter.fill_round_rect(thumb_x, @height - bar_height + 2.0, thumb_w, bar_height - 4.0, 2.0, thumb_color)
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
#: (WheelEvent ev) -> void
|
|
247
|
+
def mouse_wheel(ev)
|
|
248
|
+
if @is_scrollable
|
|
249
|
+
scroll_speed = 30.0
|
|
250
|
+
@scroll_offset = @scroll_offset - ev.delta_y * scroll_speed
|
|
251
|
+
# Clamp scroll offset
|
|
252
|
+
max_scroll = @content_width - @width
|
|
253
|
+
if max_scroll < 0.0
|
|
254
|
+
max_scroll = 0.0
|
|
255
|
+
end
|
|
256
|
+
if @scroll_offset < 0.0
|
|
257
|
+
@scroll_offset = 0.0
|
|
258
|
+
end
|
|
259
|
+
if @scroll_offset > max_scroll
|
|
260
|
+
@scroll_offset = max_scroll
|
|
261
|
+
end
|
|
262
|
+
# Toggle pin_to_end: disable on scroll left, re-enable at end
|
|
263
|
+
if ev.delta_y > 0.0
|
|
264
|
+
@pin_right = false
|
|
265
|
+
end
|
|
266
|
+
if max_scroll > 0.0 && @scroll_offset >= max_scroll
|
|
267
|
+
@pin_right = true
|
|
268
|
+
end
|
|
269
|
+
@external_scroll_state&.set_x(@scroll_offset)
|
|
270
|
+
mark_dirty
|
|
271
|
+
update
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
end
|
|
275
|
+
|
|
276
|
+
# Top-level helper
|
|
277
|
+
#: (*untyped children) -> Row
|
|
278
|
+
def Row(*children)
|
|
279
|
+
row = Row.new
|
|
280
|
+
i = 0
|
|
281
|
+
while i < children.length
|
|
282
|
+
row.add(children[i])
|
|
283
|
+
i = i + 1
|
|
284
|
+
end
|
|
285
|
+
row
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
end
|