1use std::{
7 cell::{Cell, RefCell},
8 sync::OnceLock,
9};
10
11use cranpose_ui::{Modifier, current_density};
12use cranpose_ui_graphics::{
13 Color, GLASS_ACTIVITY_UNIFORM, GLASS_ADAPTIVE_FROST_UNIFORM, GLASS_ADAPTIVE_TONE_UNIFORM,
14 GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM, GLASS_EDGE_REFRACTION_REACH_UNIFORM,
15 GLASS_EDGE_RETURN_DEPTH_UNIFORM, GLASS_EDGE_SPECTRUM_UNIFORM, GLASS_EFFECT_DENSITY_UNIFORM,
16 GLASS_FACE_LIGHTING_OFF_UNIFORM, GLASS_FACE_RESPONSE_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM,
17 GLASS_INNER_SHADOW_UNIFORM, GLASS_KEY_FILL_UNIFORM, GLASS_LAYER_CLIPPED_UNIFORM,
18 GLASS_LIGHT_DIRECTION_UNIFORM, GLASS_MENISCUS_ABSORPTION_UNIFORM,
19 GLASS_OPTICAL_PROJECTION_UNIFORM, GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
20 GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
21 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM,
22 GLASS_REFRACTION_MODE_UNIFORM, GLASS_RESTING_TINT_UNIFORM, GLASS_TOUCH_RADIUS_UNIFORM,
23 GLASS_TRANSMISSION_REFRACTION_UNIFORM, GraphicsLayer, LIQUID_GLASS_WGSL, LayerShape, Rect,
24 RenderEffect, RoundedCornerShape, RuntimeShader, TileMode, liquid_glass_runtime_effect,
25 specialize_liquid_glass,
26};
27
28use crate::{appearance::GlassTintAmount, theme::LiquidColors};
29
30thread_local! {
31 static GLASS_LIGHT_RETURN: Cell<(f32, f32)> = const { Cell::new((0.0, 1.0)) };
32}
33
34pub fn set_glass_light_direction(direction: (f32, f32)) {
37 GLASS_LIGHT_RETURN.with(|cell| cell.set(direction));
38}
39
40pub fn glass_light_direction() -> (f32, f32) {
42 GLASS_LIGHT_RETURN.with(|cell| cell.get())
43}
44
45const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
46
47const CAPSULE_SHADER_RADIUS: f32 = -1.0;
48
49const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
50
51#[derive(Clone, Copy, Debug, PartialEq, Default)]
53pub enum LiquidShape {
54 #[default]
56 Capsule,
57 RoundedRect(f32),
59 Circle,
61}
62
63impl LiquidShape {
64 pub fn clip_shape(&self) -> RoundedCornerShape {
66 match self {
67 LiquidShape::Capsule | LiquidShape::Circle => {
68 RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
69 }
70 LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
71 }
72 }
73
74 pub fn layer_shape(&self) -> LayerShape {
76 LayerShape::Rounded(self.clip_shape())
77 }
78
79 fn shader_radius_px(&self, density: f32) -> f32 {
80 match self {
81 LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
82 LiquidShape::RoundedRect(radius) => radius * density,
83 }
84 }
85}
86
87#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
90pub enum GlassVariant {
91 #[default]
93 Regular,
94 Clear,
96 Lens,
100}
101
102#[derive(Clone, Copy, Debug, PartialEq, Default)]
104pub enum GlassRefraction {
105 #[default]
107 Radial,
108 Surface {
110 reach_dp: f32,
112 },
113 EdgeLens {
117 reach_dp: f32,
119 },
120}
121
122impl GlassRefraction {
123 fn shader_parameters(self) -> (f32, f32) {
124 match self {
125 Self::Radial => (0.0, 0.0),
126 Self::Surface { reach_dp } => (1.0, normalized_refraction_reach(reach_dp)),
127 Self::EdgeLens { reach_dp } => (2.0, normalized_refraction_reach(reach_dp)),
128 }
129 }
130}
131
132fn normalized_refraction_reach(reach_dp: f32) -> f32 {
133 if reach_dp.is_finite() {
134 reach_dp.max(0.0)
135 } else {
136 0.0
137 }
138}
139
140#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct GlassShadow {
145 pub color: Color,
146 pub radius: f32,
147 pub offset_y: f32,
148 pub spread: f32,
149}
150
151impl GlassShadow {
152 pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
153 Self {
154 color,
155 radius: radius.max(0.0),
156 offset_y,
157 spread,
158 }
159 }
160
161 fn uniforms(self) -> Option<[f32; 8]> {
162 let values = [
163 self.color.r(),
164 self.color.g(),
165 self.color.b(),
166 self.color.a(),
167 self.radius,
168 self.offset_y,
169 self.spread,
170 1.0,
171 ];
172 (values.iter().all(|value| value.is_finite())
173 && self.radius >= 0.0
174 && (0.0..=1.0).contains(&self.color.a()))
175 .then_some(values)
176 }
177}
178
179#[derive(Clone, Debug, PartialEq, Default)]
182pub struct GlassDynamics {
183 pub edge_return_depth_dp: Option<f32>,
186 pub optical_projection: Option<(f32, f32)>,
190 pub activity: Option<f32>,
194 pub resting_tint: Option<Color>,
197 pub highlight_boost: f32,
199 pub saturation_boost: f32,
202 pub tint_alpha_multiplier: Option<f32>,
205 pub morph: Option<GlassMorph>,
208 pub touch: Option<(f32, f32, f32)>,
212 pub touch_radius_dp: Option<f32>,
214 pub press_depth: Option<f32>,
220}
221
222impl GlassDynamics {
223 fn projection(&self) -> (f32, f32) {
224 self.optical_projection
225 .filter(|(x, y)| x.is_finite() && y.is_finite() && *x > 0.0 && *y > 0.0)
226 .unwrap_or((1.0, 1.0))
227 }
228
229 pub fn touched_up(
236 mut self,
237 press: f32,
238 finger: Option<(f32, f32)>,
239 center: (f32, f32),
240 ) -> Self {
241 let press = press.clamp(0.0, 1.0);
242 if press <= 0.0 {
243 return self;
244 }
245 self.highlight_boost += 0.85 * press;
246 self.saturation_boost += 0.45 * press;
247 let (fx, fy) = finger.unwrap_or(center);
248 let carried = self.touch.map(|(_, _, i)| i).unwrap_or(0.0);
249 self.touch = Some((fx, fy, (carried + press).clamp(0.0, 1.0)));
250 self
251 }
252}
253
254fn foreground_is_dark(foreground: Color) -> bool {
255 let foreground_luma =
256 0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
257 foreground_luma < 0.5
258}
259
260fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
261 if boost.abs() <= f32::EPSILON {
262 return tint;
263 }
264 let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
265 let saturation = (1.0 + boost).max(0.0);
266 Color::rgba(
267 (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
268 (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
269 (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
270 tint.a(),
271 )
272}
273
274pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
275 if foreground_is_dark(foreground) {
276 Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
277 } else {
278 Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
279 }
280}
281
282#[derive(Clone, Debug, PartialEq, Default)]
293pub struct GlassMorph {
294 pub node_size: (f32, f32),
300 pub primary: (f32, f32, f32, f32, f32),
301 pub shapes: Vec<(f32, f32, f32, f32, f32)>,
303 pub glue: f32,
305 pub wobble_amplitude: f32,
307 pub wobble_phase: f32,
308 pub bulge_amplitude: f32,
312 pub bulge_direction: f32,
313 pub ellipse_blend: f32,
317 pub capsule_smoothing_dp: f32,
322 pub deformation: Option<GlassDeformation>,
326 pub zoom_anchor: (f32, f32),
332}
333
334#[derive(Clone, Copy, Debug, PartialEq)]
338pub struct GlassDeformation {
339 axis: (f32, f32),
340 along: f32,
341}
342
343impl GlassDeformation {
344 pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
345 let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
346 let axis = if length > f32::EPSILON {
347 (axis.0 / length, axis.1 / length)
348 } else {
349 (1.0, 0.0)
350 };
351 Self {
352 axis,
353 along: along.max(f32::EPSILON),
354 }
355 }
356
357 pub fn axis(self) -> (f32, f32) {
358 self.axis
359 }
360
361 pub fn along(self) -> f32 {
362 self.along
363 }
364
365 pub fn across(self) -> f32 {
366 1.0 / self.along
367 }
368}
369
370impl GlassMorph {
371 pub const MAX_SHAPES: usize = 8;
373}
374
375#[derive(Clone, Copy, Debug, PartialEq)]
378pub struct GlassKeyFill {
379 pub height_dp: f32,
381 pub curvature: f32,
383 pub angle_radians: f32,
385 pub saturation: f32,
387 pub luma_gain: f32,
389 pub offset: f32,
391 pub scale_with_surface: bool,
393}
394
395impl GlassKeyFill {
396 fn uniforms(self) -> Option<[f32; 7]> {
397 let values = [
398 self.height_dp,
399 self.curvature,
400 self.angle_radians,
401 self.saturation,
402 self.luma_gain,
403 self.offset,
404 f32::from(self.scale_with_surface),
405 ];
406 (values.iter().all(|value| value.is_finite())
407 && self.height_dp > 0.0
408 && self.curvature > 0.0)
409 .then_some(values)
410 }
411}
412
413#[derive(Clone, Copy, Debug, PartialEq)]
416pub struct GlassSpectrum {
417 pub angle_radians: f32,
420 pub step_dp: f32,
422 pub vertical_scale: f32,
424 pub opacity_near: f32,
426 pub opacity_far: f32,
428 pub fade_depth_dp: f32,
430 pub extent_dp: f32,
432}
433
434impl GlassSpectrum {
435 fn uniforms(self) -> Option<[f32; 8]> {
436 let values = [
437 self.angle_radians,
438 self.step_dp,
439 self.vertical_scale,
440 self.opacity_near,
441 self.opacity_far,
442 self.fade_depth_dp,
443 self.extent_dp,
444 1.0,
445 ];
446 (values.iter().all(|value| value.is_finite())
447 && self.vertical_scale >= 0.0
448 && (0.0..=1.0).contains(&self.opacity_near)
449 && (0.0..=1.0).contains(&self.opacity_far)
450 && self.fade_depth_dp >= 0.0
451 && self.extent_dp >= 0.0)
452 .then_some(values)
453 }
454
455 fn capture_reach(self) -> f32 {
456 self.step_dp.abs() * 3.0 * self.vertical_scale.max(1.0)
457 }
458}
459
460#[derive(Clone, Copy, Debug, PartialEq)]
464pub struct GlassFaceResponse {
465 pub gain: f32,
467 pub start_dp: f32,
469 pub end_dp: f32,
472 pub illumination: f32,
474}
475
476impl GlassFaceResponse {
477 fn uniforms(self) -> Option<[f32; 4]> {
478 let values = [
479 1.0 - self.gain,
480 self.start_dp,
481 self.end_dp,
482 self.illumination,
483 ];
484 (values.iter().all(|value| value.is_finite())
485 && (0.0..=1.0).contains(&self.gain)
486 && self.start_dp >= 0.0
487 && self.end_dp >= self.start_dp)
488 .then_some(values)
489 }
490}
491
492#[derive(Clone, Debug, PartialEq)]
495pub struct Glass {
496 pub variant: GlassVariant,
497 pub shape: LiquidShape,
498 pub tint: Option<Color>,
500 pub tint_amount: Option<GlassTintAmount>,
502 pub blur_radius: Option<f32>,
504 pub backdrop_blur: Option<(f32, f32)>,
506 pub saturation: Option<f32>,
508 pub refraction_depth: f32,
512 pub refraction_depth_dp: Option<f32>,
516 pub refraction: GlassRefraction,
518 pub refraction_curve: f32,
520 pub dispersion: f32,
522 pub edge_spectrum: Option<GlassSpectrum>,
524 pub transmission_refraction: f32,
528 pub meniscus_absorption: f32,
531 pub fold_depth: f32,
535 pub optical_zoom: f32,
538 pub rim_reflection: f32,
541 pub ink_recolor: Option<(Color, f32)>,
545 pub highlight: f32,
547 pub face_lighting: bool,
549 pub key_fill: Option<GlassKeyFill>,
551 pub face_response: Option<GlassFaceResponse>,
553 pub inner_shadow: Option<GlassShadow>,
555 pub lift: Option<f32>,
558 pub contrast: Option<f32>,
563 pub shadow: bool,
565 pub shadow_style: Option<GlassShadow>,
567 pub clip: bool,
570 pub foreground: Option<Color>,
573 pub adaptive_frost: f32,
575 pub adaptive_tone: bool,
578}
579
580impl Glass {
581 pub fn regular() -> Self {
583 Self {
584 variant: GlassVariant::Regular,
585 shape: LiquidShape::Capsule,
586 tint: None,
587 tint_amount: None,
588 blur_radius: None,
589 backdrop_blur: None,
590 saturation: None,
591 refraction_depth: 0.0,
592 refraction_depth_dp: None,
593 refraction: GlassRefraction::Radial,
594 refraction_curve: 0.25,
595 dispersion: 0.0,
596 edge_spectrum: None,
597 transmission_refraction: 0.0,
598 meniscus_absorption: 1.0,
599 fold_depth: 0.0,
600 optical_zoom: 1.0,
601 rim_reflection: 1.0,
602 ink_recolor: None,
603 highlight: 0.9,
604 face_lighting: true,
605 key_fill: None,
606 face_response: None,
607 inner_shadow: None,
608 lift: None,
609 contrast: None,
610 shadow: true,
611 shadow_style: None,
612 clip: true,
613 foreground: None,
614 adaptive_frost: 0.65,
615 adaptive_tone: false,
616 }
617 }
618
619 pub fn clear() -> Self {
620 Self {
621 variant: GlassVariant::Clear,
622 ..Self::regular()
623 }
624 }
625
626 pub fn lens() -> Self {
629 Self {
630 variant: GlassVariant::Lens,
631 shape: LiquidShape::Capsule,
632 tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
633 tint_amount: None,
634 blur_radius: None,
635 backdrop_blur: None,
636 saturation: None,
637 refraction_depth: 0.34,
638 refraction_depth_dp: None,
639 refraction: GlassRefraction::Radial,
640 refraction_curve: 1.0,
641 dispersion: 0.30,
642 edge_spectrum: None,
643 transmission_refraction: 1.0,
644 meniscus_absorption: 1.0,
645 fold_depth: 0.0,
646 optical_zoom: 1.0,
647 rim_reflection: 1.0,
648 ink_recolor: None,
649 highlight: 1.15,
650 face_lighting: true,
651 key_fill: None,
652 face_response: None,
653 inner_shadow: None,
654 lift: None,
655 contrast: None,
656 shadow: true,
657 shadow_style: None,
658 clip: true,
659 foreground: None,
660 adaptive_frost: 0.0,
661 adaptive_tone: false,
662 }
663 }
664
665 pub fn face_lighting(mut self, enabled: bool) -> Self {
667 self.face_lighting = enabled;
668 self
669 }
670
671 pub fn key_fill(mut self, lighting: GlassKeyFill) -> Self {
674 self.key_fill = Some(lighting);
675 self
676 }
677
678 pub fn face_response(mut self, response: GlassFaceResponse) -> Self {
680 self.face_response = Some(response);
681 self
682 }
683
684 pub fn inner_shadow(mut self, shadow: GlassShadow) -> Self {
688 self.inner_shadow = shadow.uniforms().map(|_| shadow);
689 self
690 }
691
692 pub fn shape(mut self, shape: LiquidShape) -> Self {
693 self.shape = shape;
694 self
695 }
696
697 pub fn tint(mut self, tint: Color) -> Self {
698 self.tint = Some(tint);
699 self
700 }
701
702 pub fn blur_radius(mut self, radius_dp: f32) -> Self {
703 self.blur_radius = Some(radius_dp);
704 self
705 }
706
707 pub fn saturation(mut self, saturation: f32) -> Self {
708 self.saturation = Some(saturation);
709 self
710 }
711
712 pub fn refraction_depth(mut self, fraction: f32) -> Self {
713 self.refraction_depth = fraction.clamp(0.0, 2.0);
714 self.refraction_depth_dp = None;
715 self
716 }
717
718 pub fn refraction_depth_dp(mut self, depth_dp: f32) -> Self {
721 self.refraction_depth_dp = Some(depth_dp.max(0.0));
722 self
723 }
724
725 pub fn surface_refraction(mut self, reach_dp: f32) -> Self {
730 self.refraction = GlassRefraction::Surface {
731 reach_dp: normalized_refraction_reach(reach_dp),
732 };
733 self
734 }
735
736 pub fn edge_spectrum(mut self, spectrum: GlassSpectrum) -> Self {
739 self.edge_spectrum = spectrum.uniforms().map(|_| spectrum);
740 self
741 }
742
743 pub fn edge_refraction(mut self, reach_dp: f32) -> Self {
747 self.refraction = GlassRefraction::EdgeLens {
748 reach_dp: normalized_refraction_reach(reach_dp),
749 };
750 self
751 }
752
753 pub fn refraction_curve(mut self, curve: f32) -> Self {
754 self.refraction_curve = curve.clamp(0.05, 1.0);
755 self
756 }
757
758 pub fn dispersion(mut self, strength: f32) -> Self {
759 self.dispersion = strength.clamp(0.0, 1.0);
760 self
761 }
762
763 pub fn transmission_refraction(mut self, strength: f32) -> Self {
764 self.transmission_refraction = strength.clamp(0.0, 1.0);
765 self
766 }
767
768 pub fn meniscus_absorption(mut self, strength: f32) -> Self {
769 self.meniscus_absorption = strength.clamp(0.0, 1.0);
770 self
771 }
772
773 pub fn fold_depth(mut self, depth_dp: f32) -> Self {
775 self.fold_depth = depth_dp.max(0.0);
776 self
777 }
778
779 pub fn optical_zoom(mut self, zoom: f32) -> Self {
781 self.optical_zoom = zoom.max(1.0);
782 self
783 }
784
785 pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
787 self.rim_reflection = reflectivity.clamp(0.0, 2.0);
788 self
789 }
790
791 pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
794 self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
795 self
796 }
797
798 pub fn highlight(mut self, highlight: f32) -> Self {
799 self.highlight = highlight;
800 self
801 }
802
803 pub fn lift(mut self, lift: f32) -> Self {
806 self.lift = Some(lift);
807 self
808 }
809
810 pub fn contrast(mut self, contrast: f32) -> Self {
813 self.contrast = Some(contrast.max(0.05));
814 self
815 }
816
817 pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
818 self.foreground = Some(foreground);
819 self.adaptive_frost = strength.clamp(0.0, 1.0);
820 self
821 }
822
823 pub fn adaptive_tone(mut self, foreground: Color) -> Self {
829 self.foreground = Some(foreground);
830 self.adaptive_tone = true;
831 self.adaptive_frost = 0.0;
832 self
833 }
834
835 pub fn shadow(mut self, shadow: bool) -> Self {
836 self.shadow = shadow;
837 self
838 }
839
840 pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
841 self.shadow_style = Some(shadow);
842 self
843 }
844
845 pub fn no_clip(mut self) -> Self {
848 self.clip = false;
849 self
850 }
851
852 pub fn tint_amount(mut self, amount: GlassTintAmount) -> Self {
855 self.tint_amount = Some(amount);
856 self
857 }
858
859 pub fn backdrop_blur(mut self, radius_dp: f32, opacity: f32) -> Self {
863 self.backdrop_blur = (radius_dp.is_finite() && opacity.is_finite())
864 .then_some((radius_dp.max(0.0), opacity.clamp(0.0, 1.0)));
865 self
866 }
867
868 fn default_blur_radius(&self) -> f32 {
869 match self.variant {
870 GlassVariant::Regular => 8.0,
871 GlassVariant::Clear => 3.0,
872 GlassVariant::Lens => 0.0,
873 }
874 }
875
876 fn default_saturation(&self) -> f32 {
877 match self.variant {
878 GlassVariant::Regular => 1.5,
879 GlassVariant::Clear => 1.25,
880 GlassVariant::Lens => 1.0,
881 }
882 }
883
884 pub fn backdrop_effect(
888 &self,
889 colors: &LiquidColors,
890 density: f32,
891 dynamics: GlassDynamics,
892 ) -> RenderEffect {
893 self.resolve(colors).backdrop_effect(density, dynamics)
894 }
895
896 fn preferred_foreground(&self, colors: &LiquidColors) -> Color {
897 self.foreground
898 .filter(|color| {
899 [color.r(), color.g(), color.b(), color.a()]
900 .iter()
901 .all(|value| value.is_finite())
902 })
903 .unwrap_or(colors.label)
904 }
905
906 pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
907 let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
908 (GlassVariant::Regular, false) => 0.42,
909 (GlassVariant::Regular, true) => -0.38,
910 (GlassVariant::Clear, false) => 0.12,
911 (GlassVariant::Clear, true) => -0.12,
912 (GlassVariant::Lens, false) => 0.10,
913 (GlassVariant::Lens, true) => -0.08,
914 });
915 let foreground = self.preferred_foreground(colors);
916 let shadow = self.shadow_style.unwrap_or_else(|| {
917 GlassShadow::new(
918 Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
919 (GlassVariant::Lens, false) => 0.14,
920 (GlassVariant::Lens, true) => 0.28,
921 (_, false) => 0.16,
922 (_, true) => 0.5,
923 }),
924 if self.variant == GlassVariant::Lens {
925 10.0
926 } else {
927 22.0
928 },
929 if self.variant == GlassVariant::Lens {
930 3.0
931 } else {
932 8.0
933 },
934 if self.variant == GlassVariant::Lens {
935 -6.0
936 } else {
937 -2.0
938 },
939 )
940 });
941 ResolvedGlass {
942 shape: self.shape,
943 tint: self.tint.unwrap_or(colors.glass_tint),
944 tint_amount: self.tint_amount,
945 backdrop_blur: self.backdrop_blur,
946 blur_radius_dp: self
947 .blur_radius
948 .unwrap_or_else(|| self.default_blur_radius()),
949 saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
950 refraction_depth: self.refraction_depth,
951 refraction_depth_dp: self.refraction_depth_dp,
952 refraction: self.refraction,
953 refraction_curve: self.refraction_curve,
954 dispersion: self.dispersion,
955 edge_spectrum: self.edge_spectrum,
956 transmission_refraction: self.transmission_refraction,
957 meniscus_absorption: self.meniscus_absorption,
958 fold_depth: self.fold_depth,
959 optical_zoom: self.optical_zoom,
960 rim_reflection: self.rim_reflection,
961 ink_recolor: self.ink_recolor,
962 highlight: self.highlight,
963 face_lighting: self.face_lighting,
964 key_fill: self.key_fill,
965 face_response: self.face_response,
966 inner_shadow: self.inner_shadow,
967 lift,
968 contrast: self.contrast.unwrap_or(match self.variant {
969 GlassVariant::Lens => 1.0,
970 _ => 1.03,
971 }),
972 shadow: self.shadow,
973 clip: self.clip,
974 foreground_luma: 0.2126 * foreground.r()
975 + 0.7152 * foreground.g()
976 + 0.0722 * foreground.b(),
977 adaptive_frost: self.adaptive_frost,
978 adaptive_tone: self.adaptive_tone,
979 rim_style: if self.variant == GlassVariant::Lens {
980 1.0
981 } else {
982 0.0
983 },
984 shadow_color: shadow.color,
985 shadow_radius: shadow.radius,
986 shadow_offset_y: shadow.offset_y,
987 shadow_spread: shadow.spread,
988 }
989 }
990}
991
992impl Default for Glass {
993 fn default() -> Self {
994 Self::regular()
995 }
996}
997
998#[derive(Clone, Debug, PartialEq)]
999pub(crate) struct ResolvedGlass {
1000 pub shape: LiquidShape,
1001 pub tint: Color,
1002 tint_amount: Option<GlassTintAmount>,
1003 backdrop_blur: Option<(f32, f32)>,
1004 pub blur_radius_dp: f32,
1005 pub saturation: f32,
1006 pub refraction_depth: f32,
1007 pub refraction_depth_dp: Option<f32>,
1008 pub refraction: GlassRefraction,
1009 pub refraction_curve: f32,
1010 pub dispersion: f32,
1011 pub edge_spectrum: Option<GlassSpectrum>,
1012 pub transmission_refraction: f32,
1013 pub meniscus_absorption: f32,
1014 pub fold_depth: f32,
1015 pub optical_zoom: f32,
1016 pub rim_reflection: f32,
1017 pub ink_recolor: Option<(Color, f32)>,
1018 pub highlight: f32,
1019 pub face_lighting: bool,
1020 pub key_fill: Option<GlassKeyFill>,
1021 pub face_response: Option<GlassFaceResponse>,
1023 pub lift: f32,
1024 pub inner_shadow: Option<GlassShadow>,
1026 pub contrast: f32,
1027 pub shadow: bool,
1028 pub clip: bool,
1029 pub rim_style: f32,
1030 pub foreground_luma: f32,
1031 pub adaptive_frost: f32,
1032 pub adaptive_tone: bool,
1033 pub shadow_color: Color,
1034 pub shadow_radius: f32,
1035 pub shadow_offset_y: f32,
1036 pub shadow_spread: f32,
1037}
1038
1039fn set_edge_return_depth(shader: &mut RuntimeShader, depth: Option<f32>) {
1040 if let Some(depth) = depth.filter(|depth| depth.is_finite() && *depth >= 0.0) {
1041 shader.set_float2(GLASS_EDGE_RETURN_DEPTH_UNIFORM, depth, 1.0);
1042 }
1043}
1044
1045impl ResolvedGlass {
1046 fn clips_in_shader(&self) -> bool {
1047 self.key_fill.and_then(GlassKeyFill::uniforms).is_some()
1048 }
1049 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
1050 self.runtime_effect(density, &dynamics, false)
1051 }
1052
1053 fn blur_radii(&self, density: f32, activity: f32, content_mask: bool) -> (f32, f32) {
1054 if content_mask || self.tint_amount.is_some() {
1055 return (0.0, 0.0);
1056 }
1057 let radius = self.blur_radius_dp * density * activity;
1058 if radius > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
1059 (0.0, radius)
1060 } else {
1061 (radius, 0.0)
1062 }
1063 }
1064
1065 fn runtime_effect(
1066 &self,
1067 density: f32,
1068 dynamics: &GlassDynamics,
1069 content_mask: bool,
1070 ) -> RenderEffect {
1071 self.runtime_effect_with_content(density, dynamics, content_mask, false)
1072 }
1073
1074 fn runtime_effect_with_content(
1075 &self,
1076 density: f32,
1077 dynamics: &GlassDynamics,
1078 content_mask: bool,
1079 separate_content: bool,
1080 ) -> RenderEffect {
1081 let density = density.max(f32::EPSILON);
1082 let activity = dynamics
1083 .activity
1084 .filter(|value| value.is_finite())
1085 .unwrap_or(1.0)
1086 .clamp(0.0, 1.0);
1087 static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
1088 let mut shader = SHADER
1089 .get_or_init(|| {
1090 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
1091 shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
1092 shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
1093 specialize_liquid_glass(&mut shader);
1094 shader
1095 })
1096 .clone();
1097 if let Some(morph) = dynamics.morph.as_ref() {
1098 set_morph_uniforms(&mut shader, morph, activity);
1099 } else {
1100 shader.set_float2(0, 0.0, 0.0);
1101 shader.set_float(6, self.shape.shader_radius_px(density));
1102 }
1103 let projection = dynamics.projection();
1104 shader.set_float2(GLASS_OPTICAL_PROJECTION_UNIFORM, projection.0, projection.1);
1105 let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
1106 shader.set_float(9, self.refraction_depth * activity * press_depth);
1107 let (refraction_mode, refraction_reach) = self.refraction.shader_parameters();
1108 shader.set_float(GLASS_REFRACTION_MODE_UNIFORM, refraction_mode);
1109 set_edge_return_depth(&mut shader, dynamics.edge_return_depth_dp);
1110 shader.set_float(
1111 GLASS_EDGE_REFRACTION_REACH_UNIFORM,
1112 refraction_reach * activity * press_depth,
1113 );
1114 shader.set_float(
1115 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
1116 self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
1117 );
1118 shader.set_float(
1119 GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
1120 if self.refraction_depth_dp.is_some() {
1121 1.0
1122 } else {
1123 0.0
1124 },
1125 );
1126 shader.set_float(
1127 GLASS_REFRACTION_CURVE_UNIFORM,
1128 self.refraction_curve * activity,
1129 );
1130 shader.set_float(
1131 GLASS_DISPERSION_UNIFORM,
1132 self.dispersion * activity * press_depth,
1133 );
1134 let spectrum_reach = self.set_spectrum_uniforms(&mut shader);
1135 shader.set_float(
1136 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
1137 self.transmission_refraction * activity,
1138 );
1139 shader.set_float(
1140 GLASS_MENISCUS_ABSORPTION_UNIFORM,
1141 self.meniscus_absorption * press_depth,
1142 );
1143 shader.set_float(
1144 GLASS_FOLD_DEPTH_UNIFORM,
1145 self.fold_depth.max(0.0) * press_depth,
1146 );
1147 shader.set_float(
1148 GLASS_OPTICAL_ZOOM_UNIFORM,
1149 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
1150 );
1151 let zoom_anchor = dynamics
1152 .morph
1153 .as_ref()
1154 .map(|morph| morph.zoom_anchor)
1155 .unwrap_or((0.0, 0.0));
1156 shader.set_float2(
1157 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1158 zoom_anchor.0,
1159 zoom_anchor.1,
1160 );
1161 shader.set_float(121, self.rim_reflection.max(0.001));
1162 let (ink_color, ink_strength) = self
1163 .ink_recolor
1164 .map(|(color, strength)| (color, strength * activity))
1165 .unwrap_or((Color::TRANSPARENT, 0.0));
1166 shader.set_float(124, ink_color.r());
1167 shader.set_float(125, ink_color.g());
1168 shader.set_float(126, ink_color.b());
1169 shader.set_float(127, ink_strength);
1170 let (light_x, light_y) = glass_light_direction();
1171 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
1172 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
1173 self.set_lighting_uniforms(&mut shader, dynamics);
1174 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
1175 shader.set_float(
1176 11,
1177 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
1178 );
1179 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
1180 let dynamic_tint_alpha = (dynamic_tint.a()
1181 * dynamics
1182 .tint_alpha_multiplier
1183 .unwrap_or(1.0)
1184 .clamp(0.0, 2.0)
1185 * activity)
1186 .clamp(0.0, 1.0);
1187 shader.set_float4(
1188 14,
1189 dynamic_tint.r(),
1190 dynamic_tint.g(),
1191 dynamic_tint.b(),
1192 dynamic_tint_alpha,
1193 );
1194 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
1195 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
1196 shader.set_float(20, self.lift * activity);
1197 shader.set_float(21, 0.5 * activity);
1198 shader.set_float2(22, 0.0, 1.0);
1199 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
1200 shader.set_float(28, self.rim_style * activity);
1201 let (wcksrd_blur_radius, gaussian_blur_radius) =
1202 self.blur_radii(density, activity, content_mask);
1203 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
1204 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
1205 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
1206 shader.set_float4(
1207 GLASS_RESTING_TINT_UNIFORM,
1208 resting_tint.r(),
1209 resting_tint.g(),
1210 resting_tint.b(),
1211 resting_tint.a(),
1212 );
1213 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
1214 shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
1215 shader.set_float(97, self.foreground_luma);
1216 shader.set_float(GLASS_ADAPTIVE_TONE_UNIFORM, f32::from(self.adaptive_tone));
1217 self.set_backdrop_uniforms(&mut shader, density, content_mask);
1218 shader.set_float(
1219 cranpose_ui_graphics::GLASS_FOREGROUND_CONTENT_UNIFORM,
1220 f32::from(separate_content),
1221 );
1222 let dynamic_shadow = !self.clip && self.shadow;
1223 shader.set_float(
1224 102,
1225 if dynamic_shadow {
1226 self.shadow_color.a() * 0.55 * activity
1227 } else {
1228 0.0
1229 },
1230 );
1231 shader.set_float(103, self.shadow_radius);
1232 shader.set_float(104, self.shadow_offset_y);
1233 shader.set_float(105, self.shadow_spread);
1234 let morph_pad = dynamics
1235 .morph
1236 .as_ref()
1237 .map(|morph| {
1238 let (px, py, pw, ph, _) = morph.primary;
1239 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1240 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1241 let mut shape_reach = 0.0f32;
1242 for (sx, sy, sw, sh, _) in &morph.shapes {
1243 let reach_x = ((sx + sw * 0.5) - right)
1244 .max(left - (sx - sw * 0.5))
1245 .max(0.0);
1246 let reach_y = ((sy + sh * 0.5) - bottom)
1247 .max(top - (sy - sh * 0.5))
1248 .max(0.0);
1249 shape_reach = shape_reach.max(reach_x.max(reach_y));
1250 }
1251 let glue_pad = if morph.shapes.is_empty() {
1252 0.0
1253 } else {
1254 morph.glue * 2.0
1255 };
1256 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1257 })
1258 .unwrap_or(0.0);
1259 shader.set_input_padding(
1260 (self.input_padding()
1261 + if refraction_mode >= 1.5 {
1262 refraction_reach * (26.5 / 9.0) + spectrum_reach
1263 } else {
1264 refraction_reach
1265 }
1266 + morph_pad
1267 + wcksrd_blur_radius / density)
1268 * projection.0.max(projection.1).max(1.0),
1269 );
1270 if let Some(morph) = dynamics.morph.as_ref() {
1271 let shadow_reach = if dynamic_shadow {
1272 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
1273 } else {
1274 0.0
1275 };
1276 let support = morph_output_support(
1277 morph,
1278 activity,
1279 morph_pad,
1280 shadow_reach,
1281 if dynamic_shadow {
1282 self.shadow_offset_y.abs()
1283 } else {
1284 0.0
1285 },
1286 );
1287 let (cx, cy, _, _, _) = morph.primary;
1288 let support = Rect {
1289 x: cx + (support.x - cx) * projection.0,
1290 y: cy + (support.y - cy) * projection.1,
1291 width: support.width * projection.0,
1292 height: support.height * projection.1,
1293 };
1294 let (node_w, node_h) = morph.node_size;
1295 let overhang = (-support.x)
1296 .max(-support.y)
1297 .max(support.x + support.width - node_w)
1298 .max(support.y + support.height - node_h)
1299 .max(0.0);
1300 shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1301 shader.set_output_support(Some(support));
1302 }
1303
1304 let optical_effect = liquid_glass_runtime_effect(shader);
1305 if gaussian_blur_radius > f32::EPSILON {
1306 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1307 .then(optical_effect)
1308 } else {
1309 optical_effect
1310 }
1311 }
1312
1313 fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1314 if let Some(amount) = self.tint_amount {
1315 shader.set_float(
1316 cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1317 8.0 * density,
1318 );
1319 shader.set_float(
1320 cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1321 amount.percent() / 100.0,
1322 );
1323 }
1324 if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1325 shader.set_float2(
1326 cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1327 radius * density,
1328 opacity,
1329 );
1330 }
1331 }
1332
1333 fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1334 let spectrum = self
1335 .edge_spectrum
1336 .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1337 if let Some((spectrum, values)) = spectrum {
1338 for (index, value) in values.into_iter().enumerate() {
1339 shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1340 }
1341 spectrum.capture_reach()
1342 } else {
1343 40.787 * self.dispersion
1344 }
1345 }
1346
1347 fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1348 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1349 shader.set_float(118, touch_x);
1350 shader.set_float(119, touch_y);
1351 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1352 shader.set_float(
1353 GLASS_FACE_LIGHTING_OFF_UNIFORM,
1354 f32::from(!self.face_lighting),
1355 );
1356 shader.set_float(
1357 GLASS_TOUCH_RADIUS_UNIFORM,
1358 dynamics
1359 .touch_radius_dp
1360 .filter(|radius| radius.is_finite() && *radius > 0.0)
1361 .unwrap_or(58.0),
1362 );
1363 shader.set_float(
1364 GLASS_LAYER_CLIPPED_UNIFORM,
1365 f32::from(self.clip && !self.clips_in_shader()),
1366 );
1367 if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1368 for (index, value) in values.into_iter().enumerate() {
1369 shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1370 }
1371 }
1372 if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1373 for (index, value) in values.into_iter().enumerate() {
1374 shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1375 }
1376 }
1377 if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1378 for (index, value) in values.into_iter().enumerate() {
1379 shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1380 }
1381 }
1382 }
1383
1384 fn input_padding(&self) -> f32 {
1385 2.0
1386 }
1387}
1388
1389pub trait LiquidModifierExt {
1391 fn glass_effect(self, glass: Glass) -> Modifier;
1397
1398 fn glass_effect_with(
1402 self,
1403 glass: Glass,
1404 dynamics: impl Fn() -> GlassDynamics + 'static,
1405 ) -> Modifier;
1406}
1407
1408impl LiquidModifierExt for Modifier {
1409 fn glass_effect(self, glass: Glass) -> Modifier {
1410 self.glass_effect_with(glass, GlassDynamics::default)
1411 }
1412
1413 fn glass_effect_with(
1414 self,
1415 glass: Glass,
1416 dynamics: impl Fn() -> GlassDynamics + 'static,
1417 ) -> Modifier {
1418 let colors = crate::theme::liquid_colors();
1419 let resolved = glass.resolve(&colors);
1420 let shape = resolved.shape;
1421
1422 let mut modifier = self;
1423 if resolved.shadow && resolved.clip {
1424 let shadow_color = resolved.shadow_color;
1425 let (radius, offset_y, spread) = (
1426 resolved.shadow_radius,
1427 resolved.shadow_offset_y,
1428 resolved.shadow_spread,
1429 );
1430 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1431 scope.radius = radius;
1432 scope.spread = spread;
1433 scope.offset.y = offset_y;
1434 scope.color = shadow_color;
1435 scope.cutout = true;
1436 });
1437 }
1438
1439 let layer = cached_glass_layer(resolved);
1440 modifier.graphics_layer(move || layer(current_density(), dynamics()))
1441 }
1442}
1443
1444struct CachedGlassLayer<T> {
1445 density: u32,
1446 light: [u32; 2],
1447 dynamics: GlassDynamics,
1448 value: T,
1449}
1450
1451fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1452 cached_glass_layers(resolved, false, std::convert::identity)
1453}
1454
1455pub(crate) fn cached_glass_content_layers(
1456 resolved: ResolvedGlass,
1457) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1458 cached_glass_layers(resolved, true, |mut layer| {
1459 let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1460 panic!("content layers require a sequential edge lens");
1461 };
1462 layer.backdrop_effect = Some((*second).clone());
1463 [
1464 GraphicsLayer {
1465 backdrop_effect: Some((*first).clone()),
1466 ..Default::default()
1467 },
1468 layer,
1469 ]
1470 })
1471}
1472
1473fn cached_glass_layers<T: Clone>(
1474 resolved: ResolvedGlass,
1475 separate_content: bool,
1476 assemble: impl Fn(GraphicsLayer) -> T,
1477) -> impl Fn(f32, GlassDynamics) -> T {
1478 let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1479 move |density, dynamics| {
1480 let direction = glass_light_direction();
1481 let light = [direction.0, direction.1].map(f32::to_bits);
1482 let mut cached = cached.borrow_mut();
1483 if let Some(previous) = &*cached
1484 && previous.density == density.to_bits()
1485 && previous.light == light
1486 && glass_dynamics_match(&previous.dynamics, &dynamics)
1487 {
1488 return previous.value.clone();
1489 }
1490 let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1491 || (resolved.clip && resolved.clips_in_shader()))
1492 .then(|| resolved.runtime_effect(density, &dynamics, true));
1493 let layer = GraphicsLayer {
1494 backdrop_effect: Some(resolved.runtime_effect_with_content(
1495 density,
1496 &dynamics,
1497 false,
1498 separate_content,
1499 )),
1500 render_effect,
1501 shape: resolved.shape.layer_shape(),
1502 clip: resolved.clip && !resolved.clips_in_shader(),
1503 ..Default::default()
1504 };
1505 let value = assemble(layer);
1506 *cached = Some(CachedGlassLayer {
1507 density: density.to_bits(),
1508 light,
1509 dynamics,
1510 value: value.clone(),
1511 });
1512 value
1513 }
1514}
1515
1516fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1517 let bits = |d: &GlassDynamics| {
1518 let GlassDynamics {
1519 edge_return_depth_dp,
1520 optical_projection,
1521 activity,
1522 resting_tint,
1523 highlight_boost,
1524 saturation_boost,
1525 tint_alpha_multiplier,
1526 morph: _,
1527 touch,
1528 touch_radius_dp,
1529 press_depth,
1530 } = d;
1531 (
1532 edge_return_depth_dp.map(f32::to_bits),
1533 optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
1534 activity.map(f32::to_bits),
1535 resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1536 [*highlight_boost, *saturation_boost].map(f32::to_bits),
1537 tint_alpha_multiplier.map(f32::to_bits),
1538 touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1539 press_depth.map(f32::to_bits),
1540 touch_radius_dp.map(f32::to_bits),
1541 )
1542 };
1543 bits(a) == bits(b)
1544 && match (&a.morph, &b.morph) {
1545 (None, None) => true,
1546 (Some(a), Some(b)) => glass_morphs_match(a, b),
1547 _ => false,
1548 }
1549}
1550
1551fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1552 let shape_bits =
1553 |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1554 let bits = |m: &GlassMorph| {
1555 let GlassMorph {
1556 node_size,
1557 primary,
1558 shapes: _,
1559 glue,
1560 wobble_amplitude,
1561 wobble_phase,
1562 bulge_amplitude,
1563 bulge_direction,
1564 ellipse_blend,
1565 capsule_smoothing_dp,
1566 deformation,
1567 zoom_anchor,
1568 } = m;
1569 (
1570 [
1571 node_size.0,
1572 node_size.1,
1573 *glue,
1574 *wobble_amplitude,
1575 *wobble_phase,
1576 *bulge_amplitude,
1577 *bulge_direction,
1578 *ellipse_blend,
1579 *capsule_smoothing_dp,
1580 zoom_anchor.0,
1581 zoom_anchor.1,
1582 ]
1583 .map(f32::to_bits),
1584 shape_bits(primary),
1585 deformation
1586 .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1587 )
1588 };
1589 bits(a) == bits(b)
1590 && a.shapes
1591 .iter()
1592 .map(shape_bits)
1593 .eq(b.shapes.iter().map(shape_bits))
1594}
1595
1596fn morph_output_support(
1597 morph: &GlassMorph,
1598 activity: f32,
1599 morph_pad: f32,
1600 shadow_reach: f32,
1601 shadow_offset: f32,
1602) -> Rect {
1603 let (px, py, pw, ph, _) = morph.primary;
1604 let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1605 (
1606 1.0 + (deformation.along() - 1.0) * activity,
1607 deformation.axis(),
1608 )
1609 });
1610 let across = 1.0 / along;
1611 let smallest_strain = along.min(across);
1612 let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1613 let rows = [
1614 (
1615 (along * ax * ax + across * ay * ay).abs(),
1616 ((along - across) * ax * ay).abs(),
1617 ),
1618 (
1619 ((along - across) * ax * ay).abs(),
1620 (along * ay * ay + across * ax * ax).abs(),
1621 ),
1622 ];
1623 let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1624 let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1625 let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1626 let (mut left, mut top) = (px - extent_x, py - extent_y);
1627 let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1628 for (sx, sy, sw, sh, _) in &morph.shapes {
1629 left = left.min(sx - sw * 0.5 - reach);
1630 top = top.min(sy - sh * 0.5 - reach);
1631 right = right.max(sx + sw * 0.5 + reach);
1632 bottom = bottom.max(sy + sh * 0.5 + reach);
1633 }
1634 Rect {
1635 x: left,
1636 y: top - shadow_offset,
1637 width: right - left,
1638 height: bottom - top + 2.0 * shadow_offset,
1639 }
1640}
1641
1642fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1643 let (node_w, node_h) = morph.node_size;
1644 let (cx, cy, w, h, radius) = morph.primary;
1645 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1646 shader.set_float2(2, cx, cy);
1647 shader.set_float2(4, w, h);
1648 shader.set_float(6, radius);
1649 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1650 shader.set_float(30, count as f32);
1651 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1652 let base = 36 + index * 5;
1653 shader.set_float(base, *sx);
1654 shader.set_float(base + 1, *sy);
1655 shader.set_float(base + 2, *sw);
1656 shader.set_float(base + 3, *sh);
1657 shader.set_float(base + 4, *sr);
1658 }
1659 shader.set_float(31, morph.glue);
1660 shader.set_float(32, morph.wobble_amplitude * activity);
1661 shader.set_float(33, morph.wobble_phase);
1662 shader.set_float(26, morph.bulge_amplitude * activity);
1663 shader.set_float(27, morph.bulge_direction);
1664 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1665 shader.set_float(
1666 cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
1667 if morph.capsule_smoothing_dp.is_finite() {
1668 morph.capsule_smoothing_dp.max(0.0) * activity
1669 } else {
1670 0.0
1671 },
1672 );
1673 if let Some(deformation) = morph.deformation {
1674 let axis = deformation.axis();
1675 let along = 1.0 + (deformation.along() - 1.0) * activity;
1676 shader.set_float2(106, axis.0, axis.1);
1677 shader.set_float(108, along);
1678 shader.set_float(109, 1.0 / along);
1679 } else {
1680 shader.set_float2(106, 1.0, 0.0);
1681 shader.set_float2(108, 1.0, 1.0);
1682 }
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687 use super::*;
1688
1689 #[test]
1690 fn backdrop_blur_validates_radius_and_opacity() {
1691 for (radius, opacity, expected) in [
1692 (2.0, 0.5, Some((2.0, 0.5))),
1693 (-2.0, 3.0, Some((0.0, 1.0))),
1694 (2.0, -1.0, Some((2.0, 0.0))),
1695 (f32::NAN, 1.0, None),
1696 (2.0, f32::INFINITY, None),
1697 ] {
1698 assert_eq!(
1699 Glass::lens().backdrop_blur(radius, opacity).backdrop_blur,
1700 expected
1701 );
1702 }
1703 let glass = Glass::lens()
1704 .backdrop_blur(2.0, 0.75)
1705 .edge_refraction(0.0)
1706 .resolve(&light_colors());
1707 let shader = terminal_shader(glass.backdrop_effect(3.0, GlassDynamics::default()));
1708 assert_eq!(shader.uniforms()[172..174], [6.0, 0.75]);
1709 let mask = terminal_shader(glass.runtime_effect(3.0, &GlassDynamics::default(), true));
1710 assert_eq!(
1711 mask.uniforms().get(172..174).unwrap_or(&[0.0, 0.0]),
1712 [0.0, 0.0]
1713 );
1714 }
1715
1716 #[test]
1717 fn pane_tint_preserves_the_sharp_source_at_every_percentage() {
1718 for percent in [0.0, 25.0, 50.0, 100.0] {
1719 let amount = GlassTintAmount::from_percent(percent).unwrap();
1720 let glass = Glass::regular()
1721 .adaptive_frost(Color::BLACK, 0.0)
1722 .tint_amount(amount);
1723 assert_eq!(glass.tint_amount, Some(amount));
1724 let effect = glass.backdrop_effect(&light_colors(), 3.0, GlassDynamics::default());
1725 let RenderEffect::Shader { shader } = effect else {
1726 panic!("the sharp backdrop must not be destroyed by a preceding blur");
1727 };
1728 assert_eq!(
1729 shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM],
1730 24.0
1731 );
1732 assert_eq!(
1733 shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1],
1734 percent / 100.0
1735 );
1736 assert_eq!(
1737 shader.substrates(),
1738 &[cranpose_ui_graphics::SubstrateSpec::Blur { radius_px: 24.0 }]
1739 );
1740 }
1741 }
1742
1743 fn light_colors() -> LiquidColors {
1744 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
1745 }
1746
1747 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
1748 match effect {
1749 RenderEffect::Shader { shader } => std::sync::Arc::unwrap_or_clone(shader),
1750 RenderEffect::Chain { second, .. } => {
1751 terminal_shader(std::sync::Arc::unwrap_or_clone(second))
1752 }
1753 effect => panic!("expected runtime shader, got {effect:?}"),
1754 }
1755 }
1756
1757 #[test]
1758 fn optical_projection_validates_scales_and_projects_output_support() {
1759 let glass = Glass::lens().resolve(&light_colors());
1760 let mut dynamics = GlassDynamics {
1761 morph: Some(GlassMorph {
1762 node_size: (200.0, 120.0),
1763 primary: (100.0, 60.0, 111.0, 70.0, 35.0),
1764 ..Default::default()
1765 }),
1766 ..Default::default()
1767 };
1768 let base = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1769 for projection in [
1770 None,
1771 Some((0.0, 1.0)),
1772 Some((-1.0, 1.0)),
1773 Some((1.0, f32::NAN)),
1774 Some((f32::INFINITY, 1.0)),
1775 ] {
1776 dynamics.optical_projection = projection;
1777 let shader = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1778 assert_eq!(
1779 &shader.uniforms()
1780 [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1781 &[1.0, 1.0]
1782 );
1783 assert_eq!(shader.output_support(), base.output_support());
1784 }
1785 dynamics.optical_projection = Some((1.2, 0.8));
1786 let projected = terminal_shader(glass.backdrop_effect(3.0, dynamics));
1787 assert_eq!(
1788 &projected.uniforms()
1789 [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1790 &[1.2, 0.8]
1791 );
1792 let a = base.output_support().unwrap();
1793 let b = projected.output_support().unwrap();
1794 assert!((b.width - a.width * 1.2).abs() < 0.001);
1795 assert!((b.height - a.height * 0.8).abs() < 0.001);
1796 }
1797
1798 #[test]
1799 fn content_layers_preserve_the_complete_material_effect_and_cache() {
1800 let resolved = Glass::lens()
1801 .edge_refraction(9.0)
1802 .no_clip()
1803 .resolve(&light_colors());
1804 let layers = cached_glass_content_layers(resolved.clone());
1805 for density in [1.0, 3.0] {
1806 for activity in [0.0, 0.5, 1.0] {
1807 let dynamics = GlassDynamics {
1808 activity: Some(activity),
1809 ..Default::default()
1810 };
1811 let actual = layers(density, dynamics.clone());
1812 let joined = actual[0]
1813 .backdrop_effect
1814 .clone()
1815 .unwrap()
1816 .then(actual[1].backdrop_effect.clone().unwrap());
1817 assert_eq!(
1818 joined,
1819 resolved.runtime_effect_with_content(density, &dynamics, false, true)
1820 );
1821 assert!(actual[0].render_effect.is_none());
1822 assert!(!actual[0].clip);
1823 assert_eq!(
1824 terminal_shader(actual[1].backdrop_effect.clone().unwrap()).uniforms()[147],
1825 3.0
1826 );
1827 let mut repeated = layers(density, dynamics.clone());
1828 assert_eq!(actual, repeated);
1829 repeated[0].backdrop_effect = None;
1830 assert_eq!(actual, layers(density, dynamics));
1831 }
1832 }
1833 }
1834
1835 #[test]
1836 fn cached_glass_layers_refresh_all_live_inputs_and_keep_clones_independent() {
1837 let resolved = Glass::regular().no_clip().resolve(&light_colors());
1838 let layer = cached_glass_layer(resolved.clone());
1839 let mut dynamics = GlassDynamics::default();
1840 let mut density = 2.0;
1841 let mut previous = None;
1842 for step in 0..16 {
1843 match step {
1844 1 => density = 3.0,
1845 2 => set_glass_light_direction((0.5, -0.5)),
1846 3 => dynamics.highlight_boost = 0.25,
1847 4 => dynamics.saturation_boost = 0.4,
1848 5 => dynamics.activity = Some(0.5),
1849 6 => dynamics.resting_tint = Some(Color::RED),
1850 7 => dynamics.tint_alpha_multiplier = Some(0.3),
1851 8 => dynamics.touch = Some((5.0, 8.0, 0.7)),
1852 9 => dynamics.press_depth = Some(0.4),
1853 10 => {
1854 dynamics.morph = Some(GlassMorph {
1855 node_size: (60.0, 30.0),
1856 primary: (30.0, 15.0, 50.0, 20.0, 4.0),
1857 shapes: vec![(10.0, 15.0, 8.0, 8.0, -1.0)],
1858 ..Default::default()
1859 })
1860 }
1861 11 => {
1862 let morph = dynamics.morph.as_mut().unwrap();
1863 morph.primary.0 += 3.0;
1864 morph.shapes[0].0 += 2.0;
1865 morph.deformation = Some(GlassDeformation::incompressible((1.0, 1.0), 1.3));
1866 }
1867 12 => dynamics.morph = None,
1868 13 => dynamics.touch_radius_dp = Some(144.0),
1869 14 => dynamics.edge_return_depth_dp = Some(3.5),
1870 15 => dynamics.optical_projection = Some((1.2, 0.8)),
1871 _ => {}
1872 }
1873 let actual = layer(density, dynamics.clone());
1874 let expected = GraphicsLayer {
1875 backdrop_effect: Some(resolved.backdrop_effect(density, dynamics.clone())),
1876 render_effect: dynamics
1877 .morph
1878 .as_ref()
1879 .map(|_| resolved.runtime_effect(density, &dynamics, true)),
1880 shape: resolved.shape.layer_shape(),
1881 clip: resolved.clip,
1882 ..Default::default()
1883 };
1884 assert_eq!(actual, expected, "live input step {step}");
1885 assert_ne!(
1886 previous.as_ref(),
1887 Some(&actual),
1888 "input change must reach the layer at step {step}"
1889 );
1890 let mut repeated = layer(density, dynamics.clone());
1891 assert_eq!(repeated, actual);
1892 repeated.backdrop_effect = None;
1893 assert_eq!(layer(density, dynamics.clone()), actual);
1894 previous = Some(actual);
1895 }
1896 set_glass_light_direction((0.0, 1.0));
1897 let replacement = cached_glass_layer(
1898 Glass::clear()
1899 .shape(LiquidShape::RoundedRect(9.0))
1900 .resolve(&light_colors()),
1901 );
1902 assert_ne!(
1903 replacement(density, dynamics.clone()),
1904 layer(density, dynamics)
1905 );
1906 }
1907
1908 #[test]
1909 fn glass_cache_input_equality_preserves_signed_zero_and_nan_bits() {
1910 let mut a = GlassDynamics {
1911 touch: Some((0.0, 1.0, 0.5)),
1912 ..Default::default()
1913 };
1914 let mut b = a.clone();
1915 b.touch.as_mut().unwrap().0 = -0.0;
1916 assert!(!glass_dynamics_match(&a, &b));
1917 a.highlight_boost = f32::NAN;
1918 assert!(glass_dynamics_match(&a, &a));
1919 a.morph = Some(GlassMorph::default());
1920 b = a.clone();
1921 b.morph.as_mut().unwrap().zoom_anchor.0 = -0.0;
1922 assert!(!glass_dynamics_match(&a, &b));
1923 b = a.clone();
1924 b.morph
1925 .as_mut()
1926 .unwrap()
1927 .shapes
1928 .push((0.0, 1.0, 2.0, 3.0, 4.0));
1929 assert!(!glass_dynamics_match(&a, &b));
1930 }
1931
1932 #[test]
1933 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1934 assert_eq!(glass_light_direction(), (0.0, 1.0));
1935 let resolved = Glass::regular().resolve(&light_colors());
1936 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1937 let shader = terminal_shader(effect);
1938 let u = shader.uniforms();
1939 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1940 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1941
1942 set_glass_light_direction((1.0, 0.0));
1943 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1944 let u_rotated = terminal_shader(effect);
1945 let u_rotated = u_rotated.uniforms();
1946 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1947 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1948 set_glass_light_direction((0.0, 1.0));
1949 }
1950
1951 #[test]
1952 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1953 for shape in [
1954 LiquidShape::Capsule,
1955 LiquidShape::Circle,
1956 LiquidShape::RoundedRect(12.0),
1957 ] {
1958 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1959 }
1960 }
1961
1962 #[test]
1963 fn glass_shadow_clamps_negative_radius() {
1964 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1965 assert_eq!(shadow.radius, 0.0);
1966 assert_eq!(shadow.offset_y, 3.0);
1967 assert_eq!(shadow.spread, -1.0);
1968 }
1969
1970 #[test]
1971 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1972 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1973 assert_eq!(deformation.axis(), (0.6, 0.8));
1974 assert_eq!(deformation.along(), 1.25);
1975 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1976 assert_eq!(
1977 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1978 (1.0, 0.0)
1979 );
1980 }
1981
1982 #[test]
1983 fn glass_builders_clamp_physical_inputs() {
1984 let glass = Glass::lens()
1985 .shape(LiquidShape::Circle)
1986 .tint(Color::BLACK)
1987 .blur_radius(-2.0)
1988 .saturation(1.2)
1989 .refraction_depth(3.0)
1990 .refraction_depth_dp(-3.0)
1991 .refraction_curve(2.0)
1992 .dispersion(2.0)
1993 .transmission_refraction(2.0)
1994 .meniscus_absorption(2.0)
1995 .highlight(0.4)
1996 .lift(-0.2)
1997 .adaptive_frost(Color::WHITE, 2.0)
1998 .shadow(false)
1999 .no_clip();
2000 assert_eq!(glass.shape, LiquidShape::Circle);
2001 assert_eq!(glass.tint, Some(Color::BLACK));
2002 assert_eq!(glass.blur_radius, Some(-2.0));
2003 assert_eq!(glass.saturation, Some(1.2));
2004 assert_eq!(glass.refraction_depth, 2.0);
2005 assert_eq!(glass.refraction_depth_dp, Some(0.0));
2006 assert_eq!(glass.refraction_curve, 1.0);
2007 assert_eq!(glass.dispersion, 1.0);
2008 assert_eq!(glass.transmission_refraction, 1.0);
2009 assert_eq!(glass.meniscus_absorption, 1.0);
2010 assert_eq!(glass.highlight, 0.4);
2011 assert_eq!(glass.lift, Some(-0.2));
2012 assert_eq!(glass.adaptive_frost, 1.0);
2013 assert!(!glass.shadow);
2014 assert!(!glass.clip);
2015 }
2016
2017 #[test]
2018 fn material_variants_resolve_distinct_frost_levels() {
2019 let regular = Glass::regular().resolve(&light_colors());
2020 let clear = Glass::clear().resolve(&light_colors());
2021 let lens = Glass::lens().resolve(&light_colors());
2022 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
2023 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
2024 assert!(regular.saturation > clear.saturation);
2025 assert_eq!(lens.rim_style, 1.0);
2026 assert_eq!(regular.refraction_depth, 0.0);
2027 assert_eq!(regular.transmission_refraction, 0.0);
2028 assert_eq!(regular.refraction_curve, 0.25);
2029 assert_eq!(lens.refraction_curve, 1.0);
2030 assert_eq!(regular.dispersion, 0.0);
2031 assert_eq!(lens.dispersion, 0.30);
2032 }
2033
2034 #[test]
2035 fn neutral_surface_helpers_follow_foreground_polarity() {
2036 assert_eq!(
2037 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
2038 Color::BLACK.with_alpha(0.08)
2039 );
2040 assert_eq!(
2041 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
2042 Color::WHITE.with_alpha(0.10)
2043 );
2044 }
2045
2046 #[test]
2047 fn dynamic_saturation_reaches_the_material_tint() {
2048 let resting = Color::from_rgb_u8(0, 199, 208);
2049 let raised = boost_tint_saturation(resting, 0.55);
2050 assert_eq!(raised.r(), 0.0);
2051 assert!(raised.g() > resting.g());
2052 assert!(raised.b() > resting.b());
2053 assert_eq!(raised.a(), resting.a());
2054 }
2055
2056 #[test]
2057 fn template_specialization_follows_each_materials_dispersion_and_activity() {
2058 for (dispersion, activity) in [(0.6, 0.0), (0.0, 0.5), (0.3, 1.0), (0.0, 0.0)] {
2059 let resolved = Glass::regular()
2060 .dispersion(dispersion)
2061 .resolve(&light_colors());
2062 let mut shader = terminal_shader(resolved.backdrop_effect(
2063 1.0,
2064 GlassDynamics {
2065 activity: Some(activity),
2066 ..GlassDynamics::default()
2067 },
2068 ));
2069 cranpose_ui_graphics::specialize_liquid_glass_with_folds(&mut shader, true);
2070 assert_eq!(
2071 shader.uniforms()[GLASS_DISPERSION_UNIFORM],
2072 dispersion * activity
2073 );
2074 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], activity);
2075 assert_eq!(
2076 shader
2077 .overrides()
2078 .iter()
2079 .any(|(name, _)| { *name == cranpose_ui_graphics::GLASS_DISPERSION_OFF_FLAG }),
2080 dispersion * activity == 0.0
2081 );
2082 }
2083 }
2084
2085 fn spectrum_fixture() -> GlassSpectrum {
2086 GlassSpectrum {
2087 angle_radians: 0.7,
2088 step_dp: -1.5,
2089 vertical_scale: 1.25,
2090 opacity_near: 0.25,
2091 opacity_far: 0.75,
2092 fade_depth_dp: 3.5,
2093 extent_dp: 12.0,
2094 }
2095 }
2096
2097 #[test]
2098 fn edge_spectrum_preserves_signed_taps_and_captures_every_ray() {
2099 let spectrum = spectrum_fixture();
2100 let material = Glass::lens()
2101 .edge_refraction(9.0)
2102 .dispersion(0.0)
2103 .edge_spectrum(spectrum);
2104 assert_eq!(material.edge_spectrum, Some(spectrum));
2105 let resolved = material.resolve(&light_colors());
2106 let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2107 assert_eq!(
2108 &shader.uniforms()[GLASS_EDGE_SPECTRUM_UNIFORM..GLASS_EDGE_SPECTRUM_UNIFORM + 8],
2109 &spectrum.uniforms().unwrap()
2110 );
2111 let ordinary = Glass::lens()
2112 .edge_refraction(9.0)
2113 .dispersion(0.0)
2114 .resolve(&light_colors());
2115 let base = terminal_shader(ordinary.backdrop_effect(3.0, GlassDynamics::default()));
2116 assert!((shader.input_padding() - base.input_padding() - 5.625).abs() < 0.001);
2117 }
2118
2119 #[test]
2120 fn edge_spectrum_rejects_invalid_fields_at_both_entry_points() {
2121 for index in 0..7 {
2122 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2123 let mut spectrum = spectrum_fixture();
2124 let fields = [
2125 &mut spectrum.angle_radians,
2126 &mut spectrum.step_dp,
2127 &mut spectrum.vertical_scale,
2128 &mut spectrum.opacity_near,
2129 &mut spectrum.opacity_far,
2130 &mut spectrum.fade_depth_dp,
2131 &mut spectrum.extent_dp,
2132 ];
2133 *fields.into_iter().nth(index).unwrap() = invalid;
2134 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2135 let material = Glass {
2136 edge_spectrum: Some(spectrum),
2137 ..Glass::lens()
2138 };
2139 let shader = terminal_shader(
2140 material
2141 .resolve(&light_colors())
2142 .backdrop_effect(1.0, GlassDynamics::default()),
2143 );
2144 assert_eq!(
2145 shader
2146 .uniforms()
2147 .get(GLASS_EDGE_SPECTRUM_UNIFORM + 7)
2148 .copied()
2149 .unwrap_or(0.0),
2150 0.0
2151 );
2152 }
2153 }
2154 for index in 2..7 {
2155 let mut spectrum = spectrum_fixture();
2156 let fields = [
2157 &mut spectrum.vertical_scale,
2158 &mut spectrum.opacity_near,
2159 &mut spectrum.opacity_far,
2160 &mut spectrum.fade_depth_dp,
2161 &mut spectrum.extent_dp,
2162 ];
2163 *fields.into_iter().nth(index - 2).unwrap() = -0.1;
2164 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2165 }
2166 for near in [true, false] {
2167 let mut spectrum = spectrum_fixture();
2168 if near {
2169 spectrum.opacity_near = 1.1;
2170 } else {
2171 spectrum.opacity_far = 1.1;
2172 }
2173 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2174 }
2175 let zero = GlassSpectrum {
2176 angle_radians: 0.0,
2177 step_dp: 0.0,
2178 vertical_scale: 0.0,
2179 opacity_near: 0.0,
2180 opacity_far: 1.0,
2181 fade_depth_dp: 0.0,
2182 extent_dp: 0.0,
2183 };
2184 assert_eq!(Glass::lens().edge_spectrum(zero).edge_spectrum, Some(zero));
2185 }
2186
2187 #[test]
2188 fn material_template_preserves_each_instances_optical_zoom_anchor() {
2189 let resolved = Glass::lens().resolve(&light_colors());
2190 for anchor in [(3.5, -2.25), (0.0, 0.0), (-7.0, 11.0)] {
2191 let shader = terminal_shader(resolved.backdrop_effect(
2192 1.5,
2193 GlassDynamics {
2194 morph: Some(GlassMorph {
2195 node_size: (120.0, 72.0),
2196 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2197 zoom_anchor: anchor,
2198 ..Default::default()
2199 }),
2200 ..Default::default()
2201 },
2202 ));
2203 assert_eq!(
2204 &shader.uniforms()
2205 [GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM..GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM + 2],
2206 &[anchor.0, anchor.1]
2207 );
2208 }
2209 }
2210
2211 #[test]
2212 fn key_fill_preserves_valid_parameters_and_rejects_invalid_fields() {
2213 let lighting = GlassKeyFill {
2214 height_dp: 1.0,
2215 curvature: 0.8,
2216 angle_radians: -std::f32::consts::FRAC_PI_4,
2217 saturation: 1.5,
2218 luma_gain: 0.4367,
2219 offset: 1.125,
2220 scale_with_surface: true,
2221 };
2222 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2223 assert!(glass.key_fill.is_none());
2224 let material = glass.key_fill(lighting);
2225 assert_eq!(material.key_fill, Some(lighting));
2226 let shader = terminal_shader(material.backdrop_effect(
2227 &light_colors(),
2228 3.0,
2229 GlassDynamics::default(),
2230 ));
2231 assert_eq!(
2232 &shader.uniforms()[GLASS_KEY_FILL_UNIFORM..GLASS_KEY_FILL_UNIFORM + 7],
2233 &lighting.uniforms().unwrap()
2234 );
2235 }
2236 for scale_with_surface in [false, true] {
2237 let value = GlassKeyFill {
2238 scale_with_surface,
2239 ..lighting
2240 };
2241 assert_eq!(value.uniforms().unwrap()[6], f32::from(scale_with_surface));
2242 let layer = cached_glass_layer(Glass::clear().key_fill(value).resolve(&light_colors()))(
2243 3.0,
2244 GlassDynamics::default(),
2245 );
2246 assert!(!layer.clip);
2247 assert!(layer.render_effect.is_some());
2248 assert!(layer.backdrop_effect.is_some());
2249 }
2250 for index in 0..6 {
2251 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2252 let mut value = lighting;
2253 match index {
2254 0 => value.height_dp = invalid,
2255 1 => value.curvature = invalid,
2256 2 => value.angle_radians = invalid,
2257 3 => value.saturation = invalid,
2258 4 => value.luma_gain = invalid,
2259 _ => value.offset = invalid,
2260 }
2261 let shader = terminal_shader(Glass::clear().key_fill(value).backdrop_effect(
2262 &light_colors(),
2263 3.0,
2264 GlassDynamics::default(),
2265 ));
2266 assert_eq!(
2267 shader
2268 .uniforms()
2269 .get(GLASS_KEY_FILL_UNIFORM)
2270 .copied()
2271 .unwrap_or_default(),
2272 0.0
2273 );
2274 }
2275 }
2276 for invalid in [0.0, -1.0] {
2277 assert!(
2278 GlassKeyFill {
2279 height_dp: invalid,
2280 ..lighting
2281 }
2282 .uniforms()
2283 .is_none()
2284 );
2285 assert!(
2286 GlassKeyFill {
2287 curvature: invalid,
2288 ..lighting
2289 }
2290 .uniforms()
2291 .is_none()
2292 );
2293 }
2294 }
2295
2296 #[test]
2297 fn independent_edge_return_depth_preserves_zero_and_rejects_invalid_values() {
2298 for depth in [
2299 None,
2300 Some(0.0),
2301 Some(3.5),
2302 Some(-1.0),
2303 Some(f32::NAN),
2304 Some(f32::INFINITY),
2305 ] {
2306 let shader = terminal_shader(
2307 Glass::lens()
2308 .edge_refraction(9.0)
2309 .refraction_depth_dp(36.0)
2310 .backdrop_effect(
2311 &light_colors(),
2312 3.0,
2313 GlassDynamics {
2314 edge_return_depth_dp: depth,
2315 ..GlassDynamics::default()
2316 },
2317 ),
2318 );
2319 let valid = depth.filter(|value| value.is_finite() && *value >= 0.0);
2320 assert_eq!(
2321 shader
2322 .uniforms()
2323 .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM)
2324 .copied()
2325 .unwrap_or(0.0),
2326 valid.unwrap_or(0.0)
2327 );
2328 assert_eq!(
2329 shader
2330 .uniforms()
2331 .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM + 1)
2332 .copied()
2333 .unwrap_or(0.0),
2334 f32::from(valid.is_some())
2335 );
2336 assert_eq!(
2337 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2338 36.0
2339 );
2340 }
2341 }
2342
2343 #[test]
2344 fn inner_shadow_validates_and_resolves_its_gaussian_profile() {
2345 let shadow = GlassShadow::new(Color::BLACK.with_alpha(0.12), 3.0, 7.0, 0.0);
2346 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2347 assert_eq!(glass.inner_shadow, None);
2348 let material = glass.inner_shadow(shadow);
2349 assert_eq!(material.inner_shadow, Some(shadow));
2350 let shader = terminal_shader(material.backdrop_effect(
2351 &light_colors(),
2352 3.0,
2353 GlassDynamics::default(),
2354 ));
2355 assert_eq!(
2356 &shader.uniforms()[GLASS_INNER_SHADOW_UNIFORM..GLASS_INNER_SHADOW_UNIFORM + 8],
2357 &shadow.uniforms().unwrap()
2358 );
2359 }
2360 for alpha in [0.0, 1.0] {
2361 assert!(
2362 Glass::lens()
2363 .inner_shadow(GlassShadow::new(
2364 Color::BLACK.with_alpha(alpha),
2365 0.0,
2366 -2.0,
2367 -3.0
2368 ))
2369 .inner_shadow
2370 .is_some()
2371 );
2372 }
2373 for index in 0..7 {
2374 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2375 let mut values = shadow.uniforms().unwrap();
2376 values[index] = value;
2377 let invalid = GlassShadow {
2378 color: Color(values[0], values[1], values[2], values[3]),
2379 radius: values[4],
2380 offset_y: values[5],
2381 spread: values[6],
2382 };
2383 assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2384 let material = Glass {
2385 inner_shadow: Some(invalid),
2386 ..Glass::lens()
2387 };
2388 let shader = terminal_shader(material.backdrop_effect(
2389 &light_colors(),
2390 3.0,
2391 GlassDynamics::default(),
2392 ));
2393 assert_eq!(
2394 shader
2395 .uniforms()
2396 .get(GLASS_INNER_SHADOW_UNIFORM + 7)
2397 .copied()
2398 .unwrap_or(0.0),
2399 0.0
2400 );
2401 }
2402 }
2403 for invalid in [
2404 GlassShadow {
2405 radius: -1.0,
2406 ..shadow
2407 },
2408 GlassShadow {
2409 color: Color(0.0, 0.0, 0.0, -0.1),
2410 ..shadow
2411 },
2412 GlassShadow {
2413 color: Color(0.0, 0.0, 0.0, 1.1),
2414 ..shadow
2415 },
2416 ] {
2417 assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2418 }
2419 }
2420
2421 #[test]
2422 fn adaptive_tone_resolves_foreground_and_declares_the_shared_mean() {
2423 use cranpose_ui_graphics::SubstrateSpec;
2424 for foreground in [Color::BLACK, Color::WHITE, Color(f32::NAN, 0.0, 0.0, 1.0)] {
2425 let material = Glass::clear().adaptive_tone(foreground);
2426 assert!(material.adaptive_tone);
2427 let resolved = material.resolve(&light_colors());
2428 assert!(resolved.foreground_luma.is_finite());
2429 let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2430 assert_eq!(shader.uniforms()[GLASS_ADAPTIVE_TONE_UNIFORM], 1.0);
2431 assert_eq!(shader.substrates(), &[SubstrateSpec::Mean]);
2432 let expected = if foreground.r().is_finite() {
2433 foreground.r()
2434 } else {
2435 0.2126 * light_colors().label.r()
2436 + 0.7152 * light_colors().label.g()
2437 + 0.0722 * light_colors().label.b()
2438 };
2439 assert!((resolved.foreground_luma - expected).abs() < 0.0001);
2440 }
2441 assert!(!Glass::clear().adaptive_tone);
2442 assert!(!Glass::regular().adaptive_tone);
2443 }
2444
2445 #[test]
2446 fn face_response_preserves_valid_ranges_and_rejects_invalid_values() {
2447 let response = GlassFaceResponse {
2448 gain: 0.97,
2449 start_dp: 1.0 / 3.0,
2450 end_dp: 2.0 / 3.0,
2451 illumination: 0.05,
2452 };
2453 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2454 assert!(glass.face_response.is_none());
2455 let material = glass.face_response(response);
2456 assert_eq!(material.face_response, Some(response));
2457 let shader = terminal_shader(material.backdrop_effect(
2458 &light_colors(),
2459 3.0,
2460 GlassDynamics::default(),
2461 ));
2462 assert_eq!(
2463 &shader.uniforms()[GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4],
2464 &response.uniforms().unwrap()
2465 );
2466 }
2467 for gain in [0.0, 1.0] {
2468 assert!(
2469 GlassFaceResponse {
2470 gain,
2471 start_dp: 0.0,
2472 end_dp: 0.0,
2473 ..response
2474 }
2475 .uniforms()
2476 .is_some()
2477 );
2478 }
2479 let mut invalid = vec![
2480 GlassFaceResponse {
2481 gain: -0.1,
2482 ..response
2483 },
2484 GlassFaceResponse {
2485 gain: 1.1,
2486 ..response
2487 },
2488 GlassFaceResponse {
2489 start_dp: -1.0,
2490 ..response
2491 },
2492 GlassFaceResponse {
2493 end_dp: 0.0,
2494 ..response
2495 },
2496 ];
2497 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2498 invalid.extend([
2499 GlassFaceResponse {
2500 gain: value,
2501 ..response
2502 },
2503 GlassFaceResponse {
2504 start_dp: value,
2505 ..response
2506 },
2507 GlassFaceResponse {
2508 end_dp: value,
2509 ..response
2510 },
2511 GlassFaceResponse {
2512 illumination: value,
2513 ..response
2514 },
2515 ]);
2516 }
2517 for response in invalid {
2518 assert!(response.uniforms().is_none());
2519 let shader = terminal_shader(Glass::clear().face_response(response).backdrop_effect(
2520 &light_colors(),
2521 3.0,
2522 GlassDynamics::default(),
2523 ));
2524 assert!(
2525 shader
2526 .uniforms()
2527 .get(GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4)
2528 .is_none_or(|values| values.iter().all(|value| *value == 0.0))
2529 );
2530 }
2531 }
2532
2533 #[test]
2534 fn face_lighting_is_explicit_and_preserves_each_material_default() {
2535 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2536 assert!(glass.face_lighting);
2537 for enabled in [false, true] {
2538 let material = glass.clone().face_lighting(enabled);
2539 assert_eq!(material.face_lighting, enabled);
2540 let resolved = material.resolve(&light_colors());
2541 assert_eq!(resolved.face_lighting, enabled);
2542 let shader =
2543 terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2544 assert_eq!(
2545 shader.uniforms()[GLASS_FACE_LIGHTING_OFF_UNIFORM],
2546 f32::from(!enabled)
2547 );
2548 }
2549 }
2550 }
2551
2552 #[test]
2553 fn layer_clipping_has_one_owner_for_material_coverage() {
2554 for clipped in [false, true] {
2555 let mut glass = Glass::regular();
2556 if !clipped {
2557 glass = glass.no_clip();
2558 }
2559 let shader = terminal_shader(glass.backdrop_effect(
2560 &light_colors(),
2561 3.0,
2562 GlassDynamics::default(),
2563 ));
2564 assert_eq!(
2565 shader.uniforms()[GLASS_LAYER_CLIPPED_UNIFORM],
2566 f32::from(clipped)
2567 );
2568 }
2569 }
2570
2571 #[test]
2572 fn touch_radius_preserves_valid_values_and_normalizes_invalid_values() {
2573 for radius in [
2574 None,
2575 Some(-1.0),
2576 Some(0.0),
2577 Some(f32::NAN),
2578 Some(f32::INFINITY),
2579 Some(f32::NEG_INFINITY),
2580 Some(0.5),
2581 Some(144.0),
2582 ] {
2583 let expected = radius
2584 .filter(|value| value.is_finite() && *value > 0.0)
2585 .unwrap_or(58.0);
2586 for density in [1.0, 3.0] {
2587 let shader = terminal_shader(Glass::regular().backdrop_effect(
2588 &light_colors(),
2589 density,
2590 GlassDynamics {
2591 touch_radius_dp: radius,
2592 ..Default::default()
2593 },
2594 ));
2595 assert_eq!(shader.uniforms()[GLASS_TOUCH_RADIUS_UNIFORM], expected);
2596 }
2597 }
2598 }
2599
2600 #[test]
2601 fn material_instances_share_source_without_sharing_uniforms_or_overrides() {
2602 let resolved = Glass::regular().resolve(&light_colors());
2603 let mut first = terminal_shader(resolved.backdrop_effect(2.0, GlassDynamics::default()));
2604 let untouched = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2605 let unused_slot = RuntimeShader::MAX_USER_UNIFORMS - 1;
2606 first.set_float(unused_slot, 17.0);
2607 first.set_override("INSTANCE_ONLY", 1.0);
2608 let second = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2609 assert_eq!(second.uniforms(), untouched.uniforms());
2610 assert_eq!(second.overrides(), untouched.overrides());
2611 assert_eq!(
2612 second.uniforms().get(unused_slot).copied().unwrap_or(0.0),
2613 0.0
2614 );
2615 assert!(
2616 !second
2617 .overrides()
2618 .iter()
2619 .any(|(name, _)| *name == "INSTANCE_ONLY")
2620 );
2621 assert_eq!(first.uniforms()[unused_slot], 17.0);
2622 assert_eq!(first.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2623 assert_eq!(second.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
2624 assert_eq!(first.source().as_ptr(), second.source().as_ptr());
2625 }
2626
2627 #[test]
2628 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
2629 let resolved = Glass::lens()
2630 .refraction_depth(0.72)
2631 .refraction_depth_dp(21.0)
2632 .refraction_curve(0.8)
2633 .dispersion(0.42)
2634 .transmission_refraction(0.35)
2635 .meniscus_absorption(0.3)
2636 .blur_radius(3.0)
2637 .tint(Color::BLACK.with_alpha(0.8))
2638 .resolve(&light_colors());
2639 let effect = resolved.backdrop_effect(
2640 2.0,
2641 GlassDynamics {
2642 highlight_boost: 0.2,
2643 saturation_boost: 0.35,
2644 tint_alpha_multiplier: Some(0.25),
2645 ..Default::default()
2646 },
2647 );
2648 let RenderEffect::Chain { first, .. } = &effect else {
2649 panic!("macroscopic frost must precede the wcKSRD optical pass");
2650 };
2651 assert!(matches!(
2652 first.as_ref(),
2653 RenderEffect::Blur {
2654 radius_x: 6.0,
2655 radius_y: 6.0,
2656 ..
2657 }
2658 ));
2659 let shader = terminal_shader(effect);
2660 assert_eq!(shader.uniforms()[9], 0.72);
2661 assert_eq!(
2662 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2663 21.0
2664 );
2665 assert_eq!(
2666 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2667 1.0
2668 );
2669 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2670 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
2671 assert_eq!(
2672 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
2673 0.35
2674 );
2675 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
2676 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
2677 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
2678 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
2679 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2680 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2681 }
2682
2683 #[test]
2684 fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
2685 let effect = Glass::regular()
2686 .resolve(&light_colors())
2687 .backdrop_effect(3.0, GlassDynamics::default());
2688 let RenderEffect::Chain { first, .. } = &effect else {
2689 panic!("heavy blur must ride the separable Gaussian pre-pass");
2690 };
2691 assert!(matches!(
2692 first.as_ref(),
2693 RenderEffect::Blur {
2694 radius_x: 24.0,
2695 radius_y: 24.0,
2696 ..
2697 }
2698 ));
2699 assert_eq!(
2700 terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
2701 0.0
2702 );
2703 }
2704
2705 #[test]
2706 fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
2707 let RenderEffect::Shader { shader } = Glass::regular()
2708 .blur_radius(0.5)
2709 .resolve(&light_colors())
2710 .backdrop_effect(3.0, GlassDynamics::default())
2711 else {
2712 panic!("a sub-cap blur must not build a Gaussian chain");
2713 };
2714 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
2715 }
2716
2717 #[test]
2718 fn raised_tint_density_stays_premultiplied_alpha_safe() {
2719 let effect = Glass::lens()
2720 .tint(Color::WHITE.with_alpha(0.8))
2721 .resolve(&light_colors())
2722 .backdrop_effect(
2723 1.0,
2724 GlassDynamics {
2725 tint_alpha_multiplier: Some(2.0),
2726 ..Default::default()
2727 },
2728 );
2729 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
2730 }
2731
2732 #[test]
2733 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
2734 let resolved = Glass::lens()
2735 .refraction_depth(0.72)
2736 .refraction_curve(0.8)
2737 .dispersion(0.42)
2738 .blur_radius(3.0)
2739 .saturation(1.4)
2740 .highlight(0.6)
2741 .lift(0.2)
2742 .tint(Color::BLACK.with_alpha(0.8))
2743 .no_clip()
2744 .resolve(&light_colors());
2745 let morph = GlassMorph {
2746 node_size: (120.0, 72.0),
2747 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2748 wobble_amplitude: 3.0,
2749 bulge_amplitude: 4.0,
2750 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
2751 ..Default::default()
2752 };
2753
2754 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
2755 2.0,
2756 GlassDynamics {
2757 activity: Some(0.0),
2758 morph: Some(morph),
2759 ..Default::default()
2760 },
2761 ) else {
2762 panic!("material must resolve to the shared wcKSRD shader");
2763 };
2764 let uniforms = shader.uniforms();
2765 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
2766 assert_eq!(uniforms[9], 0.0);
2767 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
2768 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
2769 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
2770 assert_eq!(uniforms[11], 0.0);
2771 assert_eq!(uniforms[17], 0.0);
2772 assert_eq!(uniforms[18], 1.0);
2773 assert_eq!(uniforms[20], 0.0);
2774 assert_eq!(uniforms[21], 0.0);
2775 assert_eq!(uniforms[24], 1.0);
2776 assert_eq!(uniforms[28], 0.0);
2777 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2778 assert_eq!(uniforms[91], 0.0);
2779 assert_eq!(uniforms[102], 0.0);
2780 assert_eq!(
2781 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2782 &[0.0, 0.0, 0.0, 0.0]
2783 );
2784 assert_eq!(uniforms[32], 0.0);
2785 assert_eq!(uniforms[26], 0.0);
2786 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
2787 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
2788 }
2789
2790 #[test]
2791 fn surface_refraction_is_explicit_and_reaches_the_shader() {
2792 for glass in [Glass::regular(), Glass::lens()] {
2793 assert_eq!(glass.refraction, GlassRefraction::Radial);
2794 let surface = glass.surface_refraction(31.0);
2795 assert_eq!(
2796 surface.refraction,
2797 GlassRefraction::Surface { reach_dp: 31.0 }
2798 );
2799 let shader = terminal_shader(surface.backdrop_effect(
2800 &light_colors(),
2801 3.0,
2802 GlassDynamics::default(),
2803 ));
2804 assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 1.0);
2805 assert_eq!(shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM], 31.0);
2806 assert!(
2807 !shader
2808 .overrides()
2809 .iter()
2810 .any(|(name, value)| *name == "GLASS_DIRECTIONAL_REFRACTION_OFF"
2811 && *value != 0.0)
2812 );
2813 }
2814 }
2815
2816 #[test]
2817 fn edge_refraction_preserves_face_zoom_and_scales_outward_reach_with_contact() {
2818 assert_eq!(GlassRefraction::default(), GlassRefraction::Radial);
2819 let glass = Glass::lens()
2820 .surface_refraction(31.0)
2821 .edge_refraction(12.0)
2822 .optical_zoom(1.2);
2823 assert_eq!(
2824 glass.refraction,
2825 GlassRefraction::EdgeLens { reach_dp: 12.0 }
2826 );
2827 for (activity, press, reach) in [(1.0, 1.0, 12.0), (0.5, 0.5, 3.0), (0.0, 1.0, 0.0)] {
2828 let shader = terminal_shader(glass.backdrop_effect(
2829 &light_colors(),
2830 3.0,
2831 GlassDynamics {
2832 activity: Some(activity),
2833 press_depth: Some(press),
2834 ..Default::default()
2835 },
2836 ));
2837 assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 2.0);
2838 assert_eq!(
2839 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2840 reach
2841 );
2842 assert_eq!(
2843 shader.uniforms()[GLASS_OPTICAL_ZOOM_UNIFORM],
2844 1.0 + 0.2 * activity
2845 );
2846 }
2847 assert_eq!(
2848 glass.surface_refraction(31.0).refraction,
2849 GlassRefraction::Surface { reach_dp: 31.0 }
2850 );
2851 }
2852
2853 #[test]
2854 fn edge_refraction_captures_the_farthest_spectral_ray() {
2855 for (reach, dispersion, required) in
2856 [(20.0, 1.0, 101.67), (0.0, 1.0, 42.78), (20.0, 0.0, 60.88)]
2857 {
2858 let shader = terminal_shader(
2859 Glass::lens()
2860 .edge_refraction(reach)
2861 .dispersion(dispersion)
2862 .backdrop_effect(&light_colors(), 3.0, GlassDynamics::default()),
2863 );
2864 assert!(shader.input_padding() >= required);
2865 }
2866 }
2867
2868 #[test]
2869 fn directional_refraction_rejects_invalid_reach_at_both_public_entry_points() {
2870 for input in [-1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, 12.0] {
2871 let expected = if input.is_finite() {
2872 input.max(0.0)
2873 } else {
2874 0.0
2875 };
2876 for (built, direct, normalized) in [
2877 (
2878 Glass::lens().edge_refraction(input),
2879 GlassRefraction::EdgeLens { reach_dp: input },
2880 GlassRefraction::EdgeLens { reach_dp: expected },
2881 ),
2882 (
2883 Glass::regular().surface_refraction(input),
2884 GlassRefraction::Surface { reach_dp: input },
2885 GlassRefraction::Surface { reach_dp: expected },
2886 ),
2887 ] {
2888 assert_eq!(built.refraction, normalized);
2889 let direct = Glass {
2890 refraction: direct,
2891 ..built
2892 };
2893 let shader = terminal_shader(direct.backdrop_effect(
2894 &light_colors(),
2895 1.0,
2896 GlassDynamics::default(),
2897 ));
2898 assert_eq!(
2899 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2900 expected
2901 );
2902 }
2903 }
2904 }
2905
2906 #[test]
2907 fn surface_reach_is_independent_of_depth_and_tracks_contact() {
2908 for depth in [4.0, 15.5, 30.0] {
2909 for (activity, press, reach) in [(1.0, 1.0, 31.0), (0.5, 0.5, 7.75), (0.0, 1.0, 0.0)] {
2910 let shader = terminal_shader(
2911 Glass::regular()
2912 .surface_refraction(31.0)
2913 .refraction_depth_dp(depth)
2914 .backdrop_effect(
2915 &light_colors(),
2916 3.0,
2917 GlassDynamics {
2918 activity: Some(activity),
2919 press_depth: Some(press),
2920 ..Default::default()
2921 },
2922 ),
2923 );
2924 assert_eq!(
2925 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2926 reach
2927 );
2928 assert_eq!(
2929 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2930 depth * activity * press
2931 );
2932 assert!(shader.input_padding() >= 31.0);
2933 }
2934 }
2935 }
2936
2937 #[test]
2938 fn resting_surface_tint_survives_zero_optical_activity() {
2939 let tint = Color::BLACK.with_alpha(0.11);
2940 let RenderEffect::Shader { shader } = Glass::lens()
2941 .no_clip()
2942 .resolve(&light_colors())
2943 .backdrop_effect(
2944 1.0,
2945 GlassDynamics {
2946 activity: Some(0.0),
2947 resting_tint: Some(tint),
2948 ..Default::default()
2949 },
2950 )
2951 else {
2952 panic!("resting surface must use the shared wcKSRD shader");
2953 };
2954 assert_eq!(
2955 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2956 &[tint.r(), tint.g(), tint.b(), tint.a()]
2957 );
2958 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
2959 }
2960
2961 #[test]
2962 fn full_optical_activity_preserves_the_resolved_material() {
2963 let resolved = Glass::lens()
2964 .refraction_depth(0.72)
2965 .refraction_curve(0.8)
2966 .dispersion(0.42)
2967 .blur_radius(3.0)
2968 .resolve(&light_colors());
2969 let shader = terminal_shader(resolved.backdrop_effect(
2970 2.0,
2971 GlassDynamics {
2972 activity: Some(1.0),
2973 ..Default::default()
2974 },
2975 ));
2976 let uniforms = shader.uniforms();
2977 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
2978 assert_eq!(uniforms[9], 0.72);
2979 assert_eq!(
2980 uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2981 0.0
2982 );
2983 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2984 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
2985 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
2986 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2987 }
2988
2989 #[test]
2990 fn morph_geometry_and_incompressible_strain_are_packed() {
2991 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
2992 let morph = GlassMorph {
2993 node_size: (78.0, 59.0),
2994 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
2995 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
2996 glue: 8.0,
2997 wobble_amplitude: 1.0,
2998 wobble_phase: 0.5,
2999 bulge_amplitude: 2.0,
3000 bulge_direction: 0.25,
3001 ellipse_blend: 0.3,
3002 capsule_smoothing_dp: 4.0,
3003 deformation: Some(deformation),
3004 zoom_anchor: (0.0, 0.0),
3005 };
3006 let RenderEffect::Shader { shader } = Glass::lens()
3007 .no_clip()
3008 .resolve(&light_colors())
3009 .backdrop_effect(
3010 1.0,
3011 GlassDynamics {
3012 morph: Some(morph),
3013 ..Default::default()
3014 },
3015 )
3016 else {
3017 panic!("morph must use the shared wcKSRD shader");
3018 };
3019 let uniforms = shader.uniforms();
3020 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
3021 assert_eq!(uniforms[30], 1.0);
3022 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
3023 assert_eq!(uniforms[110], 0.3);
3024 assert_eq!(
3025 uniforms[cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3026 4.0
3027 );
3028 assert!(shader.output_padding() > 0.0);
3029 }
3030
3031 #[test]
3032 fn capsule_smoothing_is_finite_nonnegative_and_invalidates_the_morph() {
3033 for value in [-1.0, 0.0, 4.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
3034 let morph = GlassMorph {
3035 node_size: (120.0, 80.0),
3036 primary: (60.0, 40.0, 111.0, 70.0, -1.0),
3037 capsule_smoothing_dp: value,
3038 ..Default::default()
3039 };
3040 let expected = if value.is_finite() {
3041 value.max(0.0)
3042 } else {
3043 0.0
3044 };
3045 let shader = terminal_shader(Glass::lens().backdrop_effect(
3046 &light_colors(),
3047 3.0,
3048 GlassDynamics {
3049 morph: Some(morph.clone()),
3050 ..Default::default()
3051 },
3052 ));
3053 assert_eq!(
3054 shader.uniforms()[cranpose_ui_graphics::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3055 expected
3056 );
3057 assert!(glass_morphs_match(&morph, &morph));
3058 let changed = GlassMorph {
3059 capsule_smoothing_dp: 5.0,
3060 ..morph.clone()
3061 };
3062 assert!(!glass_morphs_match(&morph, &changed));
3063 }
3064 }
3065
3066 #[test]
3067 fn optical_builders_hold_their_own_legal_range() {
3068 let glass = Glass::regular()
3069 .fold_depth(-4.0)
3070 .optical_zoom(0.5)
3071 .rim_reflection(9.0);
3072 assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
3073 assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
3074 assert_eq!(
3075 glass.rim_reflection, 2.0,
3076 "the rim tops out at twice the line"
3077 );
3078
3079 let glass = Glass::regular()
3080 .fold_depth(6.0)
3081 .optical_zoom(1.4)
3082 .rim_reflection(0.25);
3083 assert_eq!(glass.fold_depth, 6.0);
3084 assert_eq!(glass.optical_zoom, 1.4);
3085 assert_eq!(glass.rim_reflection, 0.25);
3086 }
3087
3088 #[test]
3089 fn ink_recolor_records_its_color_at_a_clamped_strength() {
3090 let red = Color::from_rgb_u8(255, 0, 0);
3091 let glass = Glass::regular().ink_recolor(red, 3.0);
3092 assert_eq!(glass.ink_recolor, Some((red, 1.0)));
3093
3094 let glass = Glass::regular().ink_recolor(red, -1.0);
3095 assert_eq!(glass.ink_recolor, Some((red, 0.0)));
3096 assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
3097 }
3098
3099 #[test]
3100 fn a_touched_up_surface_amplifies_what_it_already_is() {
3101 let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
3102 assert!(
3103 dynamics.highlight_boost > 0.0,
3104 "a pressed surface goes brighter"
3105 );
3106 assert!(dynamics.saturation_boost > 0.0, "and more saturated");
3107 assert_eq!(
3108 dynamics.touch,
3109 Some((10.0, 4.0, 1.0)),
3110 "with no finger the glow sits at the surface's own heart"
3111 );
3112 assert_eq!(
3113 dynamics.resting_tint, None,
3114 "a touch never repaints the surface with another color"
3115 );
3116 }
3117
3118 #[test]
3119 fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
3120 let dynamics = GlassDynamics::default()
3121 .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
3122 .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
3123 let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
3124 assert_eq!(
3125 (x, y),
3126 (3.0, 7.0),
3127 "the glow tracks the finger, not the center"
3128 );
3129 assert_eq!(intensity, 1.0, "carried press saturates at full");
3130 }
3131
3132 #[test]
3133 fn an_unpressed_surface_is_left_exactly_as_it_was() {
3134 let resting = GlassDynamics::default();
3135 let untouched = resting
3136 .clone()
3137 .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
3138 assert_eq!(untouched.highlight_boost, resting.highlight_boost);
3139 assert_eq!(untouched.saturation_boost, resting.saturation_boost);
3140 assert_eq!(untouched.touch, None, "no press, no glow");
3141 }
3142
3143 #[test]
3144 fn an_off_axis_strain_widens_the_support_by_the_affine_rows_times_the_half_extents() {
3145 let resolved = Glass::regular()
3146 .shape(LiquidShape::RoundedRect(0.0))
3147 .blur_radius(0.0)
3148 .shadow(false)
3149 .no_clip()
3150 .resolve(&light_colors());
3151 let angle = 22.5f32.to_radians();
3152 let morph = GlassMorph {
3153 node_size: (600.0, 600.0),
3154 primary: (300.0, 300.0, 200.0, 200.0, 0.0),
3155 deformation: Some(GlassDeformation::incompressible(
3156 (angle.cos(), angle.sin()),
3157 2.0,
3158 )),
3159 ..Default::default()
3160 };
3161 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
3162 1.0,
3163 GlassDynamics {
3164 activity: Some(1.0),
3165 morph: Some(morph),
3166 ..Default::default()
3167 },
3168 ) else {
3169 panic!("an unblurred glass is one shader");
3170 };
3171 let support = shader
3172 .output_support()
3173 .expect("a morphing glass declares its support");
3174 let half_x = 100.0 * (2.0 * angle.cos().powi(2) + 0.5 * angle.sin().powi(2))
3175 + 100.0 * (1.5 * angle.sin() * angle.cos());
3176 assert!(
3177 (half_x - 231.066).abs() < 0.01,
3178 "the example's half extent is {half_x}"
3179 );
3180 assert!(
3181 support.x <= 300.0 - half_x && support.x + support.width >= 300.0 + half_x,
3182 "the support {support:?} must hold the strained square's x extent {half_x}"
3183 );
3184 let half_y = 100.0 * (1.5 * angle.sin() * angle.cos())
3185 + 100.0 * (2.0 * angle.sin().powi(2) + 0.5 * angle.cos().powi(2));
3186 assert!(
3187 (half_y - 125.0).abs() < 0.01,
3188 "the example's y half extent is {half_y}"
3189 );
3190 assert!(
3191 support.y <= 300.0 - half_y && support.y + support.height >= 300.0 + half_y,
3192 "the support {support:?} must hold the strained square's y extent {half_y}"
3193 );
3194 }
3195
3196 #[test]
3197 fn a_morphing_glass_declares_its_output_support_around_its_shapes_and_their_reach() {
3198 let resolved = Glass::regular()
3199 .shape(LiquidShape::RoundedRect(12.0))
3200 .blur_radius(0.0)
3201 .shadow(true)
3202 .no_clip()
3203 .resolve(&light_colors());
3204 let morph = GlassMorph {
3205 node_size: (280.0, 160.0),
3206 primary: (140.0, 80.0, 120.0, 40.0, -1.0),
3207 shapes: vec![(230.0, 80.0, 40.0, 40.0, -1.0)],
3208 glue: 20.0,
3209 wobble_amplitude: 3.0,
3210 bulge_amplitude: 8.0,
3211 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
3212 ..Default::default()
3213 };
3214 let dynamics = GlassDynamics {
3215 activity: Some(1.0),
3216 morph: Some(morph.clone()),
3217 ..Default::default()
3218 };
3219 let RenderEffect::Shader { shader } = resolved.backdrop_effect(2.0, dynamics) else {
3220 panic!("an unblurred glass is one shader");
3221 };
3222 let support = shader
3223 .output_support()
3224 .expect("a morphing glass declares its support");
3225 let shape_reach = (230.0 + 20.0) - (140.0 + 60.0);
3226 let shadow_reach = resolved.shadow_radius
3227 + resolved.shadow_offset_y.abs()
3228 + resolved.shadow_spread.max(0.0);
3229 let field_reach = 3.0 * 2.0 + 8.0 + shape_reach + 40.0 + shadow_reach + 4.0;
3230 let smallest_strain = 0.8;
3231 let reach = field_reach / smallest_strain;
3232 let contains = |x: f32, y: f32| {
3233 support.x <= x
3234 && support.y <= y
3235 && support.x + support.width >= x
3236 && support.y + support.height >= y
3237 };
3238 for (cx, cy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
3239 let corner = ((60.0 + reach) * cx, (20.0 + reach) * cy);
3240 let stretched = (140.0 + corner.0 * 1.25, 80.0 + corner.1 * 0.8);
3241 assert!(
3242 contains(stretched.0, stretched.1 - resolved.shadow_offset_y.abs())
3243 && contains(stretched.0, stretched.1 + resolved.shadow_offset_y.abs()),
3244 "{support:?} must hold the strained, dilated corner {stretched:?}"
3245 );
3246 }
3247 assert!(
3248 contains(230.0 + 20.0 + reach, 80.0),
3249 "the glued shape's reach is inside"
3250 );
3251 assert!(shader.output_padding() >= field_reach);
3252
3253 let RenderEffect::Shader { shader } =
3254 resolved.backdrop_effect(2.0, GlassDynamics::default())
3255 else {
3256 panic!("an unblurred glass is one shader");
3257 };
3258 assert_eq!(
3259 shader.output_support(),
3260 None,
3261 "cover-mode glass fills its rect"
3262 );
3263 }
3264}