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 pub(crate) fn without_transparency(mut self, colors: &LiquidColors) -> Self {
1050 self.tint = colors.surface;
1051 self.tint_amount = None;
1052 self.backdrop_blur = None;
1053 self.blur_radius_dp = 0.0;
1054 self.saturation = 1.0;
1055 self.refraction_depth = 0.0;
1056 self.refraction_depth_dp = None;
1057 self.dispersion = 0.0;
1058 self.edge_spectrum = None;
1059 self.transmission_refraction = 0.0;
1060 self.meniscus_absorption = 0.0;
1061 self.rim_reflection = 0.0;
1062 self.adaptive_frost = 0.0;
1063 self
1064 }
1065
1066 fn clips_in_shader(&self) -> bool {
1067 self.key_fill.and_then(GlassKeyFill::uniforms).is_some()
1068 }
1069 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
1070 self.runtime_effect(density, &dynamics, false)
1071 }
1072
1073 fn blur_radii(&self, density: f32, activity: f32, content_mask: bool) -> (f32, f32) {
1074 if content_mask || self.tint_amount.is_some() {
1075 return (0.0, 0.0);
1076 }
1077 let radius = self.blur_radius_dp * density * activity;
1078 if radius > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
1079 (0.0, radius)
1080 } else {
1081 (radius, 0.0)
1082 }
1083 }
1084
1085 fn runtime_effect(
1086 &self,
1087 density: f32,
1088 dynamics: &GlassDynamics,
1089 content_mask: bool,
1090 ) -> RenderEffect {
1091 self.runtime_effect_with_content(density, dynamics, content_mask, false)
1092 }
1093
1094 fn runtime_effect_with_content(
1095 &self,
1096 density: f32,
1097 dynamics: &GlassDynamics,
1098 content_mask: bool,
1099 separate_content: bool,
1100 ) -> RenderEffect {
1101 let density = density.max(f32::EPSILON);
1102 let activity = dynamics
1103 .activity
1104 .filter(|value| value.is_finite())
1105 .unwrap_or(1.0)
1106 .clamp(0.0, 1.0);
1107 static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
1108 let mut shader = SHADER
1109 .get_or_init(|| {
1110 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
1111 shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
1112 shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
1113 specialize_liquid_glass(&mut shader);
1114 shader
1115 })
1116 .clone();
1117 if let Some(morph) = dynamics.morph.as_ref() {
1118 set_morph_uniforms(&mut shader, morph, activity);
1119 } else {
1120 shader.set_float2(0, 0.0, 0.0);
1121 shader.set_float(6, self.shape.shader_radius_px(density));
1122 }
1123 let projection = dynamics.projection();
1124 shader.set_float2(GLASS_OPTICAL_PROJECTION_UNIFORM, projection.0, projection.1);
1125 let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
1126 shader.set_float(9, self.refraction_depth * activity * press_depth);
1127 let (refraction_mode, refraction_reach) = self.refraction.shader_parameters();
1128 shader.set_float(GLASS_REFRACTION_MODE_UNIFORM, refraction_mode);
1129 set_edge_return_depth(&mut shader, dynamics.edge_return_depth_dp);
1130 shader.set_float(
1131 GLASS_EDGE_REFRACTION_REACH_UNIFORM,
1132 refraction_reach * activity * press_depth,
1133 );
1134 shader.set_float(
1135 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
1136 self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
1137 );
1138 shader.set_float(
1139 GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
1140 if self.refraction_depth_dp.is_some() {
1141 1.0
1142 } else {
1143 0.0
1144 },
1145 );
1146 shader.set_float(
1147 GLASS_REFRACTION_CURVE_UNIFORM,
1148 self.refraction_curve * activity,
1149 );
1150 shader.set_float(
1151 GLASS_DISPERSION_UNIFORM,
1152 self.dispersion * activity * press_depth,
1153 );
1154 let spectrum_reach = self.set_spectrum_uniforms(&mut shader);
1155 shader.set_float(
1156 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
1157 self.transmission_refraction * activity,
1158 );
1159 shader.set_float(
1160 GLASS_MENISCUS_ABSORPTION_UNIFORM,
1161 self.meniscus_absorption * press_depth,
1162 );
1163 shader.set_float(
1164 GLASS_FOLD_DEPTH_UNIFORM,
1165 self.fold_depth.max(0.0) * press_depth,
1166 );
1167 shader.set_float(
1168 GLASS_OPTICAL_ZOOM_UNIFORM,
1169 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
1170 );
1171 let zoom_anchor = dynamics
1172 .morph
1173 .as_ref()
1174 .map(|morph| morph.zoom_anchor)
1175 .unwrap_or((0.0, 0.0));
1176 shader.set_float2(
1177 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1178 zoom_anchor.0,
1179 zoom_anchor.1,
1180 );
1181 shader.set_float(121, self.rim_reflection.max(0.001));
1182 let (ink_color, ink_strength) = self
1183 .ink_recolor
1184 .map(|(color, strength)| (color, strength * activity))
1185 .unwrap_or((Color::TRANSPARENT, 0.0));
1186 shader.set_float(124, ink_color.r());
1187 shader.set_float(125, ink_color.g());
1188 shader.set_float(126, ink_color.b());
1189 shader.set_float(127, ink_strength);
1190 let (light_x, light_y) = glass_light_direction();
1191 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
1192 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
1193 self.set_lighting_uniforms(&mut shader, dynamics);
1194 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
1195 shader.set_float(
1196 11,
1197 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
1198 );
1199 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
1200 let dynamic_tint_alpha = (dynamic_tint.a()
1201 * dynamics
1202 .tint_alpha_multiplier
1203 .unwrap_or(1.0)
1204 .clamp(0.0, 2.0)
1205 * activity)
1206 .clamp(0.0, 1.0);
1207 shader.set_float4(
1208 14,
1209 dynamic_tint.r(),
1210 dynamic_tint.g(),
1211 dynamic_tint.b(),
1212 dynamic_tint_alpha,
1213 );
1214 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
1215 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
1216 shader.set_float(20, self.lift * activity);
1217 shader.set_float(21, 0.5 * activity);
1218 shader.set_float2(22, 0.0, 1.0);
1219 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
1220 shader.set_float(28, self.rim_style * activity);
1221 let (wcksrd_blur_radius, gaussian_blur_radius) =
1222 self.blur_radii(density, activity, content_mask);
1223 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
1224 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
1225 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
1226 shader.set_float4(
1227 GLASS_RESTING_TINT_UNIFORM,
1228 resting_tint.r(),
1229 resting_tint.g(),
1230 resting_tint.b(),
1231 resting_tint.a(),
1232 );
1233 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
1234 shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
1235 shader.set_float(97, self.foreground_luma);
1236 shader.set_float(GLASS_ADAPTIVE_TONE_UNIFORM, f32::from(self.adaptive_tone));
1237 self.set_backdrop_uniforms(&mut shader, density, content_mask);
1238 shader.set_float(
1239 cranpose_ui_graphics::GLASS_FOREGROUND_CONTENT_UNIFORM,
1240 f32::from(separate_content),
1241 );
1242 let dynamic_shadow = !self.clip && self.shadow;
1243 shader.set_float(
1244 102,
1245 if dynamic_shadow {
1246 self.shadow_color.a() * 0.55 * activity
1247 } else {
1248 0.0
1249 },
1250 );
1251 shader.set_float(103, self.shadow_radius);
1252 shader.set_float(104, self.shadow_offset_y);
1253 shader.set_float(105, self.shadow_spread);
1254 let morph_pad = dynamics
1255 .morph
1256 .as_ref()
1257 .map(|morph| {
1258 let (px, py, pw, ph, _) = morph.primary;
1259 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1260 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1261 let mut shape_reach = 0.0f32;
1262 for (sx, sy, sw, sh, _) in &morph.shapes {
1263 let reach_x = ((sx + sw * 0.5) - right)
1264 .max(left - (sx - sw * 0.5))
1265 .max(0.0);
1266 let reach_y = ((sy + sh * 0.5) - bottom)
1267 .max(top - (sy - sh * 0.5))
1268 .max(0.0);
1269 shape_reach = shape_reach.max(reach_x.max(reach_y));
1270 }
1271 let glue_pad = if morph.shapes.is_empty() {
1272 0.0
1273 } else {
1274 morph.glue * 2.0
1275 };
1276 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1277 })
1278 .unwrap_or(0.0);
1279 shader.set_input_padding(
1280 (self.input_padding()
1281 + if refraction_mode >= 1.5 {
1282 refraction_reach * (26.5 / 9.0) + spectrum_reach
1283 } else {
1284 refraction_reach
1285 }
1286 + morph_pad
1287 + wcksrd_blur_radius / density)
1288 * projection.0.max(projection.1).max(1.0),
1289 );
1290 if let Some(morph) = dynamics.morph.as_ref() {
1291 let shadow_reach = if dynamic_shadow {
1292 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
1293 } else {
1294 0.0
1295 };
1296 let support = morph_output_support(
1297 morph,
1298 activity,
1299 morph_pad,
1300 shadow_reach,
1301 if dynamic_shadow {
1302 self.shadow_offset_y.abs()
1303 } else {
1304 0.0
1305 },
1306 );
1307 let (cx, cy, _, _, _) = morph.primary;
1308 let support = Rect {
1309 x: cx + (support.x - cx) * projection.0,
1310 y: cy + (support.y - cy) * projection.1,
1311 width: support.width * projection.0,
1312 height: support.height * projection.1,
1313 };
1314 let (node_w, node_h) = morph.node_size;
1315 let overhang = (-support.x)
1316 .max(-support.y)
1317 .max(support.x + support.width - node_w)
1318 .max(support.y + support.height - node_h)
1319 .max(0.0);
1320 shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1321 shader.set_output_support(Some(support));
1322 }
1323
1324 let optical_effect = liquid_glass_runtime_effect(shader);
1325 if gaussian_blur_radius > f32::EPSILON {
1326 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1327 .then(optical_effect)
1328 } else {
1329 optical_effect
1330 }
1331 }
1332
1333 fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1334 if let Some(amount) = self.tint_amount {
1335 shader.set_float(
1336 cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1337 8.0 * density,
1338 );
1339 shader.set_float(
1340 cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1341 amount.percent() / 100.0,
1342 );
1343 }
1344 if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1345 shader.set_float2(
1346 cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1347 radius * density,
1348 opacity,
1349 );
1350 }
1351 }
1352
1353 fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1354 let spectrum = self
1355 .edge_spectrum
1356 .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1357 if let Some((spectrum, values)) = spectrum {
1358 for (index, value) in values.into_iter().enumerate() {
1359 shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1360 }
1361 spectrum.capture_reach()
1362 } else {
1363 40.787 * self.dispersion
1364 }
1365 }
1366
1367 fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1368 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1369 shader.set_float(118, touch_x);
1370 shader.set_float(119, touch_y);
1371 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1372 shader.set_float(
1373 GLASS_FACE_LIGHTING_OFF_UNIFORM,
1374 f32::from(!self.face_lighting),
1375 );
1376 shader.set_float(
1377 GLASS_TOUCH_RADIUS_UNIFORM,
1378 dynamics
1379 .touch_radius_dp
1380 .filter(|radius| radius.is_finite() && *radius > 0.0)
1381 .unwrap_or(58.0),
1382 );
1383 shader.set_float(
1384 GLASS_LAYER_CLIPPED_UNIFORM,
1385 f32::from(self.clip && !self.clips_in_shader()),
1386 );
1387 if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1388 for (index, value) in values.into_iter().enumerate() {
1389 shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1390 }
1391 }
1392 if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1393 for (index, value) in values.into_iter().enumerate() {
1394 shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1395 }
1396 }
1397 if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1398 for (index, value) in values.into_iter().enumerate() {
1399 shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1400 }
1401 }
1402 }
1403
1404 fn input_padding(&self) -> f32 {
1405 2.0
1406 }
1407}
1408
1409pub trait LiquidModifierExt {
1411 fn glass_effect(self, glass: Glass) -> Modifier;
1417
1418 fn glass_effect_with(
1422 self,
1423 glass: Glass,
1424 dynamics: impl Fn() -> GlassDynamics + 'static,
1425 ) -> Modifier;
1426}
1427
1428impl LiquidModifierExt for Modifier {
1429 fn glass_effect(self, glass: Glass) -> Modifier {
1430 self.glass_effect_with(glass, GlassDynamics::default)
1431 }
1432
1433 fn glass_effect_with(
1434 self,
1435 glass: Glass,
1436 dynamics: impl Fn() -> GlassDynamics + 'static,
1437 ) -> Modifier {
1438 let colors = crate::theme::liquid_colors();
1439 let options = cranpose_services::local_accessibility_options().current();
1440 let resolved = if options.reduce_transparency {
1441 glass.resolve(&colors).without_transparency(&colors)
1442 } else {
1443 glass.resolve(&colors)
1444 };
1445 let shape = resolved.shape;
1446
1447 let mut modifier = self;
1448 if resolved.shadow && resolved.clip {
1449 let shadow_color = resolved.shadow_color;
1450 let (radius, offset_y, spread) = (
1451 resolved.shadow_radius,
1452 resolved.shadow_offset_y,
1453 resolved.shadow_spread,
1454 );
1455 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1456 scope.radius = radius;
1457 scope.spread = spread;
1458 scope.offset.y = offset_y;
1459 scope.color = shadow_color;
1460 scope.cutout = true;
1461 });
1462 }
1463
1464 let layer = cached_glass_layer(resolved);
1465 modifier.graphics_layer(move || layer(current_density(), dynamics()))
1466 }
1467}
1468
1469struct CachedGlassLayer<T> {
1470 density: u32,
1471 light: [u32; 2],
1472 dynamics: GlassDynamics,
1473 value: T,
1474}
1475
1476fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1477 cached_glass_layers(resolved, false, std::convert::identity)
1478}
1479
1480pub(crate) fn cached_glass_content_layers(
1481 resolved: ResolvedGlass,
1482) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1483 cached_glass_layers(resolved, true, |mut layer| {
1484 let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1485 panic!("content layers require a sequential edge lens");
1486 };
1487 layer.backdrop_effect = Some((*second).clone());
1488 [
1489 GraphicsLayer {
1490 backdrop_effect: Some((*first).clone()),
1491 ..Default::default()
1492 },
1493 layer,
1494 ]
1495 })
1496}
1497
1498fn cached_glass_layers<T: Clone>(
1499 resolved: ResolvedGlass,
1500 separate_content: bool,
1501 assemble: impl Fn(GraphicsLayer) -> T,
1502) -> impl Fn(f32, GlassDynamics) -> T {
1503 let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1504 move |density, dynamics| {
1505 let direction = glass_light_direction();
1506 let light = [direction.0, direction.1].map(f32::to_bits);
1507 let mut cached = cached.borrow_mut();
1508 if let Some(previous) = &*cached
1509 && previous.density == density.to_bits()
1510 && previous.light == light
1511 && glass_dynamics_match(&previous.dynamics, &dynamics)
1512 {
1513 return previous.value.clone();
1514 }
1515 let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1516 || (resolved.clip && resolved.clips_in_shader()))
1517 .then(|| resolved.runtime_effect(density, &dynamics, true));
1518 let layer = GraphicsLayer {
1519 backdrop_effect: Some(resolved.runtime_effect_with_content(
1520 density,
1521 &dynamics,
1522 false,
1523 separate_content,
1524 )),
1525 render_effect,
1526 shape: resolved.shape.layer_shape(),
1527 clip: resolved.clip && !resolved.clips_in_shader(),
1528 ..Default::default()
1529 };
1530 let value = assemble(layer);
1531 *cached = Some(CachedGlassLayer {
1532 density: density.to_bits(),
1533 light,
1534 dynamics,
1535 value: value.clone(),
1536 });
1537 value
1538 }
1539}
1540
1541fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1542 let bits = |d: &GlassDynamics| {
1543 let GlassDynamics {
1544 edge_return_depth_dp,
1545 optical_projection,
1546 activity,
1547 resting_tint,
1548 highlight_boost,
1549 saturation_boost,
1550 tint_alpha_multiplier,
1551 morph: _,
1552 touch,
1553 touch_radius_dp,
1554 press_depth,
1555 } = d;
1556 (
1557 edge_return_depth_dp.map(f32::to_bits),
1558 optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
1559 activity.map(f32::to_bits),
1560 resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1561 [*highlight_boost, *saturation_boost].map(f32::to_bits),
1562 tint_alpha_multiplier.map(f32::to_bits),
1563 touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1564 press_depth.map(f32::to_bits),
1565 touch_radius_dp.map(f32::to_bits),
1566 )
1567 };
1568 bits(a) == bits(b)
1569 && match (&a.morph, &b.morph) {
1570 (None, None) => true,
1571 (Some(a), Some(b)) => glass_morphs_match(a, b),
1572 _ => false,
1573 }
1574}
1575
1576fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1577 let shape_bits =
1578 |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1579 let bits = |m: &GlassMorph| {
1580 let GlassMorph {
1581 node_size,
1582 primary,
1583 shapes: _,
1584 glue,
1585 wobble_amplitude,
1586 wobble_phase,
1587 bulge_amplitude,
1588 bulge_direction,
1589 ellipse_blend,
1590 capsule_smoothing_dp,
1591 deformation,
1592 zoom_anchor,
1593 } = m;
1594 (
1595 [
1596 node_size.0,
1597 node_size.1,
1598 *glue,
1599 *wobble_amplitude,
1600 *wobble_phase,
1601 *bulge_amplitude,
1602 *bulge_direction,
1603 *ellipse_blend,
1604 *capsule_smoothing_dp,
1605 zoom_anchor.0,
1606 zoom_anchor.1,
1607 ]
1608 .map(f32::to_bits),
1609 shape_bits(primary),
1610 deformation
1611 .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1612 )
1613 };
1614 bits(a) == bits(b)
1615 && a.shapes
1616 .iter()
1617 .map(shape_bits)
1618 .eq(b.shapes.iter().map(shape_bits))
1619}
1620
1621fn morph_output_support(
1622 morph: &GlassMorph,
1623 activity: f32,
1624 morph_pad: f32,
1625 shadow_reach: f32,
1626 shadow_offset: f32,
1627) -> Rect {
1628 let (px, py, pw, ph, _) = morph.primary;
1629 let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1630 (
1631 1.0 + (deformation.along() - 1.0) * activity,
1632 deformation.axis(),
1633 )
1634 });
1635 let across = 1.0 / along;
1636 let smallest_strain = along.min(across);
1637 let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1638 let rows = [
1639 (
1640 (along * ax * ax + across * ay * ay).abs(),
1641 ((along - across) * ax * ay).abs(),
1642 ),
1643 (
1644 ((along - across) * ax * ay).abs(),
1645 (along * ay * ay + across * ax * ax).abs(),
1646 ),
1647 ];
1648 let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1649 let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1650 let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1651 let (mut left, mut top) = (px - extent_x, py - extent_y);
1652 let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1653 for (sx, sy, sw, sh, _) in &morph.shapes {
1654 left = left.min(sx - sw * 0.5 - reach);
1655 top = top.min(sy - sh * 0.5 - reach);
1656 right = right.max(sx + sw * 0.5 + reach);
1657 bottom = bottom.max(sy + sh * 0.5 + reach);
1658 }
1659 Rect {
1660 x: left,
1661 y: top - shadow_offset,
1662 width: right - left,
1663 height: bottom - top + 2.0 * shadow_offset,
1664 }
1665}
1666
1667fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1668 let (node_w, node_h) = morph.node_size;
1669 let (cx, cy, w, h, radius) = morph.primary;
1670 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1671 shader.set_float2(2, cx, cy);
1672 shader.set_float2(4, w, h);
1673 shader.set_float(6, radius);
1674 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1675 shader.set_float(30, count as f32);
1676 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1677 let base = 36 + index * 5;
1678 shader.set_float(base, *sx);
1679 shader.set_float(base + 1, *sy);
1680 shader.set_float(base + 2, *sw);
1681 shader.set_float(base + 3, *sh);
1682 shader.set_float(base + 4, *sr);
1683 }
1684 shader.set_float(31, morph.glue);
1685 shader.set_float(32, morph.wobble_amplitude * activity);
1686 shader.set_float(33, morph.wobble_phase);
1687 shader.set_float(26, morph.bulge_amplitude * activity);
1688 shader.set_float(27, morph.bulge_direction);
1689 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1690 shader.set_float(
1691 cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
1692 if morph.capsule_smoothing_dp.is_finite() {
1693 morph.capsule_smoothing_dp.max(0.0) * activity
1694 } else {
1695 0.0
1696 },
1697 );
1698 if let Some(deformation) = morph.deformation {
1699 let axis = deformation.axis();
1700 let along = 1.0 + (deformation.along() - 1.0) * activity;
1701 shader.set_float2(106, axis.0, axis.1);
1702 shader.set_float(108, along);
1703 shader.set_float(109, 1.0 / along);
1704 } else {
1705 shader.set_float2(106, 1.0, 0.0);
1706 shader.set_float2(108, 1.0, 1.0);
1707 }
1708}
1709
1710#[cfg(test)]
1711mod tests {
1712 use super::*;
1713
1714 #[test]
1715 fn backdrop_blur_validates_radius_and_opacity() {
1716 for (radius, opacity, expected) in [
1717 (2.0, 0.5, Some((2.0, 0.5))),
1718 (-2.0, 3.0, Some((0.0, 1.0))),
1719 (2.0, -1.0, Some((2.0, 0.0))),
1720 (f32::NAN, 1.0, None),
1721 (2.0, f32::INFINITY, None),
1722 ] {
1723 assert_eq!(
1724 Glass::lens().backdrop_blur(radius, opacity).backdrop_blur,
1725 expected
1726 );
1727 }
1728 let glass = Glass::lens()
1729 .backdrop_blur(2.0, 0.75)
1730 .edge_refraction(0.0)
1731 .resolve(&light_colors());
1732 let shader = terminal_shader(glass.backdrop_effect(3.0, GlassDynamics::default()));
1733 assert_eq!(shader.uniforms()[172..174], [6.0, 0.75]);
1734 let mask = terminal_shader(glass.runtime_effect(3.0, &GlassDynamics::default(), true));
1735 assert_eq!(
1736 mask.uniforms().get(172..174).unwrap_or(&[0.0, 0.0]),
1737 [0.0, 0.0]
1738 );
1739 }
1740
1741 #[test]
1742 fn pane_tint_preserves_the_sharp_source_at_every_percentage() {
1743 for percent in [0.0, 25.0, 50.0, 100.0] {
1744 let amount = GlassTintAmount::from_percent(percent).unwrap();
1745 let glass = Glass::regular()
1746 .adaptive_frost(Color::BLACK, 0.0)
1747 .tint_amount(amount);
1748 assert_eq!(glass.tint_amount, Some(amount));
1749 let effect = glass.backdrop_effect(&light_colors(), 3.0, GlassDynamics::default());
1750 let RenderEffect::Shader { shader } = effect else {
1751 panic!("the sharp backdrop must not be destroyed by a preceding blur");
1752 };
1753 assert_eq!(
1754 shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM],
1755 24.0
1756 );
1757 assert_eq!(
1758 shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1],
1759 percent / 100.0
1760 );
1761 assert_eq!(
1762 shader.substrates(),
1763 &[cranpose_ui_graphics::SubstrateSpec::Blur { radius_px: 24.0 }]
1764 );
1765 }
1766 }
1767
1768 fn light_colors() -> LiquidColors {
1769 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
1770 }
1771
1772 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
1773 match effect {
1774 RenderEffect::Shader { shader } => std::sync::Arc::unwrap_or_clone(shader),
1775 RenderEffect::Chain { second, .. } => {
1776 terminal_shader(std::sync::Arc::unwrap_or_clone(second))
1777 }
1778 effect => panic!("expected runtime shader, got {effect:?}"),
1779 }
1780 }
1781
1782 #[test]
1783 fn optical_projection_validates_scales_and_projects_output_support() {
1784 let glass = Glass::lens().resolve(&light_colors());
1785 let mut dynamics = GlassDynamics {
1786 morph: Some(GlassMorph {
1787 node_size: (200.0, 120.0),
1788 primary: (100.0, 60.0, 111.0, 70.0, 35.0),
1789 ..Default::default()
1790 }),
1791 ..Default::default()
1792 };
1793 let base = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1794 for projection in [
1795 None,
1796 Some((0.0, 1.0)),
1797 Some((-1.0, 1.0)),
1798 Some((1.0, f32::NAN)),
1799 Some((f32::INFINITY, 1.0)),
1800 ] {
1801 dynamics.optical_projection = projection;
1802 let shader = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1803 assert_eq!(
1804 &shader.uniforms()
1805 [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1806 &[1.0, 1.0]
1807 );
1808 assert_eq!(shader.output_support(), base.output_support());
1809 }
1810 dynamics.optical_projection = Some((1.2, 0.8));
1811 let projected = terminal_shader(glass.backdrop_effect(3.0, dynamics));
1812 assert_eq!(
1813 &projected.uniforms()
1814 [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1815 &[1.2, 0.8]
1816 );
1817 let a = base.output_support().unwrap();
1818 let b = projected.output_support().unwrap();
1819 assert!((b.width - a.width * 1.2).abs() < 0.001);
1820 assert!((b.height - a.height * 0.8).abs() < 0.001);
1821 }
1822
1823 #[test]
1824 fn content_layers_preserve_the_complete_material_effect_and_cache() {
1825 let resolved = Glass::lens()
1826 .edge_refraction(9.0)
1827 .no_clip()
1828 .resolve(&light_colors());
1829 let layers = cached_glass_content_layers(resolved.clone());
1830 for density in [1.0, 3.0] {
1831 for activity in [0.0, 0.5, 1.0] {
1832 let dynamics = GlassDynamics {
1833 activity: Some(activity),
1834 ..Default::default()
1835 };
1836 let actual = layers(density, dynamics.clone());
1837 let joined = actual[0]
1838 .backdrop_effect
1839 .clone()
1840 .unwrap()
1841 .then(actual[1].backdrop_effect.clone().unwrap());
1842 assert_eq!(
1843 joined,
1844 resolved.runtime_effect_with_content(density, &dynamics, false, true)
1845 );
1846 assert!(actual[0].render_effect.is_none());
1847 assert!(!actual[0].clip);
1848 assert_eq!(
1849 terminal_shader(actual[1].backdrop_effect.clone().unwrap()).uniforms()[147],
1850 3.0
1851 );
1852 let mut repeated = layers(density, dynamics.clone());
1853 assert_eq!(actual, repeated);
1854 repeated[0].backdrop_effect = None;
1855 assert_eq!(actual, layers(density, dynamics));
1856 }
1857 }
1858 }
1859
1860 #[test]
1861 fn cached_glass_layers_refresh_all_live_inputs_and_keep_clones_independent() {
1862 let resolved = Glass::regular().no_clip().resolve(&light_colors());
1863 let layer = cached_glass_layer(resolved.clone());
1864 let mut dynamics = GlassDynamics::default();
1865 let mut density = 2.0;
1866 let mut previous = None;
1867 for step in 0..16 {
1868 match step {
1869 1 => density = 3.0,
1870 2 => set_glass_light_direction((0.5, -0.5)),
1871 3 => dynamics.highlight_boost = 0.25,
1872 4 => dynamics.saturation_boost = 0.4,
1873 5 => dynamics.activity = Some(0.5),
1874 6 => dynamics.resting_tint = Some(Color::RED),
1875 7 => dynamics.tint_alpha_multiplier = Some(0.3),
1876 8 => dynamics.touch = Some((5.0, 8.0, 0.7)),
1877 9 => dynamics.press_depth = Some(0.4),
1878 10 => {
1879 dynamics.morph = Some(GlassMorph {
1880 node_size: (60.0, 30.0),
1881 primary: (30.0, 15.0, 50.0, 20.0, 4.0),
1882 shapes: vec![(10.0, 15.0, 8.0, 8.0, -1.0)],
1883 ..Default::default()
1884 })
1885 }
1886 11 => {
1887 let morph = dynamics.morph.as_mut().unwrap();
1888 morph.primary.0 += 3.0;
1889 morph.shapes[0].0 += 2.0;
1890 morph.deformation = Some(GlassDeformation::incompressible((1.0, 1.0), 1.3));
1891 }
1892 12 => dynamics.morph = None,
1893 13 => dynamics.touch_radius_dp = Some(144.0),
1894 14 => dynamics.edge_return_depth_dp = Some(3.5),
1895 15 => dynamics.optical_projection = Some((1.2, 0.8)),
1896 _ => {}
1897 }
1898 let actual = layer(density, dynamics.clone());
1899 let expected = GraphicsLayer {
1900 backdrop_effect: Some(resolved.backdrop_effect(density, dynamics.clone())),
1901 render_effect: dynamics
1902 .morph
1903 .as_ref()
1904 .map(|_| resolved.runtime_effect(density, &dynamics, true)),
1905 shape: resolved.shape.layer_shape(),
1906 clip: resolved.clip,
1907 ..Default::default()
1908 };
1909 assert_eq!(actual, expected, "live input step {step}");
1910 assert_ne!(
1911 previous.as_ref(),
1912 Some(&actual),
1913 "input change must reach the layer at step {step}"
1914 );
1915 let mut repeated = layer(density, dynamics.clone());
1916 assert_eq!(repeated, actual);
1917 repeated.backdrop_effect = None;
1918 assert_eq!(layer(density, dynamics.clone()), actual);
1919 previous = Some(actual);
1920 }
1921 set_glass_light_direction((0.0, 1.0));
1922 let replacement = cached_glass_layer(
1923 Glass::clear()
1924 .shape(LiquidShape::RoundedRect(9.0))
1925 .resolve(&light_colors()),
1926 );
1927 assert_ne!(
1928 replacement(density, dynamics.clone()),
1929 layer(density, dynamics)
1930 );
1931 }
1932
1933 #[test]
1934 fn glass_cache_input_equality_preserves_signed_zero_and_nan_bits() {
1935 let mut a = GlassDynamics {
1936 touch: Some((0.0, 1.0, 0.5)),
1937 ..Default::default()
1938 };
1939 let mut b = a.clone();
1940 b.touch.as_mut().unwrap().0 = -0.0;
1941 assert!(!glass_dynamics_match(&a, &b));
1942 a.highlight_boost = f32::NAN;
1943 assert!(glass_dynamics_match(&a, &a));
1944 a.morph = Some(GlassMorph::default());
1945 b = a.clone();
1946 b.morph.as_mut().unwrap().zoom_anchor.0 = -0.0;
1947 assert!(!glass_dynamics_match(&a, &b));
1948 b = a.clone();
1949 b.morph
1950 .as_mut()
1951 .unwrap()
1952 .shapes
1953 .push((0.0, 1.0, 2.0, 3.0, 4.0));
1954 assert!(!glass_dynamics_match(&a, &b));
1955 }
1956
1957 #[test]
1958 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1959 assert_eq!(glass_light_direction(), (0.0, 1.0));
1960 let resolved = Glass::regular().resolve(&light_colors());
1961 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1962 let shader = terminal_shader(effect);
1963 let u = shader.uniforms();
1964 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1965 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1966
1967 set_glass_light_direction((1.0, 0.0));
1968 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1969 let u_rotated = terminal_shader(effect);
1970 let u_rotated = u_rotated.uniforms();
1971 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1972 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1973 set_glass_light_direction((0.0, 1.0));
1974 }
1975
1976 #[test]
1977 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1978 for shape in [
1979 LiquidShape::Capsule,
1980 LiquidShape::Circle,
1981 LiquidShape::RoundedRect(12.0),
1982 ] {
1983 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1984 }
1985 }
1986
1987 #[test]
1988 fn glass_shadow_clamps_negative_radius() {
1989 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1990 assert_eq!(shadow.radius, 0.0);
1991 assert_eq!(shadow.offset_y, 3.0);
1992 assert_eq!(shadow.spread, -1.0);
1993 }
1994
1995 #[test]
1996 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1997 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1998 assert_eq!(deformation.axis(), (0.6, 0.8));
1999 assert_eq!(deformation.along(), 1.25);
2000 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
2001 assert_eq!(
2002 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
2003 (1.0, 0.0)
2004 );
2005 }
2006
2007 #[test]
2008 fn glass_builders_clamp_physical_inputs() {
2009 let glass = Glass::lens()
2010 .shape(LiquidShape::Circle)
2011 .tint(Color::BLACK)
2012 .blur_radius(-2.0)
2013 .saturation(1.2)
2014 .refraction_depth(3.0)
2015 .refraction_depth_dp(-3.0)
2016 .refraction_curve(2.0)
2017 .dispersion(2.0)
2018 .transmission_refraction(2.0)
2019 .meniscus_absorption(2.0)
2020 .highlight(0.4)
2021 .lift(-0.2)
2022 .adaptive_frost(Color::WHITE, 2.0)
2023 .shadow(false)
2024 .no_clip();
2025 assert_eq!(glass.shape, LiquidShape::Circle);
2026 assert_eq!(glass.tint, Some(Color::BLACK));
2027 assert_eq!(glass.blur_radius, Some(-2.0));
2028 assert_eq!(glass.saturation, Some(1.2));
2029 assert_eq!(glass.refraction_depth, 2.0);
2030 assert_eq!(glass.refraction_depth_dp, Some(0.0));
2031 assert_eq!(glass.refraction_curve, 1.0);
2032 assert_eq!(glass.dispersion, 1.0);
2033 assert_eq!(glass.transmission_refraction, 1.0);
2034 assert_eq!(glass.meniscus_absorption, 1.0);
2035 assert_eq!(glass.highlight, 0.4);
2036 assert_eq!(glass.lift, Some(-0.2));
2037 assert_eq!(glass.adaptive_frost, 1.0);
2038 assert!(!glass.shadow);
2039 assert!(!glass.clip);
2040 }
2041
2042 #[test]
2043 fn material_variants_resolve_distinct_frost_levels() {
2044 let regular = Glass::regular().resolve(&light_colors());
2045 let clear = Glass::clear().resolve(&light_colors());
2046 let lens = Glass::lens().resolve(&light_colors());
2047 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
2048 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
2049 assert!(regular.saturation > clear.saturation);
2050 assert_eq!(lens.rim_style, 1.0);
2051 assert_eq!(regular.refraction_depth, 0.0);
2052 assert_eq!(regular.transmission_refraction, 0.0);
2053 assert_eq!(regular.refraction_curve, 0.25);
2054 assert_eq!(lens.refraction_curve, 1.0);
2055 assert_eq!(regular.dispersion, 0.0);
2056 assert_eq!(lens.dispersion, 0.30);
2057 }
2058
2059 #[test]
2060 fn neutral_surface_helpers_follow_foreground_polarity() {
2061 assert_eq!(
2062 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
2063 Color::BLACK.with_alpha(0.08)
2064 );
2065 assert_eq!(
2066 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
2067 Color::WHITE.with_alpha(0.10)
2068 );
2069 }
2070
2071 #[test]
2072 fn dynamic_saturation_reaches_the_material_tint() {
2073 let resting = Color::from_rgb_u8(0, 199, 208);
2074 let raised = boost_tint_saturation(resting, 0.55);
2075 assert_eq!(raised.r(), 0.0);
2076 assert!(raised.g() > resting.g());
2077 assert!(raised.b() > resting.b());
2078 assert_eq!(raised.a(), resting.a());
2079 }
2080
2081 #[test]
2082 fn template_specialization_follows_each_materials_dispersion_and_activity() {
2083 for (dispersion, activity) in [(0.6, 0.0), (0.0, 0.5), (0.3, 1.0), (0.0, 0.0)] {
2084 let resolved = Glass::regular()
2085 .dispersion(dispersion)
2086 .resolve(&light_colors());
2087 let mut shader = terminal_shader(resolved.backdrop_effect(
2088 1.0,
2089 GlassDynamics {
2090 activity: Some(activity),
2091 ..GlassDynamics::default()
2092 },
2093 ));
2094 cranpose_ui_graphics::specialize_liquid_glass_with_folds(&mut shader, true);
2095 assert_eq!(
2096 shader.uniforms()[GLASS_DISPERSION_UNIFORM],
2097 dispersion * activity
2098 );
2099 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], activity);
2100 assert_eq!(
2101 shader
2102 .overrides()
2103 .iter()
2104 .any(|(name, _)| { *name == cranpose_ui_graphics::GLASS_DISPERSION_OFF_FLAG }),
2105 dispersion * activity == 0.0
2106 );
2107 }
2108 }
2109
2110 fn spectrum_fixture() -> GlassSpectrum {
2111 GlassSpectrum {
2112 angle_radians: 0.7,
2113 step_dp: -1.5,
2114 vertical_scale: 1.25,
2115 opacity_near: 0.25,
2116 opacity_far: 0.75,
2117 fade_depth_dp: 3.5,
2118 extent_dp: 12.0,
2119 }
2120 }
2121
2122 #[test]
2123 fn edge_spectrum_preserves_signed_taps_and_captures_every_ray() {
2124 let spectrum = spectrum_fixture();
2125 let material = Glass::lens()
2126 .edge_refraction(9.0)
2127 .dispersion(0.0)
2128 .edge_spectrum(spectrum);
2129 assert_eq!(material.edge_spectrum, Some(spectrum));
2130 let resolved = material.resolve(&light_colors());
2131 let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2132 assert_eq!(
2133 &shader.uniforms()[GLASS_EDGE_SPECTRUM_UNIFORM..GLASS_EDGE_SPECTRUM_UNIFORM + 8],
2134 &spectrum.uniforms().unwrap()
2135 );
2136 let ordinary = Glass::lens()
2137 .edge_refraction(9.0)
2138 .dispersion(0.0)
2139 .resolve(&light_colors());
2140 let base = terminal_shader(ordinary.backdrop_effect(3.0, GlassDynamics::default()));
2141 assert!((shader.input_padding() - base.input_padding() - 5.625).abs() < 0.001);
2142 }
2143
2144 #[test]
2145 fn edge_spectrum_rejects_invalid_fields_at_both_entry_points() {
2146 for index in 0..7 {
2147 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2148 let mut spectrum = spectrum_fixture();
2149 let fields = [
2150 &mut spectrum.angle_radians,
2151 &mut spectrum.step_dp,
2152 &mut spectrum.vertical_scale,
2153 &mut spectrum.opacity_near,
2154 &mut spectrum.opacity_far,
2155 &mut spectrum.fade_depth_dp,
2156 &mut spectrum.extent_dp,
2157 ];
2158 *fields.into_iter().nth(index).unwrap() = invalid;
2159 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2160 let material = Glass {
2161 edge_spectrum: Some(spectrum),
2162 ..Glass::lens()
2163 };
2164 let shader = terminal_shader(
2165 material
2166 .resolve(&light_colors())
2167 .backdrop_effect(1.0, GlassDynamics::default()),
2168 );
2169 assert_eq!(
2170 shader
2171 .uniforms()
2172 .get(GLASS_EDGE_SPECTRUM_UNIFORM + 7)
2173 .copied()
2174 .unwrap_or(0.0),
2175 0.0
2176 );
2177 }
2178 }
2179 for index in 2..7 {
2180 let mut spectrum = spectrum_fixture();
2181 let fields = [
2182 &mut spectrum.vertical_scale,
2183 &mut spectrum.opacity_near,
2184 &mut spectrum.opacity_far,
2185 &mut spectrum.fade_depth_dp,
2186 &mut spectrum.extent_dp,
2187 ];
2188 *fields.into_iter().nth(index - 2).unwrap() = -0.1;
2189 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2190 }
2191 for near in [true, false] {
2192 let mut spectrum = spectrum_fixture();
2193 if near {
2194 spectrum.opacity_near = 1.1;
2195 } else {
2196 spectrum.opacity_far = 1.1;
2197 }
2198 assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2199 }
2200 let zero = GlassSpectrum {
2201 angle_radians: 0.0,
2202 step_dp: 0.0,
2203 vertical_scale: 0.0,
2204 opacity_near: 0.0,
2205 opacity_far: 1.0,
2206 fade_depth_dp: 0.0,
2207 extent_dp: 0.0,
2208 };
2209 assert_eq!(Glass::lens().edge_spectrum(zero).edge_spectrum, Some(zero));
2210 }
2211
2212 #[test]
2213 fn material_template_preserves_each_instances_optical_zoom_anchor() {
2214 let resolved = Glass::lens().resolve(&light_colors());
2215 for anchor in [(3.5, -2.25), (0.0, 0.0), (-7.0, 11.0)] {
2216 let shader = terminal_shader(resolved.backdrop_effect(
2217 1.5,
2218 GlassDynamics {
2219 morph: Some(GlassMorph {
2220 node_size: (120.0, 72.0),
2221 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2222 zoom_anchor: anchor,
2223 ..Default::default()
2224 }),
2225 ..Default::default()
2226 },
2227 ));
2228 assert_eq!(
2229 &shader.uniforms()
2230 [GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM..GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM + 2],
2231 &[anchor.0, anchor.1]
2232 );
2233 }
2234 }
2235
2236 #[test]
2237 fn key_fill_preserves_valid_parameters_and_rejects_invalid_fields() {
2238 let lighting = GlassKeyFill {
2239 height_dp: 1.0,
2240 curvature: 0.8,
2241 angle_radians: -std::f32::consts::FRAC_PI_4,
2242 saturation: 1.5,
2243 luma_gain: 0.4367,
2244 offset: 1.125,
2245 scale_with_surface: true,
2246 };
2247 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2248 assert!(glass.key_fill.is_none());
2249 let material = glass.key_fill(lighting);
2250 assert_eq!(material.key_fill, Some(lighting));
2251 let shader = terminal_shader(material.backdrop_effect(
2252 &light_colors(),
2253 3.0,
2254 GlassDynamics::default(),
2255 ));
2256 assert_eq!(
2257 &shader.uniforms()[GLASS_KEY_FILL_UNIFORM..GLASS_KEY_FILL_UNIFORM + 7],
2258 &lighting.uniforms().unwrap()
2259 );
2260 }
2261 for scale_with_surface in [false, true] {
2262 let value = GlassKeyFill {
2263 scale_with_surface,
2264 ..lighting
2265 };
2266 assert_eq!(value.uniforms().unwrap()[6], f32::from(scale_with_surface));
2267 let layer = cached_glass_layer(Glass::clear().key_fill(value).resolve(&light_colors()))(
2268 3.0,
2269 GlassDynamics::default(),
2270 );
2271 assert!(!layer.clip);
2272 assert!(layer.render_effect.is_some());
2273 assert!(layer.backdrop_effect.is_some());
2274 }
2275 for index in 0..6 {
2276 for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2277 let mut value = lighting;
2278 match index {
2279 0 => value.height_dp = invalid,
2280 1 => value.curvature = invalid,
2281 2 => value.angle_radians = invalid,
2282 3 => value.saturation = invalid,
2283 4 => value.luma_gain = invalid,
2284 _ => value.offset = invalid,
2285 }
2286 let shader = terminal_shader(Glass::clear().key_fill(value).backdrop_effect(
2287 &light_colors(),
2288 3.0,
2289 GlassDynamics::default(),
2290 ));
2291 assert_eq!(
2292 shader
2293 .uniforms()
2294 .get(GLASS_KEY_FILL_UNIFORM)
2295 .copied()
2296 .unwrap_or_default(),
2297 0.0
2298 );
2299 }
2300 }
2301 for invalid in [0.0, -1.0] {
2302 assert!(
2303 GlassKeyFill {
2304 height_dp: invalid,
2305 ..lighting
2306 }
2307 .uniforms()
2308 .is_none()
2309 );
2310 assert!(
2311 GlassKeyFill {
2312 curvature: invalid,
2313 ..lighting
2314 }
2315 .uniforms()
2316 .is_none()
2317 );
2318 }
2319 }
2320
2321 #[test]
2322 fn independent_edge_return_depth_preserves_zero_and_rejects_invalid_values() {
2323 for depth in [
2324 None,
2325 Some(0.0),
2326 Some(3.5),
2327 Some(-1.0),
2328 Some(f32::NAN),
2329 Some(f32::INFINITY),
2330 ] {
2331 let shader = terminal_shader(
2332 Glass::lens()
2333 .edge_refraction(9.0)
2334 .refraction_depth_dp(36.0)
2335 .backdrop_effect(
2336 &light_colors(),
2337 3.0,
2338 GlassDynamics {
2339 edge_return_depth_dp: depth,
2340 ..GlassDynamics::default()
2341 },
2342 ),
2343 );
2344 let valid = depth.filter(|value| value.is_finite() && *value >= 0.0);
2345 assert_eq!(
2346 shader
2347 .uniforms()
2348 .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM)
2349 .copied()
2350 .unwrap_or(0.0),
2351 valid.unwrap_or(0.0)
2352 );
2353 assert_eq!(
2354 shader
2355 .uniforms()
2356 .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM + 1)
2357 .copied()
2358 .unwrap_or(0.0),
2359 f32::from(valid.is_some())
2360 );
2361 assert_eq!(
2362 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2363 36.0
2364 );
2365 }
2366 }
2367
2368 #[test]
2369 fn inner_shadow_validates_and_resolves_its_gaussian_profile() {
2370 let shadow = GlassShadow::new(Color::BLACK.with_alpha(0.12), 3.0, 7.0, 0.0);
2371 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2372 assert_eq!(glass.inner_shadow, None);
2373 let material = glass.inner_shadow(shadow);
2374 assert_eq!(material.inner_shadow, Some(shadow));
2375 let shader = terminal_shader(material.backdrop_effect(
2376 &light_colors(),
2377 3.0,
2378 GlassDynamics::default(),
2379 ));
2380 assert_eq!(
2381 &shader.uniforms()[GLASS_INNER_SHADOW_UNIFORM..GLASS_INNER_SHADOW_UNIFORM + 8],
2382 &shadow.uniforms().unwrap()
2383 );
2384 }
2385 for alpha in [0.0, 1.0] {
2386 assert!(
2387 Glass::lens()
2388 .inner_shadow(GlassShadow::new(
2389 Color::BLACK.with_alpha(alpha),
2390 0.0,
2391 -2.0,
2392 -3.0
2393 ))
2394 .inner_shadow
2395 .is_some()
2396 );
2397 }
2398 for index in 0..7 {
2399 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2400 let mut values = shadow.uniforms().unwrap();
2401 values[index] = value;
2402 let invalid = GlassShadow {
2403 color: Color(values[0], values[1], values[2], values[3]),
2404 radius: values[4],
2405 offset_y: values[5],
2406 spread: values[6],
2407 };
2408 assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2409 let material = Glass {
2410 inner_shadow: Some(invalid),
2411 ..Glass::lens()
2412 };
2413 let shader = terminal_shader(material.backdrop_effect(
2414 &light_colors(),
2415 3.0,
2416 GlassDynamics::default(),
2417 ));
2418 assert_eq!(
2419 shader
2420 .uniforms()
2421 .get(GLASS_INNER_SHADOW_UNIFORM + 7)
2422 .copied()
2423 .unwrap_or(0.0),
2424 0.0
2425 );
2426 }
2427 }
2428 for invalid in [
2429 GlassShadow {
2430 radius: -1.0,
2431 ..shadow
2432 },
2433 GlassShadow {
2434 color: Color(0.0, 0.0, 0.0, -0.1),
2435 ..shadow
2436 },
2437 GlassShadow {
2438 color: Color(0.0, 0.0, 0.0, 1.1),
2439 ..shadow
2440 },
2441 ] {
2442 assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2443 }
2444 }
2445
2446 #[test]
2447 fn adaptive_tone_resolves_foreground_and_declares_the_shared_mean() {
2448 use cranpose_ui_graphics::SubstrateSpec;
2449 for foreground in [Color::BLACK, Color::WHITE, Color(f32::NAN, 0.0, 0.0, 1.0)] {
2450 let material = Glass::clear().adaptive_tone(foreground);
2451 assert!(material.adaptive_tone);
2452 let resolved = material.resolve(&light_colors());
2453 assert!(resolved.foreground_luma.is_finite());
2454 let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2455 assert_eq!(shader.uniforms()[GLASS_ADAPTIVE_TONE_UNIFORM], 1.0);
2456 assert_eq!(shader.substrates(), &[SubstrateSpec::Mean]);
2457 let expected = if foreground.r().is_finite() {
2458 foreground.r()
2459 } else {
2460 0.2126 * light_colors().label.r()
2461 + 0.7152 * light_colors().label.g()
2462 + 0.0722 * light_colors().label.b()
2463 };
2464 assert!((resolved.foreground_luma - expected).abs() < 0.0001);
2465 }
2466 assert!(!Glass::clear().adaptive_tone);
2467 assert!(!Glass::regular().adaptive_tone);
2468 }
2469
2470 #[test]
2471 fn face_response_preserves_valid_ranges_and_rejects_invalid_values() {
2472 let response = GlassFaceResponse {
2473 gain: 0.97,
2474 start_dp: 1.0 / 3.0,
2475 end_dp: 2.0 / 3.0,
2476 illumination: 0.05,
2477 };
2478 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2479 assert!(glass.face_response.is_none());
2480 let material = glass.face_response(response);
2481 assert_eq!(material.face_response, Some(response));
2482 let shader = terminal_shader(material.backdrop_effect(
2483 &light_colors(),
2484 3.0,
2485 GlassDynamics::default(),
2486 ));
2487 assert_eq!(
2488 &shader.uniforms()[GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4],
2489 &response.uniforms().unwrap()
2490 );
2491 }
2492 for gain in [0.0, 1.0] {
2493 assert!(
2494 GlassFaceResponse {
2495 gain,
2496 start_dp: 0.0,
2497 end_dp: 0.0,
2498 ..response
2499 }
2500 .uniforms()
2501 .is_some()
2502 );
2503 }
2504 let mut invalid = vec![
2505 GlassFaceResponse {
2506 gain: -0.1,
2507 ..response
2508 },
2509 GlassFaceResponse {
2510 gain: 1.1,
2511 ..response
2512 },
2513 GlassFaceResponse {
2514 start_dp: -1.0,
2515 ..response
2516 },
2517 GlassFaceResponse {
2518 end_dp: 0.0,
2519 ..response
2520 },
2521 ];
2522 for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2523 invalid.extend([
2524 GlassFaceResponse {
2525 gain: value,
2526 ..response
2527 },
2528 GlassFaceResponse {
2529 start_dp: value,
2530 ..response
2531 },
2532 GlassFaceResponse {
2533 end_dp: value,
2534 ..response
2535 },
2536 GlassFaceResponse {
2537 illumination: value,
2538 ..response
2539 },
2540 ]);
2541 }
2542 for response in invalid {
2543 assert!(response.uniforms().is_none());
2544 let shader = terminal_shader(Glass::clear().face_response(response).backdrop_effect(
2545 &light_colors(),
2546 3.0,
2547 GlassDynamics::default(),
2548 ));
2549 assert!(
2550 shader
2551 .uniforms()
2552 .get(GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4)
2553 .is_none_or(|values| values.iter().all(|value| *value == 0.0))
2554 );
2555 }
2556 }
2557
2558 #[test]
2559 fn face_lighting_is_explicit_and_preserves_each_material_default() {
2560 for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2561 assert!(glass.face_lighting);
2562 for enabled in [false, true] {
2563 let material = glass.clone().face_lighting(enabled);
2564 assert_eq!(material.face_lighting, enabled);
2565 let resolved = material.resolve(&light_colors());
2566 assert_eq!(resolved.face_lighting, enabled);
2567 let shader =
2568 terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2569 assert_eq!(
2570 shader.uniforms()[GLASS_FACE_LIGHTING_OFF_UNIFORM],
2571 f32::from(!enabled)
2572 );
2573 }
2574 }
2575 }
2576
2577 #[test]
2578 fn layer_clipping_has_one_owner_for_material_coverage() {
2579 for clipped in [false, true] {
2580 let mut glass = Glass::regular();
2581 if !clipped {
2582 glass = glass.no_clip();
2583 }
2584 let shader = terminal_shader(glass.backdrop_effect(
2585 &light_colors(),
2586 3.0,
2587 GlassDynamics::default(),
2588 ));
2589 assert_eq!(
2590 shader.uniforms()[GLASS_LAYER_CLIPPED_UNIFORM],
2591 f32::from(clipped)
2592 );
2593 }
2594 }
2595
2596 #[test]
2597 fn touch_radius_preserves_valid_values_and_normalizes_invalid_values() {
2598 for radius in [
2599 None,
2600 Some(-1.0),
2601 Some(0.0),
2602 Some(f32::NAN),
2603 Some(f32::INFINITY),
2604 Some(f32::NEG_INFINITY),
2605 Some(0.5),
2606 Some(144.0),
2607 ] {
2608 let expected = radius
2609 .filter(|value| value.is_finite() && *value > 0.0)
2610 .unwrap_or(58.0);
2611 for density in [1.0, 3.0] {
2612 let shader = terminal_shader(Glass::regular().backdrop_effect(
2613 &light_colors(),
2614 density,
2615 GlassDynamics {
2616 touch_radius_dp: radius,
2617 ..Default::default()
2618 },
2619 ));
2620 assert_eq!(shader.uniforms()[GLASS_TOUCH_RADIUS_UNIFORM], expected);
2621 }
2622 }
2623 }
2624
2625 #[test]
2626 fn material_instances_share_source_without_sharing_uniforms_or_overrides() {
2627 let resolved = Glass::regular().resolve(&light_colors());
2628 let mut first = terminal_shader(resolved.backdrop_effect(2.0, GlassDynamics::default()));
2629 let untouched = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2630 let unused_slot = RuntimeShader::MAX_USER_UNIFORMS - 1;
2631 first.set_float(unused_slot, 17.0);
2632 first.set_override("INSTANCE_ONLY", 1.0);
2633 let second = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2634 assert_eq!(second.uniforms(), untouched.uniforms());
2635 assert_eq!(second.overrides(), untouched.overrides());
2636 assert_eq!(
2637 second.uniforms().get(unused_slot).copied().unwrap_or(0.0),
2638 0.0
2639 );
2640 assert!(
2641 !second
2642 .overrides()
2643 .iter()
2644 .any(|(name, _)| *name == "INSTANCE_ONLY")
2645 );
2646 assert_eq!(first.uniforms()[unused_slot], 17.0);
2647 assert_eq!(first.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2648 assert_eq!(second.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
2649 assert_eq!(first.source().as_ptr(), second.source().as_ptr());
2650 }
2651
2652 #[test]
2653 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
2654 let resolved = Glass::lens()
2655 .refraction_depth(0.72)
2656 .refraction_depth_dp(21.0)
2657 .refraction_curve(0.8)
2658 .dispersion(0.42)
2659 .transmission_refraction(0.35)
2660 .meniscus_absorption(0.3)
2661 .blur_radius(3.0)
2662 .tint(Color::BLACK.with_alpha(0.8))
2663 .resolve(&light_colors());
2664 let effect = resolved.backdrop_effect(
2665 2.0,
2666 GlassDynamics {
2667 highlight_boost: 0.2,
2668 saturation_boost: 0.35,
2669 tint_alpha_multiplier: Some(0.25),
2670 ..Default::default()
2671 },
2672 );
2673 let RenderEffect::Chain { first, .. } = &effect else {
2674 panic!("macroscopic frost must precede the wcKSRD optical pass");
2675 };
2676 assert!(matches!(
2677 first.as_ref(),
2678 RenderEffect::Blur {
2679 radius_x: 6.0,
2680 radius_y: 6.0,
2681 ..
2682 }
2683 ));
2684 let shader = terminal_shader(effect);
2685 assert_eq!(shader.uniforms()[9], 0.72);
2686 assert_eq!(
2687 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2688 21.0
2689 );
2690 assert_eq!(
2691 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2692 1.0
2693 );
2694 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2695 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
2696 assert_eq!(
2697 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
2698 0.35
2699 );
2700 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
2701 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
2702 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
2703 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
2704 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2705 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2706 }
2707
2708 #[test]
2709 fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
2710 let effect = Glass::regular()
2711 .resolve(&light_colors())
2712 .backdrop_effect(3.0, GlassDynamics::default());
2713 let RenderEffect::Chain { first, .. } = &effect else {
2714 panic!("heavy blur must ride the separable Gaussian pre-pass");
2715 };
2716 assert!(matches!(
2717 first.as_ref(),
2718 RenderEffect::Blur {
2719 radius_x: 24.0,
2720 radius_y: 24.0,
2721 ..
2722 }
2723 ));
2724 assert_eq!(
2725 terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
2726 0.0
2727 );
2728 }
2729
2730 #[test]
2731 fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
2732 let RenderEffect::Shader { shader } = Glass::regular()
2733 .blur_radius(0.5)
2734 .resolve(&light_colors())
2735 .backdrop_effect(3.0, GlassDynamics::default())
2736 else {
2737 panic!("a sub-cap blur must not build a Gaussian chain");
2738 };
2739 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
2740 }
2741
2742 #[test]
2743 fn raised_tint_density_stays_premultiplied_alpha_safe() {
2744 let effect = Glass::lens()
2745 .tint(Color::WHITE.with_alpha(0.8))
2746 .resolve(&light_colors())
2747 .backdrop_effect(
2748 1.0,
2749 GlassDynamics {
2750 tint_alpha_multiplier: Some(2.0),
2751 ..Default::default()
2752 },
2753 );
2754 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
2755 }
2756
2757 #[test]
2758 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
2759 let resolved = Glass::lens()
2760 .refraction_depth(0.72)
2761 .refraction_curve(0.8)
2762 .dispersion(0.42)
2763 .blur_radius(3.0)
2764 .saturation(1.4)
2765 .highlight(0.6)
2766 .lift(0.2)
2767 .tint(Color::BLACK.with_alpha(0.8))
2768 .no_clip()
2769 .resolve(&light_colors());
2770 let morph = GlassMorph {
2771 node_size: (120.0, 72.0),
2772 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2773 wobble_amplitude: 3.0,
2774 bulge_amplitude: 4.0,
2775 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
2776 ..Default::default()
2777 };
2778
2779 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
2780 2.0,
2781 GlassDynamics {
2782 activity: Some(0.0),
2783 morph: Some(morph),
2784 ..Default::default()
2785 },
2786 ) else {
2787 panic!("material must resolve to the shared wcKSRD shader");
2788 };
2789 let uniforms = shader.uniforms();
2790 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
2791 assert_eq!(uniforms[9], 0.0);
2792 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
2793 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
2794 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
2795 assert_eq!(uniforms[11], 0.0);
2796 assert_eq!(uniforms[17], 0.0);
2797 assert_eq!(uniforms[18], 1.0);
2798 assert_eq!(uniforms[20], 0.0);
2799 assert_eq!(uniforms[21], 0.0);
2800 assert_eq!(uniforms[24], 1.0);
2801 assert_eq!(uniforms[28], 0.0);
2802 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2803 assert_eq!(uniforms[91], 0.0);
2804 assert_eq!(uniforms[102], 0.0);
2805 assert_eq!(
2806 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2807 &[0.0, 0.0, 0.0, 0.0]
2808 );
2809 assert_eq!(uniforms[32], 0.0);
2810 assert_eq!(uniforms[26], 0.0);
2811 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
2812 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
2813 }
2814
2815 #[test]
2816 fn surface_refraction_is_explicit_and_reaches_the_shader() {
2817 for glass in [Glass::regular(), Glass::lens()] {
2818 assert_eq!(glass.refraction, GlassRefraction::Radial);
2819 let surface = glass.surface_refraction(31.0);
2820 assert_eq!(
2821 surface.refraction,
2822 GlassRefraction::Surface { reach_dp: 31.0 }
2823 );
2824 let shader = terminal_shader(surface.backdrop_effect(
2825 &light_colors(),
2826 3.0,
2827 GlassDynamics::default(),
2828 ));
2829 assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 1.0);
2830 assert_eq!(shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM], 31.0);
2831 assert!(
2832 !shader
2833 .overrides()
2834 .iter()
2835 .any(|(name, value)| *name == "GLASS_DIRECTIONAL_REFRACTION_OFF"
2836 && *value != 0.0)
2837 );
2838 }
2839 }
2840
2841 #[test]
2842 fn edge_refraction_preserves_face_zoom_and_scales_outward_reach_with_contact() {
2843 assert_eq!(GlassRefraction::default(), GlassRefraction::Radial);
2844 let glass = Glass::lens()
2845 .surface_refraction(31.0)
2846 .edge_refraction(12.0)
2847 .optical_zoom(1.2);
2848 assert_eq!(
2849 glass.refraction,
2850 GlassRefraction::EdgeLens { reach_dp: 12.0 }
2851 );
2852 for (activity, press, reach) in [(1.0, 1.0, 12.0), (0.5, 0.5, 3.0), (0.0, 1.0, 0.0)] {
2853 let shader = terminal_shader(glass.backdrop_effect(
2854 &light_colors(),
2855 3.0,
2856 GlassDynamics {
2857 activity: Some(activity),
2858 press_depth: Some(press),
2859 ..Default::default()
2860 },
2861 ));
2862 assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 2.0);
2863 assert_eq!(
2864 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2865 reach
2866 );
2867 assert_eq!(
2868 shader.uniforms()[GLASS_OPTICAL_ZOOM_UNIFORM],
2869 1.0 + 0.2 * activity
2870 );
2871 }
2872 assert_eq!(
2873 glass.surface_refraction(31.0).refraction,
2874 GlassRefraction::Surface { reach_dp: 31.0 }
2875 );
2876 }
2877
2878 #[test]
2879 fn edge_refraction_captures_the_farthest_spectral_ray() {
2880 for (reach, dispersion, required) in
2881 [(20.0, 1.0, 101.67), (0.0, 1.0, 42.78), (20.0, 0.0, 60.88)]
2882 {
2883 let shader = terminal_shader(
2884 Glass::lens()
2885 .edge_refraction(reach)
2886 .dispersion(dispersion)
2887 .backdrop_effect(&light_colors(), 3.0, GlassDynamics::default()),
2888 );
2889 assert!(shader.input_padding() >= required);
2890 }
2891 }
2892
2893 #[test]
2894 fn directional_refraction_rejects_invalid_reach_at_both_public_entry_points() {
2895 for input in [-1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, 12.0] {
2896 let expected = if input.is_finite() {
2897 input.max(0.0)
2898 } else {
2899 0.0
2900 };
2901 for (built, direct, normalized) in [
2902 (
2903 Glass::lens().edge_refraction(input),
2904 GlassRefraction::EdgeLens { reach_dp: input },
2905 GlassRefraction::EdgeLens { reach_dp: expected },
2906 ),
2907 (
2908 Glass::regular().surface_refraction(input),
2909 GlassRefraction::Surface { reach_dp: input },
2910 GlassRefraction::Surface { reach_dp: expected },
2911 ),
2912 ] {
2913 assert_eq!(built.refraction, normalized);
2914 let direct = Glass {
2915 refraction: direct,
2916 ..built
2917 };
2918 let shader = terminal_shader(direct.backdrop_effect(
2919 &light_colors(),
2920 1.0,
2921 GlassDynamics::default(),
2922 ));
2923 assert_eq!(
2924 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2925 expected
2926 );
2927 }
2928 }
2929 }
2930
2931 #[test]
2932 fn surface_reach_is_independent_of_depth_and_tracks_contact() {
2933 for depth in [4.0, 15.5, 30.0] {
2934 for (activity, press, reach) in [(1.0, 1.0, 31.0), (0.5, 0.5, 7.75), (0.0, 1.0, 0.0)] {
2935 let shader = terminal_shader(
2936 Glass::regular()
2937 .surface_refraction(31.0)
2938 .refraction_depth_dp(depth)
2939 .backdrop_effect(
2940 &light_colors(),
2941 3.0,
2942 GlassDynamics {
2943 activity: Some(activity),
2944 press_depth: Some(press),
2945 ..Default::default()
2946 },
2947 ),
2948 );
2949 assert_eq!(
2950 shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2951 reach
2952 );
2953 assert_eq!(
2954 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2955 depth * activity * press
2956 );
2957 assert!(shader.input_padding() >= 31.0);
2958 }
2959 }
2960 }
2961
2962 #[test]
2963 fn resting_surface_tint_survives_zero_optical_activity() {
2964 let tint = Color::BLACK.with_alpha(0.11);
2965 let RenderEffect::Shader { shader } = Glass::lens()
2966 .no_clip()
2967 .resolve(&light_colors())
2968 .backdrop_effect(
2969 1.0,
2970 GlassDynamics {
2971 activity: Some(0.0),
2972 resting_tint: Some(tint),
2973 ..Default::default()
2974 },
2975 )
2976 else {
2977 panic!("resting surface must use the shared wcKSRD shader");
2978 };
2979 assert_eq!(
2980 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2981 &[tint.r(), tint.g(), tint.b(), tint.a()]
2982 );
2983 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
2984 }
2985
2986 #[test]
2987 fn full_optical_activity_preserves_the_resolved_material() {
2988 let resolved = Glass::lens()
2989 .refraction_depth(0.72)
2990 .refraction_curve(0.8)
2991 .dispersion(0.42)
2992 .blur_radius(3.0)
2993 .resolve(&light_colors());
2994 let shader = terminal_shader(resolved.backdrop_effect(
2995 2.0,
2996 GlassDynamics {
2997 activity: Some(1.0),
2998 ..Default::default()
2999 },
3000 ));
3001 let uniforms = shader.uniforms();
3002 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
3003 assert_eq!(uniforms[9], 0.72);
3004 assert_eq!(
3005 uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
3006 0.0
3007 );
3008 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
3009 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
3010 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
3011 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
3012 }
3013
3014 #[test]
3015 fn morph_geometry_and_incompressible_strain_are_packed() {
3016 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
3017 let morph = GlassMorph {
3018 node_size: (78.0, 59.0),
3019 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
3020 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
3021 glue: 8.0,
3022 wobble_amplitude: 1.0,
3023 wobble_phase: 0.5,
3024 bulge_amplitude: 2.0,
3025 bulge_direction: 0.25,
3026 ellipse_blend: 0.3,
3027 capsule_smoothing_dp: 4.0,
3028 deformation: Some(deformation),
3029 zoom_anchor: (0.0, 0.0),
3030 };
3031 let RenderEffect::Shader { shader } = Glass::lens()
3032 .no_clip()
3033 .resolve(&light_colors())
3034 .backdrop_effect(
3035 1.0,
3036 GlassDynamics {
3037 morph: Some(morph),
3038 ..Default::default()
3039 },
3040 )
3041 else {
3042 panic!("morph must use the shared wcKSRD shader");
3043 };
3044 let uniforms = shader.uniforms();
3045 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
3046 assert_eq!(uniforms[30], 1.0);
3047 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
3048 assert_eq!(uniforms[110], 0.3);
3049 assert_eq!(
3050 uniforms[cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3051 4.0
3052 );
3053 assert!(shader.output_padding() > 0.0);
3054 }
3055
3056 #[test]
3057 fn capsule_smoothing_is_finite_nonnegative_and_invalidates_the_morph() {
3058 for value in [-1.0, 0.0, 4.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
3059 let morph = GlassMorph {
3060 node_size: (120.0, 80.0),
3061 primary: (60.0, 40.0, 111.0, 70.0, -1.0),
3062 capsule_smoothing_dp: value,
3063 ..Default::default()
3064 };
3065 let expected = if value.is_finite() {
3066 value.max(0.0)
3067 } else {
3068 0.0
3069 };
3070 let shader = terminal_shader(Glass::lens().backdrop_effect(
3071 &light_colors(),
3072 3.0,
3073 GlassDynamics {
3074 morph: Some(morph.clone()),
3075 ..Default::default()
3076 },
3077 ));
3078 assert_eq!(
3079 shader.uniforms()[cranpose_ui_graphics::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3080 expected
3081 );
3082 assert!(glass_morphs_match(&morph, &morph));
3083 let changed = GlassMorph {
3084 capsule_smoothing_dp: 5.0,
3085 ..morph.clone()
3086 };
3087 assert!(!glass_morphs_match(&morph, &changed));
3088 }
3089 }
3090
3091 #[test]
3092 fn optical_builders_hold_their_own_legal_range() {
3093 let glass = Glass::regular()
3094 .fold_depth(-4.0)
3095 .optical_zoom(0.5)
3096 .rim_reflection(9.0);
3097 assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
3098 assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
3099 assert_eq!(
3100 glass.rim_reflection, 2.0,
3101 "the rim tops out at twice the line"
3102 );
3103
3104 let glass = Glass::regular()
3105 .fold_depth(6.0)
3106 .optical_zoom(1.4)
3107 .rim_reflection(0.25);
3108 assert_eq!(glass.fold_depth, 6.0);
3109 assert_eq!(glass.optical_zoom, 1.4);
3110 assert_eq!(glass.rim_reflection, 0.25);
3111 }
3112
3113 #[test]
3114 fn ink_recolor_records_its_color_at_a_clamped_strength() {
3115 let red = Color::from_rgb_u8(255, 0, 0);
3116 let glass = Glass::regular().ink_recolor(red, 3.0);
3117 assert_eq!(glass.ink_recolor, Some((red, 1.0)));
3118
3119 let glass = Glass::regular().ink_recolor(red, -1.0);
3120 assert_eq!(glass.ink_recolor, Some((red, 0.0)));
3121 assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
3122 }
3123
3124 #[test]
3125 fn a_touched_up_surface_amplifies_what_it_already_is() {
3126 let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
3127 assert!(
3128 dynamics.highlight_boost > 0.0,
3129 "a pressed surface goes brighter"
3130 );
3131 assert!(dynamics.saturation_boost > 0.0, "and more saturated");
3132 assert_eq!(
3133 dynamics.touch,
3134 Some((10.0, 4.0, 1.0)),
3135 "with no finger the glow sits at the surface's own heart"
3136 );
3137 assert_eq!(
3138 dynamics.resting_tint, None,
3139 "a touch never repaints the surface with another color"
3140 );
3141 }
3142
3143 #[test]
3144 fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
3145 let dynamics = GlassDynamics::default()
3146 .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
3147 .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
3148 let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
3149 assert_eq!(
3150 (x, y),
3151 (3.0, 7.0),
3152 "the glow tracks the finger, not the center"
3153 );
3154 assert_eq!(intensity, 1.0, "carried press saturates at full");
3155 }
3156
3157 #[test]
3158 fn an_unpressed_surface_is_left_exactly_as_it_was() {
3159 let resting = GlassDynamics::default();
3160 let untouched = resting
3161 .clone()
3162 .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
3163 assert_eq!(untouched.highlight_boost, resting.highlight_boost);
3164 assert_eq!(untouched.saturation_boost, resting.saturation_boost);
3165 assert_eq!(untouched.touch, None, "no press, no glow");
3166 }
3167
3168 #[test]
3169 fn an_off_axis_strain_widens_the_support_by_the_affine_rows_times_the_half_extents() {
3170 let resolved = Glass::regular()
3171 .shape(LiquidShape::RoundedRect(0.0))
3172 .blur_radius(0.0)
3173 .shadow(false)
3174 .no_clip()
3175 .resolve(&light_colors());
3176 let angle = 22.5f32.to_radians();
3177 let morph = GlassMorph {
3178 node_size: (600.0, 600.0),
3179 primary: (300.0, 300.0, 200.0, 200.0, 0.0),
3180 deformation: Some(GlassDeformation::incompressible(
3181 (angle.cos(), angle.sin()),
3182 2.0,
3183 )),
3184 ..Default::default()
3185 };
3186 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
3187 1.0,
3188 GlassDynamics {
3189 activity: Some(1.0),
3190 morph: Some(morph),
3191 ..Default::default()
3192 },
3193 ) else {
3194 panic!("an unblurred glass is one shader");
3195 };
3196 let support = shader
3197 .output_support()
3198 .expect("a morphing glass declares its support");
3199 let half_x = 100.0 * (2.0 * angle.cos().powi(2) + 0.5 * angle.sin().powi(2))
3200 + 100.0 * (1.5 * angle.sin() * angle.cos());
3201 assert!(
3202 (half_x - 231.066).abs() < 0.01,
3203 "the example's half extent is {half_x}"
3204 );
3205 assert!(
3206 support.x <= 300.0 - half_x && support.x + support.width >= 300.0 + half_x,
3207 "the support {support:?} must hold the strained square's x extent {half_x}"
3208 );
3209 let half_y = 100.0 * (1.5 * angle.sin() * angle.cos())
3210 + 100.0 * (2.0 * angle.sin().powi(2) + 0.5 * angle.cos().powi(2));
3211 assert!(
3212 (half_y - 125.0).abs() < 0.01,
3213 "the example's y half extent is {half_y}"
3214 );
3215 assert!(
3216 support.y <= 300.0 - half_y && support.y + support.height >= 300.0 + half_y,
3217 "the support {support:?} must hold the strained square's y extent {half_y}"
3218 );
3219 }
3220
3221 #[test]
3222 fn a_morphing_glass_declares_its_output_support_around_its_shapes_and_their_reach() {
3223 let resolved = Glass::regular()
3224 .shape(LiquidShape::RoundedRect(12.0))
3225 .blur_radius(0.0)
3226 .shadow(true)
3227 .no_clip()
3228 .resolve(&light_colors());
3229 let morph = GlassMorph {
3230 node_size: (280.0, 160.0),
3231 primary: (140.0, 80.0, 120.0, 40.0, -1.0),
3232 shapes: vec![(230.0, 80.0, 40.0, 40.0, -1.0)],
3233 glue: 20.0,
3234 wobble_amplitude: 3.0,
3235 bulge_amplitude: 8.0,
3236 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
3237 ..Default::default()
3238 };
3239 let dynamics = GlassDynamics {
3240 activity: Some(1.0),
3241 morph: Some(morph.clone()),
3242 ..Default::default()
3243 };
3244 let RenderEffect::Shader { shader } = resolved.backdrop_effect(2.0, dynamics) else {
3245 panic!("an unblurred glass is one shader");
3246 };
3247 let support = shader
3248 .output_support()
3249 .expect("a morphing glass declares its support");
3250 let shape_reach = (230.0 + 20.0) - (140.0 + 60.0);
3251 let shadow_reach = resolved.shadow_radius
3252 + resolved.shadow_offset_y.abs()
3253 + resolved.shadow_spread.max(0.0);
3254 let field_reach = 3.0 * 2.0 + 8.0 + shape_reach + 40.0 + shadow_reach + 4.0;
3255 let smallest_strain = 0.8;
3256 let reach = field_reach / smallest_strain;
3257 let contains = |x: f32, y: f32| {
3258 support.x <= x
3259 && support.y <= y
3260 && support.x + support.width >= x
3261 && support.y + support.height >= y
3262 };
3263 for (cx, cy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
3264 let corner = ((60.0 + reach) * cx, (20.0 + reach) * cy);
3265 let stretched = (140.0 + corner.0 * 1.25, 80.0 + corner.1 * 0.8);
3266 assert!(
3267 contains(stretched.0, stretched.1 - resolved.shadow_offset_y.abs())
3268 && contains(stretched.0, stretched.1 + resolved.shadow_offset_y.abs()),
3269 "{support:?} must hold the strained, dilated corner {stretched:?}"
3270 );
3271 }
3272 assert!(
3273 contains(230.0 + 20.0 + reach, 80.0),
3274 "the glued shape's reach is inside"
3275 );
3276 assert!(shader.output_padding() >= field_reach);
3277
3278 let RenderEffect::Shader { shader } =
3279 resolved.backdrop_effect(2.0, GlassDynamics::default())
3280 else {
3281 panic!("an unblurred glass is one shader");
3282 };
3283 assert_eq!(
3284 shader.output_support(),
3285 None,
3286 "cover-mode glass fills its rect"
3287 );
3288 }
3289}
3290
3291#[cfg(test)]
3292mod transparency_tests {
3293 use super::*;
3294
3295 #[test]
3296 fn less_transparency_makes_glass_a_flat_surface() {
3297 let colors = crate::theme::LiquidColors::light(Color::from_rgb_u8(0, 122, 255));
3298 let flat = Glass::regular()
3299 .resolve(&colors)
3300 .without_transparency(&colors);
3301 assert_eq!(flat.tint, colors.surface);
3302 assert_eq!(flat.blur_radius_dp, 0.0);
3303 assert_eq!(flat.refraction_depth, 0.0);
3304 assert_eq!(flat.saturation, 1.0);
3305 assert!(flat.edge_spectrum.is_none());
3306 let clear = Glass::regular().resolve(&colors);
3307 assert!(clear.blur_radius_dp > 0.0, "plain glass keeps its blur");
3308 }
3309}