presentar_core/
draw.rs

1//! Draw commands for GPU rendering.
2//!
3//! All rendering reduces to these primitives.
4
5use crate::{Color, CornerRadius, Point, Rect};
6use serde::{Deserialize, Serialize};
7
8/// Reference to a path in the path buffer.
9pub type PathRef = u32;
10
11/// Reference to a tensor in the tensor buffer.
12pub type TensorRef = u32;
13
14/// Fill rule for path filling.
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
16pub enum FillRule {
17    /// Non-zero winding rule
18    #[default]
19    NonZero,
20    /// Even-odd rule
21    EvenOdd,
22}
23
24/// Stroke style for path rendering.
25#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
26pub struct StrokeStyle {
27    /// Stroke color
28    pub color: Color,
29    /// Stroke width in pixels
30    pub width: f32,
31    /// Line cap style
32    pub cap: LineCap,
33    /// Line join style
34    pub join: LineJoin,
35    /// Dash pattern (empty = solid)
36    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/// Line cap style.
52#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
53pub enum LineCap {
54    /// Flat cap at endpoint
55    #[default]
56    Butt,
57    /// Rounded cap
58    Round,
59    /// Square cap extending beyond endpoint
60    Square,
61}
62
63/// Line join style.
64#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
65pub enum LineJoin {
66    /// Sharp corner
67    #[default]
68    Miter,
69    /// Rounded corner
70    Round,
71    /// Beveled corner
72    Bevel,
73}
74
75/// Box style for rectangles and circles.
76#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
77pub struct BoxStyle {
78    /// Fill color (None = no fill)
79    pub fill: Option<Color>,
80    /// Stroke style (None = no stroke)
81    pub stroke: Option<StrokeStyle>,
82    /// Shadow (None = no shadow)
83    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    /// Create a box with only fill color.
98    #[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    /// Create a box with only stroke.
108    #[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    /// Add a shadow to the box.
118    #[must_use]
119    pub const fn with_shadow(mut self, shadow: Shadow) -> Self {
120        self.shadow = Some(shadow);
121        self
122    }
123}
124
125/// Shadow configuration.
126#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
127pub struct Shadow {
128    /// Shadow color
129    pub color: Color,
130    /// Horizontal offset
131    pub offset_x: f32,
132    /// Vertical offset
133    pub offset_y: f32,
134    /// Blur radius
135    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/// Image sampling mode.
150#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
151pub enum Sampling {
152    /// Nearest neighbor (pixelated)
153    Nearest,
154    /// Bilinear interpolation (smooth)
155    #[default]
156    Bilinear,
157    /// Trilinear with mipmaps
158    Trilinear,
159}
160
161/// 2D transformation matrix.
162#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
163pub struct Transform2D {
164    /// Matrix elements [a, b, c, d, e, f]
165    /// | a c e |
166    /// | b d f |
167    /// | 0 0 1 |
168    pub matrix: [f32; 6],
169}
170
171impl Default for Transform2D {
172    fn default() -> Self {
173        Self::identity()
174    }
175}
176
177impl Transform2D {
178    /// Identity transformation.
179    #[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    /// Create a translation transform.
187    #[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    /// Create a scale transform.
195    #[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    /// Create a rotation transform (radians).
203    #[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    /// Chain transforms: first apply self, then apply other.
213    ///
214    /// For point p: `a.then(b).apply(p)` == `b.apply(a.apply(p))`
215    #[must_use]
216    pub fn then(&self, other: &Self) -> Self {
217        // For "first self, then other" semantics: result = other * self
218        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    /// Transform a point.
233    #[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/// Drawing primitive - all rendering reduces to these.
244#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
245pub enum DrawCommand {
246    /// Draw a path (polyline or polygon)
247    Path {
248        /// Points defining the path
249        points: Vec<Point>,
250        /// Whether the path is closed
251        closed: bool,
252        /// Stroke style
253        style: StrokeStyle,
254    },
255
256    /// Fill a path
257    Fill {
258        /// Reference to path in buffer
259        path: PathRef,
260        /// Fill color
261        color: Color,
262        /// Fill rule
263        rule: FillRule,
264    },
265
266    /// Draw a rectangle
267    Rect {
268        /// Rectangle bounds
269        bounds: Rect,
270        /// Corner radius
271        radius: CornerRadius,
272        /// Box style
273        style: BoxStyle,
274    },
275
276    /// Draw a circle
277    Circle {
278        /// Center point
279        center: Point,
280        /// Radius
281        radius: f32,
282        /// Box style
283        style: BoxStyle,
284    },
285
286    /// Draw an arc (pie slice)
287    Arc {
288        /// Center point
289        center: Point,
290        /// Radius
291        radius: f32,
292        /// Start angle in radians
293        start_angle: f32,
294        /// End angle in radians
295        end_angle: f32,
296        /// Fill color
297        color: Color,
298    },
299
300    /// Draw text
301    Text {
302        /// Text content
303        content: String,
304        /// Position
305        position: Point,
306        /// Text style
307        style: crate::widget::TextStyle,
308    },
309
310    /// Draw an image from tensor
311    Image {
312        /// Reference to tensor in buffer
313        tensor: TensorRef,
314        /// Destination bounds
315        bounds: Rect,
316        /// Sampling mode
317        sampling: Sampling,
318    },
319
320    /// Group of commands with transform
321    Group {
322        /// Child commands
323        children: Vec<Self>,
324        /// Transform to apply
325        transform: Transform2D,
326    },
327
328    /// Clip to bounds
329    Clip {
330        /// Clip bounds
331        bounds: Rect,
332        /// Child command
333        child: Box<Self>,
334    },
335
336    /// Apply opacity
337    Opacity {
338        /// Alpha value (0.0 - 1.0)
339        alpha: f32,
340        /// Child command
341        child: Box<Self>,
342    },
343}
344
345impl DrawCommand {
346    /// Create a filled rectangle.
347    #[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    /// Create a rounded rectangle.
357    #[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    /// Create a stroked rectangle.
367    #[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    /// Create a filled circle.
377    #[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    /// Create a line between two points.
387    #[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    /// Wrap in a group with transform.
397    #[must_use]
398    pub fn with_transform(self, transform: Transform2D) -> Self {
399        Self::Group {
400            children: vec![self],
401            transform,
402        }
403    }
404
405    /// Wrap with opacity.
406    #[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    /// Wrap with clip bounds.
415    #[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    // =========================================================================
429    // StrokeStyle Tests
430    // =========================================================================
431
432    #[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    // =========================================================================
457    // BoxStyle Tests
458    // =========================================================================
459
460    #[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    // =========================================================================
494    // Shadow Tests
495    // =========================================================================
496
497    #[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    // =========================================================================
506    // Transform2D Tests
507    // =========================================================================
508
509    #[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    // =========================================================================
547    // FillRule Tests
548    // =========================================================================
549
550    #[test]
551    fn test_fill_rule_default() {
552        assert_eq!(FillRule::default(), FillRule::NonZero);
553    }
554
555    // =========================================================================
556    // Sampling Tests
557    // =========================================================================
558
559    #[test]
560    fn test_sampling_default() {
561        assert_eq!(Sampling::default(), Sampling::Bilinear);
562    }
563
564    // =========================================================================
565    // DrawCommand Tests
566    // =========================================================================
567
568    #[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}