1use tiny_skia::{
2 BlendMode, Color, FillRule, Mask, Paint, Path, Pixmap, PixmapPaint, Stroke, Transform,
3};
4
5#[derive(Debug, Clone)]
7pub enum DisplayCommand {
8 FillPath {
10 path: Path,
11 fill_rule: FillRule,
12 transform: Transform,
13 color: Color,
14 alpha: f32,
15 blend_mode: BlendMode,
16 },
17 StrokePath {
19 path: Path,
20 stroke: Stroke,
21 transform: Transform,
22 color: Color,
23 alpha: f32,
24 blend_mode: BlendMode,
25 },
26 DrawImage {
28 pixmap: Pixmap,
29 transform: Transform,
30 alpha: f32,
31 blend_mode: BlendMode,
32 },
33 PushClip {
35 path: Path,
36 fill_rule: FillRule,
37 transform: Transform,
38 },
39 PopClip,
41 Save,
43 Restore,
45 BeginGroup {
47 opacity: f32,
48 blend_mode: BlendMode,
49 isolated: bool,
50 },
51 EndGroup,
53}
54
55#[derive(Debug, Clone)]
61pub struct DisplayList {
62 commands: Vec<DisplayCommand>,
63 width: u32,
64 height: u32,
65}
66
67impl DisplayList {
68 pub fn new(width: u32, height: u32) -> Self {
70 Self {
71 commands: Vec::new(),
72 width,
73 height,
74 }
75 }
76
77 pub fn push(&mut self, cmd: DisplayCommand) {
79 self.commands.push(cmd);
80 }
81
82 pub fn len(&self) -> usize {
84 self.commands.len()
85 }
86
87 pub fn is_empty(&self) -> bool {
89 self.commands.is_empty()
90 }
91
92 pub fn commands(&self) -> &[DisplayCommand] {
94 &self.commands
95 }
96
97 pub fn replay(&self, pixmap: &mut Pixmap) {
99 self.replay_with_transform(pixmap, Transform::identity());
100 }
101
102 pub fn replay_with_transform(&self, pixmap: &mut Pixmap, extra_transform: Transform) {
106 let mut clip_stack: Vec<Mask> = Vec::new();
107 let mut current_mask: Option<Mask> = None;
108 let mut group_stack: Vec<GroupState> = Vec::new();
109
110 for cmd in &self.commands {
111 match cmd {
112 DisplayCommand::FillPath {
113 path,
114 fill_rule,
115 transform,
116 color,
117 alpha,
118 blend_mode,
119 } => {
120 let target = group_target(&mut group_stack, pixmap);
121 let combined = extra_transform.post_concat(*transform);
122 let mut paint = Paint::default();
123 let c = apply_alpha(*color, *alpha);
124 paint.set_color(c);
125 paint.anti_alias = true;
126 paint.blend_mode = *blend_mode;
127 let mask_ref = current_mask.as_ref();
128 target.fill_path(path, &paint, *fill_rule, combined, mask_ref);
129 }
130
131 DisplayCommand::StrokePath {
132 path,
133 stroke,
134 transform,
135 color,
136 alpha,
137 blend_mode,
138 } => {
139 let target = group_target(&mut group_stack, pixmap);
140 let combined = extra_transform.post_concat(*transform);
141 let mut paint = Paint::default();
142 let c = apply_alpha(*color, *alpha);
143 paint.set_color(c);
144 paint.anti_alias = true;
145 paint.blend_mode = *blend_mode;
146 let mask_ref = current_mask.as_ref();
147 target.stroke_path(path, &paint, stroke, combined, mask_ref);
148 }
149
150 DisplayCommand::DrawImage {
151 pixmap: img,
152 transform,
153 alpha,
154 blend_mode,
155 } => {
156 let target = group_target(&mut group_stack, pixmap);
157 let combined = extra_transform.post_concat(*transform);
158 let mut ppaint = PixmapPaint::default();
159 ppaint.opacity = *alpha;
160 ppaint.blend_mode = *blend_mode;
161 ppaint.quality = tiny_skia::FilterQuality::Bilinear;
162 let mask_ref = current_mask.as_ref();
163 target.draw_pixmap(0, 0, img.as_ref(), &ppaint, combined, mask_ref);
164 }
165
166 DisplayCommand::PushClip {
167 path,
168 fill_rule,
169 transform,
170 } => {
171 if let Some(m) = current_mask.take() {
173 clip_stack.push(m);
174 }
175 let target = group_target(&mut group_stack, pixmap);
176 let w = target.width();
177 let h = target.height();
178 let combined = extra_transform.post_concat(*transform);
179 if let Some(mut mask) = Mask::new(w, h) {
180 mask.fill_path(path, *fill_rule, true, combined);
181 if let Some(prev) = clip_stack.last() {
183 intersect_masks(&mut mask, prev);
184 }
185 current_mask = Some(mask);
186 }
187 }
188
189 DisplayCommand::PopClip => {
190 current_mask = clip_stack.pop();
191 }
192
193 DisplayCommand::Save => {
194 }
196
197 DisplayCommand::Restore => {
198 }
200
201 DisplayCommand::BeginGroup {
202 opacity,
203 blend_mode,
204 isolated: _,
205 } => {
206 let target = group_target(&mut group_stack, pixmap);
207 let w = target.width();
208 let h = target.height();
209 if let Some(group_pixmap) = Pixmap::new(w, h) {
210 group_stack.push(GroupState {
211 pixmap: group_pixmap,
212 opacity: *opacity,
213 blend_mode: *blend_mode,
214 });
215 }
216 }
217
218 DisplayCommand::EndGroup => {
219 if let Some(group) = group_stack.pop() {
220 let target = group_target(&mut group_stack, pixmap);
221 let mut ppaint = PixmapPaint::default();
222 ppaint.opacity = group.opacity;
223 ppaint.blend_mode = group.blend_mode;
224 let mask_ref = current_mask.as_ref();
225 target.draw_pixmap(
226 0,
227 0,
228 group.pixmap.as_ref(),
229 &ppaint,
230 Transform::identity(),
231 mask_ref,
232 );
233 }
234 }
235 }
236 }
237 }
238
239 pub fn bounds(&self) -> Option<tiny_skia::Rect> {
242 let mut result: Option<tiny_skia::Rect> = None;
243
244 for cmd in &self.commands {
245 let path_bounds = match cmd {
246 DisplayCommand::FillPath { path, .. }
247 | DisplayCommand::StrokePath { path, .. }
248 | DisplayCommand::PushClip { path, .. } => Some(path.bounds()),
249 _ => None,
250 };
251
252 if let Some(b) = path_bounds {
253 result = Some(match result {
254 Some(r) => union_rect(r, b),
255 None => b,
256 });
257 }
258 }
259
260 result
261 }
262
263 pub fn optimize(&mut self) {
269 loop {
270 let before = self.commands.len();
271 let mut optimized = Vec::with_capacity(self.commands.len());
272 let mut i = 0;
273 while i < self.commands.len() {
274 if i + 1 < self.commands.len() {
275 let is_noop_pair = matches!(
276 (&self.commands[i], &self.commands[i + 1]),
277 (DisplayCommand::Save, DisplayCommand::Restore)
278 | (DisplayCommand::BeginGroup { .. }, DisplayCommand::EndGroup)
279 | (DisplayCommand::PushClip { .. }, DisplayCommand::PopClip)
280 );
281 if is_noop_pair {
282 i += 2;
283 continue;
284 }
285 }
286 optimized.push(self.commands[i].clone());
287 i += 1;
288 }
289 self.commands = optimized;
290 if self.commands.len() == before {
292 break;
293 }
294 }
295 }
296
297 pub fn width(&self) -> u32 {
299 self.width
300 }
301
302 pub fn height(&self) -> u32 {
304 self.height
305 }
306
307 pub fn render_tile(
316 &self,
317 tile_left: f32,
318 tile_top: f32,
319 tile_width: u32,
320 tile_height: u32,
321 ) -> Option<Pixmap> {
322 let mut pixmap = Pixmap::new(tile_width, tile_height)?;
323 pixmap.fill(Color::TRANSPARENT);
324 let transform = Transform::from_translate(-tile_left, -tile_top);
325 self.replay_with_transform(&mut pixmap, transform);
326 Some(pixmap)
327 }
328
329 pub fn render_tiled(&self, tile_size: u32, background: Color) -> Option<Pixmap> {
335 let mut output = Pixmap::new(self.width, self.height)?;
336 output.fill(background);
337
338 let cols = (self.width + tile_size - 1) / tile_size;
339 let rows = (self.height + tile_size - 1) / tile_size;
340
341 for row in 0..rows {
342 for col in 0..cols {
343 let tx = (col * tile_size) as f32;
344 let ty = (row * tile_size) as f32;
345 let tw = tile_size.min(self.width - col * tile_size);
346 let th = tile_size.min(self.height - row * tile_size);
347
348 if let Some(tile) = self.render_tile(tx, ty, tw, th) {
349 let paint = PixmapPaint::default();
350 output.draw_pixmap(
351 tx as i32,
352 ty as i32,
353 tile.as_ref(),
354 &paint,
355 Transform::identity(),
356 None,
357 );
358 }
359 }
360 }
361
362 Some(output)
363 }
364}
365
366struct GroupState {
372 pixmap: Pixmap,
373 opacity: f32,
374 blend_mode: BlendMode,
375}
376
377fn group_target<'a>(stack: &'a mut Vec<GroupState>, root: &'a mut Pixmap) -> &'a mut Pixmap {
380 if let Some(top) = stack.last_mut() {
381 &mut top.pixmap
382 } else {
383 root
384 }
385}
386
387fn apply_alpha(color: Color, alpha: f32) -> Color {
389 Color::from_rgba(color.red(), color.green(), color.blue(), color.alpha() * alpha)
390 .unwrap_or(color)
391}
392
393fn union_rect(a: tiny_skia::Rect, b: tiny_skia::Rect) -> tiny_skia::Rect {
395 let l = a.left().min(b.left());
396 let t = a.top().min(b.top());
397 let r = a.right().max(b.right());
398 let bot = a.bottom().max(b.bottom());
399 tiny_skia::Rect::from_ltrb(l, t, r, bot).unwrap_or(a)
400}
401
402fn intersect_masks(dst: &mut Mask, src: &Mask) {
404 let dst_data = dst.data_mut();
405 let src_data = src.data();
406 let len = dst_data.len().min(src_data.len());
407 for i in 0..len {
408 dst_data[i] = ((dst_data[i] as u16 * src_data[i] as u16) / 255) as u8;
409 }
410}
411
412#[cfg(test)]
417mod tests {
418 use super::*;
419 use tiny_skia::{PathBuilder, Stroke};
420
421 fn rect_path(x: f32, y: f32, w: f32, h: f32) -> Path {
423 let mut pb = PathBuilder::new();
424 pb.move_to(x, y);
425 pb.line_to(x + w, y);
426 pb.line_to(x + w, y + h);
427 pb.line_to(x, y + h);
428 pb.close();
429 pb.finish().unwrap()
430 }
431
432 #[test]
433 fn test_empty_display_list() {
434 let dl = DisplayList::new(100, 100);
435 assert!(dl.is_empty());
436 assert_eq!(dl.len(), 0);
437 assert_eq!(dl.width(), 100);
438 assert_eq!(dl.height(), 100);
439 assert!(dl.bounds().is_none());
440 }
441
442 #[test]
443 fn test_record_and_replay_fill() {
444 let mut dl = DisplayList::new(100, 100);
445 dl.push(DisplayCommand::FillPath {
446 path: rect_path(10.0, 10.0, 50.0, 50.0),
447 fill_rule: FillRule::Winding,
448 transform: Transform::identity(),
449 color: Color::from_rgba8(255, 0, 0, 255),
450 alpha: 1.0,
451 blend_mode: BlendMode::SourceOver,
452 });
453 assert_eq!(dl.len(), 1);
454
455 let mut pixmap = Pixmap::new(100, 100).unwrap();
456 pixmap.fill(Color::from_rgba8(255, 255, 255, 255));
457 dl.replay(&mut pixmap);
458
459 let pixel = pixmap.pixel(35, 35).unwrap();
461 assert_eq!(pixel.red(), 255);
462 assert_eq!(pixel.green(), 0);
463 assert_eq!(pixel.blue(), 0);
464 }
465
466 #[test]
467 fn test_record_and_replay_stroke() {
468 let mut dl = DisplayList::new(100, 100);
469 let mut stroke = Stroke::default();
470 stroke.width = 4.0;
471
472 dl.push(DisplayCommand::StrokePath {
473 path: rect_path(10.0, 10.0, 80.0, 80.0),
474 stroke,
475 transform: Transform::identity(),
476 color: Color::from_rgba8(0, 0, 255, 255),
477 alpha: 1.0,
478 blend_mode: BlendMode::SourceOver,
479 });
480 assert_eq!(dl.len(), 1);
481
482 let mut pixmap = Pixmap::new(100, 100).unwrap();
483 pixmap.fill(Color::from_rgba8(255, 255, 255, 255));
484 dl.replay(&mut pixmap);
485
486 let pixel = pixmap.pixel(50, 10).unwrap();
488 assert_eq!(pixel.blue(), 255);
489 }
490
491 #[test]
492 fn test_display_list_length() {
493 let mut dl = DisplayList::new(10, 10);
494 assert_eq!(dl.len(), 0);
495
496 dl.push(DisplayCommand::Save);
497 assert_eq!(dl.len(), 1);
498
499 dl.push(DisplayCommand::FillPath {
500 path: rect_path(0.0, 0.0, 5.0, 5.0),
501 fill_rule: FillRule::Winding,
502 transform: Transform::identity(),
503 color: Color::BLACK,
504 alpha: 1.0,
505 blend_mode: BlendMode::SourceOver,
506 });
507 assert_eq!(dl.len(), 2);
508
509 dl.push(DisplayCommand::Restore);
510 assert_eq!(dl.len(), 3);
511 }
512
513 #[test]
514 fn test_optimize_removes_redundant_save_restore() {
515 let mut dl = DisplayList::new(10, 10);
516 dl.push(DisplayCommand::Save);
517 dl.push(DisplayCommand::Restore);
518 dl.push(DisplayCommand::FillPath {
519 path: rect_path(0.0, 0.0, 5.0, 5.0),
520 fill_rule: FillRule::Winding,
521 transform: Transform::identity(),
522 color: Color::BLACK,
523 alpha: 1.0,
524 blend_mode: BlendMode::SourceOver,
525 });
526 dl.push(DisplayCommand::Save);
527 dl.push(DisplayCommand::Restore);
528
529 assert_eq!(dl.len(), 5);
530 dl.optimize();
531 assert_eq!(dl.len(), 1);
533 assert!(matches!(dl.commands()[0], DisplayCommand::FillPath { .. }));
534 }
535
536 #[test]
537 fn test_optimize_nested_noop() {
538 let mut dl = DisplayList::new(10, 10);
540 dl.push(DisplayCommand::Save);
541 dl.push(DisplayCommand::Save);
542 dl.push(DisplayCommand::Restore);
543 dl.push(DisplayCommand::Restore);
544
545 dl.optimize();
546 assert!(dl.is_empty());
547 }
548
549 #[test]
550 fn test_replay_with_transform_scales() {
551 let mut dl = DisplayList::new(200, 200);
552 dl.push(DisplayCommand::FillPath {
553 path: rect_path(0.0, 0.0, 50.0, 50.0),
554 fill_rule: FillRule::Winding,
555 transform: Transform::identity(),
556 color: Color::from_rgba8(0, 255, 0, 255),
557 alpha: 1.0,
558 blend_mode: BlendMode::SourceOver,
559 });
560
561 let mut pixmap = Pixmap::new(200, 200).unwrap();
562 pixmap.fill(Color::from_rgba8(0, 0, 0, 255));
563
564 let scale = Transform::from_scale(2.0, 2.0);
566 dl.replay_with_transform(&mut pixmap, scale);
567
568 let inside = pixmap.pixel(75, 75).unwrap();
570 assert_eq!(inside.green(), 255);
571
572 let outside = pixmap.pixel(150, 150).unwrap();
574 assert_eq!(outside.red(), 0);
575 assert_eq!(outside.green(), 0);
576 assert_eq!(outside.blue(), 0);
577 }
578
579 #[test]
580 fn test_render_tile_basic() {
581 let mut dl = DisplayList::new(100, 100);
582 dl.push(DisplayCommand::FillPath {
583 path: rect_path(0.0, 0.0, 100.0, 100.0),
584 fill_rule: FillRule::Winding,
585 transform: Transform::identity(),
586 color: Color::from_rgba8(255, 0, 0, 255),
587 alpha: 1.0,
588 blend_mode: BlendMode::SourceOver,
589 });
590
591 let tile = dl.render_tile(0.0, 0.0, 50, 50).unwrap();
593 assert_eq!(tile.width(), 50);
594 assert_eq!(tile.height(), 50);
595 let pixel = tile.pixel(25, 25).unwrap();
597 assert_eq!(pixel.red(), 255);
598 assert_eq!(pixel.green(), 0);
599 }
600
601 #[test]
602 fn test_render_tiled_matches_full() {
603 let mut dl = DisplayList::new(100, 100);
604 dl.push(DisplayCommand::FillPath {
605 path: rect_path(10.0, 10.0, 80.0, 80.0),
606 fill_rule: FillRule::Winding,
607 transform: Transform::identity(),
608 color: Color::from_rgba8(0, 128, 255, 255),
609 alpha: 1.0,
610 blend_mode: BlendMode::SourceOver,
611 });
612
613 let mut full = Pixmap::new(100, 100).unwrap();
615 full.fill(Color::WHITE);
616 dl.replay(&mut full);
617
618 let tiled = dl.render_tiled(32, Color::WHITE).unwrap();
620
621 for &(x, y) in &[(50, 50), (5, 5), (95, 95), (10, 10)] {
623 let fp = full.pixel(x, y).unwrap();
624 let tp = tiled.pixel(x, y).unwrap();
625 assert_eq!(
626 (fp.red(), fp.green(), fp.blue()),
627 (tp.red(), tp.green(), tp.blue()),
628 "mismatch at ({x}, {y})"
629 );
630 }
631 }
632
633 #[test]
634 fn test_bounds() {
635 let mut dl = DisplayList::new(100, 100);
636 dl.push(DisplayCommand::FillPath {
637 path: rect_path(10.0, 20.0, 30.0, 40.0),
638 fill_rule: FillRule::Winding,
639 transform: Transform::identity(),
640 color: Color::BLACK,
641 alpha: 1.0,
642 blend_mode: BlendMode::SourceOver,
643 });
644 dl.push(DisplayCommand::StrokePath {
645 path: rect_path(50.0, 60.0, 10.0, 5.0),
646 stroke: Stroke::default(),
647 transform: Transform::identity(),
648 color: Color::BLACK,
649 alpha: 1.0,
650 blend_mode: BlendMode::SourceOver,
651 });
652
653 let b = dl.bounds().unwrap();
654 assert!((b.left() - 10.0).abs() < 0.01);
655 assert!((b.top() - 20.0).abs() < 0.01);
656 assert!((b.right() - 60.0).abs() < 0.01);
657 assert!((b.bottom() - 65.0).abs() < 0.01);
658 }
659}