presentar_core/
canvas.rs

1//! Canvas implementations for rendering.
2
3use crate::draw::{BoxStyle, DrawCommand, StrokeStyle, Transform2D};
4use crate::widget::{Canvas, TextStyle};
5use crate::{Color, Point, Rect};
6
7/// A Canvas implementation that records draw operations as `DrawCommand`s.
8///
9/// This is useful for:
10/// - Testing (verify what was painted)
11/// - Serialization (send commands to GPU/WASM)
12/// - Diffing (compare render outputs)
13#[derive(Debug, Default)]
14pub struct RecordingCanvas {
15    commands: Vec<DrawCommand>,
16    clip_stack: Vec<Rect>,
17    transform_stack: Vec<Transform2D>,
18}
19
20impl RecordingCanvas {
21    /// Create a new empty recording canvas.
22    #[must_use]
23    pub fn new() -> Self {
24        Self::default()
25    }
26
27    /// Get the recorded draw commands.
28    #[must_use]
29    pub fn commands(&self) -> &[DrawCommand] {
30        &self.commands
31    }
32
33    /// Take ownership of the recorded commands, clearing the canvas.
34    pub fn take_commands(&mut self) -> Vec<DrawCommand> {
35        std::mem::take(&mut self.commands)
36    }
37
38    /// Get the number of recorded commands.
39    #[must_use]
40    pub fn command_count(&self) -> usize {
41        self.commands.len()
42    }
43
44    /// Check if no commands have been recorded.
45    #[must_use]
46    pub fn is_empty(&self) -> bool {
47        self.commands.is_empty()
48    }
49
50    /// Clear all recorded commands.
51    pub fn clear(&mut self) {
52        self.commands.clear();
53        self.clip_stack.clear();
54        self.transform_stack.clear();
55    }
56
57    /// Get the current transform (identity if no transforms pushed).
58    #[must_use]
59    pub fn current_transform(&self) -> Transform2D {
60        self.transform_stack
61            .last()
62            .copied()
63            .unwrap_or_else(Transform2D::identity)
64    }
65
66    /// Get the current clip bounds (None if no clips pushed).
67    #[must_use]
68    pub fn current_clip(&self) -> Option<Rect> {
69        self.clip_stack.last().copied()
70    }
71
72    /// Get the clip stack depth.
73    #[must_use]
74    pub fn clip_depth(&self) -> usize {
75        self.clip_stack.len()
76    }
77
78    /// Get the transform stack depth.
79    #[must_use]
80    pub fn transform_depth(&self) -> usize {
81        self.transform_stack.len()
82    }
83
84    /// Add a raw draw command.
85    pub fn add_command(&mut self, command: DrawCommand) {
86        self.commands.push(command);
87    }
88
89    /// Draw a filled circle.
90    pub fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
91        self.commands
92            .push(DrawCommand::filled_circle(center, radius, color));
93    }
94
95    /// Draw a line between two points.
96    pub fn draw_line(&mut self, from: Point, to: Point, color: Color, width: f32) {
97        self.commands.push(DrawCommand::line(
98            from,
99            to,
100            StrokeStyle {
101                color,
102                width,
103                ..Default::default()
104            },
105        ));
106    }
107
108    /// Draw a path (polyline).
109    pub fn draw_path(&mut self, points: &[Point], closed: bool, color: Color, width: f32) {
110        self.commands.push(DrawCommand::Path {
111            points: points.to_vec(),
112            closed,
113            style: StrokeStyle {
114                color,
115                width,
116                ..Default::default()
117            },
118        });
119    }
120
121    /// Draw a rounded rectangle.
122    pub fn fill_rounded_rect(&mut self, rect: Rect, radius: f32, color: Color) {
123        self.commands
124            .push(DrawCommand::rounded_rect(rect, radius, color));
125    }
126}
127
128impl Canvas for RecordingCanvas {
129    fn fill_rect(&mut self, rect: Rect, color: Color) {
130        self.commands.push(DrawCommand::Rect {
131            bounds: rect,
132            radius: crate::CornerRadius::ZERO,
133            style: BoxStyle::fill(color),
134        });
135    }
136
137    fn stroke_rect(&mut self, rect: Rect, color: Color, width: f32) {
138        self.commands.push(DrawCommand::Rect {
139            bounds: rect,
140            radius: crate::CornerRadius::ZERO,
141            style: BoxStyle::stroke(StrokeStyle {
142                color,
143                width,
144                ..Default::default()
145            }),
146        });
147    }
148
149    fn draw_text(&mut self, text: &str, position: Point, style: &TextStyle) {
150        self.commands.push(DrawCommand::Text {
151            content: text.to_string(),
152            position,
153            style: style.clone(),
154        });
155    }
156
157    fn draw_line(&mut self, from: Point, to: Point, color: Color, width: f32) {
158        self.commands.push(DrawCommand::Path {
159            points: vec![from, to],
160            closed: false,
161            style: StrokeStyle {
162                color,
163                width,
164                ..Default::default()
165            },
166        });
167    }
168
169    fn fill_circle(&mut self, center: Point, radius: f32, color: Color) {
170        self.commands
171            .push(DrawCommand::filled_circle(center, radius, color));
172    }
173
174    fn stroke_circle(&mut self, center: Point, radius: f32, color: Color, width: f32) {
175        self.commands.push(DrawCommand::Circle {
176            center,
177            radius,
178            style: BoxStyle::stroke(StrokeStyle {
179                color,
180                width,
181                ..Default::default()
182            }),
183        });
184    }
185
186    fn fill_arc(
187        &mut self,
188        center: Point,
189        radius: f32,
190        start_angle: f32,
191        end_angle: f32,
192        color: Color,
193    ) {
194        self.commands.push(DrawCommand::Arc {
195            center,
196            radius,
197            start_angle,
198            end_angle,
199            color,
200        });
201    }
202
203    fn draw_path(&mut self, points: &[Point], color: Color, width: f32) {
204        self.commands.push(DrawCommand::Path {
205            points: points.to_vec(),
206            closed: false,
207            style: StrokeStyle {
208                color,
209                width,
210                ..Default::default()
211            },
212        });
213    }
214
215    fn fill_polygon(&mut self, points: &[Point], color: Color) {
216        // For filled polygons, we use a closed path
217        // A proper implementation would triangulate the polygon
218        // For now, we record the vertices
219        self.commands.push(DrawCommand::Path {
220            points: points.to_vec(),
221            closed: true,
222            style: StrokeStyle {
223                color,
224                width: 0.0, // Fill only
225                ..Default::default()
226            },
227        });
228    }
229
230    fn push_clip(&mut self, rect: Rect) {
231        self.clip_stack.push(rect);
232    }
233
234    fn pop_clip(&mut self) {
235        self.clip_stack.pop();
236    }
237
238    fn push_transform(&mut self, transform: crate::widget::Transform2D) {
239        // Convert from widget::Transform2D to draw::Transform2D
240        let draw_transform = Transform2D {
241            matrix: transform.matrix,
242        };
243        self.transform_stack.push(draw_transform);
244    }
245
246    fn pop_transform(&mut self) {
247        self.transform_stack.pop();
248    }
249}
250
251#[cfg(test)]
252mod tests {
253    use super::*;
254    use crate::widget::FontWeight;
255
256    // =========================================================================
257    // RecordingCanvas Creation Tests
258    // =========================================================================
259
260    #[test]
261    fn test_recording_canvas_new() {
262        let canvas = RecordingCanvas::new();
263        assert!(canvas.is_empty());
264        assert_eq!(canvas.command_count(), 0);
265    }
266
267    #[test]
268    fn test_recording_canvas_default() {
269        let canvas = RecordingCanvas::default();
270        assert!(canvas.is_empty());
271    }
272
273    // =========================================================================
274    // Basic Drawing Tests
275    // =========================================================================
276
277    #[test]
278    fn test_fill_rect() {
279        let mut canvas = RecordingCanvas::new();
280        canvas.fill_rect(Rect::new(10.0, 20.0, 100.0, 50.0), Color::RED);
281
282        assert_eq!(canvas.command_count(), 1);
283        match &canvas.commands()[0] {
284            DrawCommand::Rect { bounds, style, .. } => {
285                assert_eq!(bounds.x, 10.0);
286                assert_eq!(bounds.y, 20.0);
287                assert_eq!(bounds.width, 100.0);
288                assert_eq!(bounds.height, 50.0);
289                assert_eq!(style.fill, Some(Color::RED));
290            }
291            _ => panic!("Expected Rect command"),
292        }
293    }
294
295    #[test]
296    fn test_stroke_rect() {
297        let mut canvas = RecordingCanvas::new();
298        canvas.stroke_rect(Rect::new(0.0, 0.0, 50.0, 50.0), Color::BLUE, 2.0);
299
300        assert_eq!(canvas.command_count(), 1);
301        match &canvas.commands()[0] {
302            DrawCommand::Rect { style, .. } => {
303                assert!(style.fill.is_none());
304                let stroke = style.stroke.as_ref().unwrap();
305                assert_eq!(stroke.color, Color::BLUE);
306                assert_eq!(stroke.width, 2.0);
307            }
308            _ => panic!("Expected Rect command"),
309        }
310    }
311
312    #[test]
313    fn test_draw_text() {
314        let mut canvas = RecordingCanvas::new();
315        let style = TextStyle {
316            size: 14.0,
317            color: Color::BLACK,
318            weight: FontWeight::Bold,
319            ..Default::default()
320        };
321        canvas.draw_text("Hello World", Point::new(10.0, 20.0), &style);
322
323        assert_eq!(canvas.command_count(), 1);
324        match &canvas.commands()[0] {
325            DrawCommand::Text {
326                content,
327                position,
328                style: text_style,
329            } => {
330                assert_eq!(content, "Hello World");
331                assert_eq!(position.x, 10.0);
332                assert_eq!(position.y, 20.0);
333                assert_eq!(text_style.size, 14.0);
334                assert_eq!(text_style.weight, FontWeight::Bold);
335            }
336            _ => panic!("Expected Text command"),
337        }
338    }
339
340    #[test]
341    fn test_fill_circle() {
342        let mut canvas = RecordingCanvas::new();
343        canvas.fill_circle(Point::new(50.0, 50.0), 25.0, Color::GREEN);
344
345        assert_eq!(canvas.command_count(), 1);
346        match &canvas.commands()[0] {
347            DrawCommand::Circle {
348                center,
349                radius,
350                style,
351            } => {
352                assert_eq!(*center, Point::new(50.0, 50.0));
353                assert_eq!(*radius, 25.0);
354                assert_eq!(style.fill, Some(Color::GREEN));
355            }
356            _ => panic!("Expected Circle command"),
357        }
358    }
359
360    #[test]
361    fn test_draw_line() {
362        let mut canvas = RecordingCanvas::new();
363        canvas.draw_line(
364            Point::new(0.0, 0.0),
365            Point::new(100.0, 100.0),
366            Color::BLACK,
367            1.5,
368        );
369
370        assert_eq!(canvas.command_count(), 1);
371        match &canvas.commands()[0] {
372            DrawCommand::Path {
373                points,
374                closed,
375                style,
376            } => {
377                assert_eq!(points.len(), 2);
378                assert_eq!(points[0], Point::new(0.0, 0.0));
379                assert_eq!(points[1], Point::new(100.0, 100.0));
380                assert!(!closed);
381                assert_eq!(style.color, Color::BLACK);
382                assert_eq!(style.width, 1.5);
383            }
384            _ => panic!("Expected Path command"),
385        }
386    }
387
388    #[test]
389    fn test_draw_path() {
390        let mut canvas = RecordingCanvas::new();
391        let points = vec![
392            Point::new(0.0, 0.0),
393            Point::new(100.0, 0.0),
394            Point::new(50.0, 100.0),
395        ];
396        canvas.draw_path(&points, true, Color::BLUE, 2.0);
397
398        assert_eq!(canvas.command_count(), 1);
399        match &canvas.commands()[0] {
400            DrawCommand::Path {
401                points: p,
402                closed,
403                style,
404            } => {
405                assert_eq!(p.len(), 3);
406                assert!(*closed);
407                assert_eq!(style.color, Color::BLUE);
408            }
409            _ => panic!("Expected Path command"),
410        }
411    }
412
413    #[test]
414    fn test_fill_rounded_rect() {
415        let mut canvas = RecordingCanvas::new();
416        canvas.fill_rounded_rect(Rect::new(0.0, 0.0, 100.0, 50.0), 8.0, Color::WHITE);
417
418        assert_eq!(canvas.command_count(), 1);
419        match &canvas.commands()[0] {
420            DrawCommand::Rect { radius, style, .. } => {
421                assert_eq!(radius.top_left, 8.0);
422                assert!(radius.is_uniform());
423                assert_eq!(style.fill, Some(Color::WHITE));
424            }
425            _ => panic!("Expected Rect command"),
426        }
427    }
428
429    // =========================================================================
430    // Clip Stack Tests
431    // =========================================================================
432
433    #[test]
434    fn test_push_pop_clip() {
435        let mut canvas = RecordingCanvas::new();
436        assert_eq!(canvas.clip_depth(), 0);
437        assert!(canvas.current_clip().is_none());
438
439        canvas.push_clip(Rect::new(10.0, 10.0, 100.0, 100.0));
440        assert_eq!(canvas.clip_depth(), 1);
441        assert_eq!(
442            canvas.current_clip(),
443            Some(Rect::new(10.0, 10.0, 100.0, 100.0))
444        );
445
446        canvas.push_clip(Rect::new(20.0, 20.0, 50.0, 50.0));
447        assert_eq!(canvas.clip_depth(), 2);
448        assert_eq!(
449            canvas.current_clip(),
450            Some(Rect::new(20.0, 20.0, 50.0, 50.0))
451        );
452
453        canvas.pop_clip();
454        assert_eq!(canvas.clip_depth(), 1);
455        assert_eq!(
456            canvas.current_clip(),
457            Some(Rect::new(10.0, 10.0, 100.0, 100.0))
458        );
459
460        canvas.pop_clip();
461        assert_eq!(canvas.clip_depth(), 0);
462        assert!(canvas.current_clip().is_none());
463    }
464
465    // =========================================================================
466    // Transform Stack Tests
467    // =========================================================================
468
469    #[test]
470    fn test_push_pop_transform() {
471        let mut canvas = RecordingCanvas::new();
472        assert_eq!(canvas.transform_depth(), 0);
473        assert_eq!(
474            canvas.current_transform().matrix,
475            Transform2D::identity().matrix
476        );
477
478        let t1 = crate::widget::Transform2D::translate(10.0, 20.0);
479        canvas.push_transform(t1);
480        assert_eq!(canvas.transform_depth(), 1);
481        assert_eq!(canvas.current_transform().matrix[4], 10.0);
482        assert_eq!(canvas.current_transform().matrix[5], 20.0);
483
484        let t2 = crate::widget::Transform2D::scale(2.0, 2.0);
485        canvas.push_transform(t2);
486        assert_eq!(canvas.transform_depth(), 2);
487        assert_eq!(canvas.current_transform().matrix[0], 2.0);
488
489        canvas.pop_transform();
490        assert_eq!(canvas.transform_depth(), 1);
491        assert_eq!(canvas.current_transform().matrix[4], 10.0);
492
493        canvas.pop_transform();
494        assert_eq!(canvas.transform_depth(), 0);
495    }
496
497    // =========================================================================
498    // Command Management Tests
499    // =========================================================================
500
501    #[test]
502    fn test_take_commands() {
503        let mut canvas = RecordingCanvas::new();
504        canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
505        canvas.fill_rect(Rect::new(20.0, 20.0, 10.0, 10.0), Color::BLUE);
506
507        assert_eq!(canvas.command_count(), 2);
508
509        let commands = canvas.take_commands();
510        assert_eq!(commands.len(), 2);
511        assert!(canvas.is_empty());
512    }
513
514    #[test]
515    fn test_clear() {
516        let mut canvas = RecordingCanvas::new();
517        canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
518        canvas.push_clip(Rect::new(0.0, 0.0, 100.0, 100.0));
519        canvas.push_transform(crate::widget::Transform2D::translate(5.0, 5.0));
520
521        assert!(!canvas.is_empty());
522        assert_eq!(canvas.clip_depth(), 1);
523        assert_eq!(canvas.transform_depth(), 1);
524
525        canvas.clear();
526
527        assert!(canvas.is_empty());
528        assert_eq!(canvas.clip_depth(), 0);
529        assert_eq!(canvas.transform_depth(), 0);
530    }
531
532    #[test]
533    fn test_add_command() {
534        let mut canvas = RecordingCanvas::new();
535        let cmd = DrawCommand::filled_circle(Point::new(50.0, 50.0), 10.0, Color::RED);
536        canvas.add_command(cmd);
537
538        assert_eq!(canvas.command_count(), 1);
539    }
540
541    // =========================================================================
542    // Multiple Commands Tests
543    // =========================================================================
544
545    #[test]
546    fn test_multiple_commands_order() {
547        let mut canvas = RecordingCanvas::new();
548
549        canvas.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::WHITE);
550        canvas.stroke_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::BLACK, 1.0);
551        canvas.draw_text("Hello", Point::new(10.0, 50.0), &TextStyle::default());
552
553        assert_eq!(canvas.command_count(), 3);
554
555        // Verify order
556        match &canvas.commands()[0] {
557            DrawCommand::Rect { style, .. } => assert!(style.fill.is_some()),
558            _ => panic!("Expected fill rect first"),
559        }
560        match &canvas.commands()[1] {
561            DrawCommand::Rect { style, .. } => assert!(style.stroke.is_some()),
562            _ => panic!("Expected stroke rect second"),
563        }
564        match &canvas.commands()[2] {
565            DrawCommand::Text { .. } => {}
566            _ => panic!("Expected text third"),
567        }
568    }
569
570    // =========================================================================
571    // Edge Case Tests
572    // =========================================================================
573
574    #[test]
575    fn test_pop_empty_clip_stack() {
576        let mut canvas = RecordingCanvas::new();
577        canvas.pop_clip(); // Should not panic
578        assert_eq!(canvas.clip_depth(), 0);
579    }
580
581    #[test]
582    fn test_pop_empty_transform_stack() {
583        let mut canvas = RecordingCanvas::new();
584        canvas.pop_transform(); // Should not panic
585        assert_eq!(canvas.transform_depth(), 0);
586    }
587
588    #[test]
589    fn test_zero_size_rect() {
590        let mut canvas = RecordingCanvas::new();
591        canvas.fill_rect(Rect::new(10.0, 10.0, 0.0, 0.0), Color::RED);
592        assert_eq!(canvas.command_count(), 1);
593    }
594
595    #[test]
596    fn test_empty_text() {
597        let mut canvas = RecordingCanvas::new();
598        canvas.draw_text("", Point::new(0.0, 0.0), &TextStyle::default());
599        assert_eq!(canvas.command_count(), 1);
600        match &canvas.commands()[0] {
601            DrawCommand::Text { content, .. } => assert!(content.is_empty()),
602            _ => panic!("Expected Text command"),
603        }
604    }
605
606    #[test]
607    fn test_zero_radius_circle() {
608        let mut canvas = RecordingCanvas::new();
609        canvas.fill_circle(Point::new(50.0, 50.0), 0.0, Color::RED);
610        assert_eq!(canvas.command_count(), 1);
611    }
612
613    #[test]
614    fn test_empty_path() {
615        let mut canvas = RecordingCanvas::new();
616        canvas.draw_path(&[], false, Color::BLACK, 1.0);
617        assert_eq!(canvas.command_count(), 1);
618        match &canvas.commands()[0] {
619            DrawCommand::Path { points, .. } => assert!(points.is_empty()),
620            _ => panic!("Expected Path command"),
621        }
622    }
623
624    // =========================================================================
625    // Canvas Trait Implementation Tests
626    // =========================================================================
627
628    #[test]
629    fn test_canvas_draw_line() {
630        let mut canvas = RecordingCanvas::new();
631        Canvas::draw_line(
632            &mut canvas,
633            Point::new(0.0, 0.0),
634            Point::new(100.0, 100.0),
635            Color::RED,
636            2.0,
637        );
638
639        assert_eq!(canvas.command_count(), 1);
640        match &canvas.commands()[0] {
641            DrawCommand::Path { points, style, .. } => {
642                assert_eq!(points.len(), 2);
643                assert_eq!(style.color, Color::RED);
644                assert_eq!(style.width, 2.0);
645            }
646            _ => panic!("Expected Path command"),
647        }
648    }
649
650    #[test]
651    fn test_canvas_fill_circle() {
652        let mut canvas = RecordingCanvas::new();
653        Canvas::fill_circle(&mut canvas, Point::new(50.0, 50.0), 25.0, Color::GREEN);
654
655        assert_eq!(canvas.command_count(), 1);
656        match &canvas.commands()[0] {
657            DrawCommand::Circle {
658                center,
659                radius,
660                style,
661            } => {
662                assert_eq!(*center, Point::new(50.0, 50.0));
663                assert_eq!(*radius, 25.0);
664                assert_eq!(style.fill, Some(Color::GREEN));
665            }
666            _ => panic!("Expected Circle command"),
667        }
668    }
669
670    #[test]
671    fn test_canvas_stroke_circle() {
672        let mut canvas = RecordingCanvas::new();
673        Canvas::stroke_circle(&mut canvas, Point::new(50.0, 50.0), 20.0, Color::BLUE, 3.0);
674
675        assert_eq!(canvas.command_count(), 1);
676        match &canvas.commands()[0] {
677            DrawCommand::Circle { radius, style, .. } => {
678                assert_eq!(*radius, 20.0);
679                let stroke = style.stroke.as_ref().unwrap();
680                assert_eq!(stroke.color, Color::BLUE);
681                assert_eq!(stroke.width, 3.0);
682            }
683            _ => panic!("Expected Circle command"),
684        }
685    }
686
687    #[test]
688    fn test_canvas_fill_arc() {
689        let mut canvas = RecordingCanvas::new();
690        Canvas::fill_arc(
691            &mut canvas,
692            Point::new(100.0, 100.0),
693            50.0,
694            0.0,
695            std::f32::consts::PI,
696            Color::new(1.0, 0.5, 0.0, 1.0),
697        );
698
699        assert_eq!(canvas.command_count(), 1);
700        match &canvas.commands()[0] {
701            DrawCommand::Arc {
702                center,
703                radius,
704                start_angle,
705                end_angle,
706                color,
707            } => {
708                assert_eq!(*center, Point::new(100.0, 100.0));
709                assert_eq!(*radius, 50.0);
710                assert_eq!(*start_angle, 0.0);
711                assert!((end_angle - std::f32::consts::PI).abs() < 0.001);
712                assert_eq!(color.r, 1.0);
713            }
714            _ => panic!("Expected Arc command"),
715        }
716    }
717
718    #[test]
719    fn test_canvas_draw_path() {
720        let mut canvas = RecordingCanvas::new();
721        let points = [
722            Point::new(0.0, 0.0),
723            Point::new(50.0, 100.0),
724            Point::new(100.0, 0.0),
725        ];
726        Canvas::draw_path(&mut canvas, &points, Color::BLACK, 1.5);
727
728        assert_eq!(canvas.command_count(), 1);
729        match &canvas.commands()[0] {
730            DrawCommand::Path {
731                points: p,
732                closed,
733                style,
734            } => {
735                assert_eq!(p.len(), 3);
736                assert!(!closed);
737                assert_eq!(style.width, 1.5);
738            }
739            _ => panic!("Expected Path command"),
740        }
741    }
742
743    #[test]
744    fn test_canvas_fill_polygon() {
745        let mut canvas = RecordingCanvas::new();
746        let points = [
747            Point::new(0.0, 0.0),
748            Point::new(100.0, 0.0),
749            Point::new(50.0, 100.0),
750        ];
751        Canvas::fill_polygon(&mut canvas, &points, Color::BLUE);
752
753        assert_eq!(canvas.command_count(), 1);
754        match &canvas.commands()[0] {
755            DrawCommand::Path {
756                points: p,
757                closed,
758                style,
759            } => {
760                assert_eq!(p.len(), 3);
761                assert!(*closed);
762                assert_eq!(style.color, Color::BLUE);
763            }
764            _ => panic!("Expected Path command"),
765        }
766    }
767}