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,279 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# StackedBarChart - stacked bar chart widget
|
|
3
|
+
# Supports multiple series stacked on top of each other, hover highlight
|
|
4
|
+
|
|
5
|
+
class StackedBarChart < BaseChart
|
|
6
|
+
def initialize(categories, series_data, series_names)
|
|
7
|
+
super()
|
|
8
|
+
@categories = categories
|
|
9
|
+
@series_data = series_data
|
|
10
|
+
@series_names = series_names
|
|
11
|
+
@show_values = false
|
|
12
|
+
@bar_radius = 0.0
|
|
13
|
+
@data_min = 0.0
|
|
14
|
+
@data_max = 0.0
|
|
15
|
+
@y_scale = nil
|
|
16
|
+
@y_ticks = nil
|
|
17
|
+
@band = nil
|
|
18
|
+
@bar_w = 2.0
|
|
19
|
+
@baseline_y = 0.0
|
|
20
|
+
@num_series = 0
|
|
21
|
+
@num_cats = 0
|
|
22
|
+
@y_range_min = 0.0
|
|
23
|
+
@y_range_max = 1.0
|
|
24
|
+
@chart_px = 0.0
|
|
25
|
+
@chart_py = 0.0
|
|
26
|
+
@chart_pw = 0.0
|
|
27
|
+
@chart_ph = 0.0
|
|
28
|
+
compute_stacked_range
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def show_values(v)
|
|
32
|
+
@show_values = v
|
|
33
|
+
self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def bar_radius(r)
|
|
37
|
+
@bar_radius = r
|
|
38
|
+
self
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def set_data(categories, series_data, series_names)
|
|
42
|
+
@categories = categories
|
|
43
|
+
@series_data = series_data
|
|
44
|
+
@series_names = series_names
|
|
45
|
+
compute_stacked_range
|
|
46
|
+
mark_dirty
|
|
47
|
+
update
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def render_chart(painter, px, py, pw, ph)
|
|
51
|
+
return if @categories.length == 0
|
|
52
|
+
return if @series_data.length == 0
|
|
53
|
+
@chart_px = px
|
|
54
|
+
@chart_py = py
|
|
55
|
+
@chart_pw = pw
|
|
56
|
+
@chart_ph = ph
|
|
57
|
+
@num_series = @series_data.length
|
|
58
|
+
@num_cats = @categories.length
|
|
59
|
+
setup_stacked_ticks
|
|
60
|
+
setup_stacked_y_scale
|
|
61
|
+
setup_stacked_band
|
|
62
|
+
@baseline_y = @y_scale.map(0.0)
|
|
63
|
+
bmax = @chart_py + @chart_ph
|
|
64
|
+
if @baseline_y > bmax
|
|
65
|
+
@baseline_y = bmax
|
|
66
|
+
end
|
|
67
|
+
draw_y_axis(painter, px, py, pw, ph, @y_ticks, @y_scale)
|
|
68
|
+
draw_x_axis_line(painter, px, py, pw, ph)
|
|
69
|
+
draw_stacked_cat_labels(painter)
|
|
70
|
+
draw_all_stacked_bars(painter)
|
|
71
|
+
draw_stacked_legend(painter, px, py)
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
def setup_stacked_ticks
|
|
75
|
+
@y_ticks = compute_ticks(@data_min, @data_max, 5)
|
|
76
|
+
@y_range_min = @data_min
|
|
77
|
+
@y_range_max = @data_max
|
|
78
|
+
if @y_ticks.length > 0
|
|
79
|
+
@y_range_min = @y_ticks[0]
|
|
80
|
+
@y_range_max = @y_ticks[@y_ticks.length - 1]
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
def setup_stacked_y_scale
|
|
85
|
+
bottom = @chart_py + @chart_ph
|
|
86
|
+
@y_scale = LinearScale.new(@y_range_min, @y_range_max, bottom, @chart_py)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def setup_stacked_band
|
|
90
|
+
right = @chart_px + @chart_pw
|
|
91
|
+
nc = @num_cats * 1.0
|
|
92
|
+
@band = BandScale.new(nc, @chart_px, right, 12.0)
|
|
93
|
+
@bar_w = @band.band_width
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def draw_stacked_cat_labels(painter)
|
|
97
|
+
lc = Kumiki.theme.text_secondary
|
|
98
|
+
i = 0
|
|
99
|
+
while i < @num_cats
|
|
100
|
+
i_f = i * 1.0
|
|
101
|
+
bx = @band.map(i_f)
|
|
102
|
+
label = @categories[i]
|
|
103
|
+
lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
|
|
104
|
+
ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
|
|
105
|
+
label_x = bx + @bar_w / 2.0 - lw / 2.0
|
|
106
|
+
label_y = @chart_py + @chart_ph + 14.0 + ascent
|
|
107
|
+
painter.draw_text(label, label_x, label_y, Kumiki.theme.font_family, 11.0, lc)
|
|
108
|
+
i = i + 1
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
def draw_all_stacked_bars(painter)
|
|
113
|
+
ci = 0
|
|
114
|
+
while ci < @num_cats
|
|
115
|
+
draw_stacked_column(painter, ci)
|
|
116
|
+
ci = ci + 1
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def draw_stacked_column(painter, ci)
|
|
121
|
+
ci_f = ci * 1.0
|
|
122
|
+
bx = @band.map(ci_f)
|
|
123
|
+
cumulative = 0.0
|
|
124
|
+
si = 0
|
|
125
|
+
while si < @num_series
|
|
126
|
+
val = @series_data[si][ci]
|
|
127
|
+
top_val = cumulative + val
|
|
128
|
+
bar_bottom = @y_scale.map(cumulative)
|
|
129
|
+
bar_top = @y_scale.map(top_val)
|
|
130
|
+
bar_h = bar_bottom - bar_top
|
|
131
|
+
if bar_h < 0.0
|
|
132
|
+
bar_h = 0.0 - bar_h
|
|
133
|
+
bar_top = bar_bottom
|
|
134
|
+
end
|
|
135
|
+
c = series_color(si)
|
|
136
|
+
if @hover_index == ci
|
|
137
|
+
if @hover_series == si
|
|
138
|
+
c = painter.lighten_color(c, 0.3)
|
|
139
|
+
end
|
|
140
|
+
end
|
|
141
|
+
painter.fill_round_rect(bx, bar_top, @bar_w, bar_h, @bar_radius, c)
|
|
142
|
+
if @show_values
|
|
143
|
+
if val > 0.0
|
|
144
|
+
vl = painter.number_to_string(val)
|
|
145
|
+
vw = painter.measure_text_width(vl, Kumiki.theme.font_family, 9.0)
|
|
146
|
+
ascent = painter.get_text_ascent(Kumiki.theme.font_family, 9.0)
|
|
147
|
+
vx = bx + @bar_w / 2.0 - vw / 2.0
|
|
148
|
+
vy = bar_top + bar_h / 2.0 + ascent / 2.0
|
|
149
|
+
painter.draw_text(vl, vx, vy, Kumiki.theme.font_family, 9.0, 4294967295)
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
cumulative = top_val
|
|
153
|
+
si = si + 1
|
|
154
|
+
end
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def draw_stacked_legend(painter, px, py)
|
|
158
|
+
if @show_legend
|
|
159
|
+
if @series_names.length > 1
|
|
160
|
+
colors = []
|
|
161
|
+
si = 0
|
|
162
|
+
while si < @series_names.length
|
|
163
|
+
colors << series_color(si)
|
|
164
|
+
si = si + 1
|
|
165
|
+
end
|
|
166
|
+
draw_legend(painter, @series_names, colors, px + 8.0, py - 20.0)
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def update_hover
|
|
172
|
+
return if @band == nil
|
|
173
|
+
mx = @mouse_x
|
|
174
|
+
my = @mouse_y
|
|
175
|
+
px = plot_x
|
|
176
|
+
py = plot_y
|
|
177
|
+
pw = plot_w
|
|
178
|
+
ph = plot_h
|
|
179
|
+
if mx < px
|
|
180
|
+
@hover_index = -1
|
|
181
|
+
@hover_series = -1
|
|
182
|
+
return
|
|
183
|
+
end
|
|
184
|
+
if mx > px + pw
|
|
185
|
+
@hover_index = -1
|
|
186
|
+
@hover_series = -1
|
|
187
|
+
return
|
|
188
|
+
end
|
|
189
|
+
if my < py
|
|
190
|
+
@hover_index = -1
|
|
191
|
+
@hover_series = -1
|
|
192
|
+
return
|
|
193
|
+
end
|
|
194
|
+
if my > py + ph
|
|
195
|
+
@hover_index = -1
|
|
196
|
+
@hover_series = -1
|
|
197
|
+
return
|
|
198
|
+
end
|
|
199
|
+
find_stacked_hover(mx, my)
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
def find_stacked_hover(mx, my)
|
|
203
|
+
@hover_index = -1
|
|
204
|
+
@hover_series = -1
|
|
205
|
+
ci = 0
|
|
206
|
+
while ci < @num_cats
|
|
207
|
+
ci_f = ci * 1.0
|
|
208
|
+
bx = @band.map(ci_f)
|
|
209
|
+
if mx >= bx
|
|
210
|
+
if mx <= bx + @bar_w
|
|
211
|
+
# Found the category, now find which series segment
|
|
212
|
+
cumulative = 0.0
|
|
213
|
+
si = 0
|
|
214
|
+
while si < @num_series
|
|
215
|
+
val = @series_data[si][ci]
|
|
216
|
+
top_val = cumulative + val
|
|
217
|
+
bar_bottom = @y_scale.map(cumulative)
|
|
218
|
+
bar_top = @y_scale.map(top_val)
|
|
219
|
+
if bar_top > bar_bottom
|
|
220
|
+
tmp = bar_top
|
|
221
|
+
bar_top = bar_bottom
|
|
222
|
+
bar_bottom = tmp
|
|
223
|
+
end
|
|
224
|
+
if my >= bar_top
|
|
225
|
+
if my <= bar_bottom
|
|
226
|
+
@hover_index = ci
|
|
227
|
+
@hover_series = si
|
|
228
|
+
return
|
|
229
|
+
end
|
|
230
|
+
end
|
|
231
|
+
cumulative = top_val
|
|
232
|
+
si = si + 1
|
|
233
|
+
end
|
|
234
|
+
@hover_index = ci
|
|
235
|
+
return
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
ci = ci + 1
|
|
239
|
+
end
|
|
240
|
+
end
|
|
241
|
+
|
|
242
|
+
private
|
|
243
|
+
|
|
244
|
+
def compute_stacked_range
|
|
245
|
+
@data_min = 0.0
|
|
246
|
+
@data_max = 1.0
|
|
247
|
+
return if @series_data.length == 0
|
|
248
|
+
return if @categories.length == 0
|
|
249
|
+
max_stack = 0.0
|
|
250
|
+
ci = 0
|
|
251
|
+
while ci < @categories.length
|
|
252
|
+
stack_total = 0.0
|
|
253
|
+
si = 0
|
|
254
|
+
while si < @series_data.length
|
|
255
|
+
if ci < @series_data[si].length
|
|
256
|
+
stack_total = stack_total + @series_data[si][ci]
|
|
257
|
+
end
|
|
258
|
+
si = si + 1
|
|
259
|
+
end
|
|
260
|
+
if stack_total > max_stack
|
|
261
|
+
max_stack = stack_total
|
|
262
|
+
end
|
|
263
|
+
ci = ci + 1
|
|
264
|
+
end
|
|
265
|
+
@data_max = max_stack
|
|
266
|
+
range = @data_max - @data_min
|
|
267
|
+
if range > 0.0
|
|
268
|
+
@data_max = @data_max + range * 0.1
|
|
269
|
+
else
|
|
270
|
+
@data_max = @data_min + 1.0
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def StackedBarChart(categories, series_data, series_names)
|
|
276
|
+
StackedBarChart.new(categories, series_data, series_names)
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
end
|
|
@@ -0,0 +1,351 @@
|
|
|
1
|
+
module Kumiki
|
|
2
|
+
# rbs_inline: enabled
|
|
3
|
+
|
|
4
|
+
# Column layout - vertical arrangement of children
|
|
5
|
+
|
|
6
|
+
class Column < Layout
|
|
7
|
+
def initialize
|
|
8
|
+
super
|
|
9
|
+
@spacing = 0.0
|
|
10
|
+
@is_scrollable = false
|
|
11
|
+
@scroll_offset = 0.0
|
|
12
|
+
@content_height = 0.0
|
|
13
|
+
@pin_bottom = false
|
|
14
|
+
@sb_dragging = false
|
|
15
|
+
@sb_drag_start_y = 0.0
|
|
16
|
+
@sb_drag_start_offset = 0.0
|
|
17
|
+
@external_scroll_state = nil
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
#: (Float s) -> Column
|
|
21
|
+
def spacing(s)
|
|
22
|
+
@spacing = s
|
|
23
|
+
self
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
#: (ScrollState ss) -> Column
|
|
27
|
+
def scroll_state(ss)
|
|
28
|
+
@external_scroll_state = ss
|
|
29
|
+
@scroll_offset = ss.y
|
|
30
|
+
self
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
#: () -> Column
|
|
34
|
+
def scrollable
|
|
35
|
+
@is_scrollable = true
|
|
36
|
+
# Retroactively downgrade existing EXPANDING children to CONTENT
|
|
37
|
+
i = 0
|
|
38
|
+
while i < @children.length
|
|
39
|
+
if @children[i].get_height_policy == EXPANDING
|
|
40
|
+
@children[i].set_height_policy(CONTENT)
|
|
41
|
+
end
|
|
42
|
+
i = i + 1
|
|
43
|
+
end
|
|
44
|
+
self
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
#: () -> Column
|
|
48
|
+
def pin_to_bottom
|
|
49
|
+
@pin_bottom = true
|
|
50
|
+
self
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
#: () -> bool
|
|
54
|
+
def is_scrollable
|
|
55
|
+
@is_scrollable
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
#: (bool is_direction_x) -> bool
|
|
59
|
+
def has_scrollbar(is_direction_x)
|
|
60
|
+
if is_direction_x
|
|
61
|
+
false
|
|
62
|
+
else
|
|
63
|
+
@is_scrollable
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
#: () -> Float
|
|
68
|
+
def get_scroll_offset
|
|
69
|
+
@scroll_offset
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
#: (Float v) -> void
|
|
73
|
+
def set_scroll_offset(v)
|
|
74
|
+
@scroll_offset = v
|
|
75
|
+
@external_scroll_state&.set_y(v)
|
|
76
|
+
mark_dirty
|
|
77
|
+
update
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Override add: auto-downgrade EXPANDING height to CONTENT in scrollable Column
|
|
81
|
+
#: (untyped w) -> Column
|
|
82
|
+
def add(w)
|
|
83
|
+
if w == nil
|
|
84
|
+
return self
|
|
85
|
+
end
|
|
86
|
+
if @is_scrollable && w.get_height_policy == EXPANDING
|
|
87
|
+
w.set_height_policy(CONTENT)
|
|
88
|
+
end
|
|
89
|
+
super(w)
|
|
90
|
+
self
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
#: (untyped painter) -> Size
|
|
94
|
+
def measure(painter)
|
|
95
|
+
total_h = 0.0
|
|
96
|
+
max_w = 0.0
|
|
97
|
+
i = 0
|
|
98
|
+
while i < @children.length
|
|
99
|
+
c = @children[i]
|
|
100
|
+
cs = c.measure(painter)
|
|
101
|
+
if c.get_height_policy == FIXED
|
|
102
|
+
child_h = c.get_height
|
|
103
|
+
else
|
|
104
|
+
child_h = cs.height
|
|
105
|
+
end
|
|
106
|
+
total_h = total_h + child_h
|
|
107
|
+
total_h = total_h + @spacing if i > 0
|
|
108
|
+
max_w = cs.width if cs.width > max_w
|
|
109
|
+
i = i + 1
|
|
110
|
+
end
|
|
111
|
+
Size.new(max_w + @pad_left + @pad_right, total_h + @pad_top + @pad_bottom)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Unified layout: two-pass flex distribution + scroll offset.
|
|
115
|
+
# With approach C (auto-downgrade), scrollable containers have no EXPANDING
|
|
116
|
+
# children, so flex distribution is a no-op and content stacks sequentially.
|
|
117
|
+
#: (untyped painter) -> void
|
|
118
|
+
def relocate_children(painter)
|
|
119
|
+
# Account for padding
|
|
120
|
+
inner_w = @width - @pad_left - @pad_right
|
|
121
|
+
inner_h = @height - @pad_top - @pad_bottom
|
|
122
|
+
if inner_w < 0.0
|
|
123
|
+
inner_w = 0.0
|
|
124
|
+
end
|
|
125
|
+
if inner_h < 0.0
|
|
126
|
+
inner_h = 0.0
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
remaining = inner_h
|
|
130
|
+
expanding_total_flex = 0
|
|
131
|
+
|
|
132
|
+
# First pass: measure CONTENT/FIXED children, collect EXPANDING flex totals
|
|
133
|
+
i = 0
|
|
134
|
+
while i < @children.length
|
|
135
|
+
c = @children[i]
|
|
136
|
+
if c.get_height_policy != EXPANDING
|
|
137
|
+
# Set width before measure (for word-wrap, centering, etc.)
|
|
138
|
+
if c.get_width_policy != FIXED
|
|
139
|
+
c.resize_wh(inner_w, c.get_height)
|
|
140
|
+
end
|
|
141
|
+
cs = c.measure(painter)
|
|
142
|
+
# Use explicit height for FIXED, measured height for CONTENT
|
|
143
|
+
if c.get_height_policy == FIXED
|
|
144
|
+
child_h = c.get_height
|
|
145
|
+
else
|
|
146
|
+
child_h = cs.height
|
|
147
|
+
end
|
|
148
|
+
if c.get_width_policy == FIXED
|
|
149
|
+
c.resize_wh(cs.width, child_h)
|
|
150
|
+
else
|
|
151
|
+
c.resize_wh(inner_w, child_h)
|
|
152
|
+
end
|
|
153
|
+
remaining = remaining - child_h
|
|
154
|
+
else
|
|
155
|
+
expanding_total_flex = expanding_total_flex + c.get_flex
|
|
156
|
+
end
|
|
157
|
+
remaining = remaining - @spacing if i > 0
|
|
158
|
+
i = i + 1
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
if remaining < 0.0
|
|
162
|
+
remaining = 0.0
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
# Second pass: distribute remaining space to EXPANDING, position all
|
|
166
|
+
cy = @y + @pad_top
|
|
167
|
+
if @is_scrollable
|
|
168
|
+
cy = cy - @scroll_offset
|
|
169
|
+
end
|
|
170
|
+
total_content_h = 0.0
|
|
171
|
+
i = 0
|
|
172
|
+
while i < @children.length
|
|
173
|
+
c = @children[i]
|
|
174
|
+
if c.get_height_policy == EXPANDING
|
|
175
|
+
h = 0.0
|
|
176
|
+
if expanding_total_flex > 0 && remaining > 0.0
|
|
177
|
+
h = remaining * c.get_flex / expanding_total_flex
|
|
178
|
+
end
|
|
179
|
+
c.resize_wh(inner_w, h)
|
|
180
|
+
else
|
|
181
|
+
# In a Column, non-FIXED children fill the column width
|
|
182
|
+
if c.get_width_policy != FIXED
|
|
183
|
+
c.resize_wh(inner_w, c.get_height)
|
|
184
|
+
end
|
|
185
|
+
end
|
|
186
|
+
c.move_xy(@x + @pad_left, cy)
|
|
187
|
+
cy = cy + c.get_height + @spacing
|
|
188
|
+
total_content_h = total_content_h + c.get_height
|
|
189
|
+
total_content_h = total_content_h + @spacing if i > 0
|
|
190
|
+
i = i + 1
|
|
191
|
+
end
|
|
192
|
+
@content_height = total_content_h
|
|
193
|
+
|
|
194
|
+
# Auto-scroll to bottom when pinned
|
|
195
|
+
if @pin_bottom && @is_scrollable
|
|
196
|
+
max_scroll = @content_height - inner_h
|
|
197
|
+
if max_scroll > 0.0
|
|
198
|
+
@scroll_offset = max_scroll
|
|
199
|
+
@external_scroll_state&.set_y(@scroll_offset)
|
|
200
|
+
end
|
|
201
|
+
end
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
#: (untyped painter, bool completely) -> void
|
|
205
|
+
def redraw(painter, completely)
|
|
206
|
+
saved_bg = Kumiki._bg_clear_color
|
|
207
|
+
# When this layout has a custom background and is dirty, we handle clearing
|
|
208
|
+
# ourselves to preserve rounded corners. Clear dirty flag so redraw_children
|
|
209
|
+
# won't overwrite with a solid fill_rect.
|
|
210
|
+
if @custom_bg && is_dirty
|
|
211
|
+
parent_bg = saved_bg
|
|
212
|
+
if parent_bg == nil || parent_bg == 0
|
|
213
|
+
parent_bg = Kumiki.theme.bg_canvas
|
|
214
|
+
end
|
|
215
|
+
painter.fill_rect(0.0, 0.0, @width, @height, parent_bg)
|
|
216
|
+
set_dirty(false)
|
|
217
|
+
completely = true
|
|
218
|
+
end
|
|
219
|
+
draw_visual_background(painter)
|
|
220
|
+
relocate_children(painter)
|
|
221
|
+
redraw_children(painter, completely)
|
|
222
|
+
draw_scrollbar(painter) if @is_scrollable
|
|
223
|
+
Kumiki._bg_clear_color = saved_bg
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
#: (untyped painter) -> void
|
|
227
|
+
def draw_scrollbar(painter)
|
|
228
|
+
viewport_h = @height
|
|
229
|
+
content_h = @content_height
|
|
230
|
+
return if content_h <= viewport_h
|
|
231
|
+
|
|
232
|
+
bar_width = 8.0
|
|
233
|
+
thumb_color = 0xC0AAAAAA
|
|
234
|
+
|
|
235
|
+
# Thumb
|
|
236
|
+
thumb_h = viewport_h * viewport_h / content_h
|
|
237
|
+
if thumb_h < 20.0
|
|
238
|
+
thumb_h = 20.0
|
|
239
|
+
end
|
|
240
|
+
thumb_y = (@scroll_offset / content_h) * viewport_h
|
|
241
|
+
if thumb_y + thumb_h > viewport_h
|
|
242
|
+
thumb_y = viewport_h - thumb_h
|
|
243
|
+
end
|
|
244
|
+
painter.fill_round_rect(@width - bar_width + 2.0, thumb_y, bar_width - 4.0, thumb_h, 2.0, thumb_color)
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
# Intercept clicks on the scrollbar area before dispatching to children
|
|
248
|
+
#: (Point p) -> Array
|
|
249
|
+
def dispatch(p)
|
|
250
|
+
if @is_scrollable && @content_height > @height && contain(p)
|
|
251
|
+
local_x = p.x - @x
|
|
252
|
+
if local_x >= @width - 8.0
|
|
253
|
+
local_p = Point.new(local_x, p.y - @y)
|
|
254
|
+
return [self, local_p]
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
super(p)
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
#: (MouseEvent ev) -> void
|
|
261
|
+
def mouse_down(ev)
|
|
262
|
+
if @is_scrollable && @content_height > @height
|
|
263
|
+
@sb_dragging = true
|
|
264
|
+
@sb_drag_start_y = ev.pos.y
|
|
265
|
+
@sb_drag_start_offset = @scroll_offset
|
|
266
|
+
# Jump scroll to clicked position
|
|
267
|
+
viewport_h = @height
|
|
268
|
+
content_h = @content_height
|
|
269
|
+
max_scroll = content_h - viewport_h
|
|
270
|
+
@scroll_offset = (ev.pos.y / viewport_h) * max_scroll
|
|
271
|
+
if @scroll_offset < 0.0
|
|
272
|
+
@scroll_offset = 0.0
|
|
273
|
+
end
|
|
274
|
+
if @scroll_offset > max_scroll
|
|
275
|
+
@scroll_offset = max_scroll
|
|
276
|
+
end
|
|
277
|
+
@external_scroll_state&.set_y(@scroll_offset)
|
|
278
|
+
mark_dirty
|
|
279
|
+
update
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
#: (MouseEvent ev) -> void
|
|
284
|
+
def mouse_drag(ev)
|
|
285
|
+
if @sb_dragging
|
|
286
|
+
viewport_h = @height
|
|
287
|
+
content_h = @content_height
|
|
288
|
+
if content_h > viewport_h
|
|
289
|
+
max_scroll = content_h - viewport_h
|
|
290
|
+
@scroll_offset = (ev.pos.y / viewport_h) * max_scroll
|
|
291
|
+
if @scroll_offset < 0.0
|
|
292
|
+
@scroll_offset = 0.0
|
|
293
|
+
end
|
|
294
|
+
if @scroll_offset > max_scroll
|
|
295
|
+
@scroll_offset = max_scroll
|
|
296
|
+
end
|
|
297
|
+
@external_scroll_state&.set_y(@scroll_offset)
|
|
298
|
+
mark_dirty
|
|
299
|
+
update
|
|
300
|
+
end
|
|
301
|
+
end
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
#: (MouseEvent ev) -> void
|
|
305
|
+
def mouse_up(ev)
|
|
306
|
+
@sb_dragging = false
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
#: (WheelEvent ev) -> void
|
|
310
|
+
def mouse_wheel(ev)
|
|
311
|
+
if @is_scrollable
|
|
312
|
+
scroll_speed = 30.0
|
|
313
|
+
@scroll_offset = @scroll_offset - ev.delta_y * scroll_speed
|
|
314
|
+
# Clamp scroll offset
|
|
315
|
+
max_scroll = @content_height - @height
|
|
316
|
+
if max_scroll < 0.0
|
|
317
|
+
max_scroll = 0.0
|
|
318
|
+
end
|
|
319
|
+
if @scroll_offset < 0.0
|
|
320
|
+
@scroll_offset = 0.0
|
|
321
|
+
end
|
|
322
|
+
if @scroll_offset > max_scroll
|
|
323
|
+
@scroll_offset = max_scroll
|
|
324
|
+
end
|
|
325
|
+
# Toggle pin_to_bottom: disable on scroll up, re-enable at bottom
|
|
326
|
+
if ev.delta_y > 0.0
|
|
327
|
+
@pin_bottom = false
|
|
328
|
+
end
|
|
329
|
+
if max_scroll > 0.0 && @scroll_offset >= max_scroll
|
|
330
|
+
@pin_bottom = true
|
|
331
|
+
end
|
|
332
|
+
@external_scroll_state&.set_y(@scroll_offset)
|
|
333
|
+
mark_dirty
|
|
334
|
+
update
|
|
335
|
+
end
|
|
336
|
+
end
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
# Top-level helper
|
|
340
|
+
#: (*untyped children) -> Column
|
|
341
|
+
def Column(*children)
|
|
342
|
+
col = Column.new
|
|
343
|
+
i = 0
|
|
344
|
+
while i < children.length
|
|
345
|
+
col.add(children[i])
|
|
346
|
+
i = i + 1
|
|
347
|
+
end
|
|
348
|
+
col
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
end
|