Skip to main content

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)]
252#[allow(clippy::unwrap_used, clippy::disallowed_methods)]
253mod tests {
254    use super::*;
255    use crate::widget::FontWeight;
256
257    // =========================================================================
258    // RecordingCanvas Creation Tests
259    // =========================================================================
260
261    #[test]
262    fn test_recording_canvas_new() {
263        let canvas = RecordingCanvas::new();
264        assert!(canvas.is_empty());
265        assert_eq!(canvas.command_count(), 0);
266    }
267
268    #[test]
269    fn test_recording_canvas_default() {
270        let canvas = RecordingCanvas::default();
271        assert!(canvas.is_empty());
272    }
273
274    // =========================================================================
275    // Basic Drawing Tests
276    // =========================================================================
277
278    #[test]
279    fn test_fill_rect() {
280        let mut canvas = RecordingCanvas::new();
281        canvas.fill_rect(Rect::new(10.0, 20.0, 100.0, 50.0), Color::RED);
282
283        assert_eq!(canvas.command_count(), 1);
284        match &canvas.commands()[0] {
285            DrawCommand::Rect { bounds, style, .. } => {
286                assert_eq!(bounds.x, 10.0);
287                assert_eq!(bounds.y, 20.0);
288                assert_eq!(bounds.width, 100.0);
289                assert_eq!(bounds.height, 50.0);
290                assert_eq!(style.fill, Some(Color::RED));
291            }
292            _ => panic!("Expected Rect command"),
293        }
294    }
295
296    #[test]
297    fn test_stroke_rect() {
298        let mut canvas = RecordingCanvas::new();
299        canvas.stroke_rect(Rect::new(0.0, 0.0, 50.0, 50.0), Color::BLUE, 2.0);
300
301        assert_eq!(canvas.command_count(), 1);
302        match &canvas.commands()[0] {
303            DrawCommand::Rect { style, .. } => {
304                assert!(style.fill.is_none());
305                let stroke = style.stroke.as_ref().unwrap();
306                assert_eq!(stroke.color, Color::BLUE);
307                assert_eq!(stroke.width, 2.0);
308            }
309            _ => panic!("Expected Rect command"),
310        }
311    }
312
313    #[test]
314    fn test_draw_text() {
315        let mut canvas = RecordingCanvas::new();
316        let style = TextStyle {
317            size: 14.0,
318            color: Color::BLACK,
319            weight: FontWeight::Bold,
320            ..Default::default()
321        };
322        canvas.draw_text("Hello World", Point::new(10.0, 20.0), &style);
323
324        assert_eq!(canvas.command_count(), 1);
325        match &canvas.commands()[0] {
326            DrawCommand::Text {
327                content,
328                position,
329                style: text_style,
330            } => {
331                assert_eq!(content, "Hello World");
332                assert_eq!(position.x, 10.0);
333                assert_eq!(position.y, 20.0);
334                assert_eq!(text_style.size, 14.0);
335                assert_eq!(text_style.weight, FontWeight::Bold);
336            }
337            _ => panic!("Expected Text command"),
338        }
339    }
340
341    #[test]
342    fn test_fill_circle() {
343        let mut canvas = RecordingCanvas::new();
344        canvas.fill_circle(Point::new(50.0, 50.0), 25.0, Color::GREEN);
345
346        assert_eq!(canvas.command_count(), 1);
347        match &canvas.commands()[0] {
348            DrawCommand::Circle {
349                center,
350                radius,
351                style,
352            } => {
353                assert_eq!(*center, Point::new(50.0, 50.0));
354                assert_eq!(*radius, 25.0);
355                assert_eq!(style.fill, Some(Color::GREEN));
356            }
357            _ => panic!("Expected Circle command"),
358        }
359    }
360
361    #[test]
362    fn test_draw_line() {
363        let mut canvas = RecordingCanvas::new();
364        canvas.draw_line(
365            Point::new(0.0, 0.0),
366            Point::new(100.0, 100.0),
367            Color::BLACK,
368            1.5,
369        );
370
371        assert_eq!(canvas.command_count(), 1);
372        match &canvas.commands()[0] {
373            DrawCommand::Path {
374                points,
375                closed,
376                style,
377            } => {
378                assert_eq!(points.len(), 2);
379                assert_eq!(points[0], Point::new(0.0, 0.0));
380                assert_eq!(points[1], Point::new(100.0, 100.0));
381                assert!(!closed);
382                assert_eq!(style.color, Color::BLACK);
383                assert_eq!(style.width, 1.5);
384            }
385            _ => panic!("Expected Path command"),
386        }
387    }
388
389    #[test]
390    fn test_draw_path() {
391        let mut canvas = RecordingCanvas::new();
392        let points = vec![
393            Point::new(0.0, 0.0),
394            Point::new(100.0, 0.0),
395            Point::new(50.0, 100.0),
396        ];
397        canvas.draw_path(&points, true, Color::BLUE, 2.0);
398
399        assert_eq!(canvas.command_count(), 1);
400        match &canvas.commands()[0] {
401            DrawCommand::Path {
402                points: p,
403                closed,
404                style,
405            } => {
406                assert_eq!(p.len(), 3);
407                assert!(*closed);
408                assert_eq!(style.color, Color::BLUE);
409            }
410            _ => panic!("Expected Path command"),
411        }
412    }
413
414    #[test]
415    fn test_fill_rounded_rect() {
416        let mut canvas = RecordingCanvas::new();
417        canvas.fill_rounded_rect(Rect::new(0.0, 0.0, 100.0, 50.0), 8.0, Color::WHITE);
418
419        assert_eq!(canvas.command_count(), 1);
420        match &canvas.commands()[0] {
421            DrawCommand::Rect { radius, style, .. } => {
422                assert_eq!(radius.top_left, 8.0);
423                assert!(radius.is_uniform());
424                assert_eq!(style.fill, Some(Color::WHITE));
425            }
426            _ => panic!("Expected Rect command"),
427        }
428    }
429
430    // =========================================================================
431    // Clip Stack Tests
432    // =========================================================================
433
434    #[test]
435    fn test_push_pop_clip() {
436        let mut canvas = RecordingCanvas::new();
437        assert_eq!(canvas.clip_depth(), 0);
438        assert!(canvas.current_clip().is_none());
439
440        canvas.push_clip(Rect::new(10.0, 10.0, 100.0, 100.0));
441        assert_eq!(canvas.clip_depth(), 1);
442        assert_eq!(
443            canvas.current_clip(),
444            Some(Rect::new(10.0, 10.0, 100.0, 100.0))
445        );
446
447        canvas.push_clip(Rect::new(20.0, 20.0, 50.0, 50.0));
448        assert_eq!(canvas.clip_depth(), 2);
449        assert_eq!(
450            canvas.current_clip(),
451            Some(Rect::new(20.0, 20.0, 50.0, 50.0))
452        );
453
454        canvas.pop_clip();
455        assert_eq!(canvas.clip_depth(), 1);
456        assert_eq!(
457            canvas.current_clip(),
458            Some(Rect::new(10.0, 10.0, 100.0, 100.0))
459        );
460
461        canvas.pop_clip();
462        assert_eq!(canvas.clip_depth(), 0);
463        assert!(canvas.current_clip().is_none());
464    }
465
466    // =========================================================================
467    // Transform Stack Tests
468    // =========================================================================
469
470    #[test]
471    fn test_push_pop_transform() {
472        let mut canvas = RecordingCanvas::new();
473        assert_eq!(canvas.transform_depth(), 0);
474        assert_eq!(
475            canvas.current_transform().matrix,
476            Transform2D::identity().matrix
477        );
478
479        let t1 = crate::widget::Transform2D::translate(10.0, 20.0);
480        canvas.push_transform(t1);
481        assert_eq!(canvas.transform_depth(), 1);
482        assert_eq!(canvas.current_transform().matrix[4], 10.0);
483        assert_eq!(canvas.current_transform().matrix[5], 20.0);
484
485        let t2 = crate::widget::Transform2D::scale(2.0, 2.0);
486        canvas.push_transform(t2);
487        assert_eq!(canvas.transform_depth(), 2);
488        assert_eq!(canvas.current_transform().matrix[0], 2.0);
489
490        canvas.pop_transform();
491        assert_eq!(canvas.transform_depth(), 1);
492        assert_eq!(canvas.current_transform().matrix[4], 10.0);
493
494        canvas.pop_transform();
495        assert_eq!(canvas.transform_depth(), 0);
496    }
497
498    // =========================================================================
499    // Command Management Tests
500    // =========================================================================
501
502    #[test]
503    fn test_take_commands() {
504        let mut canvas = RecordingCanvas::new();
505        canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
506        canvas.fill_rect(Rect::new(20.0, 20.0, 10.0, 10.0), Color::BLUE);
507
508        assert_eq!(canvas.command_count(), 2);
509
510        let commands = canvas.take_commands();
511        assert_eq!(commands.len(), 2);
512        assert!(canvas.is_empty());
513    }
514
515    #[test]
516    fn test_clear() {
517        let mut canvas = RecordingCanvas::new();
518        canvas.fill_rect(Rect::new(0.0, 0.0, 10.0, 10.0), Color::RED);
519        canvas.push_clip(Rect::new(0.0, 0.0, 100.0, 100.0));
520        canvas.push_transform(crate::widget::Transform2D::translate(5.0, 5.0));
521
522        assert!(!canvas.is_empty());
523        assert_eq!(canvas.clip_depth(), 1);
524        assert_eq!(canvas.transform_depth(), 1);
525
526        canvas.clear();
527
528        assert!(canvas.is_empty());
529        assert_eq!(canvas.clip_depth(), 0);
530        assert_eq!(canvas.transform_depth(), 0);
531    }
532
533    #[test]
534    fn test_add_command() {
535        let mut canvas = RecordingCanvas::new();
536        let cmd = DrawCommand::filled_circle(Point::new(50.0, 50.0), 10.0, Color::RED);
537        canvas.add_command(cmd);
538
539        assert_eq!(canvas.command_count(), 1);
540    }
541
542    // =========================================================================
543    // Multiple Commands Tests
544    // =========================================================================
545
546    #[test]
547    fn test_multiple_commands_order() {
548        let mut canvas = RecordingCanvas::new();
549
550        canvas.fill_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::WHITE);
551        canvas.stroke_rect(Rect::new(0.0, 0.0, 100.0, 100.0), Color::BLACK, 1.0);
552        canvas.draw_text("Hello", Point::new(10.0, 50.0), &TextStyle::default());
553
554        assert_eq!(canvas.command_count(), 3);
555
556        // Verify order
557        match &canvas.commands()[0] {
558            DrawCommand::Rect { style, .. } => assert!(style.fill.is_some()),
559            _ => panic!("Expected fill rect first"),
560        }
561        match &canvas.commands()[1] {
562            DrawCommand::Rect { style, .. } => assert!(style.stroke.is_some()),
563            _ => panic!("Expected stroke rect second"),
564        }
565        match &canvas.commands()[2] {
566            DrawCommand::Text { .. } => {}
567            _ => panic!("Expected text third"),
568        }
569    }
570
571    // =========================================================================
572    // Edge Case Tests
573    // =========================================================================
574
575    #[test]
576    fn test_pop_empty_clip_stack() {
577        let mut canvas = RecordingCanvas::new();
578        canvas.pop_clip(); // Should not panic
579        assert_eq!(canvas.clip_depth(), 0);
580    }
581
582    #[test]
583    fn test_pop_empty_transform_stack() {
584        let mut canvas = RecordingCanvas::new();
585        canvas.pop_transform(); // Should not panic
586        assert_eq!(canvas.transform_depth(), 0);
587    }
588
589    #[test]
590    fn test_zero_size_rect() {
591        let mut canvas = RecordingCanvas::new();
592        canvas.fill_rect(Rect::new(10.0, 10.0, 0.0, 0.0), Color::RED);
593        assert_eq!(canvas.command_count(), 1);
594    }
595
596    #[test]
597    fn test_empty_text() {
598        let mut canvas = RecordingCanvas::new();
599        canvas.draw_text("", Point::new(0.0, 0.0), &TextStyle::default());
600        assert_eq!(canvas.command_count(), 1);
601        match &canvas.commands()[0] {
602            DrawCommand::Text { content, .. } => assert!(content.is_empty()),
603            _ => panic!("Expected Text command"),
604        }
605    }
606
607    #[test]
608    fn test_zero_radius_circle() {
609        let mut canvas = RecordingCanvas::new();
610        canvas.fill_circle(Point::new(50.0, 50.0), 0.0, Color::RED);
611        assert_eq!(canvas.command_count(), 1);
612    }
613
614    #[test]
615    fn test_empty_path() {
616        let mut canvas = RecordingCanvas::new();
617        canvas.draw_path(&[], false, Color::BLACK, 1.0);
618        assert_eq!(canvas.command_count(), 1);
619        match &canvas.commands()[0] {
620            DrawCommand::Path { points, .. } => assert!(points.is_empty()),
621            _ => panic!("Expected Path command"),
622        }
623    }
624
625    // =========================================================================
626    // Canvas Trait Implementation Tests
627    // =========================================================================
628
629    #[test]
630    fn test_canvas_draw_line() {
631        let mut canvas = RecordingCanvas::new();
632        Canvas::draw_line(
633            &mut canvas,
634            Point::new(0.0, 0.0),
635            Point::new(100.0, 100.0),
636            Color::RED,
637            2.0,
638        );
639
640        assert_eq!(canvas.command_count(), 1);
641        match &canvas.commands()[0] {
642            DrawCommand::Path { points, style, .. } => {
643                assert_eq!(points.len(), 2);
644                assert_eq!(style.color, Color::RED);
645                assert_eq!(style.width, 2.0);
646            }
647            _ => panic!("Expected Path command"),
648        }
649    }
650
651    #[test]
652    fn test_canvas_fill_circle() {
653        let mut canvas = RecordingCanvas::new();
654        Canvas::fill_circle(&mut canvas, Point::new(50.0, 50.0), 25.0, Color::GREEN);
655
656        assert_eq!(canvas.command_count(), 1);
657        match &canvas.commands()[0] {
658            DrawCommand::Circle {
659                center,
660                radius,
661                style,
662            } => {
663                assert_eq!(*center, Point::new(50.0, 50.0));
664                assert_eq!(*radius, 25.0);
665                assert_eq!(style.fill, Some(Color::GREEN));
666            }
667            _ => panic!("Expected Circle command"),
668        }
669    }
670
671    #[test]
672    fn test_canvas_stroke_circle() {
673        let mut canvas = RecordingCanvas::new();
674        Canvas::stroke_circle(&mut canvas, Point::new(50.0, 50.0), 20.0, Color::BLUE, 3.0);
675
676        assert_eq!(canvas.command_count(), 1);
677        match &canvas.commands()[0] {
678            DrawCommand::Circle { radius, style, .. } => {
679                assert_eq!(*radius, 20.0);
680                let stroke = style.stroke.as_ref().unwrap();
681                assert_eq!(stroke.color, Color::BLUE);
682                assert_eq!(stroke.width, 3.0);
683            }
684            _ => panic!("Expected Circle command"),
685        }
686    }
687
688    #[test]
689    fn test_canvas_fill_arc() {
690        let mut canvas = RecordingCanvas::new();
691        Canvas::fill_arc(
692            &mut canvas,
693            Point::new(100.0, 100.0),
694            50.0,
695            0.0,
696            std::f32::consts::PI,
697            Color::new(1.0, 0.5, 0.0, 1.0),
698        );
699
700        assert_eq!(canvas.command_count(), 1);
701        match &canvas.commands()[0] {
702            DrawCommand::Arc {
703                center,
704                radius,
705                start_angle,
706                end_angle,
707                color,
708            } => {
709                assert_eq!(*center, Point::new(100.0, 100.0));
710                assert_eq!(*radius, 50.0);
711                assert_eq!(*start_angle, 0.0);
712                assert!((end_angle - std::f32::consts::PI).abs() < 0.001);
713                assert_eq!(color.r, 1.0);
714            }
715            _ => panic!("Expected Arc command"),
716        }
717    }
718
719    #[test]
720    fn test_canvas_draw_path() {
721        let mut canvas = RecordingCanvas::new();
722        let points = [
723            Point::new(0.0, 0.0),
724            Point::new(50.0, 100.0),
725            Point::new(100.0, 0.0),
726        ];
727        Canvas::draw_path(&mut canvas, &points, Color::BLACK, 1.5);
728
729        assert_eq!(canvas.command_count(), 1);
730        match &canvas.commands()[0] {
731            DrawCommand::Path {
732                points: p,
733                closed,
734                style,
735            } => {
736                assert_eq!(p.len(), 3);
737                assert!(!closed);
738                assert_eq!(style.width, 1.5);
739            }
740            _ => panic!("Expected Path command"),
741        }
742    }
743
744    #[test]
745    fn test_canvas_fill_polygon() {
746        let mut canvas = RecordingCanvas::new();
747        let points = [
748            Point::new(0.0, 0.0),
749            Point::new(100.0, 0.0),
750            Point::new(50.0, 100.0),
751        ];
752        Canvas::fill_polygon(&mut canvas, &points, Color::BLUE);
753
754        assert_eq!(canvas.command_count(), 1);
755        match &canvas.commands()[0] {
756            DrawCommand::Path {
757                points: p,
758                closed,
759                style,
760            } => {
761                assert_eq!(p.len(), 3);
762                assert!(*closed);
763                assert_eq!(style.color, Color::BLUE);
764            }
765            _ => panic!("Expected Path command"),
766        }
767    }
768}