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)]
252mod tests {
253 use super::*;
254 use crate::widget::FontWeight;
255
256 #[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 #[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 #[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 #[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 #[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 #[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 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 #[test]
575 fn test_pop_empty_clip_stack() {
576 let mut canvas = RecordingCanvas::new();
577 canvas.pop_clip(); 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(); 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 #[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}