1use crate::draw::{BoxStyle, DrawCommand, StrokeStyle, Transform2D};
4use crate::widget::{Canvas, TextStyle};
5use crate::{Color, Point, Rect};
6
7#[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 #[must_use]
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 #[must_use]
29 pub fn commands(&self) -> &[DrawCommand] {
30 &self.commands
31 }
32
33 pub fn take_commands(&mut self) -> Vec<DrawCommand> {
35 std::mem::take(&mut self.commands)
36 }
37
38 #[must_use]
40 pub fn command_count(&self) -> usize {
41 self.commands.len()
42 }
43
44 #[must_use]
46 pub fn is_empty(&self) -> bool {
47 self.commands.is_empty()
48 }
49
50 pub fn clear(&mut self) {
52 self.commands.clear();
53 self.clip_stack.clear();
54 self.transform_stack.clear();
55 }
56
57 #[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 #[must_use]
68 pub fn current_clip(&self) -> Option<Rect> {
69 self.clip_stack.last().copied()
70 }
71
72 #[must_use]
74 pub fn clip_depth(&self) -> usize {
75 self.clip_stack.len()
76 }
77
78 #[must_use]
80 pub fn transform_depth(&self) -> usize {
81 self.transform_stack.len()
82 }
83
84 pub fn add_command(&mut self, command: DrawCommand) {
86 self.commands.push(command);
87 }
88
89 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 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 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 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 self.commands.push(DrawCommand::Path {
220 points: points.to_vec(),
221 closed: true,
222 style: StrokeStyle {
223 color,
224 width: 0.0, ..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 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 #[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 #[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 #[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 #[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 #[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 #[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 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 #[test]
576 fn test_pop_empty_clip_stack() {
577 let mut canvas = RecordingCanvas::new();
578 canvas.pop_clip(); 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(); 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 #[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}