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_BLUR_RADIUS_UNIFORM,
14 GLASS_DISPERSION_UNIFORM, GLASS_EFFECT_DENSITY_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM,
15 GLASS_LIGHT_DIRECTION_UNIFORM, GLASS_MENISCUS_ABSORPTION_UNIFORM,
16 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, GLASS_OPTICAL_ZOOM_UNIFORM,
17 GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM, GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
18 GLASS_REFRACTION_CURVE_UNIFORM, GLASS_RESTING_TINT_UNIFORM,
19 GLASS_TRANSMISSION_REFRACTION_UNIFORM, GraphicsLayer, LIQUID_GLASS_WGSL, LayerShape, Rect,
20 RenderEffect, RoundedCornerShape, RuntimeShader, TileMode, liquid_glass_runtime_effect,
21 specialize_liquid_glass,
22};
23
24use crate::theme::LiquidColors;
25
26thread_local! {
27 static GLASS_LIGHT_RETURN: Cell<(f32, f32)> = const { Cell::new((0.0, 1.0)) };
28}
29
30pub fn set_glass_light_direction(direction: (f32, f32)) {
33 GLASS_LIGHT_RETURN.with(|cell| cell.set(direction));
34}
35
36pub fn glass_light_direction() -> (f32, f32) {
38 GLASS_LIGHT_RETURN.with(|cell| cell.get())
39}
40
41const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
42
43const CAPSULE_SHADER_RADIUS: f32 = -1.0;
44
45const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
46
47#[derive(Clone, Copy, Debug, PartialEq, Default)]
49pub enum LiquidShape {
50 #[default]
52 Capsule,
53 RoundedRect(f32),
55 Circle,
57}
58
59impl LiquidShape {
60 pub fn clip_shape(&self) -> RoundedCornerShape {
62 match self {
63 LiquidShape::Capsule | LiquidShape::Circle => {
64 RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
65 }
66 LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
67 }
68 }
69
70 pub fn layer_shape(&self) -> LayerShape {
72 LayerShape::Rounded(self.clip_shape())
73 }
74
75 fn shader_radius_px(&self, density: f32) -> f32 {
76 match self {
77 LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
78 LiquidShape::RoundedRect(radius) => radius * density,
79 }
80 }
81}
82
83#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
86pub enum GlassVariant {
87 #[default]
89 Regular,
90 Clear,
92 Lens,
96}
97
98#[derive(Clone, Copy, Debug, PartialEq)]
102pub struct GlassShadow {
103 pub color: Color,
104 pub radius: f32,
105 pub offset_y: f32,
106 pub spread: f32,
107}
108
109impl GlassShadow {
110 pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
111 Self {
112 color,
113 radius: radius.max(0.0),
114 offset_y,
115 spread,
116 }
117 }
118}
119
120#[derive(Clone, Debug, PartialEq, Default)]
123pub struct GlassDynamics {
124 pub activity: Option<f32>,
128 pub resting_tint: Option<Color>,
131 pub highlight_boost: f32,
133 pub saturation_boost: f32,
136 pub tint_alpha_multiplier: Option<f32>,
139 pub morph: Option<GlassMorph>,
142 pub touch: Option<(f32, f32, f32)>,
146 pub press_depth: Option<f32>,
152}
153
154impl GlassDynamics {
155 pub fn touched_up(
162 mut self,
163 press: f32,
164 finger: Option<(f32, f32)>,
165 center: (f32, f32),
166 ) -> Self {
167 let press = press.clamp(0.0, 1.0);
168 if press <= 0.0 {
169 return self;
170 }
171 self.highlight_boost += 0.85 * press;
172 self.saturation_boost += 0.45 * press;
173 let (fx, fy) = finger.unwrap_or(center);
174 let carried = self.touch.map(|(_, _, i)| i).unwrap_or(0.0);
175 self.touch = Some((fx, fy, (carried + press).clamp(0.0, 1.0)));
176 self
177 }
178}
179
180fn foreground_is_dark(foreground: Color) -> bool {
181 let foreground_luma =
182 0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
183 foreground_luma < 0.5
184}
185
186fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
187 if boost.abs() <= f32::EPSILON {
188 return tint;
189 }
190 let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
191 let saturation = (1.0 + boost).max(0.0);
192 Color::rgba(
193 (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
194 (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
195 (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
196 tint.a(),
197 )
198}
199
200pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
201 if foreground_is_dark(foreground) {
202 Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
203 } else {
204 Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
205 }
206}
207
208pub(crate) fn neutral_surface_lift(foreground: Color, light_lift: f32, dark_lift: f32) -> f32 {
209 if foreground_is_dark(foreground) {
210 light_lift
211 } else {
212 dark_lift
213 }
214}
215
216#[derive(Clone, Debug, PartialEq, Default)]
227pub struct GlassMorph {
228 pub node_size: (f32, f32),
234 pub primary: (f32, f32, f32, f32, f32),
235 pub shapes: Vec<(f32, f32, f32, f32, f32)>,
237 pub glue: f32,
239 pub wobble_amplitude: f32,
241 pub wobble_phase: f32,
242 pub bulge_amplitude: f32,
246 pub bulge_direction: f32,
247 pub ellipse_blend: f32,
251 pub deformation: Option<GlassDeformation>,
255 pub zoom_anchor: (f32, f32),
261}
262
263#[derive(Clone, Copy, Debug, PartialEq)]
267pub struct GlassDeformation {
268 axis: (f32, f32),
269 along: f32,
270}
271
272impl GlassDeformation {
273 pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
274 let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
275 let axis = if length > f32::EPSILON {
276 (axis.0 / length, axis.1 / length)
277 } else {
278 (1.0, 0.0)
279 };
280 Self {
281 axis,
282 along: along.max(f32::EPSILON),
283 }
284 }
285
286 pub fn axis(self) -> (f32, f32) {
287 self.axis
288 }
289
290 pub fn along(self) -> f32 {
291 self.along
292 }
293
294 pub fn across(self) -> f32 {
295 1.0 / self.along
296 }
297}
298
299impl GlassMorph {
300 pub const MAX_SHAPES: usize = 8;
302}
303
304#[derive(Clone, Debug, PartialEq)]
307pub struct Glass {
308 pub variant: GlassVariant,
309 pub shape: LiquidShape,
310 pub tint: Option<Color>,
312 pub blur_radius: Option<f32>,
314 pub saturation: Option<f32>,
316 pub refraction_depth: f32,
320 pub refraction_depth_dp: Option<f32>,
324 pub refraction_curve: f32,
326 pub dispersion: f32,
328 pub transmission_refraction: f32,
332 pub meniscus_absorption: f32,
335 pub fold_depth: f32,
339 pub optical_zoom: f32,
343 pub rim_reflection: f32,
346 pub ink_recolor: Option<(Color, f32)>,
350 pub highlight: f32,
352 pub lift: Option<f32>,
355 pub contrast: Option<f32>,
360 pub shadow: bool,
362 pub shadow_style: Option<GlassShadow>,
364 pub clip: bool,
367 pub foreground: Option<Color>,
370 pub adaptive_frost: f32,
372}
373
374impl Glass {
375 pub fn regular() -> Self {
377 Self {
378 variant: GlassVariant::Regular,
379 shape: LiquidShape::Capsule,
380 tint: None,
381 blur_radius: None,
382 saturation: None,
383 refraction_depth: 0.0,
384 refraction_depth_dp: None,
385 refraction_curve: 0.25,
386 dispersion: 0.0,
387 transmission_refraction: 0.0,
388 meniscus_absorption: 1.0,
389 fold_depth: 0.0,
390 optical_zoom: 1.0,
391 rim_reflection: 1.0,
392 ink_recolor: None,
393 highlight: 0.9,
394 lift: None,
395 contrast: None,
396 shadow: true,
397 shadow_style: None,
398 clip: true,
399 foreground: None,
400 adaptive_frost: 0.65,
401 }
402 }
403
404 pub fn clear() -> Self {
405 Self {
406 variant: GlassVariant::Clear,
407 ..Self::regular()
408 }
409 }
410
411 pub fn lens() -> Self {
415 Self {
416 variant: GlassVariant::Lens,
417 shape: LiquidShape::Capsule,
418 tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
419 blur_radius: None,
420 saturation: None,
421 refraction_depth: 0.34,
422 refraction_depth_dp: None,
423 refraction_curve: 1.0,
424 dispersion: 0.30,
425 transmission_refraction: 1.0,
426 meniscus_absorption: 1.0,
427 fold_depth: 0.0,
428 optical_zoom: 1.0,
429 rim_reflection: 1.0,
430 ink_recolor: None,
431 highlight: 1.15,
432 lift: None,
433 contrast: None,
434 shadow: true,
435 shadow_style: None,
436 clip: true,
437 foreground: None,
438 adaptive_frost: 0.0,
439 }
440 }
441
442 pub fn shape(mut self, shape: LiquidShape) -> Self {
443 self.shape = shape;
444 self
445 }
446
447 pub fn tint(mut self, tint: Color) -> Self {
448 self.tint = Some(tint);
449 self
450 }
451
452 pub fn blur_radius(mut self, radius_dp: f32) -> Self {
453 self.blur_radius = Some(radius_dp);
454 self
455 }
456
457 pub fn saturation(mut self, saturation: f32) -> Self {
458 self.saturation = Some(saturation);
459 self
460 }
461
462 pub fn refraction_depth(mut self, fraction: f32) -> Self {
463 self.refraction_depth = fraction.clamp(0.0, 2.0);
464 self.refraction_depth_dp = None;
465 self
466 }
467
468 pub fn refraction_depth_dp(mut self, depth_dp: f32) -> Self {
471 self.refraction_depth_dp = Some(depth_dp.max(0.0));
472 self
473 }
474
475 pub fn refraction_curve(mut self, curve: f32) -> Self {
476 self.refraction_curve = curve.clamp(0.05, 1.0);
477 self
478 }
479
480 pub fn dispersion(mut self, strength: f32) -> Self {
481 self.dispersion = strength.clamp(0.0, 1.0);
482 self
483 }
484
485 pub fn transmission_refraction(mut self, strength: f32) -> Self {
486 self.transmission_refraction = strength.clamp(0.0, 1.0);
487 self
488 }
489
490 pub fn meniscus_absorption(mut self, strength: f32) -> Self {
491 self.meniscus_absorption = strength.clamp(0.0, 1.0);
492 self
493 }
494
495 pub fn fold_depth(mut self, depth_dp: f32) -> Self {
497 self.fold_depth = depth_dp.max(0.0);
498 self
499 }
500
501 pub fn optical_zoom(mut self, zoom: f32) -> Self {
503 self.optical_zoom = zoom.max(1.0);
504 self
505 }
506
507 pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
509 self.rim_reflection = reflectivity.clamp(0.0, 2.0);
510 self
511 }
512
513 pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
516 self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
517 self
518 }
519
520 pub fn highlight(mut self, highlight: f32) -> Self {
521 self.highlight = highlight;
522 self
523 }
524
525 pub fn lift(mut self, lift: f32) -> Self {
528 self.lift = Some(lift);
529 self
530 }
531
532 pub fn contrast(mut self, contrast: f32) -> Self {
535 self.contrast = Some(contrast.max(0.05));
536 self
537 }
538
539 pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
540 self.foreground = Some(foreground);
541 self.adaptive_frost = strength.clamp(0.0, 1.0);
542 self
543 }
544
545 pub fn shadow(mut self, shadow: bool) -> Self {
546 self.shadow = shadow;
547 self
548 }
549
550 pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
551 self.shadow_style = Some(shadow);
552 self
553 }
554
555 pub fn no_clip(mut self) -> Self {
558 self.clip = false;
559 self
560 }
561
562 fn default_blur_radius(&self) -> f32 {
563 match self.variant {
564 GlassVariant::Regular => 8.0,
565 GlassVariant::Clear => 3.0,
566 GlassVariant::Lens => 0.0,
567 }
568 }
569
570 fn default_saturation(&self) -> f32 {
571 match self.variant {
572 GlassVariant::Regular => 1.5,
573 GlassVariant::Clear => 1.25,
574 GlassVariant::Lens => 1.0,
575 }
576 }
577
578 pub fn backdrop_effect(
582 &self,
583 colors: &LiquidColors,
584 density: f32,
585 dynamics: GlassDynamics,
586 ) -> RenderEffect {
587 self.resolve(colors).backdrop_effect(density, dynamics)
588 }
589
590 pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
591 let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
592 (GlassVariant::Regular, false) => 0.42,
593 (GlassVariant::Regular, true) => -0.38,
594 (GlassVariant::Clear, false) => 0.12,
595 (GlassVariant::Clear, true) => -0.12,
596 (GlassVariant::Lens, false) => 0.10,
597 (GlassVariant::Lens, true) => -0.08,
598 });
599 let foreground = self.foreground.unwrap_or(colors.label);
600 let shadow = self.shadow_style.unwrap_or_else(|| {
601 GlassShadow::new(
602 Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
603 (GlassVariant::Lens, false) => 0.14,
604 (GlassVariant::Lens, true) => 0.28,
605 (_, false) => 0.16,
606 (_, true) => 0.5,
607 }),
608 if self.variant == GlassVariant::Lens {
609 10.0
610 } else {
611 22.0
612 },
613 if self.variant == GlassVariant::Lens {
614 3.0
615 } else {
616 8.0
617 },
618 if self.variant == GlassVariant::Lens {
619 -6.0
620 } else {
621 -2.0
622 },
623 )
624 });
625 ResolvedGlass {
626 shape: self.shape,
627 tint: self.tint.unwrap_or(colors.glass_tint),
628 blur_radius_dp: self
629 .blur_radius
630 .unwrap_or_else(|| self.default_blur_radius()),
631 saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
632 refraction_depth: self.refraction_depth,
633 refraction_depth_dp: self.refraction_depth_dp,
634 refraction_curve: self.refraction_curve,
635 dispersion: self.dispersion,
636 transmission_refraction: self.transmission_refraction,
637 meniscus_absorption: self.meniscus_absorption,
638 fold_depth: self.fold_depth,
639 optical_zoom: self.optical_zoom,
640 rim_reflection: self.rim_reflection,
641 ink_recolor: self.ink_recolor,
642 highlight: self.highlight,
643 lift,
644 contrast: self.contrast.unwrap_or(match self.variant {
645 GlassVariant::Lens => 1.0,
646 _ => 1.03,
647 }),
648 shadow: self.shadow,
649 clip: self.clip,
650 foreground_luma: 0.2126 * foreground.r()
651 + 0.7152 * foreground.g()
652 + 0.0722 * foreground.b(),
653 adaptive_frost: self.adaptive_frost,
654 rim_style: if self.variant == GlassVariant::Lens {
655 1.0
656 } else {
657 0.0
658 },
659 shadow_color: shadow.color,
660 shadow_radius: shadow.radius,
661 shadow_offset_y: shadow.offset_y,
662 shadow_spread: shadow.spread,
663 }
664 }
665}
666
667impl Default for Glass {
668 fn default() -> Self {
669 Self::regular()
670 }
671}
672
673#[derive(Clone, Debug, PartialEq)]
674pub(crate) struct ResolvedGlass {
675 pub shape: LiquidShape,
676 pub tint: Color,
677 pub blur_radius_dp: f32,
678 pub saturation: f32,
679 pub refraction_depth: f32,
680 pub refraction_depth_dp: Option<f32>,
681 pub refraction_curve: f32,
682 pub dispersion: f32,
683 pub transmission_refraction: f32,
684 pub meniscus_absorption: f32,
685 pub fold_depth: f32,
686 pub optical_zoom: f32,
687 pub rim_reflection: f32,
688 pub ink_recolor: Option<(Color, f32)>,
689 pub highlight: f32,
690 pub lift: f32,
691 pub contrast: f32,
692 pub shadow: bool,
693 pub clip: bool,
694 pub rim_style: f32,
695 pub foreground_luma: f32,
696 pub adaptive_frost: f32,
697 pub shadow_color: Color,
698 pub shadow_radius: f32,
699 pub shadow_offset_y: f32,
700 pub shadow_spread: f32,
701}
702
703impl ResolvedGlass {
704 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
705 self.runtime_effect(density, &dynamics, false)
706 }
707
708 fn runtime_effect(
709 &self,
710 density: f32,
711 dynamics: &GlassDynamics,
712 content_mask: bool,
713 ) -> RenderEffect {
714 let density = density.max(f32::EPSILON);
715 let activity = dynamics
716 .activity
717 .filter(|value| value.is_finite())
718 .unwrap_or(1.0)
719 .clamp(0.0, 1.0);
720 static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
721 let mut shader = SHADER
722 .get_or_init(|| {
723 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
724 shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
725 shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
726 specialize_liquid_glass(&mut shader);
727 shader
728 })
729 .clone();
730 if let Some(morph) = dynamics.morph.as_ref() {
731 set_morph_uniforms(&mut shader, morph, activity);
732 } else {
733 shader.set_float2(0, 0.0, 0.0);
734 shader.set_float(6, self.shape.shader_radius_px(density));
735 }
736 let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
737 shader.set_float(9, self.refraction_depth * activity * press_depth);
738 shader.set_float(
739 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
740 self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
741 );
742 shader.set_float(
743 GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
744 if self.refraction_depth_dp.is_some() {
745 1.0
746 } else {
747 0.0
748 },
749 );
750 shader.set_float(
751 GLASS_REFRACTION_CURVE_UNIFORM,
752 self.refraction_curve * activity,
753 );
754 shader.set_float(
755 GLASS_DISPERSION_UNIFORM,
756 self.dispersion * activity * press_depth,
757 );
758 shader.set_float(
759 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
760 self.transmission_refraction * activity,
761 );
762 shader.set_float(
763 GLASS_MENISCUS_ABSORPTION_UNIFORM,
764 self.meniscus_absorption * press_depth,
765 );
766 shader.set_float(
767 GLASS_FOLD_DEPTH_UNIFORM,
768 self.fold_depth.max(0.0) * press_depth,
769 );
770 shader.set_float(
771 GLASS_OPTICAL_ZOOM_UNIFORM,
772 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
773 );
774 let zoom_anchor = dynamics
775 .morph
776 .as_ref()
777 .map(|morph| morph.zoom_anchor)
778 .unwrap_or((0.0, 0.0));
779 shader.set_float2(
780 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
781 zoom_anchor.0,
782 zoom_anchor.1,
783 );
784 shader.set_float(121, self.rim_reflection.max(0.001));
785 let (ink_color, ink_strength) = self
786 .ink_recolor
787 .map(|(color, strength)| (color, strength * activity))
788 .unwrap_or((Color::TRANSPARENT, 0.0));
789 shader.set_float(124, ink_color.r());
790 shader.set_float(125, ink_color.g());
791 shader.set_float(126, ink_color.b());
792 shader.set_float(127, ink_strength);
793 let (light_x, light_y) = glass_light_direction();
794 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
795 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
796 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
797 shader.set_float(118, touch_x);
798 shader.set_float(119, touch_y);
799 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
800 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
801 shader.set_float(
802 11,
803 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
804 );
805 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
806 let dynamic_tint_alpha = (dynamic_tint.a()
807 * dynamics
808 .tint_alpha_multiplier
809 .unwrap_or(1.0)
810 .clamp(0.0, 2.0)
811 * activity)
812 .clamp(0.0, 1.0);
813 shader.set_float4(
814 14,
815 dynamic_tint.r(),
816 dynamic_tint.g(),
817 dynamic_tint.b(),
818 dynamic_tint_alpha,
819 );
820 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
821 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
822 shader.set_float(20, self.lift * activity);
823 shader.set_float(21, 0.5 * activity);
824 shader.set_float2(22, 0.0, 1.0);
825 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
826 shader.set_float(28, self.rim_style * activity);
827 let requested_blur_radius_px = if content_mask {
828 0.0
829 } else {
830 self.blur_radius_dp * density * activity
831 };
832 let (wcksrd_blur_radius, gaussian_blur_radius) =
833 if requested_blur_radius_px > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
834 (0.0, requested_blur_radius_px)
835 } else {
836 (requested_blur_radius_px, 0.0)
837 };
838 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
839 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
840 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
841 shader.set_float4(
842 GLASS_RESTING_TINT_UNIFORM,
843 resting_tint.r(),
844 resting_tint.g(),
845 resting_tint.b(),
846 resting_tint.a(),
847 );
848 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
849 shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
850 shader.set_float(97, self.foreground_luma);
851 let dynamic_shadow = !self.clip && self.shadow;
852 shader.set_float(
853 102,
854 if dynamic_shadow {
855 self.shadow_color.a() * 0.55 * activity
856 } else {
857 0.0
858 },
859 );
860 shader.set_float(103, self.shadow_radius);
861 shader.set_float(104, self.shadow_offset_y);
862 shader.set_float(105, self.shadow_spread);
863 let morph_pad = dynamics
864 .morph
865 .as_ref()
866 .map(|morph| {
867 let (px, py, pw, ph, _) = morph.primary;
868 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
869 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
870 let mut shape_reach = 0.0f32;
871 for (sx, sy, sw, sh, _) in &morph.shapes {
872 let reach_x = ((sx + sw * 0.5) - right)
873 .max(left - (sx - sw * 0.5))
874 .max(0.0);
875 let reach_y = ((sy + sh * 0.5) - bottom)
876 .max(top - (sy - sh * 0.5))
877 .max(0.0);
878 shape_reach = shape_reach.max(reach_x.max(reach_y));
879 }
880 let glue_pad = if morph.shapes.is_empty() {
881 0.0
882 } else {
883 morph.glue * 2.0
884 };
885 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
886 })
887 .unwrap_or(0.0);
888 shader.set_input_padding(self.input_padding() + morph_pad + wcksrd_blur_radius / density);
889 if let Some(morph) = dynamics.morph.as_ref() {
890 let shadow_reach = if dynamic_shadow {
891 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
892 } else {
893 0.0
894 };
895 let support = morph_output_support(
896 morph,
897 activity,
898 morph_pad,
899 shadow_reach,
900 if dynamic_shadow {
901 self.shadow_offset_y.abs()
902 } else {
903 0.0
904 },
905 );
906 let (node_w, node_h) = morph.node_size;
907 let overhang = (-support.x)
908 .max(-support.y)
909 .max(support.x + support.width - node_w)
910 .max(support.y + support.height - node_h)
911 .max(0.0);
912 shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
913 shader.set_output_support(Some(support));
914 }
915
916 let optical_effect = liquid_glass_runtime_effect(shader);
917 if gaussian_blur_radius > f32::EPSILON {
918 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
919 .then(optical_effect)
920 } else {
921 optical_effect
922 }
923 }
924
925 fn input_padding(&self) -> f32 {
926 2.0
927 }
928}
929
930pub trait LiquidModifierExt {
932 fn glass_effect(self, glass: Glass) -> Modifier;
938
939 fn glass_effect_with(
943 self,
944 glass: Glass,
945 dynamics: impl Fn() -> GlassDynamics + 'static,
946 ) -> Modifier;
947}
948
949impl LiquidModifierExt for Modifier {
950 fn glass_effect(self, glass: Glass) -> Modifier {
951 self.glass_effect_with(glass, GlassDynamics::default)
952 }
953
954 fn glass_effect_with(
955 self,
956 glass: Glass,
957 dynamics: impl Fn() -> GlassDynamics + 'static,
958 ) -> Modifier {
959 let colors = crate::theme::liquid_colors();
960 let resolved = glass.resolve(&colors);
961 let shape = resolved.shape;
962
963 let mut modifier = self;
964 if resolved.shadow && resolved.clip {
965 let shadow_color = resolved.shadow_color;
966 let (radius, offset_y, spread) = (
967 resolved.shadow_radius,
968 resolved.shadow_offset_y,
969 resolved.shadow_spread,
970 );
971 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
972 scope.radius = radius;
973 scope.spread = spread;
974 scope.offset.y = offset_y;
975 scope.color = shadow_color;
976 scope.cutout = true;
977 });
978 }
979
980 let layer = cached_glass_layer(resolved);
981 modifier.graphics_layer(move || layer(current_density(), dynamics()))
982 }
983}
984
985struct CachedGlassLayer {
986 density: u32,
987 light: [u32; 2],
988 dynamics: GlassDynamics,
989 layer: GraphicsLayer,
990}
991
992fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
993 let cached = RefCell::new(None::<CachedGlassLayer>);
994 move |density, dynamics| {
995 let direction = glass_light_direction();
996 let light = [direction.0, direction.1].map(f32::to_bits);
997 let mut cached = cached.borrow_mut();
998 if let Some(previous) = &*cached
999 && previous.density == density.to_bits()
1000 && previous.light == light
1001 && glass_dynamics_match(&previous.dynamics, &dynamics)
1002 {
1003 return previous.layer.clone();
1004 }
1005 let render_effect = (!resolved.clip && dynamics.morph.is_some())
1006 .then(|| resolved.runtime_effect(density, &dynamics, true));
1007 let layer = GraphicsLayer {
1008 backdrop_effect: Some(resolved.runtime_effect(density, &dynamics, false)),
1009 render_effect,
1010 shape: resolved.shape.layer_shape(),
1011 clip: resolved.clip,
1012 ..Default::default()
1013 };
1014 *cached = Some(CachedGlassLayer {
1015 density: density.to_bits(),
1016 light,
1017 dynamics,
1018 layer: layer.clone(),
1019 });
1020 layer
1021 }
1022}
1023
1024fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1025 let bits = |d: &GlassDynamics| {
1026 let GlassDynamics {
1027 activity,
1028 resting_tint,
1029 highlight_boost,
1030 saturation_boost,
1031 tint_alpha_multiplier,
1032 morph: _,
1033 touch,
1034 press_depth,
1035 } = d;
1036 (
1037 activity.map(f32::to_bits),
1038 resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1039 [*highlight_boost, *saturation_boost].map(f32::to_bits),
1040 tint_alpha_multiplier.map(f32::to_bits),
1041 touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1042 press_depth.map(f32::to_bits),
1043 )
1044 };
1045 bits(a) == bits(b)
1046 && match (&a.morph, &b.morph) {
1047 (None, None) => true,
1048 (Some(a), Some(b)) => glass_morphs_match(a, b),
1049 _ => false,
1050 }
1051}
1052
1053fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1054 let shape_bits =
1055 |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1056 let bits = |m: &GlassMorph| {
1057 let GlassMorph {
1058 node_size,
1059 primary,
1060 shapes: _,
1061 glue,
1062 wobble_amplitude,
1063 wobble_phase,
1064 bulge_amplitude,
1065 bulge_direction,
1066 ellipse_blend,
1067 deformation,
1068 zoom_anchor,
1069 } = m;
1070 (
1071 [
1072 node_size.0,
1073 node_size.1,
1074 *glue,
1075 *wobble_amplitude,
1076 *wobble_phase,
1077 *bulge_amplitude,
1078 *bulge_direction,
1079 *ellipse_blend,
1080 zoom_anchor.0,
1081 zoom_anchor.1,
1082 ]
1083 .map(f32::to_bits),
1084 shape_bits(primary),
1085 deformation
1086 .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1087 )
1088 };
1089 bits(a) == bits(b)
1090 && a.shapes
1091 .iter()
1092 .map(shape_bits)
1093 .eq(b.shapes.iter().map(shape_bits))
1094}
1095
1096fn morph_output_support(
1097 morph: &GlassMorph,
1098 activity: f32,
1099 morph_pad: f32,
1100 shadow_reach: f32,
1101 shadow_offset: f32,
1102) -> Rect {
1103 let (px, py, pw, ph, _) = morph.primary;
1104 let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1105 (
1106 1.0 + (deformation.along() - 1.0) * activity,
1107 deformation.axis(),
1108 )
1109 });
1110 let across = 1.0 / along;
1111 let smallest_strain = along.min(across);
1112 let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1113 let rows = [
1114 (
1115 (along * ax * ax + across * ay * ay).abs(),
1116 ((along - across) * ax * ay).abs(),
1117 ),
1118 (
1119 ((along - across) * ax * ay).abs(),
1120 (along * ay * ay + across * ax * ax).abs(),
1121 ),
1122 ];
1123 let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1124 let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1125 let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1126 let (mut left, mut top) = (px - extent_x, py - extent_y);
1127 let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1128 for (sx, sy, sw, sh, _) in &morph.shapes {
1129 left = left.min(sx - sw * 0.5 - reach);
1130 top = top.min(sy - sh * 0.5 - reach);
1131 right = right.max(sx + sw * 0.5 + reach);
1132 bottom = bottom.max(sy + sh * 0.5 + reach);
1133 }
1134 Rect {
1135 x: left,
1136 y: top - shadow_offset,
1137 width: right - left,
1138 height: bottom - top + 2.0 * shadow_offset,
1139 }
1140}
1141
1142fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1143 let (node_w, node_h) = morph.node_size;
1144 let (cx, cy, w, h, radius) = morph.primary;
1145 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1146 shader.set_float2(2, cx, cy);
1147 shader.set_float2(4, w, h);
1148 shader.set_float(6, radius);
1149 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1150 shader.set_float(30, count as f32);
1151 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1152 let base = 36 + index * 5;
1153 shader.set_float(base, *sx);
1154 shader.set_float(base + 1, *sy);
1155 shader.set_float(base + 2, *sw);
1156 shader.set_float(base + 3, *sh);
1157 shader.set_float(base + 4, *sr);
1158 }
1159 shader.set_float(31, morph.glue);
1160 shader.set_float(32, morph.wobble_amplitude * activity);
1161 shader.set_float(33, morph.wobble_phase);
1162 shader.set_float(26, morph.bulge_amplitude * activity);
1163 shader.set_float(27, morph.bulge_direction);
1164 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1165 if let Some(deformation) = morph.deformation {
1166 let axis = deformation.axis();
1167 let along = 1.0 + (deformation.along() - 1.0) * activity;
1168 shader.set_float2(106, axis.0, axis.1);
1169 shader.set_float(108, along);
1170 shader.set_float(109, 1.0 / along);
1171 } else {
1172 shader.set_float2(106, 1.0, 0.0);
1173 shader.set_float2(108, 1.0, 1.0);
1174 }
1175}
1176
1177#[cfg(test)]
1178mod tests {
1179 use super::*;
1180
1181 fn light_colors() -> LiquidColors {
1182 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
1183 }
1184
1185 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
1186 match effect {
1187 RenderEffect::Shader { shader } => std::sync::Arc::unwrap_or_clone(shader),
1188 RenderEffect::Chain { second, .. } => {
1189 terminal_shader(std::sync::Arc::unwrap_or_clone(second))
1190 }
1191 effect => panic!("expected runtime shader, got {effect:?}"),
1192 }
1193 }
1194
1195 #[test]
1196 fn cached_glass_layers_refresh_all_live_inputs_and_keep_clones_independent() {
1197 let resolved = Glass::regular().no_clip().resolve(&light_colors());
1198 let layer = cached_glass_layer(resolved.clone());
1199 let mut dynamics = GlassDynamics::default();
1200 let mut density = 2.0;
1201 let mut previous = None;
1202 for step in 0..13 {
1203 match step {
1204 1 => density = 3.0,
1205 2 => set_glass_light_direction((0.5, -0.5)),
1206 3 => dynamics.highlight_boost = 0.25,
1207 4 => dynamics.saturation_boost = 0.4,
1208 5 => dynamics.activity = Some(0.5),
1209 6 => dynamics.resting_tint = Some(Color::RED),
1210 7 => dynamics.tint_alpha_multiplier = Some(0.3),
1211 8 => dynamics.touch = Some((5.0, 8.0, 0.7)),
1212 9 => dynamics.press_depth = Some(0.4),
1213 10 => {
1214 dynamics.morph = Some(GlassMorph {
1215 node_size: (60.0, 30.0),
1216 primary: (30.0, 15.0, 50.0, 20.0, 4.0),
1217 shapes: vec![(10.0, 15.0, 8.0, 8.0, -1.0)],
1218 ..Default::default()
1219 })
1220 }
1221 11 => {
1222 let morph = dynamics.morph.as_mut().unwrap();
1223 morph.primary.0 += 3.0;
1224 morph.shapes[0].0 += 2.0;
1225 morph.deformation = Some(GlassDeformation::incompressible((1.0, 1.0), 1.3));
1226 }
1227 12 => dynamics.morph = None,
1228 _ => {}
1229 }
1230 let actual = layer(density, dynamics.clone());
1231 let expected = GraphicsLayer {
1232 backdrop_effect: Some(resolved.backdrop_effect(density, dynamics.clone())),
1233 render_effect: dynamics
1234 .morph
1235 .as_ref()
1236 .map(|_| resolved.runtime_effect(density, &dynamics, true)),
1237 shape: resolved.shape.layer_shape(),
1238 clip: resolved.clip,
1239 ..Default::default()
1240 };
1241 assert_eq!(actual, expected, "live input step {step}");
1242 assert_ne!(
1243 previous.as_ref(),
1244 Some(&actual),
1245 "input change must reach the layer at step {step}"
1246 );
1247 let mut repeated = layer(density, dynamics.clone());
1248 assert_eq!(repeated, actual);
1249 repeated.backdrop_effect = None;
1250 assert_eq!(layer(density, dynamics.clone()), actual);
1251 previous = Some(actual);
1252 }
1253 set_glass_light_direction((0.0, 1.0));
1254 let replacement = cached_glass_layer(
1255 Glass::clear()
1256 .shape(LiquidShape::RoundedRect(9.0))
1257 .resolve(&light_colors()),
1258 );
1259 assert_ne!(
1260 replacement(density, dynamics.clone()),
1261 layer(density, dynamics)
1262 );
1263 }
1264
1265 #[test]
1266 fn glass_cache_input_equality_preserves_signed_zero_and_nan_bits() {
1267 let mut a = GlassDynamics {
1268 touch: Some((0.0, 1.0, 0.5)),
1269 ..Default::default()
1270 };
1271 let mut b = a.clone();
1272 b.touch.as_mut().unwrap().0 = -0.0;
1273 assert!(!glass_dynamics_match(&a, &b));
1274 a.highlight_boost = f32::NAN;
1275 assert!(glass_dynamics_match(&a, &a));
1276 a.morph = Some(GlassMorph::default());
1277 b = a.clone();
1278 b.morph.as_mut().unwrap().zoom_anchor.0 = -0.0;
1279 assert!(!glass_dynamics_match(&a, &b));
1280 b = a.clone();
1281 b.morph
1282 .as_mut()
1283 .unwrap()
1284 .shapes
1285 .push((0.0, 1.0, 2.0, 3.0, 4.0));
1286 assert!(!glass_dynamics_match(&a, &b));
1287 }
1288
1289 #[test]
1290 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1291 assert_eq!(glass_light_direction(), (0.0, 1.0));
1292 let resolved = Glass::regular().resolve(&light_colors());
1293 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1294 let shader = terminal_shader(effect);
1295 let u = shader.uniforms();
1296 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1297 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1298
1299 set_glass_light_direction((1.0, 0.0));
1300 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1301 let u_rotated = terminal_shader(effect);
1302 let u_rotated = u_rotated.uniforms();
1303 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1304 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1305 set_glass_light_direction((0.0, 1.0));
1306 }
1307
1308 #[test]
1309 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1310 for shape in [
1311 LiquidShape::Capsule,
1312 LiquidShape::Circle,
1313 LiquidShape::RoundedRect(12.0),
1314 ] {
1315 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1316 }
1317 }
1318
1319 #[test]
1320 fn glass_shadow_clamps_negative_radius() {
1321 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1322 assert_eq!(shadow.radius, 0.0);
1323 assert_eq!(shadow.offset_y, 3.0);
1324 assert_eq!(shadow.spread, -1.0);
1325 }
1326
1327 #[test]
1328 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1329 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1330 assert_eq!(deformation.axis(), (0.6, 0.8));
1331 assert_eq!(deformation.along(), 1.25);
1332 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1333 assert_eq!(
1334 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1335 (1.0, 0.0)
1336 );
1337 }
1338
1339 #[test]
1340 fn glass_builders_clamp_physical_inputs() {
1341 let glass = Glass::lens()
1342 .shape(LiquidShape::Circle)
1343 .tint(Color::BLACK)
1344 .blur_radius(-2.0)
1345 .saturation(1.2)
1346 .refraction_depth(3.0)
1347 .refraction_depth_dp(-3.0)
1348 .refraction_curve(2.0)
1349 .dispersion(2.0)
1350 .transmission_refraction(2.0)
1351 .meniscus_absorption(2.0)
1352 .highlight(0.4)
1353 .lift(-0.2)
1354 .adaptive_frost(Color::WHITE, 2.0)
1355 .shadow(false)
1356 .no_clip();
1357 assert_eq!(glass.shape, LiquidShape::Circle);
1358 assert_eq!(glass.tint, Some(Color::BLACK));
1359 assert_eq!(glass.blur_radius, Some(-2.0));
1360 assert_eq!(glass.saturation, Some(1.2));
1361 assert_eq!(glass.refraction_depth, 2.0);
1362 assert_eq!(glass.refraction_depth_dp, Some(0.0));
1363 assert_eq!(glass.refraction_curve, 1.0);
1364 assert_eq!(glass.dispersion, 1.0);
1365 assert_eq!(glass.transmission_refraction, 1.0);
1366 assert_eq!(glass.meniscus_absorption, 1.0);
1367 assert_eq!(glass.highlight, 0.4);
1368 assert_eq!(glass.lift, Some(-0.2));
1369 assert_eq!(glass.adaptive_frost, 1.0);
1370 assert!(!glass.shadow);
1371 assert!(!glass.clip);
1372 }
1373
1374 #[test]
1375 fn material_variants_resolve_distinct_frost_levels() {
1376 let regular = Glass::regular().resolve(&light_colors());
1377 let clear = Glass::clear().resolve(&light_colors());
1378 let lens = Glass::lens().resolve(&light_colors());
1379 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
1380 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
1381 assert!(regular.saturation > clear.saturation);
1382 assert_eq!(lens.rim_style, 1.0);
1383 assert_eq!(regular.refraction_depth, 0.0);
1384 assert_eq!(regular.transmission_refraction, 0.0);
1385 assert_eq!(regular.refraction_curve, 0.25);
1386 assert_eq!(lens.refraction_curve, 1.0);
1387 assert_eq!(regular.dispersion, 0.0);
1388 assert_eq!(lens.dispersion, 0.30);
1389 }
1390
1391 #[test]
1392 fn neutral_surface_helpers_follow_foreground_polarity() {
1393 assert_eq!(
1394 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
1395 Color::BLACK.with_alpha(0.08)
1396 );
1397 assert_eq!(
1398 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
1399 Color::WHITE.with_alpha(0.10)
1400 );
1401 assert_eq!(neutral_surface_lift(Color::BLACK, 0.7, -0.3), 0.7);
1402 assert_eq!(neutral_surface_lift(Color::WHITE, 0.7, -0.3), -0.3);
1403 }
1404
1405 #[test]
1406 fn dynamic_saturation_reaches_the_material_tint() {
1407 let resting = Color::from_rgb_u8(0, 199, 208);
1408 let raised = boost_tint_saturation(resting, 0.55);
1409 assert_eq!(raised.r(), 0.0);
1410 assert!(raised.g() > resting.g());
1411 assert!(raised.b() > resting.b());
1412 assert_eq!(raised.a(), resting.a());
1413 }
1414
1415 #[test]
1416 fn template_specialization_follows_each_materials_dispersion_and_activity() {
1417 for (dispersion, activity) in [(0.6, 0.0), (0.0, 0.5), (0.3, 1.0), (0.0, 0.0)] {
1418 let resolved = Glass::regular()
1419 .dispersion(dispersion)
1420 .resolve(&light_colors());
1421 let mut shader = terminal_shader(resolved.backdrop_effect(
1422 1.0,
1423 GlassDynamics {
1424 activity: Some(activity),
1425 ..GlassDynamics::default()
1426 },
1427 ));
1428 cranpose_ui_graphics::specialize_liquid_glass_with_folds(&mut shader, true);
1429 assert_eq!(
1430 shader.uniforms()[GLASS_DISPERSION_UNIFORM],
1431 dispersion * activity
1432 );
1433 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], activity);
1434 assert_eq!(
1435 shader
1436 .overrides()
1437 .iter()
1438 .any(|(name, _)| { *name == cranpose_ui_graphics::GLASS_DISPERSION_OFF_FLAG }),
1439 dispersion * activity == 0.0
1440 );
1441 }
1442 }
1443
1444 #[test]
1445 fn material_template_preserves_each_instances_optical_zoom_anchor() {
1446 let resolved = Glass::lens().resolve(&light_colors());
1447 for anchor in [(3.5, -2.25), (0.0, 0.0), (-7.0, 11.0)] {
1448 let shader = terminal_shader(resolved.backdrop_effect(
1449 1.5,
1450 GlassDynamics {
1451 morph: Some(GlassMorph {
1452 node_size: (120.0, 72.0),
1453 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
1454 zoom_anchor: anchor,
1455 ..Default::default()
1456 }),
1457 ..Default::default()
1458 },
1459 ));
1460 assert_eq!(
1461 &shader.uniforms()
1462 [GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM..GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM + 2],
1463 &[anchor.0, anchor.1]
1464 );
1465 }
1466 }
1467
1468 #[test]
1469 fn material_instances_share_source_without_sharing_uniforms_or_overrides() {
1470 let resolved = Glass::regular().resolve(&light_colors());
1471 let mut first = terminal_shader(resolved.backdrop_effect(2.0, GlassDynamics::default()));
1472 let untouched = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
1473 let unused_slot = RuntimeShader::MAX_USER_UNIFORMS - 1;
1474 first.set_float(unused_slot, 17.0);
1475 first.set_override("INSTANCE_ONLY", 1.0);
1476 let second = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
1477 assert_eq!(second.uniforms(), untouched.uniforms());
1478 assert_eq!(second.overrides(), untouched.overrides());
1479 assert_eq!(
1480 second.uniforms().get(unused_slot).copied().unwrap_or(0.0),
1481 0.0
1482 );
1483 assert!(
1484 !second
1485 .overrides()
1486 .iter()
1487 .any(|(name, _)| *name == "INSTANCE_ONLY")
1488 );
1489 assert_eq!(first.uniforms()[unused_slot], 17.0);
1490 assert_eq!(first.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
1491 assert_eq!(second.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
1492 assert_eq!(first.source().as_ptr(), second.source().as_ptr());
1493 }
1494
1495 #[test]
1496 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
1497 let resolved = Glass::lens()
1498 .refraction_depth(0.72)
1499 .refraction_depth_dp(21.0)
1500 .refraction_curve(0.8)
1501 .dispersion(0.42)
1502 .transmission_refraction(0.35)
1503 .meniscus_absorption(0.3)
1504 .blur_radius(3.0)
1505 .tint(Color::BLACK.with_alpha(0.8))
1506 .resolve(&light_colors());
1507 let effect = resolved.backdrop_effect(
1508 2.0,
1509 GlassDynamics {
1510 highlight_boost: 0.2,
1511 saturation_boost: 0.35,
1512 tint_alpha_multiplier: Some(0.25),
1513 ..Default::default()
1514 },
1515 );
1516 let RenderEffect::Chain { first, .. } = &effect else {
1517 panic!("macroscopic frost must precede the wcKSRD optical pass");
1518 };
1519 assert!(matches!(
1520 first.as_ref(),
1521 RenderEffect::Blur {
1522 radius_x: 6.0,
1523 radius_y: 6.0,
1524 ..
1525 }
1526 ));
1527 let shader = terminal_shader(effect);
1528 assert_eq!(shader.uniforms()[9], 0.72);
1529 assert_eq!(
1530 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
1531 21.0
1532 );
1533 assert_eq!(
1534 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
1535 1.0
1536 );
1537 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1538 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
1539 assert_eq!(
1540 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
1541 0.35
1542 );
1543 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
1544 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
1545 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
1546 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
1547 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1548 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
1549 }
1550
1551 #[test]
1552 fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
1553 let effect = Glass::regular()
1554 .resolve(&light_colors())
1555 .backdrop_effect(3.0, GlassDynamics::default());
1556 let RenderEffect::Chain { first, .. } = &effect else {
1557 panic!("heavy blur must ride the separable Gaussian pre-pass");
1558 };
1559 assert!(matches!(
1560 first.as_ref(),
1561 RenderEffect::Blur {
1562 radius_x: 24.0,
1563 radius_y: 24.0,
1564 ..
1565 }
1566 ));
1567 assert_eq!(
1568 terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
1569 0.0
1570 );
1571 }
1572
1573 #[test]
1574 fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
1575 let RenderEffect::Shader { shader } = Glass::regular()
1576 .blur_radius(0.5)
1577 .resolve(&light_colors())
1578 .backdrop_effect(3.0, GlassDynamics::default())
1579 else {
1580 panic!("a sub-cap blur must not build a Gaussian chain");
1581 };
1582 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
1583 }
1584
1585 #[test]
1586 fn raised_tint_density_stays_premultiplied_alpha_safe() {
1587 let effect = Glass::lens()
1588 .tint(Color::WHITE.with_alpha(0.8))
1589 .resolve(&light_colors())
1590 .backdrop_effect(
1591 1.0,
1592 GlassDynamics {
1593 tint_alpha_multiplier: Some(2.0),
1594 ..Default::default()
1595 },
1596 );
1597 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
1598 }
1599
1600 #[test]
1601 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
1602 let resolved = Glass::lens()
1603 .refraction_depth(0.72)
1604 .refraction_curve(0.8)
1605 .dispersion(0.42)
1606 .blur_radius(3.0)
1607 .saturation(1.4)
1608 .highlight(0.6)
1609 .lift(0.2)
1610 .tint(Color::BLACK.with_alpha(0.8))
1611 .no_clip()
1612 .resolve(&light_colors());
1613 let morph = GlassMorph {
1614 node_size: (120.0, 72.0),
1615 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
1616 wobble_amplitude: 3.0,
1617 bulge_amplitude: 4.0,
1618 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
1619 ..Default::default()
1620 };
1621
1622 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
1623 2.0,
1624 GlassDynamics {
1625 activity: Some(0.0),
1626 morph: Some(morph),
1627 ..Default::default()
1628 },
1629 ) else {
1630 panic!("material must resolve to the shared wcKSRD shader");
1631 };
1632 let uniforms = shader.uniforms();
1633 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
1634 assert_eq!(uniforms[9], 0.0);
1635 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
1636 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
1637 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
1638 assert_eq!(uniforms[11], 0.0);
1639 assert_eq!(uniforms[17], 0.0);
1640 assert_eq!(uniforms[18], 1.0);
1641 assert_eq!(uniforms[20], 0.0);
1642 assert_eq!(uniforms[21], 0.0);
1643 assert_eq!(uniforms[24], 1.0);
1644 assert_eq!(uniforms[28], 0.0);
1645 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1646 assert_eq!(uniforms[91], 0.0);
1647 assert_eq!(uniforms[102], 0.0);
1648 assert_eq!(
1649 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1650 &[0.0, 0.0, 0.0, 0.0]
1651 );
1652 assert_eq!(uniforms[32], 0.0);
1653 assert_eq!(uniforms[26], 0.0);
1654 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
1655 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
1656 }
1657
1658 #[test]
1659 fn resting_surface_tint_survives_zero_optical_activity() {
1660 let tint = Color::BLACK.with_alpha(0.11);
1661 let RenderEffect::Shader { shader } = Glass::lens()
1662 .no_clip()
1663 .resolve(&light_colors())
1664 .backdrop_effect(
1665 1.0,
1666 GlassDynamics {
1667 activity: Some(0.0),
1668 resting_tint: Some(tint),
1669 ..Default::default()
1670 },
1671 )
1672 else {
1673 panic!("resting surface must use the shared wcKSRD shader");
1674 };
1675 assert_eq!(
1676 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1677 &[tint.r(), tint.g(), tint.b(), tint.a()]
1678 );
1679 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
1680 }
1681
1682 #[test]
1683 fn full_optical_activity_preserves_the_resolved_material() {
1684 let resolved = Glass::lens()
1685 .refraction_depth(0.72)
1686 .refraction_curve(0.8)
1687 .dispersion(0.42)
1688 .blur_radius(3.0)
1689 .resolve(&light_colors());
1690 let shader = terminal_shader(resolved.backdrop_effect(
1691 2.0,
1692 GlassDynamics {
1693 activity: Some(1.0),
1694 ..Default::default()
1695 },
1696 ));
1697 let uniforms = shader.uniforms();
1698 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
1699 assert_eq!(uniforms[9], 0.72);
1700 assert_eq!(
1701 uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
1702 0.0
1703 );
1704 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1705 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
1706 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
1707 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1708 }
1709
1710 #[test]
1711 fn morph_geometry_and_incompressible_strain_are_packed() {
1712 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
1713 let morph = GlassMorph {
1714 node_size: (78.0, 59.0),
1715 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
1716 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
1717 glue: 8.0,
1718 wobble_amplitude: 1.0,
1719 wobble_phase: 0.5,
1720 bulge_amplitude: 2.0,
1721 bulge_direction: 0.25,
1722 ellipse_blend: 0.3,
1723 deformation: Some(deformation),
1724 zoom_anchor: (0.0, 0.0),
1725 };
1726 let RenderEffect::Shader { shader } = Glass::lens()
1727 .no_clip()
1728 .resolve(&light_colors())
1729 .backdrop_effect(
1730 1.0,
1731 GlassDynamics {
1732 morph: Some(morph),
1733 ..Default::default()
1734 },
1735 )
1736 else {
1737 panic!("morph must use the shared wcKSRD shader");
1738 };
1739 let uniforms = shader.uniforms();
1740 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
1741 assert_eq!(uniforms[30], 1.0);
1742 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
1743 assert_eq!(uniforms[110], 0.3);
1744 assert!(shader.output_padding() > 0.0);
1745 }
1746
1747 #[test]
1748 fn optical_builders_hold_their_own_legal_range() {
1749 let glass = Glass::regular()
1750 .fold_depth(-4.0)
1751 .optical_zoom(0.5)
1752 .rim_reflection(9.0);
1753 assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
1754 assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
1755 assert_eq!(
1756 glass.rim_reflection, 2.0,
1757 "the rim tops out at twice the line"
1758 );
1759
1760 let glass = Glass::regular()
1761 .fold_depth(6.0)
1762 .optical_zoom(1.4)
1763 .rim_reflection(0.25);
1764 assert_eq!(glass.fold_depth, 6.0);
1765 assert_eq!(glass.optical_zoom, 1.4);
1766 assert_eq!(glass.rim_reflection, 0.25);
1767 }
1768
1769 #[test]
1770 fn ink_recolor_records_its_color_at_a_clamped_strength() {
1771 let red = Color::from_rgb_u8(255, 0, 0);
1772 let glass = Glass::regular().ink_recolor(red, 3.0);
1773 assert_eq!(glass.ink_recolor, Some((red, 1.0)));
1774
1775 let glass = Glass::regular().ink_recolor(red, -1.0);
1776 assert_eq!(glass.ink_recolor, Some((red, 0.0)));
1777 assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
1778 }
1779
1780 #[test]
1781 fn a_touched_up_surface_amplifies_what_it_already_is() {
1782 let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
1783 assert!(
1784 dynamics.highlight_boost > 0.0,
1785 "a pressed surface goes brighter"
1786 );
1787 assert!(dynamics.saturation_boost > 0.0, "and more saturated");
1788 assert_eq!(
1789 dynamics.touch,
1790 Some((10.0, 4.0, 1.0)),
1791 "with no finger the glow sits at the surface's own heart"
1792 );
1793 assert_eq!(
1794 dynamics.resting_tint, None,
1795 "a touch never repaints the surface with another color"
1796 );
1797 }
1798
1799 #[test]
1800 fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
1801 let dynamics = GlassDynamics::default()
1802 .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
1803 .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
1804 let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
1805 assert_eq!(
1806 (x, y),
1807 (3.0, 7.0),
1808 "the glow tracks the finger, not the center"
1809 );
1810 assert_eq!(intensity, 1.0, "carried press saturates at full");
1811 }
1812
1813 #[test]
1814 fn an_unpressed_surface_is_left_exactly_as_it_was() {
1815 let resting = GlassDynamics::default();
1816 let untouched = resting
1817 .clone()
1818 .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
1819 assert_eq!(untouched.highlight_boost, resting.highlight_boost);
1820 assert_eq!(untouched.saturation_boost, resting.saturation_boost);
1821 assert_eq!(untouched.touch, None, "no press, no glow");
1822 }
1823
1824 #[test]
1825 fn an_off_axis_strain_widens_the_support_by_the_affine_rows_times_the_half_extents() {
1826 let resolved = Glass::regular()
1827 .shape(LiquidShape::RoundedRect(0.0))
1828 .blur_radius(0.0)
1829 .shadow(false)
1830 .no_clip()
1831 .resolve(&light_colors());
1832 let angle = 22.5f32.to_radians();
1833 let morph = GlassMorph {
1834 node_size: (600.0, 600.0),
1835 primary: (300.0, 300.0, 200.0, 200.0, 0.0),
1836 deformation: Some(GlassDeformation::incompressible(
1837 (angle.cos(), angle.sin()),
1838 2.0,
1839 )),
1840 ..Default::default()
1841 };
1842 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
1843 1.0,
1844 GlassDynamics {
1845 activity: Some(1.0),
1846 morph: Some(morph),
1847 ..Default::default()
1848 },
1849 ) else {
1850 panic!("an unblurred glass is one shader");
1851 };
1852 let support = shader
1853 .output_support()
1854 .expect("a morphing glass declares its support");
1855 let half_x = 100.0 * (2.0 * angle.cos().powi(2) + 0.5 * angle.sin().powi(2))
1856 + 100.0 * (1.5 * angle.sin() * angle.cos());
1857 assert!(
1858 (half_x - 231.066).abs() < 0.01,
1859 "the example's half extent is {half_x}"
1860 );
1861 assert!(
1862 support.x <= 300.0 - half_x && support.x + support.width >= 300.0 + half_x,
1863 "the support {support:?} must hold the strained square's x extent {half_x}"
1864 );
1865 let half_y = 100.0 * (1.5 * angle.sin() * angle.cos())
1866 + 100.0 * (2.0 * angle.sin().powi(2) + 0.5 * angle.cos().powi(2));
1867 assert!(
1868 (half_y - 125.0).abs() < 0.01,
1869 "the example's y half extent is {half_y}"
1870 );
1871 assert!(
1872 support.y <= 300.0 - half_y && support.y + support.height >= 300.0 + half_y,
1873 "the support {support:?} must hold the strained square's y extent {half_y}"
1874 );
1875 }
1876
1877 #[test]
1878 fn a_morphing_glass_declares_its_output_support_around_its_shapes_and_their_reach() {
1879 let resolved = Glass::regular()
1880 .shape(LiquidShape::RoundedRect(12.0))
1881 .blur_radius(0.0)
1882 .shadow(true)
1883 .no_clip()
1884 .resolve(&light_colors());
1885 let morph = GlassMorph {
1886 node_size: (280.0, 160.0),
1887 primary: (140.0, 80.0, 120.0, 40.0, -1.0),
1888 shapes: vec![(230.0, 80.0, 40.0, 40.0, -1.0)],
1889 glue: 20.0,
1890 wobble_amplitude: 3.0,
1891 bulge_amplitude: 8.0,
1892 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
1893 ..Default::default()
1894 };
1895 let dynamics = GlassDynamics {
1896 activity: Some(1.0),
1897 morph: Some(morph.clone()),
1898 ..Default::default()
1899 };
1900 let RenderEffect::Shader { shader } = resolved.backdrop_effect(2.0, dynamics) else {
1901 panic!("an unblurred glass is one shader");
1902 };
1903 let support = shader
1904 .output_support()
1905 .expect("a morphing glass declares its support");
1906 let shape_reach = (230.0 + 20.0) - (140.0 + 60.0);
1907 let shadow_reach = resolved.shadow_radius
1908 + resolved.shadow_offset_y.abs()
1909 + resolved.shadow_spread.max(0.0);
1910 let field_reach = 3.0 * 2.0 + 8.0 + shape_reach + 40.0 + shadow_reach + 4.0;
1911 let smallest_strain = 0.8;
1912 let reach = field_reach / smallest_strain;
1913 let contains = |x: f32, y: f32| {
1914 support.x <= x
1915 && support.y <= y
1916 && support.x + support.width >= x
1917 && support.y + support.height >= y
1918 };
1919 for (cx, cy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
1920 let corner = ((60.0 + reach) * cx, (20.0 + reach) * cy);
1921 let stretched = (140.0 + corner.0 * 1.25, 80.0 + corner.1 * 0.8);
1922 assert!(
1923 contains(stretched.0, stretched.1 - resolved.shadow_offset_y.abs())
1924 && contains(stretched.0, stretched.1 + resolved.shadow_offset_y.abs()),
1925 "{support:?} must hold the strained, dilated corner {stretched:?}"
1926 );
1927 }
1928 assert!(
1929 contains(230.0 + 20.0 + reach, 80.0),
1930 "the glued shape's reach is inside"
1931 );
1932 assert!(shader.output_padding() >= field_reach);
1933
1934 let RenderEffect::Shader { shader } =
1935 resolved.backdrop_effect(2.0, GlassDynamics::default())
1936 else {
1937 panic!("an unblurred glass is one shader");
1938 };
1939 assert_eq!(
1940 shader.output_support(),
1941 None,
1942 "cover-mode glass fills its rect"
1943 );
1944 }
1945}