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,570 @@
1
+ # frozen_string_literal: true
2
+
3
+ # RanmaFrame + RanmaPainter — window and rendering backend for kumiki
4
+ #
5
+ # Separates Frame (window/events) from Painter (drawing).
6
+ # RanmaFrame#_do_redraw passes the RanmaPainter instance to the on_redraw callback.
7
+ #
8
+ # Usage:
9
+ # require "kumiki/frame_ranma"
10
+ # frame = Kumiki::RanmaFrame.new("My App", 800, 600)
11
+ # app = Kumiki::App.new(frame, widget)
12
+ # app.run
13
+
14
+ begin
15
+ require "ranma"
16
+ rescue LoadError => e
17
+ raise LoadError, "kumiki/frame_ranma requires the 'ranma' gem: #{e.message}"
18
+ end
19
+
20
+ module Kumiki
21
+ # Key ordinals used by kumiki widgets (via RANMA_KEY_MAP)
22
+ # ENTER=11, BACKSPACE=12, TAB=13, ESCAPE=17, END=21, HOME=22
23
+ # LEFT=23, UP=24, RIGHT=25, DOWN=26, DELETE=75
24
+ # A=43, C=45, V=64, X=66
25
+ RANMA_KEY_MAP = {
26
+ enter: 11, backspace: 12, tab: 13, escape: 17,
27
+ end: 21, home: 22, left: 23, up: 24, right: 25, down: 26,
28
+ delete: 75, a: 43, c: 45, v: 64, x: 66,
29
+ }.freeze
30
+
31
+ # ─── Net image download cache (shared across all RanmaPainter instances) ───
32
+ require 'tmpdir'
33
+ NET_IMG_CACHE = {}
34
+ NET_IMG_MUTEX = Mutex.new
35
+ NET_IMG_DIR = File.join(Dir.tmpdir, "kumiki_net_#{Process.pid}")
36
+ NET_IMG_HAS_NEW = [false] # set true by download thread; cleared in _do_redraw
37
+
38
+ # ─── Painter ──────────────────────────────────────────────────────────────
39
+ # Wraps Ranma::Painter and implements kumiki's painter protocol.
40
+
41
+ class RanmaPainter
42
+ def initialize(surface)
43
+ @inner = Ranma::Painter.new(surface)
44
+ # image cache: path -> integer ID
45
+ @image_store = {}
46
+ @image_path_to_id = {}
47
+ @next_image_id = 1
48
+ # font metrics cache: "family_size" -> RbPainterFontMetrics
49
+ @metrics_cache = {}
50
+ end
51
+
52
+ # --- Canvas state ---
53
+
54
+ def save = @inner.save
55
+ def restore = @inner.restore
56
+ def translate(dx, dy) = @inner.translate(dx.to_f, dy.to_f)
57
+ def scale(sx, sy) = @inner.scale(sx.to_f, sy.to_f)
58
+ def clip_rect(x, y, w, h) = @inner.clip(x.to_f, y.to_f, w.to_f, h.to_f)
59
+
60
+ def flush = @inner.flush
61
+
62
+ # --- Drawing primitives (colors are 0xAARRGGBB integers) ---
63
+
64
+ def clear(color)
65
+ @inner.clear_all(int_to_hex(color))
66
+ end
67
+
68
+ def fill_rect(x, y, w, h, color)
69
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
70
+ @inner.fill_rect(x.to_f, y.to_f, w.to_f, h.to_f)
71
+ end
72
+
73
+ def stroke_rect(x, y, w, h, color, sw)
74
+ @inner.style(Ranma::PainterStyle.new(stroke_color: int_to_hex(color), stroke_width: sw.to_f))
75
+ @inner.stroke_rect(x.to_f, y.to_f, w.to_f, h.to_f)
76
+ end
77
+
78
+ def fill_round_rect(x, y, w, h, r, color)
79
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color), border_radius: r.to_f))
80
+ @inner.fill_rect(x.to_f, y.to_f, w.to_f, h.to_f)
81
+ end
82
+
83
+ def stroke_round_rect(x, y, w, h, r, color, sw)
84
+ @inner.style(Ranma::PainterStyle.new(
85
+ stroke_color: int_to_hex(color), stroke_width: sw.to_f, border_radius: r.to_f
86
+ ))
87
+ @inner.stroke_rect(x.to_f, y.to_f, w.to_f, h.to_f)
88
+ end
89
+
90
+ def fill_circle(cx, cy, r, color)
91
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
92
+ @inner.fill_circle(cx.to_f, cy.to_f, r.to_f)
93
+ end
94
+
95
+ def stroke_circle(cx, cy, r, color, sw)
96
+ @inner.style(Ranma::PainterStyle.new(stroke_color: int_to_hex(color), stroke_width: sw.to_f))
97
+ @inner.stroke_circle(cx.to_f, cy.to_f, r.to_f)
98
+ end
99
+
100
+ def draw_line(x1, y1, x2, y2, color, w)
101
+ @inner.style(Ranma::PainterStyle.new(stroke_color: int_to_hex(color), stroke_width: w.to_f))
102
+ @inner.draw_line(x1.to_f, y1.to_f, x2.to_f, y2.to_f)
103
+ end
104
+
105
+ def fill_arc(cx, cy, r, start_angle, sweep_angle, color)
106
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
107
+ @inner.fill_arc(cx.to_f, cy.to_f, r.to_f, start_angle.to_f, sweep_angle.to_f)
108
+ end
109
+
110
+ def stroke_arc(cx, cy, r, start_angle, sweep_angle, color, sw)
111
+ @inner.style(Ranma::PainterStyle.new(stroke_color: int_to_hex(color), stroke_width: sw.to_f))
112
+ @inner.stroke_arc(cx.to_f, cy.to_f, r.to_f, start_angle.to_f, sweep_angle.to_f)
113
+ end
114
+
115
+ def draw_polyline(x1, y1, x2, y2, color, sw, _dummy)
116
+ draw_line(x1, y1, x2, y2, color, sw)
117
+ end
118
+
119
+ def fill_triangle(x1, y1, x2, y2, x3, y3, color)
120
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
121
+ @inner.fill_triangle(x1.to_f, y1.to_f, x2.to_f, y2.to_f, x3.to_f, y3.to_f)
122
+ end
123
+
124
+ # --- Text drawing ---
125
+ # y is the baseline position (Skia convention). ranma uses top, so subtract ascent.
126
+
127
+ def draw_text(text, x, y, font_family, font_size, color, *_extra)
128
+ opts = { fill_color: int_to_hex(color), font_size: font_size.to_f }
129
+ f = ranma_font(font_family)
130
+ opts[:font_family] = f if f
131
+ @inner.style(Ranma::PainterStyle.new(**opts))
132
+ top_y = y.to_f - get_ascent(font_family, font_size)
133
+ @inner.fill_text(text.to_s, x.to_f, top_y, nil)
134
+ end
135
+
136
+ # --- Text measurement ---
137
+
138
+ def measure_text_width(text, font_family, font_size)
139
+ @inner.measure_text_with_font(text.to_s, ranma_font(font_family) || "", font_size.to_f)
140
+ end
141
+
142
+ def measure_text_height(font_family, font_size)
143
+ cached_metrics(font_family, font_size).height
144
+ end
145
+
146
+ def get_text_ascent(font_family, font_size)
147
+ get_ascent(font_family, font_size)
148
+ end
149
+
150
+ # --- Path operations ---
151
+
152
+ def begin_path = @inner.begin_path
153
+ def path_move_to(x, y) = @inner.path_move_to(x.to_f, y.to_f)
154
+ def path_line_to(x, y) = @inner.path_line_to(x.to_f, y.to_f)
155
+
156
+ def close_fill_path(color)
157
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
158
+ @inner.close_fill_path
159
+ end
160
+
161
+ def fill_path(color)
162
+ @inner.style(Ranma::PainterStyle.new(fill_color: int_to_hex(color)))
163
+ @inner.fill_path
164
+ end
165
+
166
+ # --- Image operations ---
167
+
168
+ def load_image(path)
169
+ return @image_path_to_id[path] if @image_path_to_id.key?(path)
170
+ id = @next_image_id
171
+ @next_image_id += 1
172
+ @image_store[id] = path
173
+ @image_path_to_id[path] = id
174
+ id
175
+ end
176
+
177
+ def load_net_image(url)
178
+ return @image_path_to_id[url] if @image_path_to_id.key?(url)
179
+
180
+ status = NET_IMG_MUTEX.synchronize { NET_IMG_CACHE[url] }
181
+ case status
182
+ when String # download complete — register with painter
183
+ id = @next_image_id; @next_image_id += 1
184
+ @image_store[id] = status; @image_path_to_id[url] = id
185
+ return id
186
+ when :pending, :failed
187
+ return 0
188
+ end
189
+
190
+ # First request: kick off background download
191
+ NET_IMG_MUTEX.synchronize { NET_IMG_CACHE[url] = :pending }
192
+ painter = self
193
+ Thread.new do
194
+ begin
195
+ painter.send(:_download_net_image, url)
196
+ rescue Exception => e
197
+ $stderr.puts "NetImage thread error: #{e.class}: #{e}"
198
+ NET_IMG_MUTEX.synchronize { NET_IMG_CACHE[url] = :failed }
199
+ end
200
+ end
201
+ 0
202
+ rescue => e
203
+ $stderr.puts "NetImage load error: #{e}"
204
+ 0
205
+ end
206
+
207
+ def draw_image(image_id, x, y, w, h)
208
+ path = @image_store[image_id]
209
+ return unless path
210
+ begin
211
+ @inner.draw_image(path, x.to_f, y.to_f, w.to_f, h.to_f)
212
+ rescue; end
213
+ end
214
+
215
+ def get_image_width(image_id)
216
+ path = @image_store[image_id]
217
+ return 0 unless path
218
+ begin; @inner.measure_image(path)[0]; rescue; 0; end
219
+ end
220
+
221
+ def get_image_height(image_id)
222
+ path = @image_store[image_id]
223
+ return 0 unless path
224
+ begin; @inner.measure_image(path)[1]; rescue; 0; end
225
+ end
226
+
227
+ # --- Color utilities (0xAARRGGBB) ---
228
+
229
+ def interpolate_color(c1, c2, t)
230
+ a1, r1, g1, b1 = int_to_argb(c1)
231
+ a2, r2, g2, b2 = int_to_argb(c2)
232
+ argb_to_int(
233
+ lerp(a1, a2, t), lerp(r1, r2, t), lerp(g1, g2, t), lerp(b1, b2, t)
234
+ )
235
+ end
236
+
237
+ def with_alpha(color, alpha)
238
+ _a, r, g, b = int_to_argb(color)
239
+ argb_to_int((alpha * 255).to_i.clamp(0, 255), r, g, b)
240
+ end
241
+
242
+ def lighten_color(color, amount)
243
+ a, r, g, b = int_to_argb(color)
244
+ amt = (amount * 255).to_i
245
+ argb_to_int(a, (r + amt).clamp(0, 255), (g + amt).clamp(0, 255), (b + amt).clamp(0, 255))
246
+ end
247
+
248
+ def darken_color(color, amount)
249
+ a, r, g, b = int_to_argb(color)
250
+ amt = (amount * 255).to_i
251
+ argb_to_int(a, (r - amt).clamp(0, 255), (g - amt).clamp(0, 255), (b - amt).clamp(0, 255))
252
+ end
253
+
254
+ # --- Math / time (called on painter by kumiki App) ---
255
+
256
+ def math_cos(r) = Math.cos(r)
257
+ def math_sin(r) = Math.sin(r)
258
+ def math_sqrt(v) = Math.sqrt(v)
259
+ def math_atan2(y, x) = Math.atan2(y, x)
260
+ def math_abs(v) = v.abs
261
+
262
+ def current_time_millis
263
+ (Process.clock_gettime(Process::CLOCK_MONOTONIC) * 1000).to_i
264
+ end
265
+
266
+ def number_to_string(v) = v.to_s
267
+
268
+ # --- Sub-painter support (vello scene caching) ---
269
+
270
+ def supports_sub_painter? = true
271
+
272
+ def create_sub_painter
273
+ sub = RanmaPainter.allocate
274
+ sub.instance_variable_set(:@inner, @inner.create_sub_painter)
275
+ sub.instance_variable_set(:@image_store, @image_store)
276
+ sub.instance_variable_set(:@image_path_to_id, @image_path_to_id)
277
+ sub.instance_variable_set(:@next_image_id, @next_image_id)
278
+ sub.instance_variable_set(:@metrics_cache, @metrics_cache)
279
+ sub
280
+ end
281
+
282
+ def append(sub_painter, x = 0.0, y = 0.0)
283
+ @inner.append(sub_painter.instance_variable_get(:@inner), x.to_f, y.to_f)
284
+ end
285
+
286
+ def reset
287
+ @inner.reset
288
+ end
289
+
290
+ private
291
+
292
+ def ranma_font(family)
293
+ return nil if family.nil? || family.empty? || family == "default"
294
+ family
295
+ end
296
+
297
+ def get_ascent(font_family, font_size)
298
+ cached_metrics(font_family, font_size).ascent
299
+ end
300
+
301
+ def cached_metrics(font_family, font_size)
302
+ key = "#{font_family}_#{font_size}"
303
+ @metrics_cache[key] ||= @inner.get_font_metrics_with_font(
304
+ ranma_font(font_family) || "", font_size.to_f
305
+ )
306
+ end
307
+
308
+ # 0xAARRGGBB -> "#rrggbbaa"
309
+ def int_to_hex(color)
310
+ a = (color >> 24) & 0xFF
311
+ r = (color >> 16) & 0xFF
312
+ g = (color >> 8) & 0xFF
313
+ b = color & 0xFF
314
+ "#%02x%02x%02x%02x" % [r, g, b, a]
315
+ end
316
+
317
+ def int_to_argb(color)
318
+ [(color >> 24) & 0xFF, (color >> 16) & 0xFF, (color >> 8) & 0xFF, color & 0xFF]
319
+ end
320
+
321
+ def argb_to_int(a, r, g, b)
322
+ ((a & 0xFF) << 24) | ((r & 0xFF) << 16) | ((g & 0xFF) << 8) | (b & 0xFF)
323
+ end
324
+
325
+ def lerp(a, b, t)
326
+ (a + (b - a) * t).to_i.clamp(0, 255)
327
+ end
328
+
329
+ def _download_net_image(url)
330
+ require 'open-uri'; require 'fileutils'; require 'digest'; require 'uri'
331
+ FileUtils.mkdir_p(NET_IMG_DIR)
332
+ ext = File.extname(URI.parse(url).path).then { |e| e.empty? ? ".jpg" : e }
333
+ path = File.join(NET_IMG_DIR, "#{Digest::MD5.hexdigest(url)}#{ext}")
334
+ unless File.exist?(path)
335
+ URI.open(url, "rb") { |f| File.binwrite(path, f.read) }
336
+ end
337
+ NET_IMG_MUTEX.synchronize { NET_IMG_CACHE[url] = path; NET_IMG_HAS_NEW[0] = true }
338
+ Ranma::App.request_redraw
339
+ rescue => e
340
+ $stderr.puts "NetImage download failed for #{url}: #{e}"
341
+ NET_IMG_MUTEX.synchronize { NET_IMG_CACHE[url] = :failed }
342
+ end
343
+ end
344
+
345
+ # ─── Frame ────────────────────────────────────────────────────────────────
346
+ # Creates the window via Ranma::App.start and translates events into kumiki callbacks.
347
+ # Passes a RanmaPainter instance (not self) to the on_redraw callback.
348
+
349
+ class RanmaFrame
350
+ def initialize(title, width, height)
351
+ @title = title
352
+ @width = width.to_i
353
+ @height = height.to_i
354
+
355
+ # event callbacks
356
+ @on_redraw = nil
357
+ @on_mouse_down = nil
358
+ @on_mouse_up = nil
359
+ @on_cursor_pos = nil
360
+ @on_mouse_wheel = nil
361
+ @on_input_char = nil
362
+ @on_input_key = nil
363
+ @on_resize = nil
364
+ @on_ime_preedit = nil
365
+
366
+ # runtime state
367
+ @window = nil
368
+ @surface = nil
369
+ @ranma_painter = nil # created in _update_painter
370
+ @hidpi_scale = 1.0
371
+ @size = Size.new(width.to_f, height.to_f)
372
+
373
+ @last_cursor_x = 0.0
374
+ @last_cursor_y = 0.0
375
+ @mods = 0 # modifier bitmask
376
+ @in_redraw = false
377
+ @skip_redraw_requested = false
378
+ @animation_pending = false
379
+ end
380
+
381
+ attr_reader :window
382
+
383
+ # --- Callback registration ---
384
+
385
+ def on_redraw(&block) = (@on_redraw = block)
386
+ def on_mouse_down(&block) = (@on_mouse_down = block)
387
+ def on_mouse_up(&block) = (@on_mouse_up = block)
388
+ def on_cursor_pos(&block) = (@on_cursor_pos = block)
389
+ def on_mouse_wheel(&block) = (@on_mouse_wheel = block)
390
+ def on_input_char(&block) = (@on_input_char = block)
391
+ def on_input_key(&block) = (@on_input_key = block)
392
+ def on_resize(&block) = (@on_resize = block)
393
+ def on_ime_preedit(&block) = (@on_ime_preedit = block)
394
+
395
+ # --- Frame queries ---
396
+
397
+ def get_painter = @ranma_painter
398
+ def get_size = @size
399
+
400
+ def is_dark_mode
401
+ Ranma::Theme.detect == :dark
402
+ end
403
+
404
+ def post_update(_ev)
405
+ if @in_redraw
406
+ # Called from within on_redraw (e.g. animation tick) — schedule next frame.
407
+ # Set flag so :redraw_requested handler knows NOT to skip (animation is pending).
408
+ @animation_pending = true
409
+ @window&.request_redraw
410
+ else
411
+ # Called from event handler — render immediately for responsiveness
412
+ _do_redraw(false)
413
+ end
414
+ end
415
+
416
+ # --- IME / text input ---
417
+
418
+ def enable_text_input = nil # IME always active
419
+ def disable_text_input = nil
420
+
421
+ def set_ime_cursor_rect(x, y, _w, _h)
422
+ return unless @window
423
+ begin
424
+ @window.set_ime_position(Ranma::LogicalPosition.new(x.to_f, y.to_f))
425
+ rescue; end
426
+ end
427
+
428
+ # --- Clipboard ---
429
+
430
+ def get_clipboard_text
431
+ Ranma::Clipboard.get_text
432
+ rescue
433
+ ""
434
+ end
435
+
436
+ def set_clipboard_text(text)
437
+ Ranma::Clipboard.set_text(text)
438
+ rescue
439
+ nil
440
+ end
441
+
442
+ # --- Main run ---
443
+
444
+ def run
445
+ Ranma::App.start do
446
+ @window = Ranma::AppWindow.new(
447
+ title: @title,
448
+ inner_size: Ranma::LogicalSize.new(@width, @height)
449
+ )
450
+
451
+ @hidpi_scale = begin
452
+ @window.scale_factor
453
+ rescue
454
+ 1.0
455
+ end
456
+
457
+ @surface = Ranma::GpuSurface.new(@window)
458
+ _update_painter # create initial RanmaPainter
459
+
460
+ phys_w = @surface.width
461
+ phys_h = @surface.height
462
+ @size = Size.new(phys_w.to_f / @hidpi_scale, phys_h.to_f / @hidpi_scale)
463
+
464
+ @window.setup_ime_preedit
465
+ @window.on_event { |event| _handle_event(event) }
466
+ @window.visible = true
467
+ # No request_redraw here: set_visible triggers :resized which renders
468
+ # synchronously, and the OS fires :redraw_requested right after (consumed by skip).
469
+ # An explicit request_redraw would create an extra :redraw_requested that could
470
+ # interfere with the skip timing and consume the animation loop's first frame.
471
+ end
472
+ end
473
+
474
+ private
475
+
476
+ def _update_painter
477
+ @ranma_painter = RanmaPainter.new(@surface)
478
+ end
479
+
480
+ def _handle_event(event)
481
+ case event[:type]
482
+ when :close_requested
483
+ Ranma::App.exit
484
+
485
+ when :resized
486
+ phys_w = event[:width]
487
+ phys_h = event[:height]
488
+ @surface.resize(phys_w, phys_h)
489
+ @size = Size.new(phys_w.to_f / @hidpi_scale, phys_h.to_f / @hidpi_scale)
490
+ @on_resize&.call
491
+ _do_redraw(true)
492
+ @skip_redraw_requested = true # consume the OS-fired :redraw_requested that follows
493
+
494
+ when :scale_factor_changed
495
+ @hidpi_scale = event[:scale_factor].to_f
496
+ nw = event[:new_width] || (@width * @hidpi_scale).to_i
497
+ nh = event[:new_height] || (@height * @hidpi_scale).to_i
498
+ @surface.resize(nw, nh)
499
+ @size = Size.new(nw.to_f / @hidpi_scale, nh.to_f / @hidpi_scale)
500
+ _do_redraw(true)
501
+ @skip_redraw_requested = true
502
+
503
+ when :redraw_requested
504
+ skip = @skip_redraw_requested && !@animation_pending
505
+ @skip_redraw_requested = false
506
+ @animation_pending = false
507
+ _do_redraw(false) unless skip
508
+
509
+ when :modifiers_changed
510
+ @mods = 0
511
+ @mods |= 0x0001 if event[:shift]
512
+ @mods |= 0x0002 if event[:ctrl]
513
+ @mods |= 0x0004 if event[:alt]
514
+ @mods |= 0x0008 if event[:logo]
515
+
516
+ when :cursor_moved
517
+ x = event[:x].to_f / @hidpi_scale
518
+ y = event[:y].to_f / @hidpi_scale
519
+ @last_cursor_x = x
520
+ @last_cursor_y = y
521
+ @on_cursor_pos&.call(MouseEvent.new(Point.new(x, y), 0))
522
+
523
+ when :mouse_input
524
+ pos = Point.new(@last_cursor_x, @last_cursor_y)
525
+ if event[:state] == :pressed && event[:button] == :left
526
+ @on_mouse_down&.call(MouseEvent.new(pos, 0))
527
+ elsif event[:state] == :released && event[:button] == :left
528
+ @on_mouse_up&.call(MouseEvent.new(pos, 0))
529
+ end
530
+
531
+ when :mouse_wheel
532
+ pos = Point.new(@last_cursor_x, @last_cursor_y)
533
+ delta_y = event[:delta_y].to_f
534
+ @on_mouse_wheel&.call(WheelEvent.new(pos, -delta_y * 20.0))
535
+
536
+ when :keyboard_input
537
+ if event[:state] == :pressed
538
+ key_code = RANMA_KEY_MAP[event[:key_code]] || 0
539
+ @on_input_key&.call(key_code, @mods) if key_code != 0
540
+ end
541
+
542
+ when :received_ime_text
543
+ event[:text]&.each_char { |ch| @on_input_char&.call(ch) }
544
+
545
+ when :ime_preedit
546
+ cursor_pos = event[:cursor_pos] || 0
547
+ @on_ime_preedit&.call(event[:text], cursor_pos, cursor_pos)
548
+ end
549
+ end
550
+
551
+ # Renders the frame: clear -> save/scale -> callback -> restore -> flush
552
+ def _do_redraw(force_full)
553
+ return unless @ranma_painter && @on_redraw
554
+ # If a net-image download finished since last redraw, force a full repaint
555
+ # so the sub-painter cache is bypassed and the image appears immediately.
556
+ has_new = NET_IMG_MUTEX.synchronize { v = NET_IMG_HAS_NEW[0]; NET_IMG_HAS_NEW[0] = false; v }
557
+ force_full = true if has_new
558
+
559
+ @in_redraw = true
560
+ @ranma_painter.clear(Kumiki.theme.bg_canvas)
561
+ @ranma_painter.save
562
+ @ranma_painter.scale(@hidpi_scale, @hidpi_scale) if @hidpi_scale != 1.0
563
+ @on_redraw.call(@ranma_painter, force_full)
564
+ @ranma_painter.restore
565
+ @ranma_painter.flush
566
+ ensure
567
+ @in_redraw = false
568
+ end
569
+ end
570
+ end
@@ -0,0 +1,127 @@
1
+ module Kumiki
2
+ # Markdown AST node types and node class
3
+ # Used by MarkdownParser and MarkdownRenderer
4
+
5
+ # Node type constants
6
+ MD_DOCUMENT = 0
7
+ MD_HEADING = 1
8
+ MD_PARAGRAPH = 2
9
+ MD_TEXT = 3
10
+ MD_STRONG = 4
11
+ MD_EMPHASIS = 5
12
+ MD_CODE_INLINE = 6
13
+ MD_CODE_BLOCK = 7
14
+ MD_BLOCKQUOTE = 8
15
+ MD_LIST = 9
16
+ MD_LIST_ITEM = 10
17
+ MD_LINK = 11
18
+ MD_HORIZONTAL_RULE = 12
19
+ MD_SOFT_BREAK = 13
20
+ MD_STRIKETHROUGH = 14
21
+ MD_TABLE = 15
22
+ MD_TABLE_ROW = 16
23
+ MD_TABLE_CELL = 17
24
+ MD_IMAGE = 18
25
+ MD_MERMAID = 19
26
+
27
+ class MdNode
28
+ def initialize(type)
29
+ @type = type
30
+ @children = []
31
+ @content = ""
32
+ @level = 0
33
+ @language = ""
34
+ @href = ""
35
+ @ordered = false
36
+ @start_num = 1
37
+ @checked = -1
38
+ @align = 0
39
+ @is_header = false
40
+ end
41
+
42
+ def type
43
+ @type
44
+ end
45
+
46
+ def children
47
+ @children
48
+ end
49
+
50
+ def content
51
+ @content
52
+ end
53
+
54
+ def content=(v)
55
+ @content = v
56
+ end
57
+
58
+ def level
59
+ @level
60
+ end
61
+
62
+ def level=(v)
63
+ @level = v
64
+ end
65
+
66
+ def language
67
+ @language
68
+ end
69
+
70
+ def language=(v)
71
+ @language = v
72
+ end
73
+
74
+ def href
75
+ @href
76
+ end
77
+
78
+ def href=(v)
79
+ @href = v
80
+ end
81
+
82
+ def ordered
83
+ @ordered
84
+ end
85
+
86
+ def ordered=(v)
87
+ @ordered = v
88
+ end
89
+
90
+ def start_num
91
+ @start_num
92
+ end
93
+
94
+ def start_num=(v)
95
+ @start_num = v
96
+ end
97
+
98
+ def checked
99
+ @checked
100
+ end
101
+
102
+ def checked=(v)
103
+ @checked = v
104
+ end
105
+
106
+ def align
107
+ @align
108
+ end
109
+
110
+ def align=(v)
111
+ @align = v
112
+ end
113
+
114
+ def is_header
115
+ @is_header
116
+ end
117
+
118
+ def is_header=(v)
119
+ @is_header = v
120
+ end
121
+
122
+ def add_child(node)
123
+ @children.push(node)
124
+ end
125
+ end
126
+
127
+ end