1use crate::{Color, CornerRadius, Point, Rect};
6use serde::{Deserialize, Serialize};
7
8pub type PathRef = u32;
10
11pub type TensorRef = u32;
13
14#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub enum FillRule {
17 #[default]
19 NonZero,
20 EvenOdd,
22}
23
24#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct StrokeStyle {
27 pub color: Color,
29 pub width: f32,
31 pub cap: LineCap,
33 pub join: LineJoin,
35 pub dash: Vec<f32>,
37}
38
39impl Default for StrokeStyle {
40 fn default() -> Self {
41 Self {
42 color: Color::BLACK,
43 width: 1.0,
44 cap: LineCap::Butt,
45 join: LineJoin::Miter,
46 dash: Vec::new(),
47 }
48 }
49}
50
51#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
53pub enum LineCap {
54 #[default]
56 Butt,
57 Round,
59 Square,
61}
62
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
65pub enum LineJoin {
66 #[default]
68 Miter,
69 Round,
71 Bevel,
73}
74
75#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct BoxStyle {
78 pub fill: Option<Color>,
80 pub stroke: Option<StrokeStyle>,
82 pub shadow: Option<Shadow>,
84}
85
86impl Default for BoxStyle {
87 fn default() -> Self {
88 Self {
89 fill: Some(Color::WHITE),
90 stroke: None,
91 shadow: None,
92 }
93 }
94}
95
96impl BoxStyle {
97 #[must_use]
99 pub const fn fill(color: Color) -> Self {
100 Self {
101 fill: Some(color),
102 stroke: None,
103 shadow: None,
104 }
105 }
106
107 #[must_use]
109 pub const fn stroke(style: StrokeStyle) -> Self {
110 Self {
111 fill: None,
112 stroke: Some(style),
113 shadow: None,
114 }
115 }
116
117 #[must_use]
119 pub const fn with_shadow(mut self, shadow: Shadow) -> Self {
120 self.shadow = Some(shadow);
121 self
122 }
123}
124
125#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct Shadow {
128 pub color: Color,
130 pub offset_x: f32,
132 pub offset_y: f32,
134 pub blur: f32,
136}
137
138impl Default for Shadow {
139 fn default() -> Self {
140 Self {
141 color: Color::rgba(0.0, 0.0, 0.0, 0.3),
142 offset_x: 0.0,
143 offset_y: 2.0,
144 blur: 4.0,
145 }
146 }
147}
148
149#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
151pub enum Sampling {
152 Nearest,
154 #[default]
156 Bilinear,
157 Trilinear,
159}
160
161#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
163pub struct Transform2D {
164 pub matrix: [f32; 6],
169}
170
171impl Default for Transform2D {
172 fn default() -> Self {
173 Self::identity()
174 }
175}
176
177impl Transform2D {
178 #[must_use]
180 pub const fn identity() -> Self {
181 Self {
182 matrix: [1.0, 0.0, 0.0, 1.0, 0.0, 0.0],
183 }
184 }
185
186 #[must_use]
188 pub const fn translate(x: f32, y: f32) -> Self {
189 Self {
190 matrix: [1.0, 0.0, 0.0, 1.0, x, y],
191 }
192 }
193
194 #[must_use]
196 pub const fn scale(sx: f32, sy: f32) -> Self {
197 Self {
198 matrix: [sx, 0.0, 0.0, sy, 0.0, 0.0],
199 }
200 }
201
202 #[must_use]
204 pub fn rotate(angle: f32) -> Self {
205 let cos = angle.cos();
206 let sin = angle.sin();
207 Self {
208 matrix: [cos, sin, -sin, cos, 0.0, 0.0],
209 }
210 }
211
212 #[must_use]
216 pub fn then(&self, other: &Self) -> Self {
217 let a = other.matrix;
219 let b = self.matrix;
220 Self {
221 matrix: [
222 a[0].mul_add(b[0], a[2] * b[1]),
223 a[1].mul_add(b[0], a[3] * b[1]),
224 a[0].mul_add(b[2], a[2] * b[3]),
225 a[1].mul_add(b[2], a[3] * b[3]),
226 a[0].mul_add(b[4], a[2] * b[5]) + a[4],
227 a[1].mul_add(b[4], a[3] * b[5]) + a[5],
228 ],
229 }
230 }
231
232 #[must_use]
234 pub fn apply(&self, point: Point) -> Point {
235 let m = self.matrix;
236 Point::new(
237 m[0].mul_add(point.x, m[2] * point.y) + m[4],
238 m[1].mul_add(point.x, m[3] * point.y) + m[5],
239 )
240 }
241}
242
243#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub enum DrawCommand {
246 Path {
248 points: Vec<Point>,
250 closed: bool,
252 style: StrokeStyle,
254 },
255
256 Fill {
258 path: PathRef,
260 color: Color,
262 rule: FillRule,
264 },
265
266 Rect {
268 bounds: Rect,
270 radius: CornerRadius,
272 style: BoxStyle,
274 },
275
276 Circle {
278 center: Point,
280 radius: f32,
282 style: BoxStyle,
284 },
285
286 Arc {
288 center: Point,
290 radius: f32,
292 start_angle: f32,
294 end_angle: f32,
296 color: Color,
298 },
299
300 Text {
302 content: String,
304 position: Point,
306 style: crate::widget::TextStyle,
308 },
309
310 Image {
312 tensor: TensorRef,
314 bounds: Rect,
316 sampling: Sampling,
318 },
319
320 Group {
322 children: Vec<Self>,
324 transform: Transform2D,
326 },
327
328 Clip {
330 bounds: Rect,
332 child: Box<Self>,
334 },
335
336 Opacity {
338 alpha: f32,
340 child: Box<Self>,
342 },
343}
344
345impl DrawCommand {
346 #[must_use]
348 pub const fn filled_rect(bounds: Rect, color: Color) -> Self {
349 Self::Rect {
350 bounds,
351 radius: CornerRadius::ZERO,
352 style: BoxStyle::fill(color),
353 }
354 }
355
356 #[must_use]
358 pub const fn rounded_rect(bounds: Rect, radius: f32, color: Color) -> Self {
359 Self::Rect {
360 bounds,
361 radius: CornerRadius::uniform(radius),
362 style: BoxStyle::fill(color),
363 }
364 }
365
366 #[must_use]
368 pub const fn stroked_rect(bounds: Rect, stroke: StrokeStyle) -> Self {
369 Self::Rect {
370 bounds,
371 radius: CornerRadius::ZERO,
372 style: BoxStyle::stroke(stroke),
373 }
374 }
375
376 #[must_use]
378 pub const fn filled_circle(center: Point, radius: f32, color: Color) -> Self {
379 Self::Circle {
380 center,
381 radius,
382 style: BoxStyle::fill(color),
383 }
384 }
385
386 #[must_use]
388 pub fn line(from: Point, to: Point, style: StrokeStyle) -> Self {
389 Self::Path {
390 points: vec![from, to],
391 closed: false,
392 style,
393 }
394 }
395
396 #[must_use]
398 pub fn with_transform(self, transform: Transform2D) -> Self {
399 Self::Group {
400 children: vec![self],
401 transform,
402 }
403 }
404
405 #[must_use]
407 pub fn with_opacity(self, alpha: f32) -> Self {
408 Self::Opacity {
409 alpha,
410 child: Box::new(self),
411 }
412 }
413
414 #[must_use]
416 pub fn with_clip(self, bounds: Rect) -> Self {
417 Self::Clip {
418 bounds,
419 child: Box::new(self),
420 }
421 }
422}
423
424#[cfg(test)]
425mod tests {
426 use super::*;
427
428 #[test]
433 fn test_stroke_style_default() {
434 let style = StrokeStyle::default();
435 assert_eq!(style.color, Color::BLACK);
436 assert_eq!(style.width, 1.0);
437 assert_eq!(style.cap, LineCap::Butt);
438 assert_eq!(style.join, LineJoin::Miter);
439 assert!(style.dash.is_empty());
440 }
441
442 #[test]
443 fn test_line_cap_variants() {
444 assert_eq!(LineCap::default(), LineCap::Butt);
445 let _ = LineCap::Round;
446 let _ = LineCap::Square;
447 }
448
449 #[test]
450 fn test_line_join_variants() {
451 assert_eq!(LineJoin::default(), LineJoin::Miter);
452 let _ = LineJoin::Round;
453 let _ = LineJoin::Bevel;
454 }
455
456 #[test]
461 fn test_box_style_default() {
462 let style = BoxStyle::default();
463 assert_eq!(style.fill, Some(Color::WHITE));
464 assert!(style.stroke.is_none());
465 assert!(style.shadow.is_none());
466 }
467
468 #[test]
469 fn test_box_style_fill() {
470 let style = BoxStyle::fill(Color::RED);
471 assert_eq!(style.fill, Some(Color::RED));
472 assert!(style.stroke.is_none());
473 }
474
475 #[test]
476 fn test_box_style_stroke() {
477 let stroke = StrokeStyle {
478 color: Color::BLUE,
479 width: 2.0,
480 ..Default::default()
481 };
482 let style = BoxStyle::stroke(stroke.clone());
483 assert!(style.fill.is_none());
484 assert_eq!(style.stroke, Some(stroke));
485 }
486
487 #[test]
488 fn test_box_style_with_shadow() {
489 let style = BoxStyle::fill(Color::WHITE).with_shadow(Shadow::default());
490 assert!(style.shadow.is_some());
491 }
492
493 #[test]
498 fn test_shadow_default() {
499 let shadow = Shadow::default();
500 assert_eq!(shadow.offset_x, 0.0);
501 assert_eq!(shadow.offset_y, 2.0);
502 assert_eq!(shadow.blur, 4.0);
503 }
504
505 #[test]
510 fn test_transform_identity() {
511 let t = Transform2D::identity();
512 assert_eq!(t.matrix, [1.0, 0.0, 0.0, 1.0, 0.0, 0.0]);
513 }
514
515 #[test]
516 fn test_transform_translate() {
517 let t = Transform2D::translate(10.0, 20.0);
518 let p = t.apply(Point::new(0.0, 0.0));
519 assert_eq!(p, Point::new(10.0, 20.0));
520 }
521
522 #[test]
523 fn test_transform_scale() {
524 let t = Transform2D::scale(2.0, 3.0);
525 let p = t.apply(Point::new(5.0, 10.0));
526 assert_eq!(p, Point::new(10.0, 30.0));
527 }
528
529 #[test]
530 fn test_transform_rotate_90() {
531 let t = Transform2D::rotate(std::f32::consts::FRAC_PI_2);
532 let p = t.apply(Point::new(1.0, 0.0));
533 assert!((p.x - 0.0).abs() < 0.0001);
534 assert!((p.y - 1.0).abs() < 0.0001);
535 }
536
537 #[test]
538 fn test_transform_chain() {
539 let t1 = Transform2D::translate(10.0, 0.0);
540 let t2 = Transform2D::scale(2.0, 2.0);
541 let combined = t1.then(&t2);
542 let p = combined.apply(Point::new(0.0, 0.0));
543 assert_eq!(p, Point::new(20.0, 0.0));
544 }
545
546 #[test]
551 fn test_fill_rule_default() {
552 assert_eq!(FillRule::default(), FillRule::NonZero);
553 }
554
555 #[test]
560 fn test_sampling_default() {
561 assert_eq!(Sampling::default(), Sampling::Bilinear);
562 }
563
564 #[test]
569 fn test_draw_command_filled_rect() {
570 let cmd = DrawCommand::filled_rect(Rect::new(0.0, 0.0, 100.0, 50.0), Color::RED);
571 match cmd {
572 DrawCommand::Rect {
573 bounds,
574 radius,
575 style,
576 } => {
577 assert_eq!(bounds.width, 100.0);
578 assert_eq!(bounds.height, 50.0);
579 assert!(radius.is_zero());
580 assert_eq!(style.fill, Some(Color::RED));
581 }
582 _ => panic!("Expected Rect command"),
583 }
584 }
585
586 #[test]
587 fn test_draw_command_rounded_rect() {
588 let cmd = DrawCommand::rounded_rect(Rect::new(0.0, 0.0, 100.0, 50.0), 8.0, Color::BLUE);
589 match cmd {
590 DrawCommand::Rect { radius, .. } => {
591 assert!(radius.is_uniform());
592 assert_eq!(radius.top_left, 8.0);
593 }
594 _ => panic!("Expected Rect command"),
595 }
596 }
597
598 #[test]
599 fn test_draw_command_stroked_rect() {
600 let stroke = StrokeStyle {
601 color: Color::GREEN,
602 width: 3.0,
603 ..Default::default()
604 };
605 let cmd = DrawCommand::stroked_rect(Rect::new(0.0, 0.0, 100.0, 50.0), stroke);
606 match cmd {
607 DrawCommand::Rect { style, .. } => {
608 assert!(style.fill.is_none());
609 assert!(style.stroke.is_some());
610 }
611 _ => panic!("Expected Rect command"),
612 }
613 }
614
615 #[test]
616 fn test_draw_command_filled_circle() {
617 let cmd = DrawCommand::filled_circle(Point::new(50.0, 50.0), 25.0, Color::YELLOW);
618 match cmd {
619 DrawCommand::Circle {
620 center,
621 radius,
622 style,
623 } => {
624 assert_eq!(center, Point::new(50.0, 50.0));
625 assert_eq!(radius, 25.0);
626 assert_eq!(style.fill, Some(Color::YELLOW));
627 }
628 _ => panic!("Expected Circle command"),
629 }
630 }
631
632 #[test]
633 fn test_draw_command_line() {
634 let style = StrokeStyle::default();
635 let cmd = DrawCommand::line(Point::new(0.0, 0.0), Point::new(100.0, 100.0), style);
636 match cmd {
637 DrawCommand::Path { points, closed, .. } => {
638 assert_eq!(points.len(), 2);
639 assert!(!closed);
640 }
641 _ => panic!("Expected Path command"),
642 }
643 }
644
645 #[test]
646 fn test_draw_command_with_transform() {
647 let rect = DrawCommand::filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
648 let cmd = rect.with_transform(Transform2D::translate(5.0, 5.0));
649 match cmd {
650 DrawCommand::Group {
651 children,
652 transform,
653 } => {
654 assert_eq!(children.len(), 1);
655 assert_eq!(transform.matrix[4], 5.0);
656 assert_eq!(transform.matrix[5], 5.0);
657 }
658 _ => panic!("Expected Group command"),
659 }
660 }
661
662 #[test]
663 fn test_draw_command_with_opacity() {
664 let rect = DrawCommand::filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
665 let cmd = rect.with_opacity(0.5);
666 match cmd {
667 DrawCommand::Opacity { alpha, .. } => {
668 assert_eq!(alpha, 0.5);
669 }
670 _ => panic!("Expected Opacity command"),
671 }
672 }
673
674 #[test]
675 fn test_draw_command_with_clip() {
676 let rect = DrawCommand::filled_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::RED);
677 let cmd = rect.with_clip(Rect::new(10.0, 10.0, 50.0, 50.0));
678 match cmd {
679 DrawCommand::Clip { bounds, .. } => {
680 assert_eq!(bounds.x, 10.0);
681 assert_eq!(bounds.width, 50.0);
682 }
683 _ => panic!("Expected Clip command"),
684 }
685 }
686
687 #[test]
688 fn test_draw_command_path() {
689 let cmd = DrawCommand::Path {
690 points: vec![
691 Point::new(0.0, 0.0),
692 Point::new(100.0, 0.0),
693 Point::new(50.0, 100.0),
694 ],
695 closed: true,
696 style: StrokeStyle::default(),
697 };
698 match cmd {
699 DrawCommand::Path { points, closed, .. } => {
700 assert_eq!(points.len(), 3);
701 assert!(closed);
702 }
703 _ => panic!("Expected Path command"),
704 }
705 }
706
707 #[test]
708 fn test_draw_command_text() {
709 let cmd = DrawCommand::Text {
710 content: "Hello".to_string(),
711 position: Point::new(10.0, 20.0),
712 style: crate::widget::TextStyle::default(),
713 };
714 match cmd {
715 DrawCommand::Text {
716 content, position, ..
717 } => {
718 assert_eq!(content, "Hello");
719 assert_eq!(position.x, 10.0);
720 }
721 _ => panic!("Expected Text command"),
722 }
723 }
724
725 #[test]
726 fn test_draw_command_image() {
727 let cmd = DrawCommand::Image {
728 tensor: 42,
729 bounds: Rect::new(0.0, 0.0, 200.0, 150.0),
730 sampling: Sampling::Bilinear,
731 };
732 match cmd {
733 DrawCommand::Image {
734 tensor,
735 bounds,
736 sampling,
737 } => {
738 assert_eq!(tensor, 42);
739 assert_eq!(bounds.width, 200.0);
740 assert_eq!(sampling, Sampling::Bilinear);
741 }
742 _ => panic!("Expected Image command"),
743 }
744 }
745
746 #[test]
747 fn test_draw_command_fill() {
748 let cmd = DrawCommand::Fill {
749 path: 1,
750 color: Color::GREEN,
751 rule: FillRule::EvenOdd,
752 };
753 match cmd {
754 DrawCommand::Fill { path, color, rule } => {
755 assert_eq!(path, 1);
756 assert_eq!(color, Color::GREEN);
757 assert_eq!(rule, FillRule::EvenOdd);
758 }
759 _ => panic!("Expected Fill command"),
760 }
761 }
762
763 #[test]
764 fn test_draw_command_nested_group() {
765 let inner = DrawCommand::filled_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
766 let outer = DrawCommand::Group {
767 children: vec![inner.with_transform(Transform2D::translate(5.0, 5.0))],
768 transform: Transform2D::scale(2.0, 2.0),
769 };
770 match outer {
771 DrawCommand::Group {
772 children,
773 transform,
774 } => {
775 assert_eq!(children.len(), 1);
776 assert_eq!(transform.matrix[0], 2.0);
777 }
778 _ => panic!("Expected Group command"),
779 }
780 }
781}