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,670 @@
1
+ module Kumiki
2
+ # DataTable - sortable, scrollable data table widget
3
+ # Features: column headers, sort indicators, row selection, hover highlight,
4
+ # virtual scroll (only visible rows rendered), alternating row colors,
5
+ # column resize by dragging header borders
6
+
7
+ DT_HEADER_HEIGHT = 32.0
8
+ DT_ROW_HEIGHT = 28.0
9
+ DT_SCROLLBAR_WIDTH = 8.0
10
+ DT_RESIZE_ZONE = 5.0
11
+ DT_MIN_COL_WIDTH = 30.0
12
+
13
+ # Sort direction constants
14
+ DT_SORT_NONE = 0
15
+ DT_SORT_ASC = 1
16
+ DT_SORT_DESC = 2
17
+
18
+ class DataTable < Widget
19
+ def initialize(col_names, col_widths, rows)
20
+ super()
21
+ @col_names = col_names # Array of String
22
+ @col_widths = col_widths # Array of Float
23
+ @rows = rows # Array of Array[String]
24
+ @sort_col = -1
25
+ @sort_dir = DT_SORT_NONE
26
+ @sorted_indices = nil
27
+ @selected_row = -1
28
+ @hover_row = -1
29
+ @hover_header = -1
30
+ @scroll_y = 0.0
31
+ @max_scroll = 0.0
32
+ @scrollable_flag = true
33
+ @width_policy = EXPANDING
34
+ @height_policy = EXPANDING
35
+ @font_size = 13.0
36
+ @header_font_size = 13.0
37
+ @num_cols = col_names.length
38
+ @resizing_col = -1
39
+ @resize_start_x = 0.0
40
+ @resize_start_width = 0.0
41
+ @resize_hover_col = -1
42
+ build_sorted_indices
43
+ end
44
+
45
+ def font_size(s)
46
+ @font_size = s
47
+ self
48
+ end
49
+
50
+ def header_font_size(s)
51
+ @header_font_size = s
52
+ self
53
+ end
54
+
55
+ def set_data(col_names, col_widths, rows)
56
+ @col_names = col_names
57
+ @col_widths = col_widths
58
+ @rows = rows
59
+ @sort_col = -1
60
+ @sort_dir = DT_SORT_NONE
61
+ @sorted_indices = nil
62
+ @selected_row = -1
63
+ @scroll_y = 0.0
64
+ @num_cols = col_names.length
65
+ build_sorted_indices
66
+ mark_dirty
67
+ update
68
+ end
69
+
70
+ def set_rows(rows)
71
+ @rows = rows
72
+ build_sorted_indices
73
+ if @sort_col >= 0
74
+ apply_sort
75
+ end
76
+ mark_dirty
77
+ update
78
+ end
79
+
80
+ def selected_row
81
+ @selected_row
82
+ end
83
+
84
+ def get_scrollable
85
+ @scrollable_flag
86
+ end
87
+
88
+ def redraw(painter, completely)
89
+ return if @num_cols == 0
90
+
91
+ visible_h = @height - DT_HEADER_HEIGHT
92
+ if visible_h < 0.0
93
+ visible_h = 0.0
94
+ end
95
+ compute_scroll(visible_h)
96
+
97
+ # Background
98
+ painter.fill_rect(0.0, 0.0, @width, @height, Kumiki.theme.bg_primary)
99
+
100
+ draw_header(painter)
101
+ draw_rows(painter, visible_h)
102
+ draw_scrollbar(painter, visible_h)
103
+ end
104
+
105
+ def draw_header(painter)
106
+ bg = Kumiki.theme.bg_secondary
107
+ header_bg = painter.darken_color(bg, 0.1)
108
+ painter.fill_rect(0.0, 0.0, @width, DT_HEADER_HEIGHT, header_bg)
109
+ draw_header_columns(painter)
110
+ bc = Kumiki.theme.border
111
+ painter.draw_line(0.0, DT_HEADER_HEIGHT, @width, DT_HEADER_HEIGHT, bc, 1.0)
112
+ end
113
+
114
+ def draw_header_columns(painter)
115
+ hx = 0.0
116
+ ci = 0
117
+ while ci < @num_cols
118
+ col_w = @col_widths[ci]
119
+ col_name = @col_names[ci]
120
+ draw_one_header(painter, ci, hx, col_w, col_name)
121
+ hx = hx + col_w
122
+ ci = ci + 1
123
+ end
124
+ end
125
+
126
+ def draw_one_header(painter, ci, hx, col_w, col_name)
127
+ if @hover_header == ci
128
+ ac = Kumiki.theme.accent
129
+ hc = painter.with_alpha(ac, 30)
130
+ painter.fill_rect(hx, 0.0, col_w, DT_HEADER_HEIGHT, hc)
131
+ end
132
+
133
+ painter.save
134
+ painter.clip_rect(hx, 0.0, col_w - 2.0, DT_HEADER_HEIGHT)
135
+
136
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, @header_font_size)
137
+ mh = painter.measure_text_height(Kumiki.theme.font_family, @header_font_size)
138
+ ty = (DT_HEADER_HEIGHT - mh) / 2.0 + ascent
139
+ tc = Kumiki.theme.text_primary
140
+ painter.draw_text(col_name, hx + 8.0, ty, Kumiki.theme.font_family, @header_font_size, tc)
141
+
142
+ if @sort_col == ci
143
+ draw_sort_indicator(painter, col_name, hx, ty)
144
+ end
145
+
146
+ painter.restore
147
+
148
+ sep_x = hx + col_w - 1.0
149
+ if @resize_hover_col == ci
150
+ ac = Kumiki.theme.accent
151
+ painter.draw_line(sep_x, 0.0, sep_x, DT_HEADER_HEIGHT, ac, 2.0)
152
+ else
153
+ bc = Kumiki.theme.border
154
+ painter.draw_line(sep_x, 0.0, sep_x, DT_HEADER_HEIGHT, bc, 1.0)
155
+ end
156
+ end
157
+
158
+ def draw_sort_indicator(painter, col_name, hx, ty)
159
+ nw = painter.measure_text_width(col_name, Kumiki.theme.font_family, @header_font_size)
160
+ ix = hx + 8.0 + nw + 8.0
161
+ ac = Kumiki.theme.accent
162
+ s = 4.0
163
+ cy = DT_HEADER_HEIGHT / 2.0
164
+ if @sort_dir == DT_SORT_ASC
165
+ # Up arrow
166
+ painter.fill_triangle(ix - s, cy + s / 2.0, ix + s, cy + s / 2.0, ix, cy - s / 2.0, ac)
167
+ else
168
+ # Down arrow
169
+ painter.fill_triangle(ix - s, cy - s / 2.0, ix + s, cy - s / 2.0, ix, cy + s / 2.0, ac)
170
+ end
171
+ end
172
+
173
+ def draw_rows(painter, visible_h)
174
+ painter.save
175
+ painter.clip_rect(0.0, DT_HEADER_HEIGHT, @width, visible_h)
176
+
177
+ first_row = (@scroll_y / DT_ROW_HEIGHT).to_i
178
+ if first_row < 0
179
+ first_row = 0
180
+ end
181
+ last_row = (((@scroll_y + visible_h) / DT_ROW_HEIGHT) + 1).to_i
182
+ if last_row > @rows.length
183
+ last_row = @rows.length
184
+ end
185
+
186
+ ri = first_row
187
+ while ri < last_row
188
+ draw_single_row(painter, ri)
189
+ ri = ri + 1
190
+ end
191
+
192
+ painter.restore
193
+ end
194
+
195
+ def draw_single_row(painter, ri)
196
+ actual_index = resolve_row_index(ri)
197
+ ri_f = ri * 1.0
198
+ row_y = DT_HEADER_HEIGHT + ri_f * DT_ROW_HEIGHT - @scroll_y
199
+
200
+ row_bg = compute_row_bg(painter, ri)
201
+ painter.fill_rect(0.0, row_y, @width, DT_ROW_HEIGHT, row_bg)
202
+
203
+ cx = 0.0
204
+ ci = 0
205
+ while ci < @num_cols
206
+ col_w = @col_widths[ci]
207
+ if actual_index < @rows.length
208
+ if ci < @rows[actual_index].length
209
+ cell_text = @rows[actual_index][ci]
210
+ painter.save
211
+ painter.clip_rect(cx, row_y, col_w - 2.0, DT_ROW_HEIGHT)
212
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, @font_size)
213
+ ty = row_y + (DT_ROW_HEIGHT - painter.measure_text_height(Kumiki.theme.font_family, @font_size)) / 2.0 + ascent
214
+ painter.draw_text(cell_text, cx + 8.0, ty, Kumiki.theme.font_family, @font_size, Kumiki.theme.text_primary)
215
+ painter.restore
216
+ end
217
+ end
218
+ cx = cx + col_w
219
+ ci = ci + 1
220
+ end
221
+
222
+ row_line_c = painter.with_alpha(Kumiki.theme.border, 40)
223
+ painter.draw_line(0.0, row_y + DT_ROW_HEIGHT, @width, row_y + DT_ROW_HEIGHT, row_line_c, 1.0)
224
+ end
225
+
226
+ def draw_scrollbar(painter, visible_h)
227
+ if @max_scroll <= 0.0
228
+ return
229
+ end
230
+ rows_len = @rows.length * 1.0
231
+ content_h = rows_len * DT_ROW_HEIGHT
232
+ sb_x = @width - DT_SCROLLBAR_WIDTH
233
+ sb_ratio = visible_h / content_h
234
+ sb_h = visible_h * sb_ratio
235
+ if sb_h < 20.0
236
+ sb_h = 20.0
237
+ end
238
+ sb_travel = visible_h - sb_h
239
+ sb_pos = 0.0
240
+ if @max_scroll > 0.0
241
+ sb_pos = (@scroll_y / @max_scroll) * sb_travel
242
+ end
243
+ painter.fill_rect(sb_x, DT_HEADER_HEIGHT, DT_SCROLLBAR_WIDTH, visible_h, Kumiki.theme.scrollbar_bg)
244
+ painter.fill_round_rect(sb_x + 1.0, DT_HEADER_HEIGHT + sb_pos, DT_SCROLLBAR_WIDTH - 2.0, sb_h, 3.0, Kumiki.theme.scrollbar_fg)
245
+ end
246
+
247
+ # --- Event Handlers ---
248
+
249
+ def mouse_down(ev)
250
+ mx = ev.pos.x
251
+ my = ev.pos.y
252
+ if my < DT_HEADER_HEIGHT
253
+ border_col = col_border_at_x(mx)
254
+ if border_col >= 0
255
+ @resizing_col = border_col
256
+ @resize_start_x = mx
257
+ @resize_start_width = @col_widths[border_col]
258
+ return
259
+ end
260
+ end
261
+ end
262
+
263
+ def mouse_up(ev)
264
+ if @resizing_col >= 0
265
+ @resizing_col = -1
266
+ return
267
+ end
268
+
269
+ mx = ev.pos.x
270
+ my = ev.pos.y
271
+
272
+ if my < DT_HEADER_HEIGHT
273
+ col = column_at_x(mx)
274
+ if col >= 0
275
+ toggle_sort(col)
276
+ end
277
+ return
278
+ end
279
+
280
+ row_idx = row_at_y(my)
281
+ if row_idx >= 0
282
+ if row_idx < @rows.length
283
+ @selected_row = row_idx
284
+ mark_dirty
285
+ update
286
+ end
287
+ end
288
+ end
289
+
290
+ def mouse_drag(ev)
291
+ if @resizing_col >= 0
292
+ mx = ev.pos.x
293
+ delta = mx - @resize_start_x
294
+ new_w = @resize_start_width + delta
295
+ if new_w < DT_MIN_COL_WIDTH
296
+ new_w = DT_MIN_COL_WIDTH
297
+ end
298
+ @col_widths[@resizing_col] = new_w
299
+ mark_dirty
300
+ update
301
+ end
302
+ end
303
+
304
+ def cursor_pos(ev)
305
+ mx = ev.pos.x
306
+ my = ev.pos.y
307
+ old_hr = @hover_row
308
+ old_hh = @hover_header
309
+ old_rhc = @resize_hover_col
310
+
311
+ if my < DT_HEADER_HEIGHT
312
+ @resize_hover_col = col_border_at_x(mx)
313
+ if @resize_hover_col >= 0
314
+ @hover_header = -1
315
+ else
316
+ @hover_header = column_at_x(mx)
317
+ end
318
+ @hover_row = -1
319
+ else
320
+ @hover_header = -1
321
+ @hover_row = row_at_y(my)
322
+ @resize_hover_col = -1
323
+ end
324
+
325
+ need_redraw = false
326
+ if @hover_row != old_hr
327
+ need_redraw = true
328
+ end
329
+ if @hover_header != old_hh
330
+ need_redraw = true
331
+ end
332
+ if @resize_hover_col != old_rhc
333
+ need_redraw = true
334
+ end
335
+ if need_redraw
336
+ mark_dirty
337
+ update
338
+ end
339
+ end
340
+
341
+ def mouse_out
342
+ changed = false
343
+ if @hover_row != -1
344
+ @hover_row = -1
345
+ changed = true
346
+ end
347
+ if @hover_header != -1
348
+ @hover_header = -1
349
+ changed = true
350
+ end
351
+ if @resize_hover_col != -1
352
+ @resize_hover_col = -1
353
+ changed = true
354
+ end
355
+ if changed
356
+ mark_dirty
357
+ update
358
+ end
359
+ end
360
+
361
+ def dispatch_to_scrollable(p, is_direction_x)
362
+ if contain(p)
363
+ [self, p]
364
+ else
365
+ [nil, nil]
366
+ end
367
+ end
368
+
369
+ def mouse_wheel(ev)
370
+ @scroll_y = @scroll_y - ev.delta_y * 30.0
371
+ if @scroll_y < 0.0
372
+ @scroll_y = 0.0
373
+ end
374
+ if @scroll_y > @max_scroll
375
+ @scroll_y = @max_scroll
376
+ end
377
+ mark_dirty
378
+ update
379
+ end
380
+
381
+ private
382
+
383
+ def resolve_row_index(ri)
384
+ if @sorted_indices != nil
385
+ @sorted_indices[ri]
386
+ else
387
+ ri
388
+ end
389
+ end
390
+
391
+ def compute_row_bg(painter, ri)
392
+ bg = Kumiki.theme.bg_primary
393
+ if ri == @selected_row
394
+ ac = Kumiki.theme.accent
395
+ bg = painter.with_alpha(ac, 50)
396
+ elsif ri == @hover_row
397
+ bg = painter.lighten_color(bg, 0.08)
398
+ elsif ri % 2 != 0
399
+ bg = painter.darken_color(bg, 0.05)
400
+ end
401
+ bg
402
+ end
403
+
404
+ def compute_scroll(visible_h)
405
+ rows_len = @rows.length * 1.0
406
+ content_h = rows_len * DT_ROW_HEIGHT
407
+ @max_scroll = content_h - visible_h
408
+ if @max_scroll < 0.0
409
+ @max_scroll = 0.0
410
+ end
411
+ if @scroll_y > @max_scroll
412
+ @scroll_y = @max_scroll
413
+ end
414
+ if @scroll_y < 0.0
415
+ @scroll_y = 0.0
416
+ end
417
+ end
418
+
419
+ def total_columns_width
420
+ total = 0.0
421
+ i = 0
422
+ while i < @num_cols
423
+ total = total + @col_widths[i]
424
+ i = i + 1
425
+ end
426
+ total
427
+ end
428
+
429
+ def column_at_x(x)
430
+ cx = 0.0
431
+ i = 0
432
+ while i < @num_cols
433
+ col_w = @col_widths[i]
434
+ if x >= cx
435
+ if x < cx + col_w
436
+ return i
437
+ end
438
+ end
439
+ cx = cx + col_w
440
+ i = i + 1
441
+ end
442
+ -1
443
+ end
444
+
445
+ def col_border_at_x(x)
446
+ cx = 0.0
447
+ i = 0
448
+ while i < @num_cols
449
+ cx = cx + @col_widths[i]
450
+ diff = x - cx
451
+ if diff < 0.0
452
+ diff = 0.0 - diff
453
+ end
454
+ if diff < DT_RESIZE_ZONE
455
+ return i
456
+ end
457
+ i = i + 1
458
+ end
459
+ -1
460
+ end
461
+
462
+ def row_at_y(y)
463
+ if y < DT_HEADER_HEIGHT
464
+ return -1
465
+ end
466
+ row = ((y - DT_HEADER_HEIGHT + @scroll_y) / DT_ROW_HEIGHT).to_i
467
+ if row < 0
468
+ row = -1
469
+ end
470
+ if row >= @rows.length
471
+ row = -1
472
+ end
473
+ row
474
+ end
475
+
476
+ def build_sorted_indices
477
+ @sorted_indices = []
478
+ i = 0
479
+ while i < @rows.length
480
+ @sorted_indices << i
481
+ i = i + 1
482
+ end
483
+ end
484
+
485
+ def toggle_sort(col)
486
+ if @sort_col == col
487
+ if @sort_dir == DT_SORT_ASC
488
+ @sort_dir = DT_SORT_DESC
489
+ elsif @sort_dir == DT_SORT_DESC
490
+ @sort_dir = DT_SORT_NONE
491
+ @sort_col = -1
492
+ build_sorted_indices
493
+ mark_dirty
494
+ update
495
+ return
496
+ else
497
+ @sort_dir = DT_SORT_ASC
498
+ end
499
+ else
500
+ @sort_col = col
501
+ @sort_dir = DT_SORT_ASC
502
+ end
503
+ apply_sort
504
+ mark_dirty
505
+ update
506
+ end
507
+
508
+ def apply_sort
509
+ return if @sort_col < 0
510
+ build_sorted_indices
511
+ col = @sort_col
512
+ dir = @sort_dir
513
+
514
+ n = @sorted_indices.length
515
+ i = 1
516
+ while i < n
517
+ key = @sorted_indices[i]
518
+ j = i - 1
519
+ keep_going = true
520
+ while keep_going
521
+ if j < 0
522
+ keep_going = false
523
+ else
524
+ idx_j = @sorted_indices[j]
525
+ # Inline: get cell values directly (no helper method calls)
526
+ val_a = ""
527
+ if idx_j < @rows.length
528
+ row_a = @rows[idx_j]
529
+ if col < row_a.length
530
+ val_a = row_a[col]
531
+ end
532
+ end
533
+ val_b = ""
534
+ if key < @rows.length
535
+ row_b = @rows[key]
536
+ if col < row_b.length
537
+ val_b = row_b[col]
538
+ end
539
+ end
540
+ cmp = (val_a <=> val_b)
541
+ if cmp == nil
542
+ cmp = 0
543
+ end
544
+ should_swap = false
545
+ if dir == DT_SORT_ASC
546
+ if cmp > 0
547
+ should_swap = true
548
+ end
549
+ else
550
+ if cmp < 0
551
+ should_swap = true
552
+ end
553
+ end
554
+ if should_swap
555
+ @sorted_indices[j + 1] = @sorted_indices[j]
556
+ j = j - 1
557
+ else
558
+ keep_going = false
559
+ end
560
+ end
561
+ end
562
+ @sorted_indices[j + 1] = key
563
+ i = i + 1
564
+ end
565
+ end
566
+
567
+ def compare_cells_idx(idx_a, idx_b, col, dir)
568
+ cmp_a = get_cell_value(idx_a, col)
569
+ cmp_b = get_cell_value(idx_b, col)
570
+
571
+ num_a = try_parse_float(cmp_a)
572
+ num_b = try_parse_float(cmp_b)
573
+ if num_a != nil
574
+ if num_b != nil
575
+ if dir == DT_SORT_ASC
576
+ return num_a > num_b
577
+ else
578
+ return num_a < num_b
579
+ end
580
+ end
581
+ end
582
+ # Use spaceship operator for string comparison (op_lt/op_gt not in RubyDispatch)
583
+ cmp = (cmp_a <=> cmp_b)
584
+ if cmp == nil
585
+ cmp = 0
586
+ end
587
+ if dir == DT_SORT_ASC
588
+ cmp > 0
589
+ else
590
+ cmp < 0
591
+ end
592
+ end
593
+
594
+ def get_cell_value(row_idx, col)
595
+ if row_idx < @rows.length
596
+ row = @rows[row_idx]
597
+ if col < row.length
598
+ return row[col]
599
+ end
600
+ end
601
+ ""
602
+ end
603
+
604
+ def is_digit(c)
605
+ if c == "0"
606
+ return true
607
+ end
608
+ if c == "1"
609
+ return true
610
+ end
611
+ if c == "2"
612
+ return true
613
+ end
614
+ if c == "3"
615
+ return true
616
+ end
617
+ if c == "4"
618
+ return true
619
+ end
620
+ if c == "5"
621
+ return true
622
+ end
623
+ if c == "6"
624
+ return true
625
+ end
626
+ if c == "7"
627
+ return true
628
+ end
629
+ if c == "8"
630
+ return true
631
+ end
632
+ if c == "9"
633
+ return true
634
+ end
635
+ false
636
+ end
637
+
638
+ def try_parse_float(s)
639
+ return nil if s == nil
640
+ return nil if s == ""
641
+ i = 0
642
+ has_dot = false
643
+ if i < s.length
644
+ if s[i] == "-"
645
+ i = i + 1
646
+ end
647
+ end
648
+ return nil if i >= s.length
649
+ while i < s.length
650
+ c = s[i]
651
+ if c == "."
652
+ return nil if has_dot
653
+ has_dot = true
654
+ elsif is_digit(c)
655
+ # valid digit, continue
656
+ else
657
+ return nil
658
+ end
659
+ i = i + 1
660
+ end
661
+ s.to_f
662
+ end
663
+ end
664
+
665
+ # Top-level helper
666
+ def DataTable(col_names, col_widths, rows)
667
+ DataTable.new(col_names, col_widths, rows)
668
+ end
669
+
670
+ end
@@ -0,0 +1,31 @@
1
+ module Kumiki
2
+ # Divider widget - horizontal line separator
3
+
4
+ class Divider < Widget
5
+ def initialize
6
+ super
7
+ @color_val = 0
8
+ @custom_color = false
9
+ @width_policy = EXPANDING
10
+ @height_policy = FIXED
11
+ @height = 1.0
12
+ end
13
+
14
+ def color(c)
15
+ @color_val = c
16
+ @custom_color = true
17
+ self
18
+ end
19
+
20
+ def redraw(painter, completely)
21
+ c = @custom_color ? @color_val : Kumiki.theme.border
22
+ painter.draw_line(0.0, 0.0, @width, 0.0, c, 1.0)
23
+ end
24
+ end
25
+
26
+ # Top-level helper
27
+ def Divider()
28
+ Divider.new
29
+ end
30
+
31
+ end