1use crate::{
2 Px, SvgFit, ViewportFit,
3 geometry::{Corners, Edges, Point, Rect, Transform2D},
4 ids::{EffectId, ImageId, PathId, RenderTargetId, SvgId, TextBlobId},
5};
6use serde::{Deserialize, Serialize};
7use slotmap::Key;
8
9mod composite;
10mod fingerprint;
11mod image_object_fit;
12mod mask;
13mod paint;
14mod replay;
15mod shadow;
16mod stroke;
17mod validate;
18
19pub use composite::{BlendMode, CompositeGroupDesc};
20use fingerprint::mix_scene_op;
21pub use image_object_fit::{ImageObjectFitMapped, map_image_object_fit};
22pub use mask::Mask;
23pub use paint::{
24 ColorSpace, GradientStop, LinearGradient, MAX_STOPS, MaterialParams, Paint, PaintBindingV1,
25 PaintEvalSpaceV1, RadialGradient, SweepGradient, TileMode,
26};
27pub use shadow::{ShadowRRectFallbackSpec, shadow_rrect_fallback_quads};
28pub use stroke::{DashPatternV1, StrokeStyleV1};
29pub use validate::{SceneValidationError, SceneValidationErrorKind};
30
31#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32pub enum ImageSamplingHint {
33 #[default]
35 Default,
36 Linear,
37 Nearest,
38}
39
40#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct DrawOrder(pub u32);
42
43#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
47pub struct Color {
48 pub r: f32,
49 pub g: f32,
50 pub b: f32,
51 pub a: f32,
52}
53
54impl Color {
55 pub const TRANSPARENT: Self = Self {
56 r: 0.0,
57 g: 0.0,
58 b: 0.0,
59 a: 0.0,
60 };
61
62 pub fn from_srgb_hex_rgb(hex: u32) -> Self {
64 let r = ((hex >> 16) & 0xff) as u8;
65 let g = ((hex >> 8) & 0xff) as u8;
66 let b = (hex & 0xff) as u8;
67 Self {
68 r: srgb_u8_to_linear(r),
69 g: srgb_u8_to_linear(g),
70 b: srgb_u8_to_linear(b),
71 a: 1.0,
72 }
73 }
74
75 pub fn to_srgb_hex_rgb(self) -> u32 {
77 let r = linear_to_srgb_u8(self.r) as u32;
78 let g = linear_to_srgb_u8(self.g) as u32;
79 let b = linear_to_srgb_u8(self.b) as u32;
80 (r << 16) | (g << 8) | b
81 }
82}
83
84fn srgb_f32_to_linear(c: f32) -> f32 {
85 if c <= 0.04045 {
86 c / 12.92
87 } else {
88 ((c + 0.055) / 1.055).powf(2.4)
89 }
90}
91
92fn linear_f32_to_srgb(c: f32) -> f32 {
93 if c <= 0.0031308 {
94 12.92 * c
95 } else {
96 1.055 * c.powf(1.0 / 2.4) - 0.055
97 }
98}
99
100fn srgb_u8_to_linear(u: u8) -> f32 {
101 srgb_f32_to_linear(u as f32 / 255.0)
102}
103
104fn linear_to_srgb_u8(c: f32) -> u8 {
105 let srgb = linear_f32_to_srgb(c.clamp(0.0, 1.0)).clamp(0.0, 1.0);
106 (srgb * 255.0).round() as u8
107}
108
109#[derive(Debug, Clone, Copy, PartialEq)]
115pub struct TextShadowV1 {
116 pub offset: Point,
118 pub color: Color,
119}
120
121impl TextShadowV1 {
122 pub const fn new(offset: Point, color: Color) -> Self {
123 Self { offset, color }
124 }
125}
126
127#[derive(Debug, Clone, Copy, PartialEq)]
132pub struct TextOutlineV1 {
133 pub paint: PaintBindingV1,
134 pub width_px: crate::Px,
136}
137
138impl TextOutlineV1 {
139 pub const MAX_WIDTH_PX: crate::Px = crate::Px(8.0);
140
141 pub fn sanitize(self) -> Option<Self> {
142 if !self.width_px.0.is_finite() {
143 return None;
144 }
145 let width_px = crate::Px(self.width_px.0.clamp(0.0, Self::MAX_WIDTH_PX.0));
146 if width_px.0 <= 0.0 {
147 return None;
148 }
149 Some(Self {
150 paint: self.paint.sanitize(),
151 width_px,
152 })
153 }
154}
155
156#[derive(Debug, Clone, Copy, PartialEq, Eq)]
157pub enum EffectMode {
158 FilterContent,
160 Backdrop,
162}
163
164#[derive(Debug, Clone, Copy, PartialEq, Eq)]
165pub enum EffectQuality {
166 Auto,
168 Low,
169 Medium,
170 High,
171}
172
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum DitherMode {
175 Bayer4x4,
176}
177
178#[repr(C)]
182#[derive(Debug, Clone, Copy, PartialEq)]
183pub struct EffectParamsV1 {
184 pub vec4s: [[f32; 4]; 4],
185}
186
187impl EffectParamsV1 {
188 pub const ZERO: Self = Self {
189 vec4s: [[0.0; 4]; 4],
190 };
191
192 pub fn sanitize(self) -> Self {
193 let mut out = self;
194 for v in &mut out.vec4s {
195 for x in v {
196 if !x.is_finite() {
197 *x = 0.0;
198 }
199 }
200 }
201 out
202 }
203
204 pub fn is_finite(self) -> bool {
205 self.vec4s.iter().flatten().all(|&x| x.is_finite())
206 }
207}
208
209#[derive(Debug, Clone, Copy, PartialEq)]
215pub struct NoiseV1 {
216 pub strength: f32,
218 pub scale_px: crate::Px,
220 pub phase: f32,
222}
223
224impl NoiseV1 {
225 pub const MAX_STRENGTH: f32 = 1.0;
226 pub const MIN_SCALE_PX: crate::Px = crate::Px(1.0);
227 pub const MAX_SCALE_PX: crate::Px = crate::Px(1024.0);
228
229 pub fn sanitize(self) -> Self {
230 let strength = if self.strength.is_finite() {
231 self.strength.clamp(0.0, Self::MAX_STRENGTH)
232 } else {
233 0.0
234 };
235 let scale_px = if self.scale_px.0.is_finite() {
236 crate::Px(
237 self.scale_px
238 .0
239 .clamp(Self::MIN_SCALE_PX.0, Self::MAX_SCALE_PX.0),
240 )
241 } else {
242 Self::MIN_SCALE_PX
243 };
244 let phase = if self.phase.is_finite() {
245 self.phase
246 } else {
247 0.0
248 };
249
250 Self {
251 strength,
252 scale_px,
253 phase,
254 }
255 }
256}
257
258#[derive(Debug, Clone, Copy, PartialEq)]
264pub struct BackdropWarpV1 {
265 pub strength_px: crate::Px,
267 pub scale_px: crate::Px,
269 pub phase: f32,
271 pub chromatic_aberration_px: crate::Px,
273 pub kind: BackdropWarpKindV1,
274}
275
276impl BackdropWarpV1 {
277 pub const MAX_STRENGTH_PX: crate::Px = crate::Px(24.0);
278 pub const MIN_SCALE_PX: crate::Px = crate::Px(1.0);
279 pub const MAX_SCALE_PX: crate::Px = crate::Px(1024.0);
280 pub const MAX_CHROMATIC_ABERRATION_PX: crate::Px = crate::Px(8.0);
281
282 pub fn sanitize(self) -> Self {
283 let strength_px = if self.strength_px.0.is_finite() {
284 crate::Px(self.strength_px.0.clamp(0.0, Self::MAX_STRENGTH_PX.0))
285 } else {
286 crate::Px(0.0)
287 };
288
289 let scale_px = if self.scale_px.0.is_finite() {
290 crate::Px(
291 self.scale_px
292 .0
293 .clamp(Self::MIN_SCALE_PX.0, Self::MAX_SCALE_PX.0),
294 )
295 } else {
296 Self::MIN_SCALE_PX
297 };
298
299 let phase = if self.phase.is_finite() {
300 self.phase
301 } else {
302 0.0
303 };
304
305 let chromatic_aberration_px = if self.chromatic_aberration_px.0.is_finite() {
306 crate::Px(
307 self.chromatic_aberration_px
308 .0
309 .clamp(0.0, Self::MAX_CHROMATIC_ABERRATION_PX.0),
310 )
311 } else {
312 crate::Px(0.0)
313 };
314
315 Self {
316 strength_px,
317 scale_px,
318 phase,
319 chromatic_aberration_px,
320 kind: self.kind,
321 }
322 }
323}
324
325#[derive(Debug, Clone, Copy, PartialEq, Eq)]
326pub enum WarpMapEncodingV1 {
327 RgSigned,
331 NormalRgb,
335}
336
337#[derive(Debug, Clone, Copy, PartialEq)]
338pub enum BackdropWarpFieldV2 {
339 Procedural,
341 ImageDisplacementMap {
343 image: ImageId,
344 uv: UvRect,
345 sampling: ImageSamplingHint,
346 encoding: WarpMapEncodingV1,
347 },
348}
349
350#[derive(Debug, Clone, Copy, PartialEq)]
351pub struct BackdropWarpV2 {
352 pub base: BackdropWarpV1,
354 pub field: BackdropWarpFieldV2,
355}
356
357impl BackdropWarpV2 {
358 pub fn sanitize(self) -> Self {
359 let base = self.base.sanitize();
360 let field = match self.field {
361 BackdropWarpFieldV2::Procedural => BackdropWarpFieldV2::Procedural,
362 BackdropWarpFieldV2::ImageDisplacementMap {
363 image,
364 uv,
365 sampling,
366 encoding,
367 } => {
368 let uv = if uv.u0.is_finite()
369 && uv.v0.is_finite()
370 && uv.u1.is_finite()
371 && uv.v1.is_finite()
372 {
373 uv
374 } else {
375 UvRect::FULL
376 };
377 BackdropWarpFieldV2::ImageDisplacementMap {
378 image,
379 uv,
380 sampling,
381 encoding,
382 }
383 }
384 };
385
386 Self { base, field }
387 }
388}
389
390#[derive(Debug, Clone, Copy, PartialEq, Eq)]
391pub enum BackdropWarpKindV1 {
392 Wave,
393 LensReserved,
395}
396
397#[derive(Debug, Clone, Copy, PartialEq)]
403pub struct DropShadowV1 {
404 pub offset_px: Point,
406 pub blur_radius_px: crate::Px,
408 pub downsample: u32,
410 pub color: Color,
412}
413
414impl DropShadowV1 {
415 pub const MAX_BLUR_RADIUS_PX: crate::Px = crate::Px(64.0);
416
417 pub fn sanitize(self) -> Self {
418 let offset_px = Point::new(
419 crate::Px(if self.offset_px.x.0.is_finite() {
420 self.offset_px.x.0
421 } else {
422 0.0
423 }),
424 crate::Px(if self.offset_px.y.0.is_finite() {
425 self.offset_px.y.0
426 } else {
427 0.0
428 }),
429 );
430 let blur_radius_px = if self.blur_radius_px.0.is_finite() {
431 crate::Px(self.blur_radius_px.0.clamp(0.0, Self::MAX_BLUR_RADIUS_PX.0))
432 } else {
433 crate::Px(0.0)
434 };
435 let downsample = self.downsample.clamp(1, 4);
436 let color = Color {
437 r: if self.color.r.is_finite() {
438 self.color.r.clamp(0.0, 1.0)
439 } else {
440 0.0
441 },
442 g: if self.color.g.is_finite() {
443 self.color.g.clamp(0.0, 1.0)
444 } else {
445 0.0
446 },
447 b: if self.color.b.is_finite() {
448 self.color.b.clamp(0.0, 1.0)
449 } else {
450 0.0
451 },
452 a: if self.color.a.is_finite() {
453 self.color.a.clamp(0.0, 1.0)
454 } else {
455 0.0
456 },
457 };
458
459 Self {
460 offset_px,
461 blur_radius_px,
462 downsample,
463 color,
464 }
465 }
466}
467
468#[derive(Debug, Clone, Copy, PartialEq)]
473pub struct CustomEffectImageInputV1 {
474 pub image: ImageId,
475 pub uv: UvRect,
476 pub sampling: ImageSamplingHint,
477}
478
479impl CustomEffectImageInputV1 {
480 pub const fn new(image: ImageId) -> Self {
481 Self {
482 image,
483 uv: UvRect::FULL,
484 sampling: ImageSamplingHint::Default,
485 }
486 }
487}
488
489#[derive(Debug, Clone, Copy, PartialEq, Default)]
495pub struct CustomEffectSourcesV3 {
496 pub want_raw: bool,
497 pub pyramid: Option<CustomEffectPyramidRequestV1>,
498}
499
500#[derive(Debug, Clone, Copy, PartialEq)]
502pub struct CustomEffectPyramidRequestV1 {
503 pub max_levels: u8,
504 pub max_radius_px: crate::Px,
505}
506
507#[derive(Debug, Clone, Copy, PartialEq)]
508pub enum EffectStep {
509 GaussianBlur {
510 radius_px: crate::Px,
511 downsample: u32,
512 },
513 DropShadowV1(DropShadowV1),
514 BackdropWarpV1(BackdropWarpV1),
515 BackdropWarpV2(BackdropWarpV2),
516 NoiseV1(NoiseV1),
517 ColorAdjust {
518 saturation: f32,
519 brightness: f32,
520 contrast: f32,
521 },
522 ColorMatrix {
523 m: [f32; 20],
524 },
525 AlphaThreshold {
526 cutoff: f32,
527 soft: f32,
528 },
529 Pixelate {
530 scale: u32,
531 },
532 Dither {
533 mode: DitherMode,
534 },
535 CustomV1 {
536 id: EffectId,
537 params: EffectParamsV1,
538 max_sample_offset_px: crate::Px,
547 },
548 CustomV2 {
549 id: EffectId,
550 params: EffectParamsV1,
551 max_sample_offset_px: crate::Px,
556 input_image: Option<CustomEffectImageInputV1>,
558 },
559 CustomV3 {
560 id: EffectId,
561 params: EffectParamsV1,
562 max_sample_offset_px: crate::Px,
567 user0: Option<CustomEffectImageInputV1>,
569 user1: Option<CustomEffectImageInputV1>,
571 sources: CustomEffectSourcesV3,
573 },
574}
575
576impl EffectStep {
577 pub fn sanitize(self) -> Self {
578 match self {
579 EffectStep::ColorMatrix { mut m } => {
580 for v in &mut m {
581 if !v.is_finite() {
582 *v = 0.0;
583 }
584 }
585 EffectStep::ColorMatrix { m }
586 }
587 EffectStep::BackdropWarpV1(w) => EffectStep::BackdropWarpV1(w.sanitize()),
588 EffectStep::BackdropWarpV2(w) => EffectStep::BackdropWarpV2(w.sanitize()),
589 EffectStep::DropShadowV1(s) => EffectStep::DropShadowV1(s.sanitize()),
590 EffectStep::NoiseV1(n) => EffectStep::NoiseV1(n.sanitize()),
591 EffectStep::CustomV1 {
592 id,
593 params,
594 max_sample_offset_px,
595 } => EffectStep::CustomV1 {
596 id,
597 params: params.sanitize(),
598 max_sample_offset_px: if max_sample_offset_px.0.is_finite() {
599 crate::Px(max_sample_offset_px.0.max(0.0))
600 } else {
601 crate::Px(0.0)
602 },
603 },
604 EffectStep::CustomV2 {
605 id,
606 params,
607 max_sample_offset_px,
608 input_image,
609 } => EffectStep::CustomV2 {
610 id,
611 params: params.sanitize(),
612 max_sample_offset_px: if max_sample_offset_px.0.is_finite() {
613 crate::Px(max_sample_offset_px.0.max(0.0))
614 } else {
615 crate::Px(0.0)
616 },
617 input_image: input_image.map(|mut input| {
618 if !input.uv.u0.is_finite() {
619 input.uv.u0 = 0.0;
620 }
621 if !input.uv.v0.is_finite() {
622 input.uv.v0 = 0.0;
623 }
624 if !input.uv.u1.is_finite() {
625 input.uv.u1 = 1.0;
626 }
627 if !input.uv.v1.is_finite() {
628 input.uv.v1 = 1.0;
629 }
630 input
631 }),
632 },
633 EffectStep::CustomV3 {
634 id,
635 params,
636 max_sample_offset_px,
637 user0,
638 user1,
639 mut sources,
640 } => {
641 let sanitize_input = |input: Option<CustomEffectImageInputV1>| {
642 input.map(|mut input| {
643 if !input.uv.u0.is_finite() {
644 input.uv.u0 = 0.0;
645 }
646 if !input.uv.v0.is_finite() {
647 input.uv.v0 = 0.0;
648 }
649 if !input.uv.u1.is_finite() {
650 input.uv.u1 = 1.0;
651 }
652 if !input.uv.v1.is_finite() {
653 input.uv.v1 = 1.0;
654 }
655 input
656 })
657 };
658
659 sources.pyramid = sources.pyramid.map(|req| {
660 let max_levels = req.max_levels.clamp(1, 7);
661 let max_radius_px = if req.max_radius_px.0.is_finite() {
662 crate::Px(req.max_radius_px.0.max(0.0))
663 } else {
664 crate::Px(0.0)
665 };
666 CustomEffectPyramidRequestV1 {
667 max_levels,
668 max_radius_px,
669 }
670 });
671
672 EffectStep::CustomV3 {
673 id,
674 params: params.sanitize(),
675 max_sample_offset_px: if max_sample_offset_px.0.is_finite() {
676 crate::Px(max_sample_offset_px.0.max(0.0))
677 } else {
678 crate::Px(0.0)
679 },
680 user0: sanitize_input(user0),
681 user1: sanitize_input(user1),
682 sources,
683 }
684 }
685 EffectStep::AlphaThreshold { cutoff, soft } => EffectStep::AlphaThreshold {
686 cutoff: if cutoff.is_finite() { cutoff } else { 0.0 },
687 soft: if soft.is_finite() { soft.max(0.0) } else { 0.0 },
688 },
689 other => other,
690 }
691 }
692}
693
694#[derive(Debug, Clone, Copy, PartialEq)]
695pub struct EffectChain {
696 steps: [Option<EffectStep>; 4],
697}
698
699impl EffectChain {
700 pub const MAX_STEPS: usize = 4;
701 pub const EMPTY: Self = Self {
702 steps: [None, None, None, None],
703 };
704
705 pub fn from_steps(steps: &[EffectStep]) -> Self {
706 assert!(
707 steps.len() <= Self::MAX_STEPS,
708 "EffectChain supports up to {} steps",
709 Self::MAX_STEPS
710 );
711 let mut out = Self::EMPTY;
712 for (idx, step) in steps.iter().copied().enumerate() {
713 out.steps[idx] = Some(step);
714 }
715 out
716 }
717
718 pub fn is_empty(&self) -> bool {
719 self.steps.iter().all(|s| s.is_none())
720 }
721
722 pub fn iter(&self) -> impl Iterator<Item = EffectStep> + '_ {
723 self.steps.iter().copied().flatten()
724 }
725
726 pub fn sanitize(self) -> Self {
727 let mut out = self;
728 for step in &mut out.steps {
729 *step = step.map(EffectStep::sanitize);
730 }
731 out
732 }
733}
734
735impl Default for EffectChain {
736 fn default() -> Self {
737 Self::EMPTY
738 }
739}
740
741#[derive(Debug, Default, Clone)]
742pub struct SceneRecording {
743 ops: Vec<SceneOp>,
744 fingerprint: u64,
745 #[cfg(debug_assertions)]
746 storage_swapped_since_clear: bool,
747}
748
749pub type Scene = SceneRecording;
750
751impl SceneRecording {
752 pub fn clear(&mut self) {
753 self.ops.clear();
754 self.fingerprint = 0;
755 #[cfg(debug_assertions)]
756 {
757 self.storage_swapped_since_clear = false;
758 }
759 }
760
761 pub fn push(&mut self, op: SceneOp) {
762 let op = match op {
769 SceneOp::Quad {
770 order,
771 rect,
772 background,
773 border,
774 border_paint,
775 mut corner_radii,
776 } => {
777 let max = rect.size.width.0.min(rect.size.height.0) * 0.5;
778 let max = if max.is_finite() { max.max(0.0) } else { 0.0 };
779 corner_radii.top_left = Px(corner_radii.top_left.0.min(max));
780 corner_radii.top_right = Px(corner_radii.top_right.0.min(max));
781 corner_radii.bottom_left = Px(corner_radii.bottom_left.0.min(max));
782 corner_radii.bottom_right = Px(corner_radii.bottom_right.0.min(max));
783
784 SceneOp::Quad {
785 order,
786 rect,
787 background: background.sanitize(),
788 border,
789 border_paint: border_paint.sanitize(),
790 corner_radii,
791 }
792 }
793 SceneOp::ShadowRRect {
794 order,
795 rect,
796 mut corner_radii,
797 offset,
798 spread,
799 blur_radius,
800 color,
801 } => {
802 let max = rect.size.width.0.min(rect.size.height.0) * 0.5;
803 let max = if max.is_finite() { max.max(0.0) } else { 0.0 };
804 corner_radii.top_left = Px(corner_radii.top_left.0.max(0.0).min(max));
805 corner_radii.top_right = Px(corner_radii.top_right.0.max(0.0).min(max));
806 corner_radii.bottom_left = Px(corner_radii.bottom_left.0.max(0.0).min(max));
807 corner_radii.bottom_right = Px(corner_radii.bottom_right.0.max(0.0).min(max));
808
809 let blur_radius = if blur_radius.0.is_finite() {
810 Px(blur_radius
811 .0
812 .clamp(0.0, SHADOW_RRECT_V1_MAX_BLUR_RADIUS_PX.0))
813 } else {
814 Px(0.0)
815 };
816
817 SceneOp::ShadowRRect {
818 order,
819 rect,
820 corner_radii,
821 offset,
822 spread,
823 blur_radius,
824 color,
825 }
826 }
827 SceneOp::PushEffect {
828 bounds,
829 mode,
830 chain,
831 quality,
832 } => SceneOp::PushEffect {
833 bounds,
834 mode,
835 chain: chain.sanitize(),
836 quality,
837 },
838 SceneOp::Text {
839 order,
840 origin,
841 text,
842 paint,
843 outline,
844 shadow,
845 } => SceneOp::Text {
846 order,
847 origin,
848 text,
849 paint: paint.sanitize(),
850 outline: outline.and_then(|o| o.sanitize()),
851 shadow,
852 },
853 other => other,
854 };
855
856 self.fingerprint = mix_scene_op(self.fingerprint, op);
857 self.ops.push(op);
858 }
859
860 pub fn with_transform<T>(
861 &mut self,
862 transform: Transform2D,
863 f: impl FnOnce(&mut Self) -> T,
864 ) -> T {
865 self.push(SceneOp::PushTransform { transform });
866 let out = f(self);
867 self.push(SceneOp::PopTransform);
868 out
869 }
870
871 pub fn with_opacity<T>(&mut self, opacity: f32, f: impl FnOnce(&mut Self) -> T) -> T {
872 self.push(SceneOp::PushOpacity { opacity });
873 let out = f(self);
874 self.push(SceneOp::PopOpacity);
875 out
876 }
877
878 pub fn with_layer<T>(&mut self, layer: u32, f: impl FnOnce(&mut Self) -> T) -> T {
879 self.push(SceneOp::PushLayer { layer });
880 let out = f(self);
881 self.push(SceneOp::PopLayer);
882 out
883 }
884
885 pub fn with_clip_rect<T>(&mut self, rect: Rect, f: impl FnOnce(&mut Self) -> T) -> T {
886 self.push(SceneOp::PushClipRect { rect });
887 let out = f(self);
888 self.push(SceneOp::PopClip);
889 out
890 }
891
892 pub fn with_clip_rrect<T>(
893 &mut self,
894 rect: Rect,
895 corner_radii: Corners,
896 f: impl FnOnce(&mut Self) -> T,
897 ) -> T {
898 self.push(SceneOp::PushClipRRect { rect, corner_radii });
899 let out = f(self);
900 self.push(SceneOp::PopClip);
901 out
902 }
903
904 pub fn with_clip_path<T>(
905 &mut self,
906 bounds: Rect,
907 origin: Point,
908 path: PathId,
909 f: impl FnOnce(&mut Self) -> T,
910 ) -> T {
911 self.push(SceneOp::PushClipPath {
912 bounds,
913 origin,
914 path,
915 });
916 let out = f(self);
917 self.push(SceneOp::PopClip);
918 out
919 }
920
921 pub fn with_mask<T>(&mut self, bounds: Rect, mask: Mask, f: impl FnOnce(&mut Self) -> T) -> T {
922 self.push(SceneOp::PushMask { bounds, mask });
923 let out = f(self);
924 self.push(SceneOp::PopMask);
925 out
926 }
927
928 pub fn with_effect<T>(
929 &mut self,
930 bounds: Rect,
931 mode: EffectMode,
932 chain: EffectChain,
933 quality: EffectQuality,
934 f: impl FnOnce(&mut Self) -> T,
935 ) -> T {
936 self.push(SceneOp::PushEffect {
937 bounds,
938 mode,
939 chain,
940 quality,
941 });
942 let out = f(self);
943 self.push(SceneOp::PopEffect);
944 out
945 }
946
947 pub fn with_composite_group<T>(
948 &mut self,
949 desc: CompositeGroupDesc,
950 f: impl FnOnce(&mut Self) -> T,
951 ) -> T {
952 self.push(SceneOp::PushCompositeGroup { desc });
953 let out = f(self);
954 self.push(SceneOp::PopCompositeGroup);
955 out
956 }
957
958 pub fn with_backdrop_source_group_v1<T>(
959 &mut self,
960 bounds: Rect,
961 pyramid: Option<CustomEffectPyramidRequestV1>,
962 quality: EffectQuality,
963 f: impl FnOnce(&mut Self) -> T,
964 ) -> T {
965 self.push(SceneOp::PushBackdropSourceGroupV1 {
966 bounds,
967 pyramid,
968 quality,
969 });
970 let out = f(self);
971 self.push(SceneOp::PopBackdropSourceGroup);
972 out
973 }
974
975 pub fn ops(&self) -> &[SceneOp] {
976 &self.ops
977 }
978
979 pub fn ops_len(&self) -> usize {
980 self.ops.len()
981 }
982
983 pub fn swap_storage(&mut self, other_ops: &mut Vec<SceneOp>, other_fingerprint: &mut u64) {
991 #[cfg(debug_assertions)]
992 debug_assert!(
993 !self.storage_swapped_since_clear,
994 "Scene::swap_storage() was called more than once without an intervening Scene::clear(); \
995this is not supported because swap_storage() is destructive and typically indicates multiple paint-cache ingestions"
996 );
997 std::mem::swap(&mut self.ops, other_ops);
998 std::mem::swap(&mut self.fingerprint, other_fingerprint);
999 #[cfg(debug_assertions)]
1000 {
1001 self.storage_swapped_since_clear = true;
1002 }
1003 }
1004
1005 pub fn fingerprint(&self) -> u64 {
1006 self.fingerprint
1007 }
1008}
1009
1010#[derive(Debug, Clone, Copy)]
1011pub enum SceneOp {
1012 PushTransform {
1013 transform: Transform2D,
1014 },
1015 PopTransform,
1016
1017 PushOpacity {
1021 opacity: f32,
1022 },
1023 PopOpacity,
1024
1025 PushLayer {
1027 layer: u32,
1028 },
1029 PopLayer,
1030
1031 PushClipRect {
1032 rect: Rect,
1033 },
1034 PushClipRRect {
1035 rect: Rect,
1036 corner_radii: Corners,
1037 },
1038 PushClipPath {
1045 bounds: Rect,
1046 origin: Point,
1047 path: PathId,
1048 },
1049 PopClip,
1050
1051 PushMask {
1052 bounds: Rect,
1054 mask: Mask,
1055 },
1056 PopMask,
1057
1058 PushEffect {
1059 bounds: Rect,
1061 mode: EffectMode,
1062 chain: EffectChain,
1063 quality: EffectQuality,
1064 },
1065 PopEffect,
1066
1067 PushBackdropSourceGroupV1 {
1075 bounds: Rect,
1077 pyramid: Option<CustomEffectPyramidRequestV1>,
1079 quality: EffectQuality,
1081 },
1082 PopBackdropSourceGroup,
1083
1084 PushCompositeGroup {
1085 desc: CompositeGroupDesc,
1086 },
1087 PopCompositeGroup,
1088
1089 Quad {
1090 order: DrawOrder,
1091 rect: Rect,
1092 background: PaintBindingV1,
1093 border: Edges,
1094 border_paint: PaintBindingV1,
1095 corner_radii: Corners,
1096 },
1097
1098 StrokeRRect {
1099 order: DrawOrder,
1100 rect: Rect,
1101 stroke: Edges,
1102 stroke_paint: PaintBindingV1,
1103 corner_radii: Corners,
1104 style: StrokeStyleV1,
1105 },
1106
1107 ShadowRRect {
1113 order: DrawOrder,
1114 rect: Rect,
1115 corner_radii: Corners,
1116 offset: Point,
1117 spread: Px,
1118 blur_radius: Px,
1119 color: Color,
1120 },
1121
1122 Image {
1123 order: DrawOrder,
1124 rect: Rect,
1125 image: ImageId,
1126 fit: ViewportFit,
1127 sampling: ImageSamplingHint,
1128 opacity: f32,
1129 },
1130
1131 ImageRegion {
1132 order: DrawOrder,
1133 rect: Rect,
1134 image: ImageId,
1135 uv: UvRect,
1136 sampling: ImageSamplingHint,
1137 opacity: f32,
1138 },
1139
1140 MaskImage {
1144 order: DrawOrder,
1145 rect: Rect,
1146 image: ImageId,
1147 uv: UvRect,
1148 sampling: ImageSamplingHint,
1149 color: Color,
1150 opacity: f32,
1151 },
1152
1153 SvgMaskIcon {
1155 order: DrawOrder,
1156 rect: Rect,
1157 svg: SvgId,
1158 fit: SvgFit,
1159 color: Color,
1160 opacity: f32,
1161 },
1162
1163 SvgImage {
1165 order: DrawOrder,
1166 rect: Rect,
1167 svg: SvgId,
1168 fit: SvgFit,
1169 opacity: f32,
1170 },
1171
1172 Text {
1173 order: DrawOrder,
1174 origin: Point,
1175 text: TextBlobId,
1176 paint: PaintBindingV1,
1177 outline: Option<TextOutlineV1>,
1178 shadow: Option<TextShadowV1>,
1179 },
1180
1181 Path {
1182 order: DrawOrder,
1183 origin: Point,
1184 path: PathId,
1185 paint: PaintBindingV1,
1186 },
1187
1188 ViewportSurface {
1189 order: DrawOrder,
1190 rect: Rect,
1191 target: RenderTargetId,
1192 opacity: f32,
1193 },
1194}
1195
1196#[derive(Debug, Clone, Copy, PartialEq)]
1197pub struct UvRect {
1198 pub u0: f32,
1199 pub v0: f32,
1200 pub u1: f32,
1201 pub v1: f32,
1202}
1203
1204impl UvRect {
1205 pub const FULL: Self = Self {
1206 u0: 0.0,
1207 v0: 0.0,
1208 u1: 1.0,
1209 v1: 1.0,
1210 };
1211}
1212
1213pub const SHADOW_RRECT_V1_MAX_BLUR_RADIUS_PX: crate::Px = crate::Px(64.0);
1214
1215#[cfg(test)]
1216mod tests {
1217 use super::*;
1218 use crate::geometry::{Px, Size};
1219
1220 #[test]
1221 fn replay_ops_translated_wraps_in_transform_stack() {
1222 let ops = [SceneOp::Quad {
1223 order: DrawOrder(0),
1224 rect: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(10.0), Px(10.0))),
1225 background: Paint::Solid(Color::TRANSPARENT).into(),
1226 border: Edges::all(Px(0.0)),
1227 border_paint: Paint::Solid(Color::TRANSPARENT).into(),
1228 corner_radii: Corners::all(Px(0.0)),
1229 }];
1230
1231 let mut scene = Scene::default();
1232 scene.replay_ops_translated(&ops, Point::new(Px(2.0), Px(3.0)));
1233
1234 assert_eq!(scene.ops_len(), 3);
1235 assert!(matches!(scene.ops()[0], SceneOp::PushTransform { .. }));
1236 assert!(matches!(scene.ops()[1], SceneOp::Quad { .. }));
1237 assert!(matches!(scene.ops()[2], SceneOp::PopTransform));
1238 }
1239
1240 #[test]
1241 fn validate_rejects_transform_underflow() {
1242 let mut scene = Scene::default();
1243 scene.push(SceneOp::PopTransform);
1244 assert!(matches!(
1245 scene.validate(),
1246 Err(SceneValidationError {
1247 kind: SceneValidationErrorKind::TransformUnderflow,
1248 ..
1249 })
1250 ));
1251 }
1252
1253 #[test]
1254 fn validate_rejects_unbalanced_clip_stack() {
1255 let mut scene = Scene::default();
1256 scene.push(SceneOp::PushClipRect {
1257 rect: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(1.0), Px(1.0))),
1258 });
1259 assert!(matches!(
1260 scene.validate(),
1261 Err(SceneValidationError {
1262 kind: SceneValidationErrorKind::UnbalancedClipStack { remaining: 1 },
1263 ..
1264 })
1265 ));
1266 }
1267
1268 #[test]
1269 fn validate_rejects_effect_underflow() {
1270 let mut scene = Scene::default();
1271 scene.push(SceneOp::PopEffect);
1272 assert!(matches!(
1273 scene.validate(),
1274 Err(SceneValidationError {
1275 kind: SceneValidationErrorKind::EffectUnderflow,
1276 ..
1277 })
1278 ));
1279 }
1280
1281 #[test]
1282 fn validate_rejects_unbalanced_effect_stack() {
1283 let mut scene = Scene::default();
1284 scene.push(SceneOp::PushEffect {
1285 bounds: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(1.0), Px(1.0))),
1286 mode: EffectMode::Backdrop,
1287 chain: EffectChain::from_steps(&[EffectStep::GaussianBlur {
1288 radius_px: Px(2.0),
1289 downsample: 2,
1290 }]),
1291 quality: EffectQuality::Auto,
1292 });
1293 assert!(matches!(
1294 scene.validate(),
1295 Err(SceneValidationError {
1296 kind: SceneValidationErrorKind::UnbalancedEffectStack { remaining: 1 },
1297 ..
1298 })
1299 ));
1300 }
1301
1302 #[test]
1303 fn validate_rejects_opacity_underflow() {
1304 let mut scene = Scene::default();
1305 scene.push(SceneOp::PopOpacity);
1306 assert!(matches!(
1307 scene.validate(),
1308 Err(SceneValidationError {
1309 kind: SceneValidationErrorKind::OpacityUnderflow,
1310 ..
1311 })
1312 ));
1313 }
1314
1315 #[test]
1316 fn validate_rejects_layer_underflow() {
1317 let mut scene = Scene::default();
1318 scene.push(SceneOp::PopLayer);
1319 assert!(matches!(
1320 scene.validate(),
1321 Err(SceneValidationError {
1322 kind: SceneValidationErrorKind::LayerUnderflow,
1323 ..
1324 })
1325 ));
1326 }
1327
1328 #[test]
1329 fn validate_rejects_clip_underflow() {
1330 let mut scene = Scene::default();
1331 scene.push(SceneOp::PopClip);
1332 assert!(matches!(
1333 scene.validate(),
1334 Err(SceneValidationError {
1335 kind: SceneValidationErrorKind::ClipUnderflow,
1336 ..
1337 })
1338 ));
1339 }
1340
1341 #[test]
1342 fn validate_rejects_nonfinite_opacity() {
1343 let mut scene = Scene::default();
1344 scene.push(SceneOp::PushOpacity { opacity: f32::NAN });
1345 assert!(matches!(
1346 scene.validate(),
1347 Err(SceneValidationError {
1348 kind: SceneValidationErrorKind::NonFiniteOpacity,
1349 ..
1350 })
1351 ));
1352 }
1353
1354 #[test]
1355 fn validate_rejects_nonfinite_transform() {
1356 let mut scene = Scene::default();
1357 scene.push(SceneOp::PushTransform {
1358 transform: Transform2D {
1359 a: f32::NAN,
1360 ..Transform2D::IDENTITY
1361 },
1362 });
1363 assert!(matches!(
1364 scene.validate(),
1365 Err(SceneValidationError {
1366 kind: SceneValidationErrorKind::NonFiniteTransform,
1367 ..
1368 })
1369 ));
1370 }
1371
1372 #[test]
1373 fn validate_rejects_nonfinite_draw_op_data() {
1374 let mut scene = Scene::default();
1375 scene.push(SceneOp::Quad {
1376 order: DrawOrder(0),
1377 rect: Rect::new(
1378 Point::new(Px(f32::NAN), Px(0.0)),
1379 Size::new(Px(10.0), Px(10.0)),
1380 ),
1381 background: Paint::Solid(Color::TRANSPARENT).into(),
1382 border: Edges::all(Px(0.0)),
1383 border_paint: Paint::Solid(Color::TRANSPARENT).into(),
1384 corner_radii: Corners::all(Px(0.0)),
1385 });
1386 assert!(matches!(
1387 scene.validate(),
1388 Err(SceneValidationError {
1389 kind: SceneValidationErrorKind::NonFiniteOpData,
1390 ..
1391 })
1392 ));
1393 }
1394
1395 #[test]
1396 fn push_shadow_rrect_clamps_blur_and_preserves_base_corner_radii() {
1397 let mut scene = Scene::default();
1398 scene.push(SceneOp::ShadowRRect {
1399 order: DrawOrder(0),
1400 rect: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(20.0), Px(12.0))),
1401 corner_radii: Corners::all(Px(9999.0)),
1402 offset: Point::new(Px(0.0), Px(4.0)),
1403 spread: Px(-4.0),
1404 blur_radius: Px(4096.0),
1405 color: Color::TRANSPARENT,
1406 });
1407
1408 let SceneOp::ShadowRRect {
1409 corner_radii,
1410 blur_radius,
1411 ..
1412 } = scene.ops()[0]
1413 else {
1414 panic!("expected shadow rrect");
1415 };
1416
1417 assert_eq!(blur_radius, SHADOW_RRECT_V1_MAX_BLUR_RADIUS_PX);
1418 assert_eq!(corner_radii, Corners::all(Px(6.0)));
1419 }
1420
1421 #[test]
1422 fn validate_rejects_nonfinite_shadow_rrect_data() {
1423 let mut scene = Scene::default();
1424 scene.push(SceneOp::ShadowRRect {
1425 order: DrawOrder(0),
1426 rect: Rect::new(Point::new(Px(0.0), Px(0.0)), Size::new(Px(10.0), Px(10.0))),
1427 corner_radii: Corners::all(Px(4.0)),
1428 offset: Point::new(Px(f32::NAN), Px(0.0)),
1429 spread: Px(0.0),
1430 blur_radius: Px(8.0),
1431 color: Color::TRANSPARENT,
1432 });
1433
1434 assert!(matches!(
1435 scene.validate(),
1436 Err(SceneValidationError {
1437 kind: SceneValidationErrorKind::NonFiniteOpData,
1438 ..
1439 })
1440 ));
1441 }
1442
1443 #[test]
1444 fn validate_rejects_unbalanced_opacity_stack() {
1445 let mut scene = Scene::default();
1446 scene.push(SceneOp::PushOpacity { opacity: 0.5 });
1447 assert!(matches!(
1448 scene.validate(),
1449 Err(SceneValidationError {
1450 kind: SceneValidationErrorKind::UnbalancedOpacityStack { remaining: 1 },
1451 ..
1452 })
1453 ));
1454 }
1455
1456 #[test]
1457 fn validate_rejects_unbalanced_layer_stack() {
1458 let mut scene = Scene::default();
1459 scene.push(SceneOp::PushLayer { layer: 1 });
1460 assert!(matches!(
1461 scene.validate(),
1462 Err(SceneValidationError {
1463 kind: SceneValidationErrorKind::UnbalancedLayerStack { remaining: 1 },
1464 ..
1465 })
1466 ));
1467 }
1468}