ranma 0.1.0

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.
@@ -0,0 +1,1082 @@
1
+ use std::borrow::Cow;
2
+ use std::cell::RefCell;
3
+ use std::collections::HashMap;
4
+ use magnus::{function, gc, method, prelude::*, Error, RHash, Ruby, TryConvert, Value};
5
+ use vello::kurbo::{Affine, Arc, BezPath, Circle, Line, PathEl, Rect, RoundedRect, Shape};
6
+ use vello::peniko::{self, Brush, Color, Fill};
7
+ use vello::Scene;
8
+
9
+ use crate::surface::RbGpuSurface;
10
+
11
+ // --- Thread-local shared resources ---
12
+
13
+ thread_local! {
14
+ static FONT_CONTEXT: RefCell<parley::FontContext> = RefCell::new(parley::FontContext::default());
15
+ static LAYOUT_CONTEXT: RefCell<parley::LayoutContext<[u8; 4]>> = RefCell::new(parley::LayoutContext::new());
16
+ static IMAGE_CACHE: RefCell<HashMap<String, CachedImage>> = RefCell::new(HashMap::new());
17
+ }
18
+
19
+ struct CachedImage {
20
+ data: peniko::ImageData,
21
+ }
22
+
23
+ // --- Color parsing ---
24
+
25
+ fn parse_color(s: &str) -> Color {
26
+ let s = s.strip_prefix('#').unwrap_or(s);
27
+ match s.len() {
28
+ 3 => {
29
+ let r = u8::from_str_radix(&s[0..1], 16).unwrap_or(0);
30
+ let g = u8::from_str_radix(&s[1..2], 16).unwrap_or(0);
31
+ let b = u8::from_str_radix(&s[2..3], 16).unwrap_or(0);
32
+ Color::from_rgba8(r * 17, g * 17, b * 17, 255)
33
+ }
34
+ 6 => {
35
+ let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0);
36
+ let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0);
37
+ let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0);
38
+ Color::from_rgba8(r, g, b, 255)
39
+ }
40
+ 8 => {
41
+ let r = u8::from_str_radix(&s[0..2], 16).unwrap_or(0);
42
+ let g = u8::from_str_radix(&s[2..4], 16).unwrap_or(0);
43
+ let b = u8::from_str_radix(&s[4..6], 16).unwrap_or(0);
44
+ let a = u8::from_str_radix(&s[6..8], 16).unwrap_or(255);
45
+ Color::from_rgba8(r, g, b, a)
46
+ }
47
+ _ => Color::from_rgba8(0, 0, 0, 255),
48
+ }
49
+ }
50
+
51
+ fn color_to_rgba8(c: &Color) -> [u8; 4] {
52
+ let rgba = c.to_rgba8();
53
+ [rgba.r, rgba.g, rgba.b, rgba.a]
54
+ }
55
+
56
+ // --- State structs ---
57
+
58
+ #[derive(Clone)]
59
+ struct ShadowState {
60
+ color: Color,
61
+ offset_x: f64,
62
+ offset_y: f64,
63
+ blur_radius: f64,
64
+ }
65
+
66
+ #[derive(Clone)]
67
+ struct PainterState {
68
+ transform: Affine,
69
+ fill_color: Color,
70
+ stroke_color: Color,
71
+ stroke_width: f64,
72
+ font_family: Option<String>,
73
+ font_size: f32,
74
+ border_radius: f64,
75
+ shadow: Option<ShadowState>,
76
+ clip_depth: usize,
77
+ }
78
+
79
+ struct PainterInner {
80
+ scene: Scene,
81
+ transform: Affine,
82
+ fill_color: Color,
83
+ stroke_color: Color,
84
+ stroke_width: f64,
85
+ font_family: Option<String>,
86
+ font_size: f32,
87
+ border_radius: f64,
88
+ shadow: Option<ShadowState>,
89
+ state_stack: Vec<PainterState>,
90
+ clip_depth: usize,
91
+ is_sub_painter: bool,
92
+ path: Option<BezPath>,
93
+ }
94
+
95
+ impl PainterInner {
96
+ fn default_fill_color() -> Color {
97
+ Color::from_rgba8(0, 0, 0, 255)
98
+ }
99
+
100
+ fn default_stroke_color() -> Color {
101
+ Color::from_rgba8(0, 0, 0, 255)
102
+ }
103
+
104
+ fn draw_shadow_for_rect(&mut self, rect: Rect) {
105
+ if let Some(ref shadow) = self.shadow {
106
+ let shadow_rect = Rect::new(
107
+ rect.x0 + shadow.offset_x,
108
+ rect.y0 + shadow.offset_y,
109
+ rect.x1 + shadow.offset_x,
110
+ rect.y1 + shadow.offset_y,
111
+ );
112
+ let transform = self.transform;
113
+ let border_radius = self.border_radius;
114
+ self.scene.draw_blurred_rounded_rect(
115
+ transform,
116
+ shadow_rect,
117
+ shadow.color,
118
+ border_radius,
119
+ shadow.blur_radius,
120
+ );
121
+ }
122
+ }
123
+ }
124
+
125
+ #[magnus::wrap(class = "Ranma::Painter", free_immediately, size)]
126
+ pub struct RbPainter {
127
+ surface: Value,
128
+ inner: RefCell<PainterInner>,
129
+ }
130
+
131
+ unsafe impl Send for RbPainter {}
132
+
133
+ impl RbPainter {
134
+ fn new(surface: Value) -> Result<Self, Error> {
135
+ gc::register_mark_object(surface);
136
+ Ok(RbPainter {
137
+ surface,
138
+ inner: RefCell::new(PainterInner {
139
+ scene: Scene::new(),
140
+ transform: Affine::IDENTITY,
141
+ fill_color: PainterInner::default_fill_color(),
142
+ stroke_color: PainterInner::default_stroke_color(),
143
+ stroke_width: 1.0,
144
+ font_family: None,
145
+ font_size: 16.0,
146
+ border_radius: 0.0,
147
+ shadow: None,
148
+ state_stack: Vec::new(),
149
+ clip_depth: 0,
150
+ is_sub_painter: false,
151
+ path: None,
152
+ }),
153
+ })
154
+ }
155
+
156
+ // --- Drawing methods ---
157
+
158
+ fn clear_all(&self, color: Option<String>) -> Result<(), Error> {
159
+ let mut inner = self.inner.borrow_mut();
160
+ inner.scene.reset();
161
+ if let Some(c) = color {
162
+ let color = parse_color(&c);
163
+ let rect = Rect::new(0.0, 0.0, 1e6, 1e6);
164
+ inner
165
+ .scene
166
+ .fill(Fill::NonZero, Affine::IDENTITY, color, None, &rect);
167
+ }
168
+ Ok(())
169
+ }
170
+
171
+ fn fill_rect(&self, x: f64, y: f64, w: f64, h: f64) -> Result<(), Error> {
172
+ let mut inner = self.inner.borrow_mut();
173
+ let rect = Rect::new(x, y, x + w, y + h);
174
+ inner.draw_shadow_for_rect(rect);
175
+ let transform = inner.transform;
176
+ let fill_color = inner.fill_color;
177
+ let border_radius = inner.border_radius;
178
+ if border_radius > 0.0 {
179
+ let rrect = RoundedRect::from_rect(rect, border_radius);
180
+ inner
181
+ .scene
182
+ .fill(Fill::NonZero, transform, fill_color, None, &rrect);
183
+ } else {
184
+ inner
185
+ .scene
186
+ .fill(Fill::NonZero, transform, fill_color, None, &rect);
187
+ }
188
+ Ok(())
189
+ }
190
+
191
+ fn stroke_rect(&self, x: f64, y: f64, w: f64, h: f64) -> Result<(), Error> {
192
+ let mut inner = self.inner.borrow_mut();
193
+ let rect = Rect::new(x, y, x + w, y + h);
194
+ let stroke = vello::kurbo::Stroke::new(inner.stroke_width);
195
+ let transform = inner.transform;
196
+ let stroke_color = inner.stroke_color;
197
+ let border_radius = inner.border_radius;
198
+ if border_radius > 0.0 {
199
+ let rrect = RoundedRect::from_rect(rect, border_radius);
200
+ inner
201
+ .scene
202
+ .stroke(&stroke, transform, stroke_color, None, &rrect);
203
+ } else {
204
+ inner
205
+ .scene
206
+ .stroke(&stroke, transform, stroke_color, None, &rect);
207
+ }
208
+ Ok(())
209
+ }
210
+
211
+ fn fill_circle(&self, cx: f64, cy: f64, r: f64) -> Result<(), Error> {
212
+ let mut inner = self.inner.borrow_mut();
213
+ let circle = Circle::new((cx, cy), r);
214
+ let transform = inner.transform;
215
+ let fill_color = inner.fill_color;
216
+ inner
217
+ .scene
218
+ .fill(Fill::NonZero, transform, fill_color, None, &circle);
219
+ Ok(())
220
+ }
221
+
222
+ fn stroke_circle(&self, cx: f64, cy: f64, r: f64) -> Result<(), Error> {
223
+ let mut inner = self.inner.borrow_mut();
224
+ let circle = Circle::new((cx, cy), r);
225
+ let stroke = vello::kurbo::Stroke::new(inner.stroke_width);
226
+ let transform = inner.transform;
227
+ let stroke_color = inner.stroke_color;
228
+ inner
229
+ .scene
230
+ .stroke(&stroke, transform, stroke_color, None, &circle);
231
+ Ok(())
232
+ }
233
+
234
+ fn fill_text(&self, text: String, x: f64, y: f64, max_width: Option<f32>) -> Result<(), Error> {
235
+ let mut inner = self.inner.borrow_mut();
236
+ let fill_color = inner.fill_color;
237
+ let fill_rgba = color_to_rgba8(&fill_color);
238
+ let font_size = inner.font_size;
239
+ let font_family = inner.font_family.clone();
240
+ let base_transform = inner.transform;
241
+
242
+ FONT_CONTEXT.with(|fc| {
243
+ LAYOUT_CONTEXT.with(|lc| {
244
+ let mut fc = fc.borrow_mut();
245
+ let mut lc = lc.borrow_mut();
246
+
247
+ let mut builder = lc.ranged_builder(&mut fc, &text, 1.0, false);
248
+ builder.push_default(parley::StyleProperty::FontSize(font_size));
249
+ builder.push_default(parley::StyleProperty::Brush(fill_rgba));
250
+ if let Some(ref family) = font_family {
251
+ builder.push_default(parley::StyleProperty::FontStack(
252
+ parley::FontStack::Source(Cow::Borrowed(family.as_str())),
253
+ ));
254
+ } else {
255
+ builder.push_default(parley::GenericFamily::SystemUi);
256
+ }
257
+
258
+ let mut layout = builder.build(&text);
259
+ let max_w = max_width.unwrap_or(f32::MAX);
260
+ layout.break_all_lines(Some(max_w));
261
+ layout.align(Some(max_w), parley::Alignment::Start, Default::default());
262
+
263
+ let transform = base_transform.then_translate((x, y).into());
264
+ for line in layout.lines() {
265
+ for item in line.items() {
266
+ let parley::PositionedLayoutItem::GlyphRun(glyph_run) = item else {
267
+ continue;
268
+ };
269
+ let run = glyph_run.run();
270
+ let font = run.font().clone();
271
+ let glyph_xform = run
272
+ .synthesis()
273
+ .skew()
274
+ .map(|angle| Affine::skew(angle.tan() as f64, 0.0));
275
+ let coords: Vec<_> = run
276
+ .normalized_coords()
277
+ .iter()
278
+ .map(|c| *c as vello::NormalizedCoord)
279
+ .collect();
280
+
281
+ inner
282
+ .scene
283
+ .draw_glyphs(&font)
284
+ .font_size(run.font_size())
285
+ .transform(transform)
286
+ .glyph_transform(glyph_xform)
287
+ .normalized_coords(&coords)
288
+ .brush(&fill_color)
289
+ .draw(
290
+ Fill::NonZero,
291
+ glyph_run.positioned_glyphs().map(|g| vello::Glyph {
292
+ id: g.id as u32,
293
+ x: g.x,
294
+ y: g.y,
295
+ }),
296
+ );
297
+ }
298
+ }
299
+ });
300
+ });
301
+ Ok(())
302
+ }
303
+
304
+ fn measure_text(&self, text: String) -> f32 {
305
+ if text.is_empty() {
306
+ return 0.0;
307
+ }
308
+
309
+ let inner = self.inner.borrow();
310
+ let font_size = inner.font_size;
311
+ let font_family = inner.font_family.clone();
312
+
313
+ FONT_CONTEXT.with(|fc| {
314
+ LAYOUT_CONTEXT.with(|lc| {
315
+ let mut fc = fc.borrow_mut();
316
+ let mut lc = lc.borrow_mut();
317
+
318
+ let mut builder = lc.ranged_builder(&mut fc, &text, 1.0, false);
319
+ builder.push_default(parley::StyleProperty::FontSize(font_size));
320
+ if let Some(ref family) = font_family {
321
+ builder.push_default(parley::StyleProperty::FontStack(
322
+ parley::FontStack::Source(Cow::Borrowed(family.as_str())),
323
+ ));
324
+ } else {
325
+ builder.push_default(parley::GenericFamily::SystemUi);
326
+ }
327
+
328
+ let mut layout = builder.build(&text);
329
+ layout.break_all_lines(None);
330
+ layout.full_width()
331
+ })
332
+ })
333
+ }
334
+
335
+ fn get_font_metrics(&self) -> RbPainterFontMetrics {
336
+ let inner = self.inner.borrow();
337
+ let font_size = inner.font_size;
338
+ let font_family = inner.font_family.clone();
339
+
340
+ FONT_CONTEXT.with(|fc| {
341
+ LAYOUT_CONTEXT.with(|lc| {
342
+ let mut fc = fc.borrow_mut();
343
+ let mut lc = lc.borrow_mut();
344
+
345
+ let sample = " ";
346
+ let mut builder = lc.ranged_builder(&mut fc, sample, 1.0, false);
347
+ builder.push_default(parley::StyleProperty::FontSize(font_size));
348
+ if let Some(ref family) = font_family {
349
+ builder.push_default(parley::StyleProperty::FontStack(
350
+ parley::FontStack::Source(Cow::Borrowed(family.as_str())),
351
+ ));
352
+ } else {
353
+ builder.push_default(parley::GenericFamily::SystemUi);
354
+ }
355
+
356
+ let mut layout = builder.build(sample);
357
+ layout.break_all_lines(None);
358
+
359
+ let result = if let Some(line) = layout.lines().next() {
360
+ let metrics = line.metrics();
361
+ RbPainterFontMetrics {
362
+ ascent: metrics.ascent,
363
+ descent: metrics.descent,
364
+ leading: metrics.leading,
365
+ height: metrics.ascent + metrics.descent + metrics.leading,
366
+ }
367
+ } else {
368
+ RbPainterFontMetrics {
369
+ ascent: font_size,
370
+ descent: 0.0,
371
+ leading: 0.0,
372
+ height: font_size,
373
+ }
374
+ };
375
+ result
376
+ })
377
+ })
378
+ }
379
+
380
+ fn draw_image(&self, path: String, x: f64, y: f64, w: f64, h: f64) -> Result<(), Error> {
381
+ let img = load_image(&path, true)?;
382
+
383
+ let img_w = img.width as f64;
384
+ let img_h = img.height as f64;
385
+
386
+ let scale_x = w / img_w;
387
+ let scale_y = h / img_h;
388
+
389
+ let mut inner = self.inner.borrow_mut();
390
+ let base_transform = inner.transform;
391
+ let transform = base_transform
392
+ .then_translate((x, y).into())
393
+ .then_scale_non_uniform(scale_x, scale_y);
394
+
395
+ let rect = Rect::new(0.0, 0.0, img_w, img_h);
396
+ let brush = peniko::ImageBrush::new(img);
397
+
398
+ inner
399
+ .scene
400
+ .fill(Fill::NonZero, transform, &Brush::Image(brush), None, &rect);
401
+
402
+ Ok(())
403
+ }
404
+
405
+ fn measure_image(&self, path: String) -> Result<(i32, i32), Error> {
406
+ let img = load_image(&path, true)?;
407
+ Ok((img.width as i32, img.height as i32))
408
+ }
409
+
410
+ fn draw_caret(&self, x: f64, y: f64, height: f64) -> Result<(), Error> {
411
+ let mut inner = self.inner.borrow_mut();
412
+ let rect = Rect::new(x, y, x + 1.0, y + height);
413
+ let transform = inner.transform;
414
+ let fill_color = inner.fill_color;
415
+ inner
416
+ .scene
417
+ .fill(Fill::NonZero, transform, fill_color, None, &rect);
418
+ Ok(())
419
+ }
420
+
421
+ fn draw_line(&self, x1: f64, y1: f64, x2: f64, y2: f64) -> Result<(), Error> {
422
+ let mut inner = self.inner.borrow_mut();
423
+ let line = Line::new((x1, y1), (x2, y2));
424
+ let stroke = vello::kurbo::Stroke::new(inner.stroke_width);
425
+ let transform = inner.transform;
426
+ let stroke_color = inner.stroke_color;
427
+ inner.scene.stroke(&stroke, transform, stroke_color, None, &line);
428
+ Ok(())
429
+ }
430
+
431
+ fn fill_arc(&self, cx: f64, cy: f64, r: f64, start_deg: f64, sweep_deg: f64) -> Result<(), Error> {
432
+ let mut inner = self.inner.borrow_mut();
433
+ let start_rad = start_deg.to_radians();
434
+ let sweep_rad = sweep_deg.to_radians();
435
+ // Build a pie-slice path: center → arc_start (line) → arc curve → close
436
+ let mut path = BezPath::new();
437
+ path.move_to((cx, cy));
438
+ let arc = Arc::new((cx, cy), (r, r), start_rad, sweep_rad, 0.0);
439
+ let mut elems = arc.path_elements(0.1).peekable();
440
+ if elems.peek().is_some() {
441
+ // First element is MoveTo(arc_start); replace with LineTo to connect from center
442
+ if let Some(PathEl::MoveTo(p)) = elems.next() {
443
+ path.line_to(p);
444
+ }
445
+ for elem in elems {
446
+ path.push(elem);
447
+ }
448
+ }
449
+ path.close_path();
450
+ let transform = inner.transform;
451
+ let fill_color = inner.fill_color;
452
+ inner.scene.fill(Fill::NonZero, transform, fill_color, None, &path);
453
+ Ok(())
454
+ }
455
+
456
+ fn stroke_arc(&self, cx: f64, cy: f64, r: f64, start_deg: f64, sweep_deg: f64) -> Result<(), Error> {
457
+ let mut inner = self.inner.borrow_mut();
458
+ let start_rad = start_deg.to_radians();
459
+ let sweep_rad = sweep_deg.to_radians();
460
+ let arc = Arc::new((cx, cy), (r, r), start_rad, sweep_rad, 0.0);
461
+ let mut path = BezPath::new();
462
+ for elem in arc.path_elements(0.1) {
463
+ path.push(elem);
464
+ }
465
+ let stroke = vello::kurbo::Stroke::new(inner.stroke_width);
466
+ let transform = inner.transform;
467
+ let stroke_color = inner.stroke_color;
468
+ inner.scene.stroke(&stroke, transform, stroke_color, None, &path);
469
+ Ok(())
470
+ }
471
+
472
+ fn fill_triangle(&self, x1: f64, y1: f64, x2: f64, y2: f64, x3: f64, y3: f64) -> Result<(), Error> {
473
+ let mut inner = self.inner.borrow_mut();
474
+ let mut path = BezPath::new();
475
+ path.move_to((x1, y1));
476
+ path.line_to((x2, y2));
477
+ path.line_to((x3, y3));
478
+ path.close_path();
479
+ let transform = inner.transform;
480
+ let fill_color = inner.fill_color;
481
+ inner.scene.fill(Fill::NonZero, transform, fill_color, None, &path);
482
+ Ok(())
483
+ }
484
+
485
+ fn begin_path(&self) -> Result<(), Error> {
486
+ let mut inner = self.inner.borrow_mut();
487
+ inner.path = Some(BezPath::new());
488
+ Ok(())
489
+ }
490
+
491
+ fn path_move_to(&self, x: f64, y: f64) -> Result<(), Error> {
492
+ let mut inner = self.inner.borrow_mut();
493
+ if let Some(ref mut path) = inner.path {
494
+ path.move_to((x, y));
495
+ }
496
+ Ok(())
497
+ }
498
+
499
+ fn path_line_to(&self, x: f64, y: f64) -> Result<(), Error> {
500
+ let mut inner = self.inner.borrow_mut();
501
+ if let Some(ref mut path) = inner.path {
502
+ path.line_to((x, y));
503
+ }
504
+ Ok(())
505
+ }
506
+
507
+ fn fill_path(&self) -> Result<(), Error> {
508
+ let mut inner = self.inner.borrow_mut();
509
+ let path_clone = inner.path.clone();
510
+ if let Some(ref path) = path_clone {
511
+ let transform = inner.transform;
512
+ let fill_color = inner.fill_color;
513
+ inner.scene.fill(Fill::NonZero, transform, fill_color, None, path);
514
+ }
515
+ Ok(())
516
+ }
517
+
518
+ fn close_fill_path(&self) -> Result<(), Error> {
519
+ let mut inner = self.inner.borrow_mut();
520
+ if let Some(ref mut path) = inner.path {
521
+ path.close_path();
522
+ }
523
+ let path_clone = inner.path.clone();
524
+ if let Some(ref path) = path_clone {
525
+ let transform = inner.transform;
526
+ let fill_color = inner.fill_color;
527
+ inner.scene.fill(Fill::NonZero, transform, fill_color, None, path);
528
+ }
529
+ Ok(())
530
+ }
531
+
532
+ fn measure_text_with_font(&self, text: String, font_family: String, font_size: f64) -> f32 {
533
+ if text.is_empty() {
534
+ return 0.0;
535
+ }
536
+ let font_size_f32 = font_size as f32;
537
+ FONT_CONTEXT.with(|fc| {
538
+ LAYOUT_CONTEXT.with(|lc| {
539
+ let mut fc = fc.borrow_mut();
540
+ let mut lc = lc.borrow_mut();
541
+ let mut builder = lc.ranged_builder(&mut fc, &text, 1.0, false);
542
+ builder.push_default(parley::StyleProperty::FontSize(font_size_f32));
543
+ if !font_family.is_empty() && font_family != "default" {
544
+ builder.push_default(parley::StyleProperty::FontStack(
545
+ parley::FontStack::Source(Cow::Owned(font_family)),
546
+ ));
547
+ } else {
548
+ builder.push_default(parley::GenericFamily::SystemUi);
549
+ }
550
+ let mut layout = builder.build(&text);
551
+ layout.break_all_lines(None);
552
+ layout.full_width()
553
+ })
554
+ })
555
+ }
556
+
557
+ fn get_font_metrics_with_font(&self, font_family: String, font_size: f64) -> RbPainterFontMetrics {
558
+ let font_size_f32 = font_size as f32;
559
+ FONT_CONTEXT.with(|fc| {
560
+ LAYOUT_CONTEXT.with(|lc| {
561
+ let mut fc = fc.borrow_mut();
562
+ let mut lc = lc.borrow_mut();
563
+ let sample = " ";
564
+ let mut builder = lc.ranged_builder(&mut fc, sample, 1.0, false);
565
+ builder.push_default(parley::StyleProperty::FontSize(font_size_f32));
566
+ if !font_family.is_empty() && font_family != "default" {
567
+ builder.push_default(parley::StyleProperty::FontStack(
568
+ parley::FontStack::Source(Cow::Owned(font_family)),
569
+ ));
570
+ } else {
571
+ builder.push_default(parley::GenericFamily::SystemUi);
572
+ }
573
+ let mut layout = builder.build(sample);
574
+ layout.break_all_lines(None);
575
+ let result = if let Some(line) = layout.lines().next() {
576
+ let metrics = line.metrics();
577
+ RbPainterFontMetrics {
578
+ ascent: metrics.ascent,
579
+ descent: metrics.descent,
580
+ leading: metrics.leading,
581
+ height: metrics.ascent + metrics.descent + metrics.leading,
582
+ }
583
+ } else {
584
+ RbPainterFontMetrics {
585
+ ascent: font_size_f32,
586
+ descent: 0.0,
587
+ leading: 0.0,
588
+ height: font_size_f32,
589
+ }
590
+ };
591
+ result
592
+ })
593
+ })
594
+ }
595
+
596
+ // --- State management ---
597
+
598
+ fn save(&self) -> Result<(), Error> {
599
+ let mut inner = self.inner.borrow_mut();
600
+ let state = PainterState {
601
+ transform: inner.transform,
602
+ fill_color: inner.fill_color,
603
+ stroke_color: inner.stroke_color,
604
+ stroke_width: inner.stroke_width,
605
+ font_family: inner.font_family.clone(),
606
+ font_size: inner.font_size,
607
+ border_radius: inner.border_radius,
608
+ shadow: inner.shadow.clone(),
609
+ clip_depth: inner.clip_depth,
610
+ };
611
+ inner.state_stack.push(state);
612
+ inner.clip_depth = 0;
613
+ Ok(())
614
+ }
615
+
616
+ fn restore(&self) -> Result<(), Error> {
617
+ let mut inner = self.inner.borrow_mut();
618
+ for _ in 0..inner.clip_depth {
619
+ inner.scene.pop_layer();
620
+ }
621
+
622
+ if let Some(state) = inner.state_stack.pop() {
623
+ inner.transform = state.transform;
624
+ inner.fill_color = state.fill_color;
625
+ inner.stroke_color = state.stroke_color;
626
+ inner.stroke_width = state.stroke_width;
627
+ inner.font_family = state.font_family;
628
+ inner.font_size = state.font_size;
629
+ inner.border_radius = state.border_radius;
630
+ inner.shadow = state.shadow;
631
+ inner.clip_depth = state.clip_depth;
632
+ }
633
+ Ok(())
634
+ }
635
+
636
+ fn translate(&self, x: f64, y: f64) -> Result<(), Error> {
637
+ let mut inner = self.inner.borrow_mut();
638
+ inner.transform = inner.transform.then_translate((x, y).into());
639
+ Ok(())
640
+ }
641
+
642
+ fn scale(&self, sx: f64, sy: f64) -> Result<(), Error> {
643
+ let mut inner = self.inner.borrow_mut();
644
+ let t = inner.transform;
645
+ inner.transform = Affine::scale_non_uniform(sx, sy) * t;
646
+ Ok(())
647
+ }
648
+
649
+ fn clip(&self, x: f64, y: f64, w: f64, h: f64) -> Result<(), Error> {
650
+ let mut inner = self.inner.borrow_mut();
651
+ let rect = Rect::new(x, y, x + w, y + h);
652
+ let transform = inner.transform;
653
+ inner
654
+ .scene
655
+ .push_clip_layer(Fill::NonZero, transform, &rect);
656
+ inner.clip_depth += 1;
657
+ Ok(())
658
+ }
659
+
660
+ fn style(&self, style: &RbPainterStyle) -> Result<(), Error> {
661
+ let mut inner = self.inner.borrow_mut();
662
+ if let Some(ref c) = style.fill_color {
663
+ inner.fill_color = parse_color(c);
664
+ }
665
+ if let Some(ref c) = style.stroke_color {
666
+ inner.stroke_color = parse_color(c);
667
+ }
668
+ if style.stroke_width >= 0.0 {
669
+ inner.stroke_width = style.stroke_width as f64;
670
+ }
671
+ if let Some(ref family) = style.font_family {
672
+ inner.font_family = Some(family.clone());
673
+ }
674
+ if style.font_size > 0.0 {
675
+ inner.font_size = style.font_size;
676
+ }
677
+ if style.border_radius >= 0.0 {
678
+ inner.border_radius = style.border_radius as f64;
679
+ }
680
+ if let Some(ref shadow) = style.shadow {
681
+ inner.shadow = Some(ShadowState {
682
+ color: parse_color(&shadow.color),
683
+ offset_x: shadow.offset_x as f64,
684
+ offset_y: shadow.offset_y as f64,
685
+ blur_radius: shadow.blur_radius as f64,
686
+ });
687
+ } else {
688
+ inner.shadow = None;
689
+ }
690
+ Ok(())
691
+ }
692
+
693
+ // --- Rendering ---
694
+
695
+ fn flush(&self) -> Result<(), Error> {
696
+ let inner = self.inner.borrow();
697
+ if inner.is_sub_painter {
698
+ return Ok(());
699
+ }
700
+ let surface = <&RbGpuSurface>::try_convert(self.surface).map_err(|_| {
701
+ Error::new(
702
+ unsafe { Ruby::get_unchecked() }.exception_runtime_error(),
703
+ "Invalid surface reference",
704
+ )
705
+ })?;
706
+
707
+ let mut unclosed_clips = inner.clip_depth;
708
+ for state in &inner.state_stack {
709
+ unclosed_clips += state.clip_depth;
710
+ }
711
+ if unclosed_clips > 0 {
712
+ let mut render_scene = inner.scene.clone();
713
+ for _ in 0..unclosed_clips {
714
+ render_scene.pop_layer();
715
+ }
716
+ surface.render_and_present(&render_scene)
717
+ } else {
718
+ surface.render_and_present(&inner.scene)
719
+ }
720
+ }
721
+
722
+ fn create_sub_painter(&self) -> Result<RbPainter, Error> {
723
+ Ok(RbPainter {
724
+ surface: self.surface,
725
+ inner: RefCell::new(PainterInner {
726
+ scene: Scene::new(),
727
+ transform: Affine::IDENTITY,
728
+ fill_color: PainterInner::default_fill_color(),
729
+ stroke_color: PainterInner::default_stroke_color(),
730
+ stroke_width: 1.0,
731
+ font_family: None,
732
+ font_size: 16.0,
733
+ border_radius: 0.0,
734
+ shadow: None,
735
+ state_stack: Vec::new(),
736
+ clip_depth: 0,
737
+ is_sub_painter: true,
738
+ path: None,
739
+ }),
740
+ })
741
+ }
742
+
743
+ fn append(&self, sub_painter: &RbPainter, x: f64, y: f64) -> Result<(), Error> {
744
+ let mut inner = self.inner.borrow_mut();
745
+ let sub_inner = sub_painter.inner.borrow();
746
+ let offset = Affine::translate((x, y));
747
+ let base_transform = inner.transform;
748
+ let combined = base_transform * offset;
749
+ let transform = if combined == Affine::IDENTITY {
750
+ None
751
+ } else {
752
+ Some(combined)
753
+ };
754
+ inner.scene.append(&sub_inner.scene, transform);
755
+ Ok(())
756
+ }
757
+
758
+ fn reset(&self) -> Result<(), Error> {
759
+ let mut inner = self.inner.borrow_mut();
760
+ inner.scene.reset();
761
+ inner.transform = Affine::IDENTITY;
762
+ inner.fill_color = PainterInner::default_fill_color();
763
+ inner.stroke_color = PainterInner::default_stroke_color();
764
+ inner.stroke_width = 1.0;
765
+ inner.font_family = None;
766
+ inner.font_size = 16.0;
767
+ inner.border_radius = 0.0;
768
+ inner.shadow = None;
769
+ inner.state_stack.clear();
770
+ inner.clip_depth = 0;
771
+ inner.path = None;
772
+ Ok(())
773
+ }
774
+ }
775
+
776
+ // --- Image loading ---
777
+
778
+ fn load_image(path: &str, use_cache: bool) -> Result<peniko::ImageData, Error> {
779
+ let ruby = unsafe { Ruby::get_unchecked() };
780
+
781
+ if use_cache {
782
+ let cached = IMAGE_CACHE.with(|cache| cache.borrow().get(path).map(|c| c.data.clone()));
783
+ if let Some(data) = cached {
784
+ return Ok(data);
785
+ }
786
+ }
787
+
788
+ let img = image::open(path)
789
+ .map_err(|e| {
790
+ Error::new(
791
+ ruby.exception_runtime_error(),
792
+ format!("Failed to load image '{}': {}", path, e),
793
+ )
794
+ })?
795
+ .into_rgba8();
796
+
797
+ let width = img.width();
798
+ let height = img.height();
799
+ let data = peniko::ImageData {
800
+ data: peniko::Blob::new(std::sync::Arc::new(img.into_raw())),
801
+ format: peniko::ImageFormat::Rgba8,
802
+ alpha_type: peniko::ImageAlphaType::Alpha,
803
+ width,
804
+ height,
805
+ };
806
+
807
+ if use_cache {
808
+ IMAGE_CACHE.with(|cache| {
809
+ cache.borrow_mut().insert(
810
+ path.to_string(),
811
+ CachedImage {
812
+ data: data.clone(),
813
+ },
814
+ );
815
+ });
816
+ }
817
+
818
+ Ok(data)
819
+ }
820
+
821
+ // --- PainterStyle ---
822
+
823
+ #[magnus::wrap(class = "Ranma::PainterStyle", free_immediately, size)]
824
+ pub struct RbPainterStyle {
825
+ fill_color: Option<String>,
826
+ stroke_color: Option<String>,
827
+ stroke_width: f32,
828
+ font_family: Option<String>,
829
+ font_size: f32,
830
+ border_radius: f32,
831
+ shadow: Option<RbPainterShadowData>,
832
+ }
833
+
834
+ unsafe impl Send for RbPainterStyle {}
835
+
836
+ #[derive(Clone)]
837
+ struct RbPainterShadowData {
838
+ color: String,
839
+ offset_x: f32,
840
+ offset_y: f32,
841
+ blur_radius: f32,
842
+ }
843
+
844
+ impl RbPainterStyle {
845
+ fn new(opts: Option<RHash>) -> Result<Self, Error> {
846
+ let ruby = unsafe { Ruby::get_unchecked() };
847
+ let mut style = RbPainterStyle {
848
+ fill_color: None,
849
+ stroke_color: None,
850
+ stroke_width: 1.0,
851
+ font_family: None,
852
+ font_size: 16.0,
853
+ border_radius: 0.0,
854
+ shadow: None,
855
+ };
856
+
857
+ if let Some(opts) = opts {
858
+ if let Some(v) = opts.get(ruby.sym_new("fill_color")) {
859
+ style.fill_color = String::try_convert(v).ok();
860
+ }
861
+ if let Some(v) = opts.get(ruby.sym_new("stroke_color")) {
862
+ style.stroke_color = String::try_convert(v).ok();
863
+ }
864
+ if let Some(v) = opts.get(ruby.sym_new("stroke_width")) {
865
+ if let Ok(w) = f64::try_convert(v) {
866
+ style.stroke_width = w as f32;
867
+ }
868
+ }
869
+ if let Some(v) = opts.get(ruby.sym_new("font_family")) {
870
+ style.font_family = String::try_convert(v).ok();
871
+ }
872
+ if let Some(v) = opts.get(ruby.sym_new("font_size")) {
873
+ if let Ok(s) = f64::try_convert(v) {
874
+ style.font_size = s as f32;
875
+ }
876
+ }
877
+ if let Some(v) = opts.get(ruby.sym_new("border_radius")) {
878
+ if let Ok(r) = f64::try_convert(v) {
879
+ style.border_radius = r as f32;
880
+ }
881
+ }
882
+ if let Some(v) = opts.get(ruby.sym_new("shadow")) {
883
+ if let Ok(shadow) = <&RbPainterShadow>::try_convert(v) {
884
+ style.shadow = Some(RbPainterShadowData {
885
+ color: shadow.color.clone(),
886
+ offset_x: shadow.offset_x,
887
+ offset_y: shadow.offset_y,
888
+ blur_radius: shadow.blur_radius,
889
+ });
890
+ }
891
+ }
892
+ }
893
+
894
+ Ok(style)
895
+ }
896
+
897
+ fn fill_color(&self) -> Option<String> {
898
+ self.fill_color.clone()
899
+ }
900
+
901
+ fn stroke_color(&self) -> Option<String> {
902
+ self.stroke_color.clone()
903
+ }
904
+
905
+ fn stroke_width(&self) -> f32 {
906
+ self.stroke_width
907
+ }
908
+
909
+ fn font_family(&self) -> Option<String> {
910
+ self.font_family.clone()
911
+ }
912
+
913
+ fn font_size(&self) -> f32 {
914
+ self.font_size
915
+ }
916
+
917
+ fn border_radius(&self) -> f32 {
918
+ self.border_radius
919
+ }
920
+ }
921
+
922
+ // --- PainterShadow ---
923
+
924
+ #[magnus::wrap(class = "Ranma::PainterShadow", free_immediately, size)]
925
+ pub struct RbPainterShadow {
926
+ color: String,
927
+ offset_x: f32,
928
+ offset_y: f32,
929
+ blur_radius: f32,
930
+ }
931
+
932
+ unsafe impl Send for RbPainterShadow {}
933
+
934
+ impl RbPainterShadow {
935
+ fn new(opts: Option<RHash>) -> Result<Self, Error> {
936
+ let ruby = unsafe { Ruby::get_unchecked() };
937
+ let mut shadow = RbPainterShadow {
938
+ color: "#00000040".to_string(),
939
+ offset_x: 0.0,
940
+ offset_y: 0.0,
941
+ blur_radius: 0.0,
942
+ };
943
+
944
+ if let Some(opts) = opts {
945
+ if let Some(v) = opts.get(ruby.sym_new("color")) {
946
+ if let Ok(c) = String::try_convert(v) {
947
+ shadow.color = c;
948
+ }
949
+ }
950
+ if let Some(v) = opts.get(ruby.sym_new("offset_x")) {
951
+ if let Ok(x) = f64::try_convert(v) {
952
+ shadow.offset_x = x as f32;
953
+ }
954
+ }
955
+ if let Some(v) = opts.get(ruby.sym_new("offset_y")) {
956
+ if let Ok(y) = f64::try_convert(v) {
957
+ shadow.offset_y = y as f32;
958
+ }
959
+ }
960
+ if let Some(v) = opts.get(ruby.sym_new("blur_radius")) {
961
+ if let Ok(r) = f64::try_convert(v) {
962
+ shadow.blur_radius = r as f32;
963
+ }
964
+ }
965
+ }
966
+
967
+ Ok(shadow)
968
+ }
969
+
970
+ fn color(&self) -> String {
971
+ self.color.clone()
972
+ }
973
+
974
+ fn offset_x(&self) -> f32 {
975
+ self.offset_x
976
+ }
977
+
978
+ fn offset_y(&self) -> f32 {
979
+ self.offset_y
980
+ }
981
+
982
+ fn blur_radius(&self) -> f32 {
983
+ self.blur_radius
984
+ }
985
+ }
986
+
987
+ // --- PainterFontMetrics ---
988
+
989
+ #[magnus::wrap(class = "Ranma::PainterFontMetrics", free_immediately, size)]
990
+ pub struct RbPainterFontMetrics {
991
+ ascent: f32,
992
+ descent: f32,
993
+ leading: f32,
994
+ height: f32,
995
+ }
996
+
997
+ unsafe impl Send for RbPainterFontMetrics {}
998
+
999
+ impl RbPainterFontMetrics {
1000
+ fn ascent(&self) -> f32 {
1001
+ self.ascent
1002
+ }
1003
+
1004
+ fn descent(&self) -> f32 {
1005
+ self.descent
1006
+ }
1007
+
1008
+ fn leading(&self) -> f32 {
1009
+ self.leading
1010
+ }
1011
+
1012
+ fn height(&self) -> f32 {
1013
+ self.height
1014
+ }
1015
+ }
1016
+
1017
+ // --- Define classes ---
1018
+
1019
+ pub fn define_painter_classes(ruby: &Ruby, module: &magnus::RModule) -> Result<(), Error> {
1020
+ // PainterShadow
1021
+ let shadow_class = module.define_class("PainterShadow", ruby.class_object())?;
1022
+ shadow_class.define_singleton_method("new", function!(RbPainterShadow::new, 1))?;
1023
+ shadow_class.define_method("color", method!(RbPainterShadow::color, 0))?;
1024
+ shadow_class.define_method("offset_x", method!(RbPainterShadow::offset_x, 0))?;
1025
+ shadow_class.define_method("offset_y", method!(RbPainterShadow::offset_y, 0))?;
1026
+ shadow_class.define_method("blur_radius", method!(RbPainterShadow::blur_radius, 0))?;
1027
+
1028
+ // PainterStyle
1029
+ let style_class = module.define_class("PainterStyle", ruby.class_object())?;
1030
+ style_class.define_singleton_method("new", function!(RbPainterStyle::new, 1))?;
1031
+ style_class.define_method("fill_color", method!(RbPainterStyle::fill_color, 0))?;
1032
+ style_class.define_method("stroke_color", method!(RbPainterStyle::stroke_color, 0))?;
1033
+ style_class.define_method("stroke_width", method!(RbPainterStyle::stroke_width, 0))?;
1034
+ style_class.define_method("font_family", method!(RbPainterStyle::font_family, 0))?;
1035
+ style_class.define_method("font_size", method!(RbPainterStyle::font_size, 0))?;
1036
+ style_class.define_method("border_radius", method!(RbPainterStyle::border_radius, 0))?;
1037
+
1038
+ // PainterFontMetrics
1039
+ let metrics_class = module.define_class("PainterFontMetrics", ruby.class_object())?;
1040
+ metrics_class.define_method("ascent", method!(RbPainterFontMetrics::ascent, 0))?;
1041
+ metrics_class.define_method("descent", method!(RbPainterFontMetrics::descent, 0))?;
1042
+ metrics_class.define_method("leading", method!(RbPainterFontMetrics::leading, 0))?;
1043
+ metrics_class.define_method("height", method!(RbPainterFontMetrics::height, 0))?;
1044
+
1045
+ // Painter
1046
+ let painter_class = module.define_class("Painter", ruby.class_object())?;
1047
+ painter_class.define_singleton_method("new", function!(RbPainter::new, 1))?;
1048
+ painter_class.define_method("clear_all", method!(RbPainter::clear_all, 1))?;
1049
+ painter_class.define_method("fill_rect", method!(RbPainter::fill_rect, 4))?;
1050
+ painter_class.define_method("stroke_rect", method!(RbPainter::stroke_rect, 4))?;
1051
+ painter_class.define_method("fill_circle", method!(RbPainter::fill_circle, 3))?;
1052
+ painter_class.define_method("stroke_circle", method!(RbPainter::stroke_circle, 3))?;
1053
+ painter_class.define_method("fill_text", method!(RbPainter::fill_text, 4))?;
1054
+ painter_class.define_method("measure_text", method!(RbPainter::measure_text, 1))?;
1055
+ painter_class.define_method("get_font_metrics", method!(RbPainter::get_font_metrics, 0))?;
1056
+ painter_class.define_method("draw_image", method!(RbPainter::draw_image, 5))?;
1057
+ painter_class.define_method("measure_image", method!(RbPainter::measure_image, 1))?;
1058
+ painter_class.define_method("draw_caret", method!(RbPainter::draw_caret, 3))?;
1059
+ painter_class.define_method("save", method!(RbPainter::save, 0))?;
1060
+ painter_class.define_method("restore", method!(RbPainter::restore, 0))?;
1061
+ painter_class.define_method("translate", method!(RbPainter::translate, 2))?;
1062
+ painter_class.define_method("scale", method!(RbPainter::scale, 2))?;
1063
+ painter_class.define_method("clip", method!(RbPainter::clip, 4))?;
1064
+ painter_class.define_method("style", method!(RbPainter::style, 1))?;
1065
+ painter_class.define_method("flush", method!(RbPainter::flush, 0))?;
1066
+ painter_class.define_method("create_sub_painter", method!(RbPainter::create_sub_painter, 0))?;
1067
+ painter_class.define_method("append", method!(RbPainter::append, 3))?;
1068
+ painter_class.define_method("reset", method!(RbPainter::reset, 0))?;
1069
+ painter_class.define_method("draw_line", method!(RbPainter::draw_line, 4))?;
1070
+ painter_class.define_method("fill_arc", method!(RbPainter::fill_arc, 5))?;
1071
+ painter_class.define_method("stroke_arc", method!(RbPainter::stroke_arc, 5))?;
1072
+ painter_class.define_method("fill_triangle", method!(RbPainter::fill_triangle, 6))?;
1073
+ painter_class.define_method("begin_path", method!(RbPainter::begin_path, 0))?;
1074
+ painter_class.define_method("path_move_to", method!(RbPainter::path_move_to, 2))?;
1075
+ painter_class.define_method("path_line_to", method!(RbPainter::path_line_to, 2))?;
1076
+ painter_class.define_method("fill_path", method!(RbPainter::fill_path, 0))?;
1077
+ painter_class.define_method("close_fill_path", method!(RbPainter::close_fill_path, 0))?;
1078
+ painter_class.define_method("measure_text_with_font", method!(RbPainter::measure_text_with_font, 3))?;
1079
+ painter_class.define_method("get_font_metrics_with_font", method!(RbPainter::get_font_metrics_with_font, 2))?;
1080
+
1081
+ Ok(())
1082
+ }