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,1028 @@
1
+ module Kumiki
2
+ # Calendar - date picker widget
3
+ # Features: day/month/year view modes, navigation, selection highlight
4
+ # All date computation uses integer arithmetic (no Time/Date classes)
5
+
6
+ # View mode constants
7
+ CAL_DAYS = 0
8
+ CAL_MONTHS = 1
9
+ CAL_YEARS = 2
10
+
11
+ # Layout constants
12
+ CAL_CELL_SIZE = 36.0
13
+ CAL_HEADER_HEIGHT = 40.0
14
+ CAL_WEEKDAY_HEIGHT = 24.0
15
+ CAL_NAV_BUTTON_W = 36.0
16
+
17
+ # ===== Date Utility Functions =====
18
+
19
+ def cal_is_leap(year)
20
+ if year % 400 == 0
21
+ return true
22
+ end
23
+ if year % 100 == 0
24
+ return false
25
+ end
26
+ if year % 4 == 0
27
+ return true
28
+ end
29
+ false
30
+ end
31
+
32
+ def cal_days_in_month(year, month)
33
+ if month == 1
34
+ return 31
35
+ end
36
+ if month == 2
37
+ if cal_is_leap(year)
38
+ return 29
39
+ end
40
+ return 28
41
+ end
42
+ if month == 3
43
+ return 31
44
+ end
45
+ if month == 4
46
+ return 30
47
+ end
48
+ if month == 5
49
+ return 31
50
+ end
51
+ if month == 6
52
+ return 30
53
+ end
54
+ if month == 7
55
+ return 31
56
+ end
57
+ if month == 8
58
+ return 31
59
+ end
60
+ if month == 9
61
+ return 30
62
+ end
63
+ if month == 10
64
+ return 31
65
+ end
66
+ if month == 11
67
+ return 30
68
+ end
69
+ 31
70
+ end
71
+
72
+ # Sakamoto's algorithm for day of week (0=Sunday, 1=Monday, ..., 6=Saturday)
73
+ def cal_day_of_week(year, month, day)
74
+ # t lookup table
75
+ t0 = 0
76
+ t1 = 3
77
+ t2 = 2
78
+ t3 = 5
79
+ t4 = 0
80
+ t5 = 3
81
+ t6 = 5
82
+ t7 = 1
83
+ t8 = 4
84
+ t9 = 6
85
+ t10 = 2
86
+ t11 = 4
87
+ y = year
88
+ if month < 3
89
+ y = y - 1
90
+ end
91
+ t_val = 0
92
+ if month == 1
93
+ t_val = t0
94
+ elsif month == 2
95
+ t_val = t1
96
+ elsif month == 3
97
+ t_val = t2
98
+ elsif month == 4
99
+ t_val = t3
100
+ elsif month == 5
101
+ t_val = t4
102
+ elsif month == 6
103
+ t_val = t5
104
+ elsif month == 7
105
+ t_val = t6
106
+ elsif month == 8
107
+ t_val = t7
108
+ elsif month == 9
109
+ t_val = t8
110
+ elsif month == 10
111
+ t_val = t9
112
+ elsif month == 11
113
+ t_val = t10
114
+ else
115
+ t_val = t11
116
+ end
117
+ (y + y / 4 - y / 100 + y / 400 + t_val + day) % 7
118
+ end
119
+
120
+ def cal_month_name(month)
121
+ if month == 1
122
+ return "January"
123
+ end
124
+ if month == 2
125
+ return "February"
126
+ end
127
+ if month == 3
128
+ return "March"
129
+ end
130
+ if month == 4
131
+ return "April"
132
+ end
133
+ if month == 5
134
+ return "May"
135
+ end
136
+ if month == 6
137
+ return "June"
138
+ end
139
+ if month == 7
140
+ return "July"
141
+ end
142
+ if month == 8
143
+ return "August"
144
+ end
145
+ if month == 9
146
+ return "September"
147
+ end
148
+ if month == 10
149
+ return "October"
150
+ end
151
+ if month == 11
152
+ return "November"
153
+ end
154
+ "December"
155
+ end
156
+
157
+ def cal_short_month_name(month)
158
+ if month == 1
159
+ return "Jan"
160
+ end
161
+ if month == 2
162
+ return "Feb"
163
+ end
164
+ if month == 3
165
+ return "Mar"
166
+ end
167
+ if month == 4
168
+ return "Apr"
169
+ end
170
+ if month == 5
171
+ return "May"
172
+ end
173
+ if month == 6
174
+ return "Jun"
175
+ end
176
+ if month == 7
177
+ return "Jul"
178
+ end
179
+ if month == 8
180
+ return "Aug"
181
+ end
182
+ if month == 9
183
+ return "Sep"
184
+ end
185
+ if month == 10
186
+ return "Oct"
187
+ end
188
+ if month == 11
189
+ return "Nov"
190
+ end
191
+ "Dec"
192
+ end
193
+
194
+ def cal_int_to_str(n)
195
+ if n == 1
196
+ return "1"
197
+ end
198
+ if n == 2
199
+ return "2"
200
+ end
201
+ if n == 3
202
+ return "3"
203
+ end
204
+ if n == 4
205
+ return "4"
206
+ end
207
+ if n == 5
208
+ return "5"
209
+ end
210
+ if n == 6
211
+ return "6"
212
+ end
213
+ if n == 7
214
+ return "7"
215
+ end
216
+ if n == 8
217
+ return "8"
218
+ end
219
+ if n == 9
220
+ return "9"
221
+ end
222
+ if n == 10
223
+ return "10"
224
+ end
225
+ if n == 11
226
+ return "11"
227
+ end
228
+ if n == 12
229
+ return "12"
230
+ end
231
+ if n == 13
232
+ return "13"
233
+ end
234
+ if n == 14
235
+ return "14"
236
+ end
237
+ if n == 15
238
+ return "15"
239
+ end
240
+ if n == 16
241
+ return "16"
242
+ end
243
+ if n == 17
244
+ return "17"
245
+ end
246
+ if n == 18
247
+ return "18"
248
+ end
249
+ if n == 19
250
+ return "19"
251
+ end
252
+ if n == 20
253
+ return "20"
254
+ end
255
+ if n == 21
256
+ return "21"
257
+ end
258
+ if n == 22
259
+ return "22"
260
+ end
261
+ if n == 23
262
+ return "23"
263
+ end
264
+ if n == 24
265
+ return "24"
266
+ end
267
+ if n == 25
268
+ return "25"
269
+ end
270
+ if n == 26
271
+ return "26"
272
+ end
273
+ if n == 27
274
+ return "27"
275
+ end
276
+ if n == 28
277
+ return "28"
278
+ end
279
+ if n == 29
280
+ return "29"
281
+ end
282
+ if n == 30
283
+ return "30"
284
+ end
285
+ "31"
286
+ end
287
+
288
+ # Convert year (integer) to string for display
289
+ # Simple lookup for common years, digit construction for others
290
+ def cal_year_to_str(year)
291
+ # Common decade prefix
292
+ prefix = ""
293
+ remainder = year
294
+ if year >= 2000
295
+ if year < 2100
296
+ prefix = "20"
297
+ remainder = year - 2000
298
+ end
299
+ end
300
+ if prefix == ""
301
+ if year >= 1900
302
+ if year < 2000
303
+ prefix = "19"
304
+ remainder = year - 1900
305
+ end
306
+ end
307
+ end
308
+ if prefix == ""
309
+ return "????"
310
+ end
311
+ # Convert 0-99 to two-digit string
312
+ tens = remainder / 10
313
+ ones = remainder % 10
314
+ tens_s = cal_digit_str(tens)
315
+ ones_s = cal_digit_str(ones)
316
+ prefix + tens_s + ones_s
317
+ end
318
+
319
+ def cal_digit_str(d)
320
+ if d == 0
321
+ return "0"
322
+ end
323
+ if d == 1
324
+ return "1"
325
+ end
326
+ if d == 2
327
+ return "2"
328
+ end
329
+ if d == 3
330
+ return "3"
331
+ end
332
+ if d == 4
333
+ return "4"
334
+ end
335
+ if d == 5
336
+ return "5"
337
+ end
338
+ if d == 6
339
+ return "6"
340
+ end
341
+ if d == 7
342
+ return "7"
343
+ end
344
+ if d == 8
345
+ return "8"
346
+ end
347
+ "9"
348
+ end
349
+
350
+ def cal_floor(f)
351
+ i = 0
352
+ if f < 0.0
353
+ return 0
354
+ end
355
+ while i * 1.0 <= f
356
+ i = i + 1
357
+ end
358
+ i - 1
359
+ end
360
+
361
+ # ===== CalendarState =====
362
+
363
+ class CalendarState < ObservableBase
364
+ def initialize(year, month, day)
365
+ super()
366
+ @sel_year = year
367
+ @sel_month = month
368
+ @sel_day = day
369
+ @view_year = year
370
+ @view_month = month
371
+ @view_mode = CAL_DAYS
372
+ @on_change_cb = nil
373
+ end
374
+
375
+ def sel_year
376
+ @sel_year
377
+ end
378
+
379
+ def sel_month
380
+ @sel_month
381
+ end
382
+
383
+ def sel_day
384
+ @sel_day
385
+ end
386
+
387
+ def view_year
388
+ @view_year
389
+ end
390
+
391
+ def view_month
392
+ @view_month
393
+ end
394
+
395
+ def view_mode
396
+ @view_mode
397
+ end
398
+
399
+ def prev_month
400
+ @view_month = @view_month - 1
401
+ if @view_month < 1
402
+ @view_month = 12
403
+ @view_year = @view_year - 1
404
+ end
405
+ notify_observers
406
+ end
407
+
408
+ def next_month
409
+ @view_month = @view_month + 1
410
+ if @view_month > 12
411
+ @view_month = 1
412
+ @view_year = @view_year + 1
413
+ end
414
+ notify_observers
415
+ end
416
+
417
+ def prev_year
418
+ @view_year = @view_year - 1
419
+ notify_observers
420
+ end
421
+
422
+ def next_year
423
+ @view_year = @view_year + 1
424
+ notify_observers
425
+ end
426
+
427
+ def prev_year_page
428
+ @view_year = @view_year - 20
429
+ notify_observers
430
+ end
431
+
432
+ def next_year_page
433
+ @view_year = @view_year + 20
434
+ notify_observers
435
+ end
436
+
437
+ def select_date(year, month, day)
438
+ @sel_year = year
439
+ @sel_month = month
440
+ @sel_day = day
441
+ @view_year = year
442
+ @view_month = month
443
+ @view_mode = CAL_DAYS
444
+ if @on_change_cb != nil
445
+ @on_change_cb.call(year, month, day)
446
+ end
447
+ notify_observers
448
+ end
449
+
450
+ def select_month(month)
451
+ @view_month = month
452
+ @view_mode = CAL_DAYS
453
+ notify_observers
454
+ end
455
+
456
+ def select_year(year)
457
+ @view_year = year
458
+ @view_mode = CAL_MONTHS
459
+ notify_observers
460
+ end
461
+
462
+ def set_view_mode(mode)
463
+ @view_mode = mode
464
+ notify_observers
465
+ end
466
+
467
+ def formatted_date
468
+ m = @sel_month
469
+ d = @sel_day
470
+ mname = cal_month_name(m)
471
+ dstr = cal_int_to_str(d)
472
+ mname + " " + dstr
473
+ end
474
+
475
+ def days_in_current_month
476
+ cal_days_in_month(@view_year, @view_month)
477
+ end
478
+
479
+ def first_weekday
480
+ cal_day_of_week(@view_year, @view_month, 1)
481
+ end
482
+
483
+ def header_text
484
+ mn = cal_month_name(@view_month)
485
+ yr_str = cal_year_to_str(@view_year)
486
+ mn + " " + yr_str
487
+ end
488
+
489
+ def is_current_day(day)
490
+ if day != @sel_day
491
+ return false
492
+ end
493
+ if @view_month != @sel_month
494
+ return false
495
+ end
496
+ if @view_year != @sel_year
497
+ return false
498
+ end
499
+ true
500
+ end
501
+
502
+ def year_label(y)
503
+ cal_year_to_str(y)
504
+ end
505
+
506
+ def view_year_label
507
+ cal_year_to_str(@view_year)
508
+ end
509
+
510
+ def year_range_text
511
+ start_y = @view_year - (@view_year % 20)
512
+ end_y = start_y + 19
513
+ sy_str = cal_year_to_str(start_y)
514
+ ey_str = cal_year_to_str(end_y)
515
+ sy_str + " - " + ey_str
516
+ end
517
+
518
+ def year_range_start
519
+ @view_year - (@view_year % 20)
520
+ end
521
+
522
+ def is_year_selected(year)
523
+ year == @sel_year
524
+ end
525
+
526
+ def is_month_selected(month)
527
+ month == @sel_month
528
+ end
529
+
530
+ def year_at_offset(i)
531
+ start = @view_year - (@view_year % 20)
532
+ start + i
533
+ end
534
+
535
+ def on_change(cb)
536
+ @on_change_cb = cb
537
+ self
538
+ end
539
+
540
+ def try_select_cell(row, col)
541
+ first_dow = first_weekday
542
+ cell_idx = row * 7 + col
543
+ day = cell_idx - first_dow + 1
544
+ days = days_in_current_month
545
+ if day >= 1
546
+ if day <= days
547
+ select_date(@view_year, @view_month, day)
548
+ end
549
+ end
550
+ end
551
+ end
552
+
553
+ # ===== Calendar Widget =====
554
+
555
+ class Calendar < Widget
556
+ def initialize(state)
557
+ super()
558
+ @state = state
559
+ @hover_cell = -1
560
+ @width_policy = FIXED
561
+ @height_policy = FIXED
562
+ @width = 7.0 * CAL_CELL_SIZE + 16.0
563
+ @height = CAL_HEADER_HEIGHT + CAL_WEEKDAY_HEIGHT + 6.0 * CAL_CELL_SIZE + 8.0
564
+ @state.attach(self)
565
+ end
566
+
567
+ def on_attach(observable)
568
+ end
569
+
570
+ def on_detach(observable)
571
+ end
572
+
573
+ def on_notify
574
+ mark_dirty
575
+ update
576
+ end
577
+
578
+ def redraw(painter, completely)
579
+ # Background
580
+ bg = Kumiki.theme.bg_secondary
581
+ painter.fill_round_rect(0.0, 0.0, @width, @height, 8.0, bg)
582
+ # Border
583
+ bc = Kumiki.theme.border
584
+ painter.stroke_rect(0.0, 0.0, @width, @height, bc, 1.0)
585
+
586
+ draw_cal_header(painter)
587
+
588
+ mode = @state.view_mode
589
+ if mode == CAL_DAYS
590
+ draw_days_view(painter)
591
+ elsif mode == CAL_MONTHS
592
+ draw_months_view(painter)
593
+ else
594
+ draw_years_view(painter)
595
+ end
596
+ end
597
+
598
+ def draw_cal_header(painter)
599
+ # Navigation: < [Title] >
600
+ tc = Kumiki.theme.text_primary
601
+ ac = Kumiki.theme.accent
602
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 14.0)
603
+ mh = painter.measure_text_height(Kumiki.theme.font_family, 14.0)
604
+ ty = (CAL_HEADER_HEIGHT - mh) / 2.0 + ascent
605
+
606
+ # Left arrow
607
+ left_c = tc
608
+ if @hover_cell == -2
609
+ left_c = ac
610
+ end
611
+ painter.draw_text("<", 12.0, ty, Kumiki.theme.font_family, 14.0, left_c)
612
+
613
+ # Right arrow
614
+ right_c = tc
615
+ if @hover_cell == -3
616
+ right_c = ac
617
+ end
618
+ rw = painter.measure_text_width(">", Kumiki.theme.font_family, 14.0)
619
+ painter.draw_text(">", @width - 12.0 - rw, ty, Kumiki.theme.font_family, 14.0, right_c)
620
+
621
+ # Title (clickable to change view mode)
622
+ title = cal_header_title
623
+ title_c = ac
624
+ tw = painter.measure_text_width(title, Kumiki.theme.font_family, 14.0)
625
+ tx = (@width - tw) / 2.0
626
+ painter.draw_text(title, tx, ty, Kumiki.theme.font_family, 14.0, title_c)
627
+ end
628
+
629
+ def cal_header_title
630
+ mode = @state.view_mode
631
+ if mode == CAL_DAYS
632
+ return @state.header_text
633
+ end
634
+ if mode == CAL_MONTHS
635
+ return @state.view_year_label
636
+ end
637
+ @state.year_range_text
638
+ end
639
+
640
+ # ===== DAYS VIEW =====
641
+
642
+ def draw_days_view(painter)
643
+ draw_weekday_headers(painter)
644
+ draw_day_cells(painter)
645
+ end
646
+
647
+ def draw_weekday_headers(painter)
648
+ headers = ["Su", "Mo", "Tu", "We", "Th", "Fr", "Sa"]
649
+ lc = Kumiki.theme.text_secondary
650
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 11.0)
651
+ base_y = CAL_HEADER_HEIGHT + ascent + 4.0
652
+ i = 0
653
+ while i < 7
654
+ label = headers[i]
655
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 11.0)
656
+ cx = 8.0 + i * 1.0 * CAL_CELL_SIZE + CAL_CELL_SIZE / 2.0 - lw / 2.0
657
+ painter.draw_text(label, cx, base_y, Kumiki.theme.font_family, 11.0, lc)
658
+ i = i + 1
659
+ end
660
+ end
661
+
662
+ def draw_day_cells(painter)
663
+ days = @state.days_in_current_month
664
+ first_dow = @state.first_weekday
665
+ base_y = CAL_HEADER_HEIGHT + CAL_WEEKDAY_HEIGHT
666
+ # Draw 6 rows x 7 cols grid, only draw valid days
667
+ draw_day_row(painter, 0, first_dow, days, base_y)
668
+ draw_day_row(painter, 1, first_dow, days, base_y)
669
+ draw_day_row(painter, 2, first_dow, days, base_y)
670
+ draw_day_row(painter, 3, first_dow, days, base_y)
671
+ draw_day_row(painter, 4, first_dow, days, base_y)
672
+ draw_day_row(painter, 5, first_dow, days, base_y)
673
+ end
674
+
675
+ def draw_day_row(painter, row, first_dow, days, base_y)
676
+ col = 0
677
+ while col < 7
678
+ cell_idx = row * 7 + col
679
+ day = cell_idx - first_dow + 1
680
+ if day >= 1
681
+ if day <= days
682
+ cx = 8.0 + col * 1.0 * CAL_CELL_SIZE
683
+ cy = base_y + row * 1.0 * CAL_CELL_SIZE
684
+ draw_day_bg(painter, day, cell_idx, cx, cy)
685
+ draw_day_label(painter, day, cx, cy)
686
+ end
687
+ end
688
+ col = col + 1
689
+ end
690
+ end
691
+
692
+ def draw_day_bg(painter, day, cell_idx, cx, cy)
693
+ sel = is_day_selected(day)
694
+ circle_r = CAL_CELL_SIZE / 2.0 - 2.0
695
+ circle_cx = cx + CAL_CELL_SIZE / 2.0
696
+ circle_cy = cy + CAL_CELL_SIZE / 2.0
697
+ if sel
698
+ painter.fill_circle(circle_cx, circle_cy, circle_r, Kumiki.theme.accent)
699
+ elsif @hover_cell == cell_idx
700
+ hc = painter.with_alpha(Kumiki.theme.accent, 40)
701
+ painter.fill_circle(circle_cx, circle_cy, circle_r, hc)
702
+ end
703
+ end
704
+
705
+ def is_day_selected(day)
706
+ @state.is_current_day(day)
707
+ end
708
+
709
+ def draw_day_label(painter, day, cx, cy)
710
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 13.0)
711
+ mh = painter.measure_text_height(Kumiki.theme.font_family, 13.0)
712
+ label = cal_int_to_str(day)
713
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 13.0)
714
+ lx = cx + CAL_CELL_SIZE / 2.0 - lw / 2.0
715
+ ly = cy + (CAL_CELL_SIZE - mh) / 2.0 + ascent
716
+ tc = Kumiki.theme.text_primary
717
+ if is_day_selected(day)
718
+ tc = 4294967295
719
+ end
720
+ painter.draw_text(label, lx, ly, Kumiki.theme.font_family, 13.0, tc)
721
+ end
722
+
723
+ # ===== MONTHS VIEW =====
724
+
725
+ def draw_months_view(painter)
726
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 13.0)
727
+ mh = painter.measure_text_height(Kumiki.theme.font_family, 13.0)
728
+ base_y = CAL_HEADER_HEIGHT + 8.0
729
+ cell_w = (@width - 16.0) / 3.0
730
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 4.0
731
+
732
+ m = 1
733
+ while m <= 12
734
+ row = (m - 1) / 3
735
+ col = (m - 1) % 3
736
+ cx = 8.0 + col * 1.0 * cell_w
737
+ cy = base_y + row * 1.0 * cell_h
738
+
739
+ is_sel = @state.is_month_selected(m)
740
+ cell_idx = 100 + m
741
+ is_hover = (@hover_cell == cell_idx)
742
+
743
+ if is_sel
744
+ painter.fill_round_rect(cx + 2.0, cy + 2.0, cell_w - 4.0, cell_h - 4.0, 6.0, Kumiki.theme.accent)
745
+ elsif is_hover
746
+ hc = painter.with_alpha(Kumiki.theme.accent, 40)
747
+ painter.fill_round_rect(cx + 2.0, cy + 2.0, cell_w - 4.0, cell_h - 4.0, 6.0, hc)
748
+ end
749
+
750
+ label = cal_short_month_name(m)
751
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 13.0)
752
+ lx = cx + cell_w / 2.0 - lw / 2.0
753
+ ly = cy + cell_h / 2.0 - mh / 2.0 + ascent
754
+
755
+ tc = Kumiki.theme.text_primary
756
+ if is_sel
757
+ tc = 4294967295
758
+ end
759
+ painter.draw_text(label, lx, ly, Kumiki.theme.font_family, 13.0, tc)
760
+ m = m + 1
761
+ end
762
+ end
763
+
764
+ # ===== YEARS VIEW =====
765
+
766
+ def draw_years_view(painter)
767
+ ascent = painter.get_text_ascent(Kumiki.theme.font_family, 12.0)
768
+ mh = painter.measure_text_height(Kumiki.theme.font_family, 12.0)
769
+ base_y = CAL_HEADER_HEIGHT + 8.0
770
+ cell_w = (@width - 16.0) / 4.0
771
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 5.0
772
+
773
+ i = 0
774
+ while i < 20
775
+ year = @state.year_at_offset(i)
776
+ row = i / 4
777
+ col = i % 4
778
+ cx = 8.0 + col * 1.0 * cell_w
779
+ cy = base_y + row * 1.0 * cell_h
780
+
781
+ is_sel = @state.is_year_selected(year)
782
+ cell_idx = 200 + i
783
+ is_hover = (@hover_cell == cell_idx)
784
+
785
+ if is_sel
786
+ painter.fill_round_rect(cx + 2.0, cy + 2.0, cell_w - 4.0, cell_h - 4.0, 6.0, Kumiki.theme.accent)
787
+ elsif is_hover
788
+ hc = painter.with_alpha(Kumiki.theme.accent, 40)
789
+ painter.fill_round_rect(cx + 2.0, cy + 2.0, cell_w - 4.0, cell_h - 4.0, 6.0, hc)
790
+ end
791
+
792
+ label = cal_year_to_str(year)
793
+ lw = painter.measure_text_width(label, Kumiki.theme.font_family, 12.0)
794
+ lx = cx + cell_w / 2.0 - lw / 2.0
795
+ ly = cy + cell_h / 2.0 - mh / 2.0 + ascent
796
+
797
+ tc = Kumiki.theme.text_primary
798
+ if is_sel
799
+ tc = 4294967295
800
+ end
801
+ painter.draw_text(label, lx, ly, Kumiki.theme.font_family, 12.0, tc)
802
+ i = i + 1
803
+ end
804
+ end
805
+
806
+ # ===== EVENT HANDLERS =====
807
+
808
+ def mouse_up(ev)
809
+ mx = ev.pos.x
810
+ my = ev.pos.y
811
+
812
+ # Header navigation
813
+ if my < CAL_HEADER_HEIGHT
814
+ handle_header_click(mx)
815
+ return
816
+ end
817
+
818
+ mode = @state.view_mode
819
+ if mode == CAL_DAYS
820
+ handle_day_click(mx, my)
821
+ elsif mode == CAL_MONTHS
822
+ handle_month_click(mx, my)
823
+ else
824
+ handle_year_click(mx, my)
825
+ end
826
+ end
827
+
828
+ def handle_header_click(mx)
829
+ # Left arrow
830
+ if mx < CAL_NAV_BUTTON_W
831
+ mode = @state.view_mode
832
+ if mode == CAL_DAYS
833
+ @state.prev_month
834
+ elsif mode == CAL_MONTHS
835
+ @state.prev_year
836
+ else
837
+ @state.prev_year_page
838
+ end
839
+ return
840
+ end
841
+ # Right arrow
842
+ if mx > @width - CAL_NAV_BUTTON_W
843
+ mode = @state.view_mode
844
+ if mode == CAL_DAYS
845
+ @state.next_month
846
+ elsif mode == CAL_MONTHS
847
+ @state.next_year
848
+ else
849
+ @state.next_year_page
850
+ end
851
+ return
852
+ end
853
+ # Title click -> cycle view mode
854
+ mode = @state.view_mode
855
+ if mode == CAL_DAYS
856
+ @state.set_view_mode(CAL_MONTHS)
857
+ elsif mode == CAL_MONTHS
858
+ @state.set_view_mode(CAL_YEARS)
859
+ end
860
+ end
861
+
862
+ def handle_day_click(mx, my)
863
+ base_y = CAL_HEADER_HEIGHT + CAL_WEEKDAY_HEIGHT
864
+ if my >= base_y
865
+ handle_day_click_inner(mx, my, base_y)
866
+ end
867
+ end
868
+
869
+ def handle_day_click_inner(mx, my, base_y)
870
+ col = cal_floor((mx - 8.0) / CAL_CELL_SIZE)
871
+ row = cal_floor((my - base_y) / CAL_CELL_SIZE)
872
+ if col >= 0
873
+ if col <= 6
874
+ if row >= 0
875
+ if row <= 5
876
+ try_select_day(row, col)
877
+ end
878
+ end
879
+ end
880
+ end
881
+ end
882
+
883
+ def try_select_day(row, col)
884
+ @state.try_select_cell(row, col)
885
+ end
886
+
887
+ def handle_month_click(mx, my)
888
+ base_y = CAL_HEADER_HEIGHT + 8.0
889
+ cell_w = (@width - 16.0) / 3.0
890
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 4.0
891
+ col = cal_floor((mx - 8.0) / cell_w)
892
+ row = cal_floor((my - base_y) / cell_h)
893
+ if col >= 0
894
+ if col <= 2
895
+ if row >= 0
896
+ if row <= 3
897
+ month = row * 3 + col + 1
898
+ @state.select_month(month)
899
+ end
900
+ end
901
+ end
902
+ end
903
+ end
904
+
905
+ def handle_year_click(mx, my)
906
+ base_y = CAL_HEADER_HEIGHT + 8.0
907
+ cell_w = (@width - 16.0) / 4.0
908
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 5.0
909
+ col = cal_floor((mx - 8.0) / cell_w)
910
+ row = cal_floor((my - base_y) / cell_h)
911
+ if col >= 0
912
+ if col <= 3
913
+ if row >= 0
914
+ if row <= 4
915
+ i = row * 4 + col
916
+ year = @state.year_at_offset(i)
917
+ @state.select_year(year)
918
+ end
919
+ end
920
+ end
921
+ end
922
+ end
923
+
924
+ def cursor_pos(ev)
925
+ mx = ev.pos.x
926
+ my = ev.pos.y
927
+ old_hover = @hover_cell
928
+ @hover_cell = compute_hover_cell(mx, my)
929
+ if @hover_cell != old_hover
930
+ mark_dirty
931
+ update
932
+ end
933
+ end
934
+
935
+ def compute_hover_cell(mx, my)
936
+ if my < CAL_HEADER_HEIGHT
937
+ return compute_header_hover(mx)
938
+ end
939
+ mode = @state.view_mode
940
+ if mode == CAL_DAYS
941
+ return compute_day_hover(mx, my)
942
+ end
943
+ if mode == CAL_MONTHS
944
+ return compute_month_hover(mx, my)
945
+ end
946
+ compute_year_hover(mx, my)
947
+ end
948
+
949
+ def compute_header_hover(mx)
950
+ if mx < CAL_NAV_BUTTON_W
951
+ return -2
952
+ end
953
+ if mx > @width - CAL_NAV_BUTTON_W
954
+ return -3
955
+ end
956
+ -4
957
+ end
958
+
959
+ def compute_day_hover(mx, my)
960
+ base_y = CAL_HEADER_HEIGHT + CAL_WEEKDAY_HEIGHT
961
+ if my < base_y
962
+ return -1
963
+ end
964
+ col = cal_floor((mx - 8.0) / CAL_CELL_SIZE)
965
+ row = cal_floor((my - base_y) / CAL_CELL_SIZE)
966
+ if col >= 0
967
+ if col < 7
968
+ if row >= 0
969
+ if row < 6
970
+ return row * 7 + col
971
+ end
972
+ end
973
+ end
974
+ end
975
+ -1
976
+ end
977
+
978
+ def compute_month_hover(mx, my)
979
+ base_y = CAL_HEADER_HEIGHT + 8.0
980
+ cell_w = (@width - 16.0) / 3.0
981
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 4.0
982
+ col = cal_floor((mx - 8.0) / cell_w)
983
+ row = cal_floor((my - base_y) / cell_h)
984
+ if col >= 0
985
+ if col < 3
986
+ if row >= 0
987
+ if row < 4
988
+ return 100 + row * 3 + col + 1
989
+ end
990
+ end
991
+ end
992
+ end
993
+ -1
994
+ end
995
+
996
+ def compute_year_hover(mx, my)
997
+ base_y = CAL_HEADER_HEIGHT + 8.0
998
+ cell_w = (@width - 16.0) / 4.0
999
+ cell_h = (@height - CAL_HEADER_HEIGHT - 16.0) / 5.0
1000
+ col = cal_floor((mx - 8.0) / cell_w)
1001
+ row = cal_floor((my - base_y) / cell_h)
1002
+ if col >= 0
1003
+ if col < 4
1004
+ if row >= 0
1005
+ if row < 5
1006
+ return 200 + row * 4 + col
1007
+ end
1008
+ end
1009
+ end
1010
+ end
1011
+ -1
1012
+ end
1013
+
1014
+ def mouse_out
1015
+ if @hover_cell != -1
1016
+ @hover_cell = -1
1017
+ mark_dirty
1018
+ update
1019
+ end
1020
+ end
1021
+ end
1022
+
1023
+ # Top-level helper
1024
+ def Calendar(state)
1025
+ Calendar.new(state)
1026
+ end
1027
+
1028
+ end