1use std::sync::{Arc, Mutex, OnceLock, Weak};
7
8use crate::LayerShape;
9
10const RUNTIME_SHADER_INLINE_UNIFORMS: usize = 16;
11
12#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
14pub enum TileMode {
15 #[default]
17 Clamp,
18 Repeated,
20 Mirror,
22 Decal,
24}
25
26#[derive(Clone, Copy, Debug, PartialEq)]
32pub struct BlurredEdgeTreatment {
33 shape: Option<LayerShape>,
34}
35
36impl BlurredEdgeTreatment {
37 pub const RECTANGLE: Self = Self {
39 shape: Some(LayerShape::Rectangle),
40 };
41
42 pub const UNBOUNDED: Self = Self { shape: None };
44
45 pub const fn with_shape(shape: LayerShape) -> Self {
47 Self { shape: Some(shape) }
48 }
49
50 pub fn shape(self) -> Option<LayerShape> {
51 self.shape
52 }
53
54 pub fn clip(self) -> bool {
55 self.shape.is_some()
56 }
57
58 pub fn tile_mode(self) -> TileMode {
59 if self.clip() {
60 TileMode::Clamp
61 } else {
62 TileMode::Decal
63 }
64 }
65}
66
67impl Default for BlurredEdgeTreatment {
68 fn default() -> Self {
69 Self::RECTANGLE
70 }
71}
72
73#[derive(Clone, Debug)]
90pub struct RuntimeShader {
91 source: Arc<str>,
92 source_hash: u64,
93 uniforms: RuntimeShaderUniforms,
94 input_padding: f32,
95 output_padding: f32,
96}
97
98#[derive(Clone, Debug, PartialEq)]
99struct RuntimeShaderUniforms {
100 len: usize,
101 inline: [f32; RUNTIME_SHADER_INLINE_UNIFORMS],
102 heap: Option<Vec<f32>>,
103}
104
105impl RuntimeShaderUniforms {
106 fn new() -> Self {
107 Self {
108 len: 0,
109 inline: [0.0; RUNTIME_SHADER_INLINE_UNIFORMS],
110 heap: None,
111 }
112 }
113
114 fn as_slice(&self) -> &[f32] {
115 if let Some(heap) = &self.heap {
116 heap.as_slice()
117 } else {
118 &self.inline[..self.len]
119 }
120 }
121
122 fn len(&self) -> usize {
123 self.as_slice().len()
124 }
125
126 fn ensure_len(&mut self, min_len: usize) {
127 if let Some(heap) = &mut self.heap {
128 if heap.len() < min_len {
129 heap.resize(min_len, 0.0);
130 }
131 return;
132 }
133
134 if min_len <= RUNTIME_SHADER_INLINE_UNIFORMS {
135 self.len = self.len.max(min_len);
136 return;
137 }
138
139 let mut heap = Vec::with_capacity(min_len);
140 heap.extend_from_slice(&self.inline[..self.len]);
141 heap.resize(min_len, 0.0);
142 self.heap = Some(heap);
143 }
144
145 fn set(&mut self, index: usize, value: f32) {
146 if let Some(heap) = &mut self.heap {
147 heap[index] = value;
148 } else {
149 self.inline[index] = value;
150 }
151 }
152
153 #[cfg(test)]
154 fn is_inline(&self) -> bool {
155 self.heap.is_none()
156 }
157}
158
159#[derive(Clone, Copy, Debug, Eq, PartialEq, thiserror::Error)]
161pub enum RuntimeShaderUniformError {
162 #[error(
163 "uniform range starting at {index} with width {width} exceeds user uniform range 0..{max_user_uniforms}; slots {reserved_start}..{max_uniforms} are reserved for renderer data"
164 )]
165 OutOfUserRange {
166 index: usize,
167 width: usize,
168 max_user_uniforms: usize,
169 reserved_start: usize,
170 max_uniforms: usize,
171 },
172}
173
174impl RuntimeShader {
175 pub const MAX_UNIFORMS: usize = 256;
179 pub const RESERVED_UNIFORM_START: usize = 248;
181 pub const MAX_USER_UNIFORMS: usize = Self::RESERVED_UNIFORM_START;
183
184 #[track_caller]
186 pub fn new(wgsl_source: &str) -> Self {
187 let (source, source_hash) =
188 cached_shader_source(std::panic::Location::caller(), wgsl_source);
189 Self {
190 source,
191 source_hash,
192 uniforms: RuntimeShaderUniforms::new(),
193 input_padding: 0.0,
194 output_padding: 0.0,
195 }
196 }
197
198 pub fn from_shared_source(source: Arc<str>) -> Self {
203 let source_hash = cached_shared_shader_source_hash(&source);
204 Self {
205 source,
206 source_hash,
207 uniforms: RuntimeShaderUniforms::new(),
208 input_padding: 0.0,
209 output_padding: 0.0,
210 }
211 }
212
213 pub fn set_input_padding(&mut self, padding: f32) {
217 self.input_padding = if padding.is_finite() {
218 padding.max(0.0)
219 } else {
220 0.0
221 };
222 }
223
224 pub fn input_padding(&self) -> f32 {
226 self.input_padding
227 }
228
229 pub fn set_output_padding(&mut self, padding: f32) {
234 self.output_padding = if padding.is_finite() {
235 padding.max(0.0)
236 } else {
237 0.0
238 };
239 }
240
241 pub fn output_padding(&self) -> f32 {
243 self.output_padding
244 }
245
246 pub fn set_float(&mut self, index: usize, value: f32) {
251 let _ = self.try_set_float(index, value);
252 }
253
254 pub fn try_set_float(
256 &mut self,
257 index: usize,
258 value: f32,
259 ) -> Result<(), RuntimeShaderUniformError> {
260 self.try_ensure_capacity(index, 1)?;
261 self.uniforms.set(index, value);
262 Ok(())
263 }
264
265 pub fn set_float2(&mut self, index: usize, x: f32, y: f32) {
270 let _ = self.try_set_float2(index, x, y);
271 }
272
273 pub fn try_set_float2(
275 &mut self,
276 index: usize,
277 x: f32,
278 y: f32,
279 ) -> Result<(), RuntimeShaderUniformError> {
280 self.try_ensure_capacity(index, 2)?;
281 self.uniforms.set(index, x);
282 self.uniforms.set(index + 1, y);
283 Ok(())
284 }
285
286 pub fn set_float4(&mut self, index: usize, x: f32, y: f32, z: f32, w: f32) {
291 let _ = self.try_set_float4(index, x, y, z, w);
292 }
293
294 pub fn try_set_float4(
296 &mut self,
297 index: usize,
298 x: f32,
299 y: f32,
300 z: f32,
301 w: f32,
302 ) -> Result<(), RuntimeShaderUniformError> {
303 self.try_ensure_capacity(index, 4)?;
304 self.uniforms.set(index, x);
305 self.uniforms.set(index + 1, y);
306 self.uniforms.set(index + 2, z);
307 self.uniforms.set(index + 3, w);
308 Ok(())
309 }
310
311 pub fn source(&self) -> &str {
313 &self.source
314 }
315
316 pub fn uniforms(&self) -> &[f32] {
318 self.uniforms.as_slice()
319 }
320
321 pub fn uniforms_padded(&self) -> [f32; Self::MAX_UNIFORMS] {
323 let mut padded = [0.0f32; Self::MAX_UNIFORMS];
324 let len = self.uniforms.len().min(Self::MAX_UNIFORMS);
325 padded[..len].copy_from_slice(&self.uniforms.as_slice()[..len]);
326 padded
327 }
328
329 pub fn source_hash(&self) -> u64 {
331 self.source_hash
332 }
333
334 fn try_ensure_capacity(
335 &mut self,
336 index: usize,
337 width: usize,
338 ) -> Result<(), RuntimeShaderUniformError> {
339 let min_len = index
340 .checked_add(width)
341 .ok_or_else(|| Self::uniform_range_error(index, width))?;
342 if min_len > Self::MAX_USER_UNIFORMS {
343 return Err(Self::uniform_range_error(index, width));
344 }
345 self.uniforms.ensure_len(min_len);
346 Ok(())
347 }
348
349 fn uniform_range_error(index: usize, width: usize) -> RuntimeShaderUniformError {
350 RuntimeShaderUniformError::OutOfUserRange {
351 index,
352 width,
353 max_user_uniforms: Self::MAX_USER_UNIFORMS,
354 reserved_start: Self::RESERVED_UNIFORM_START,
355 max_uniforms: Self::MAX_UNIFORMS,
356 }
357 }
358}
359
360impl PartialEq for RuntimeShader {
361 fn eq(&self, other: &Self) -> bool {
362 self.source_hash == other.source_hash
363 && (Arc::ptr_eq(&self.source, &other.source)
364 || self.source.as_ref() == other.source.as_ref())
365 && self.uniforms == other.uniforms
366 && self.input_padding.to_bits() == other.input_padding.to_bits()
367 && self.output_padding.to_bits() == other.output_padding.to_bits()
368 }
369}
370
371fn hash_shader_source(source: &str) -> u64 {
372 const FNV_OFFSET_BASIS: u64 = 0xcbf2_9ce4_8422_2325;
373 const FNV_PRIME: u64 = 0x0000_0100_0000_01b3;
374
375 source
376 .as_bytes()
377 .iter()
378 .fold(FNV_OFFSET_BASIS, |hash, byte| {
379 (hash ^ u64::from(*byte)).wrapping_mul(FNV_PRIME)
380 })
381}
382
383#[derive(Clone, Copy, Debug, PartialEq, Eq)]
384struct ShaderSourceCallsite {
385 file: &'static str,
386 line: u32,
387 column: u32,
388}
389
390struct CachedShaderSource {
391 callsite: ShaderSourceCallsite,
392 source_hash: u64,
393 source: Arc<str>,
394}
395
396struct CachedSharedShaderSourceHash {
397 byte_ptr: usize,
398 len: usize,
399 source_hash: u64,
400 source: Weak<str>,
401}
402
403fn cached_shared_shader_source_hash(source: &Arc<str>) -> u64 {
404 static CACHE: OnceLock<Mutex<Vec<CachedSharedShaderSourceHash>>> = OnceLock::new();
405 let byte_ptr = source.as_ptr() as usize;
406 let len = source.len();
407 let mut cache = CACHE
408 .get_or_init(|| Mutex::new(Vec::new()))
409 .lock()
410 .unwrap_or_else(|poisoned| poisoned.into_inner());
411
412 cache.retain(|entry| entry.source.strong_count() > 0);
413 if let Some(entry) = cache.iter().find(|entry| {
414 entry.byte_ptr == byte_ptr
415 && entry.len == len
416 && entry
417 .source
418 .upgrade()
419 .is_some_and(|cached| Arc::ptr_eq(&cached, source))
420 }) {
421 return entry.source_hash;
422 }
423
424 let source_hash = hash_shader_source(source);
425 cache.push(CachedSharedShaderSourceHash {
426 byte_ptr,
427 len,
428 source_hash,
429 source: Arc::downgrade(source),
430 });
431 source_hash
432}
433
434fn cached_shader_source(
435 location: &'static std::panic::Location<'static>,
436 source: &str,
437) -> (Arc<str>, u64) {
438 static CACHE: OnceLock<Mutex<Vec<CachedShaderSource>>> = OnceLock::new();
439 let callsite = ShaderSourceCallsite {
440 file: location.file(),
441 line: location.line(),
442 column: location.column(),
443 };
444 let mut cache = CACHE
445 .get_or_init(|| Mutex::new(Vec::new()))
446 .lock()
447 .unwrap_or_else(|poisoned| poisoned.into_inner());
448
449 if let Some(entry) = cache.iter_mut().find(|entry| entry.callsite == callsite) {
450 if entry.source.as_ref() == source {
451 return (entry.source.clone(), entry.source_hash);
452 }
453 let source_hash = hash_shader_source(source);
454 entry.source_hash = source_hash;
455 entry.source = Arc::<str>::from(source);
456 return (entry.source.clone(), entry.source_hash);
457 }
458
459 let source_hash = hash_shader_source(source);
460 let shared = Arc::<str>::from(source);
461 cache.push(CachedShaderSource {
462 callsite,
463 source_hash,
464 source: shared.clone(),
465 });
466 (shared, source_hash)
467}
468
469#[derive(Clone, Debug, PartialEq)]
474pub enum RenderEffect {
475 Blur {
477 radius_x: f32,
478 radius_y: f32,
479 edge_treatment: TileMode,
480 },
481 Offset { offset_x: f32, offset_y: f32 },
483 Shader { shader: RuntimeShader },
485 Chain {
487 first: Box<RenderEffect>,
488 second: Box<RenderEffect>,
489 },
490}
491
492impl RenderEffect {
493 pub fn blur(radius: f32) -> Self {
495 Self::blur_with_edge_treatment(radius, TileMode::default())
496 }
497
498 pub fn blur_with_edge_treatment(radius: f32, edge_treatment: TileMode) -> Self {
501 Self::Blur {
502 radius_x: radius,
503 radius_y: radius,
504 edge_treatment,
505 }
506 }
507
508 pub fn blur_xy(radius_x: f32, radius_y: f32, edge_treatment: TileMode) -> Self {
510 Self::Blur {
511 radius_x,
512 radius_y,
513 edge_treatment,
514 }
515 }
516
517 pub fn offset(offset_x: f32, offset_y: f32) -> Self {
519 Self::Offset { offset_x, offset_y }
520 }
521
522 pub fn runtime_shader(shader: RuntimeShader) -> Self {
524 Self::Shader { shader }
525 }
526
527 pub fn then(self, other: RenderEffect) -> Self {
529 Self::Chain {
530 first: Box::new(self),
531 second: Box::new(other),
532 }
533 }
534
535 pub fn contains_runtime_shader(&self) -> bool {
539 match self {
540 RenderEffect::Shader { .. } => true,
541 RenderEffect::Chain { first, second } => {
542 first.contains_runtime_shader() || second.contains_runtime_shader()
543 }
544 _ => false,
545 }
546 }
547
548 pub fn input_padding(&self) -> f32 {
550 match self {
551 RenderEffect::Blur {
552 radius_x, radius_y, ..
553 } => radius_x.abs().max(radius_y.abs()),
554 RenderEffect::Offset { offset_x, offset_y } => offset_x.abs().max(offset_y.abs()),
555 RenderEffect::Shader { shader } => shader.input_padding(),
556 RenderEffect::Chain { first, second } => first.input_padding() + second.input_padding(),
557 }
558 }
559
560 pub fn output_padding(&self) -> f32 {
564 match self {
565 RenderEffect::Blur { .. } | RenderEffect::Offset { .. } => 0.0,
566 RenderEffect::Shader { shader } => shader.output_padding(),
567 RenderEffect::Chain { first, second } => {
568 first.output_padding() + second.output_padding()
569 }
570 }
571 }
572}
573
574#[cfg(test)]
575mod tests {
576 use super::*;
577 use crate::RoundedCornerShape;
578
579 #[test]
580 fn runtime_shader_set_uniforms() {
581 let mut shader = RuntimeShader::new("// test");
582 shader.set_float(0, 1.0);
583 shader.set_float2(2, 3.0, 4.0);
584 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
585
586 assert_eq!(shader.uniforms()[0], 1.0);
587 assert_eq!(shader.uniforms()[1], 0.0); assert_eq!(shader.uniforms()[2], 3.0);
589 assert_eq!(shader.uniforms()[3], 4.0);
590 assert_eq!(shader.uniforms()[4], 5.0);
591 assert_eq!(shader.uniforms()[5], 6.0);
592 assert_eq!(shader.uniforms()[6], 7.0);
593 assert_eq!(shader.uniforms()[7], 8.0);
594 }
595
596 #[test]
597 fn runtime_shader_padded() {
598 let mut shader = RuntimeShader::new("// test");
599 shader.set_float(0, 42.0);
600 let padded = shader.uniforms_padded();
601 assert_eq!(padded[0], 42.0);
602 assert_eq!(padded[1], 0.0);
603 assert_eq!(padded[255], 0.0);
604 }
605
606 #[test]
607 fn blur_and_offset_declare_input_padding() {
608 assert_eq!(
609 RenderEffect::blur_xy(6.0, 12.0, TileMode::Clamp).input_padding(),
610 12.0
611 );
612 assert_eq!(RenderEffect::offset(-8.0, 3.0).input_padding(), 8.0);
613 }
614
615 #[test]
616 fn chained_effect_padding_accumulates_sampling_ranges() {
617 let mut shader = RuntimeShader::new("// test");
618 shader.set_input_padding(9.0);
619 let effect = RenderEffect::blur_xy(4.0, 6.0, TileMode::Clamp)
620 .then(RenderEffect::runtime_shader(shader))
621 .then(RenderEffect::offset(2.0, -5.0));
622
623 assert_eq!(effect.input_padding(), 20.0);
624 }
625
626 #[test]
627 fn runtime_shader_keeps_common_uniform_payload_inline() {
628 let mut shader = RuntimeShader::new("// test");
629 shader.set_float4(0, 1.0, 2.0, 3.0, 4.0);
630 shader.set_float4(4, 5.0, 6.0, 7.0, 8.0);
631 shader.set_float4(8, 9.0, 10.0, 11.0, 12.0);
632 shader.set_float4(12, 13.0, 14.0, 15.0, 16.0);
633
634 assert!(shader.uniforms.is_inline());
635 assert_eq!(shader.uniforms().len(), 16);
636
637 shader.set_float(16, 17.0);
638 assert!(!shader.uniforms.is_inline());
639 assert_eq!(shader.uniforms()[16], 17.0);
640 }
641
642 #[test]
643 fn runtime_shader_try_set_reports_reserved_uniform_slots() {
644 let mut shader = RuntimeShader::new("// test");
645
646 let err = shader
647 .try_set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0)
648 .unwrap_err();
649 assert_eq!(
650 err,
651 RuntimeShaderUniformError::OutOfUserRange {
652 index: RuntimeShader::RESERVED_UNIFORM_START,
653 width: 1,
654 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
655 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
656 max_uniforms: RuntimeShader::MAX_UNIFORMS,
657 }
658 );
659 assert!(shader.uniforms().is_empty());
660
661 let err = shader
662 .try_set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0)
663 .unwrap_err();
664 assert_eq!(
665 err,
666 RuntimeShaderUniformError::OutOfUserRange {
667 index: RuntimeShader::MAX_USER_UNIFORMS - 3,
668 width: 4,
669 max_user_uniforms: RuntimeShader::MAX_USER_UNIFORMS,
670 reserved_start: RuntimeShader::RESERVED_UNIFORM_START,
671 max_uniforms: RuntimeShader::MAX_UNIFORMS,
672 }
673 );
674 }
675
676 #[test]
677 fn runtime_shader_setters_ignore_invalid_uniform_slots_without_panicking() {
678 let mut shader = RuntimeShader::new("// test");
679 shader.set_float(0, 7.0);
680
681 shader.set_float(RuntimeShader::RESERVED_UNIFORM_START, 1.0);
682 shader.set_float4(RuntimeShader::MAX_USER_UNIFORMS - 3, 1.0, 2.0, 3.0, 4.0);
683
684 assert_eq!(shader.uniforms(), &[7.0]);
685 }
686
687 #[test]
688 fn render_effect_chaining() {
689 let blur = RenderEffect::blur(10.0);
690 let offset = RenderEffect::offset(5.0, 5.0);
691 let chained = blur.then(offset);
692 match chained {
693 RenderEffect::Chain { first, second } => {
694 assert!(matches!(*first, RenderEffect::Blur { .. }));
695 assert!(matches!(*second, RenderEffect::Offset { .. }));
696 }
697 _ => panic!("expected Chain"),
698 }
699 }
700
701 #[test]
702 fn blur_convenience() {
703 let effect = RenderEffect::blur(15.0);
704 match effect {
705 RenderEffect::Blur {
706 radius_x,
707 radius_y,
708 edge_treatment,
709 } => {
710 assert_eq!(radius_x, 15.0);
711 assert_eq!(radius_y, 15.0);
712 assert_eq!(edge_treatment, TileMode::Clamp);
713 }
714 _ => panic!("expected Blur"),
715 }
716 }
717
718 #[test]
719 fn blur_with_edge_treatment_uses_explicit_mode() {
720 let effect = RenderEffect::blur_with_edge_treatment(6.0, TileMode::Decal);
721 match effect {
722 RenderEffect::Blur {
723 radius_x,
724 radius_y,
725 edge_treatment,
726 } => {
727 assert_eq!(radius_x, 6.0);
728 assert_eq!(radius_y, 6.0);
729 assert_eq!(edge_treatment, TileMode::Decal);
730 }
731 _ => panic!("expected Blur"),
732 }
733 }
734
735 #[test]
736 fn source_hash_consistent() {
737 let s1 = RuntimeShader::new("fn main() {}");
738 let s2 = RuntimeShader::new("fn main() {}");
739 assert_eq!(s1.source_hash(), s2.source_hash());
740 }
741
742 #[test]
743 fn runtime_shader_from_shared_source_reuses_shared_source() {
744 let source = Arc::<str>::from("fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }");
745 let s1 = RuntimeShader::from_shared_source(source.clone());
746 let s2 = RuntimeShader::from_shared_source(source);
747
748 assert!(Arc::ptr_eq(&s1.source, &s2.source));
749 assert_eq!(s1.source_hash(), s2.source_hash());
750 }
751
752 fn runtime_shader_from_reuse_callsite(source: &str) -> RuntimeShader {
753 RuntimeShader::new(source)
754 }
755
756 fn runtime_shader_from_replacement_callsite(source: &str) -> RuntimeShader {
757 RuntimeShader::new(source)
758 }
759
760 #[test]
761 fn runtime_shader_new_reuses_same_callsite_source() {
762 let source = "fn fragment() -> vec4<f32> { return vec4<f32>(1.0); }";
763 let s1 = runtime_shader_from_reuse_callsite(source);
764 let s2 = runtime_shader_from_reuse_callsite(source);
765
766 assert!(Arc::ptr_eq(&s1.source, &s2.source));
767 assert_eq!(s1.source_hash(), s2.source_hash());
768 }
769
770 #[test]
771 fn runtime_shader_new_replaces_changed_callsite_source() {
772 let s1 = runtime_shader_from_replacement_callsite("fn a() {}");
773 let s2 = runtime_shader_from_replacement_callsite("fn b() {}");
774
775 assert!(!Arc::ptr_eq(&s1.source, &s2.source));
776 assert_ne!(s1.source_hash(), s2.source_hash());
777 assert_eq!(s2.source(), "fn b() {}");
778 }
779
780 #[test]
781 fn runtime_shader_source_storage_has_no_process_global_interner() {
782 let source = include_str!("render_effect.rs");
783 let blocked_static = ["static ", "INTERNER"].concat();
784 let blocked_type = ["ShaderSource", "Interner"].concat();
785
786 assert!(
787 !source.contains(&blocked_static) && !source.contains(&blocked_type),
788 "RuntimeShader source sharing must be explicit via from_shared_source, not a process-global interner"
789 );
790 }
791
792 #[test]
793 fn blur_xy_preserves_tile_mode() {
794 let effect = RenderEffect::blur_xy(3.0, 7.0, TileMode::Clamp);
795 match effect {
796 RenderEffect::Blur {
797 radius_x,
798 radius_y,
799 edge_treatment,
800 } => {
801 assert_eq!(radius_x, 3.0);
802 assert_eq!(radius_y, 7.0);
803 assert_eq!(edge_treatment, TileMode::Clamp);
804 }
805 _ => panic!("expected Blur"),
806 }
807 }
808
809 #[test]
810 fn offset_constructor_sets_components() {
811 let effect = RenderEffect::offset(11.0, -5.0);
812 match effect {
813 RenderEffect::Offset { offset_x, offset_y } => {
814 assert_eq!(offset_x, 11.0);
815 assert_eq!(offset_y, -5.0);
816 }
817 _ => panic!("expected Offset"),
818 }
819 }
820
821 #[test]
822 fn runtime_shader_equality_is_source_value_based() {
823 let mut s1 = RuntimeShader::new("fn main() {}");
824 let mut s2 = RuntimeShader::new("fn main() {}");
825 s1.set_float(0, 1.0);
826 s2.set_float(0, 1.0);
827 assert_eq!(s1, s2);
828 }
829
830 #[test]
831 fn blurred_edge_treatment_defaults_to_bounded_rectangle() {
832 let treatment = BlurredEdgeTreatment::default();
833 assert_eq!(treatment.shape(), Some(LayerShape::Rectangle));
834 assert!(treatment.clip());
835 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
836 }
837
838 #[test]
839 fn blurred_edge_treatment_unbounded_uses_decal_and_no_clip() {
840 let treatment = BlurredEdgeTreatment::UNBOUNDED;
841 assert_eq!(treatment.shape(), None);
842 assert!(!treatment.clip());
843 assert_eq!(treatment.tile_mode(), TileMode::Decal);
844 }
845
846 #[test]
847 fn blurred_edge_treatment_with_shape_uses_bounded_mode() {
848 let rounded = LayerShape::Rounded(RoundedCornerShape::uniform(8.0));
849 let treatment = BlurredEdgeTreatment::with_shape(rounded);
850 assert_eq!(treatment.shape(), Some(rounded));
851 assert!(treatment.clip());
852 assert_eq!(treatment.tile_mode(), TileMode::Clamp);
853 }
854}