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