1use crate::theme::LiquidColors;
7use cranpose_ui::current_density;
8use cranpose_ui::Modifier;
9use cranpose_ui_graphics::{
10 Color, GraphicsLayer, LayerShape, RenderEffect, RoundedCornerShape, RuntimeShader, TileMode,
11 GLASS_ACTIVITY_UNIFORM, GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM,
12 GLASS_EFFECT_DENSITY_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM, GLASS_LIGHT_DIRECTION_UNIFORM,
13 GLASS_MENISCUS_ABSORPTION_UNIFORM, GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
14 GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM, GLASS_RESTING_TINT_UNIFORM,
15 GLASS_TRANSMISSION_REFRACTION_UNIFORM, LIQUID_GLASS_WGSL,
16};
17use std::cell::Cell;
18use std::rc::Rc;
19
20thread_local! {
30 static GLASS_LIGHT_RETURN: Cell<(f32, f32)> = const { Cell::new((0.0, 1.0)) };
31}
32
33pub fn set_glass_light_direction(direction: (f32, f32)) {
36 GLASS_LIGHT_RETURN.with(|cell| cell.set(direction));
37}
38
39pub fn glass_light_direction() -> (f32, f32) {
41 GLASS_LIGHT_RETURN.with(|cell| cell.get())
42}
43
44const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
47
48const CAPSULE_SHADER_RADIUS: f32 = -1.0;
51
52const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
56
57#[derive(Clone, Copy, Debug, PartialEq, Default)]
59pub enum LiquidShape {
60 #[default]
62 Capsule,
63 RoundedRect(f32),
65 Circle,
67}
68
69impl LiquidShape {
70 pub fn clip_shape(&self) -> RoundedCornerShape {
72 match self {
73 LiquidShape::Capsule | LiquidShape::Circle => {
74 RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
75 }
76 LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
77 }
78 }
79
80 pub fn layer_shape(&self) -> LayerShape {
82 LayerShape::Rounded(self.clip_shape())
83 }
84
85 fn shader_radius_px(&self, density: f32) -> f32 {
87 match self {
88 LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
89 LiquidShape::RoundedRect(radius) => radius * density,
90 }
91 }
92}
93
94#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
97pub enum GlassVariant {
98 #[default]
100 Regular,
101 Clear,
103 Lens,
107}
108
109#[derive(Clone, Copy, Debug, PartialEq)]
113pub struct GlassShadow {
114 pub color: Color,
115 pub radius: f32,
116 pub offset_y: f32,
117 pub spread: f32,
118}
119
120impl GlassShadow {
121 pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
122 Self {
123 color,
124 radius: radius.max(0.0),
125 offset_y,
126 spread,
127 }
128 }
129}
130
131#[derive(Clone, Debug, PartialEq, Default)]
134pub struct GlassDynamics {
135 pub activity: Option<f32>,
139 pub resting_tint: Option<Color>,
142 pub highlight_boost: f32,
144 pub saturation_boost: f32,
147 pub tint_alpha_multiplier: Option<f32>,
150 pub morph: Option<GlassMorph>,
153 pub touch: Option<(f32, f32, f32)>,
157 pub press_depth: Option<f32>,
163}
164
165fn foreground_is_dark(foreground: Color) -> bool {
166 let foreground_luma =
167 0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
168 foreground_luma < 0.5
169}
170
171fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
172 if boost.abs() <= f32::EPSILON {
173 return tint;
174 }
175 let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
176 let saturation = (1.0 + boost).max(0.0);
177 Color::rgba(
178 (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
179 (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
180 (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
181 tint.a(),
182 )
183}
184
185pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
186 if foreground_is_dark(foreground) {
187 Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
188 } else {
189 Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
190 }
191}
192
193pub(crate) fn neutral_surface_lift(foreground: Color, light_lift: f32, dark_lift: f32) -> f32 {
194 if foreground_is_dark(foreground) {
195 light_lift
196 } else {
197 dark_lift
198 }
199}
200
201#[derive(Clone, Debug, PartialEq, Default)]
212pub struct GlassMorph {
213 pub node_size: (f32, f32),
219 pub primary: (f32, f32, f32, f32, f32),
220 pub shapes: Vec<(f32, f32, f32, f32, f32)>,
222 pub glue: f32,
224 pub wobble_amplitude: f32,
226 pub wobble_phase: f32,
227 pub bulge_amplitude: f32,
231 pub bulge_direction: f32,
232 pub ellipse_blend: f32,
236 pub deformation: Option<GlassDeformation>,
240 pub zoom_anchor: (f32, f32),
246}
247
248#[derive(Clone, Copy, Debug, PartialEq)]
252pub struct GlassDeformation {
253 axis: (f32, f32),
254 along: f32,
255}
256
257impl GlassDeformation {
258 pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
259 let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
260 let axis = if length > f32::EPSILON {
261 (axis.0 / length, axis.1 / length)
262 } else {
263 (1.0, 0.0)
264 };
265 Self {
266 axis,
267 along: along.max(f32::EPSILON),
268 }
269 }
270
271 pub fn axis(self) -> (f32, f32) {
272 self.axis
273 }
274
275 pub fn along(self) -> f32 {
276 self.along
277 }
278
279 pub fn across(self) -> f32 {
280 1.0 / self.along
281 }
282}
283
284impl GlassMorph {
285 pub const MAX_SHAPES: usize = 8;
287}
288
289#[derive(Clone, Debug, PartialEq)]
292pub struct Glass {
293 pub variant: GlassVariant,
294 pub shape: LiquidShape,
295 pub tint: Option<Color>,
297 pub blur_radius: Option<f32>,
299 pub saturation: Option<f32>,
301 pub refraction_depth: f32,
303 pub refraction_curve: f32,
305 pub dispersion: f32,
307 pub transmission_refraction: f32,
310 pub meniscus_absorption: f32,
313 pub fold_depth: f32,
317 pub optical_zoom: f32,
321 pub rim_reflection: f32,
324 pub ink_recolor: Option<(Color, f32)>,
328 pub highlight: f32,
330 pub lift: Option<f32>,
333 pub contrast: Option<f32>,
338 pub shadow: bool,
340 pub shadow_style: Option<GlassShadow>,
342 pub clip: bool,
345 pub foreground: Option<Color>,
348 pub adaptive_frost: f32,
350}
351
352impl Glass {
353 pub fn regular() -> Self {
354 Self {
355 variant: GlassVariant::Regular,
356 shape: LiquidShape::Capsule,
357 tint: None,
358 blur_radius: None,
359 saturation: None,
360 refraction_depth: 0.34,
361 refraction_curve: 0.25,
362 dispersion: 0.0,
363 transmission_refraction: 1.0,
364 meniscus_absorption: 1.0,
365 fold_depth: 0.0,
366 optical_zoom: 1.0,
367 rim_reflection: 1.0,
368 ink_recolor: None,
369 highlight: 0.9,
370 lift: None,
371 contrast: None,
372 shadow: true,
373 shadow_style: None,
374 clip: true,
375 foreground: None,
376 adaptive_frost: 0.65,
377 }
378 }
379
380 pub fn clear() -> Self {
381 Self {
382 variant: GlassVariant::Clear,
383 ..Self::regular()
384 }
385 }
386
387 pub fn lens() -> Self {
391 Self {
392 variant: GlassVariant::Lens,
393 shape: LiquidShape::Capsule,
394 tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
395 blur_radius: None,
396 saturation: None,
397 refraction_depth: 0.34,
398 refraction_curve: 1.0,
399 dispersion: 0.30,
400 transmission_refraction: 1.0,
401 meniscus_absorption: 1.0,
402 fold_depth: 0.0,
403 optical_zoom: 1.0,
404 rim_reflection: 1.0,
405 ink_recolor: None,
406 highlight: 1.15,
407 lift: None,
408 contrast: None,
409 shadow: true,
410 shadow_style: None,
411 clip: true,
412 foreground: None,
413 adaptive_frost: 0.0,
414 }
415 }
416
417 pub fn shape(mut self, shape: LiquidShape) -> Self {
418 self.shape = shape;
419 self
420 }
421
422 pub fn tint(mut self, tint: Color) -> Self {
423 self.tint = Some(tint);
424 self
425 }
426
427 pub fn blur_radius(mut self, radius_dp: f32) -> Self {
428 self.blur_radius = Some(radius_dp);
429 self
430 }
431
432 pub fn saturation(mut self, saturation: f32) -> Self {
433 self.saturation = Some(saturation);
434 self
435 }
436
437 pub fn refraction_depth(mut self, fraction: f32) -> Self {
438 self.refraction_depth = fraction.clamp(0.0, 2.0);
439 self
440 }
441
442 pub fn refraction_curve(mut self, curve: f32) -> Self {
443 self.refraction_curve = curve.clamp(0.05, 1.0);
444 self
445 }
446
447 pub fn dispersion(mut self, strength: f32) -> Self {
448 self.dispersion = strength.clamp(0.0, 1.0);
449 self
450 }
451
452 pub fn transmission_refraction(mut self, strength: f32) -> Self {
453 self.transmission_refraction = strength.clamp(0.0, 1.0);
454 self
455 }
456
457 pub fn meniscus_absorption(mut self, strength: f32) -> Self {
458 self.meniscus_absorption = strength.clamp(0.0, 1.0);
459 self
460 }
461
462 pub fn fold_depth(mut self, depth_dp: f32) -> Self {
464 self.fold_depth = depth_dp.max(0.0);
465 self
466 }
467
468 pub fn optical_zoom(mut self, zoom: f32) -> Self {
470 self.optical_zoom = zoom.max(1.0);
471 self
472 }
473
474 pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
476 self.rim_reflection = reflectivity.clamp(0.0, 2.0);
477 self
478 }
479
480 pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
483 self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
484 self
485 }
486
487 pub fn highlight(mut self, highlight: f32) -> Self {
488 self.highlight = highlight;
489 self
490 }
491
492 pub fn lift(mut self, lift: f32) -> Self {
495 self.lift = Some(lift);
496 self
497 }
498
499 pub fn contrast(mut self, contrast: f32) -> Self {
502 self.contrast = Some(contrast.max(0.05));
503 self
504 }
505
506 pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
507 self.foreground = Some(foreground);
508 self.adaptive_frost = strength.clamp(0.0, 1.0);
509 self
510 }
511
512 pub fn shadow(mut self, shadow: bool) -> Self {
513 self.shadow = shadow;
514 self
515 }
516
517 pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
518 self.shadow_style = Some(shadow);
519 self
520 }
521
522 pub fn no_clip(mut self) -> Self {
525 self.clip = false;
526 self
527 }
528
529 fn default_blur_radius(&self) -> f32 {
530 match self.variant {
531 GlassVariant::Regular => 8.0,
532 GlassVariant::Clear => 3.0,
533 GlassVariant::Lens => 0.0,
534 }
535 }
536
537 fn default_saturation(&self) -> f32 {
538 match self.variant {
539 GlassVariant::Regular => 1.5,
540 GlassVariant::Clear => 1.25,
541 GlassVariant::Lens => 1.0,
542 }
543 }
544
545 pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
547 let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
554 (GlassVariant::Regular, false) => 0.42,
555 (GlassVariant::Regular, true) => -0.38,
556 (GlassVariant::Clear, false) => 0.12,
557 (GlassVariant::Clear, true) => -0.12,
558 (GlassVariant::Lens, false) => 0.10,
559 (GlassVariant::Lens, true) => -0.08,
560 });
561 let foreground = self.foreground.unwrap_or(colors.label);
562 let shadow = self.shadow_style.unwrap_or_else(|| {
563 GlassShadow::new(
564 Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
565 (GlassVariant::Lens, false) => 0.14,
566 (GlassVariant::Lens, true) => 0.28,
567 (_, false) => 0.16,
568 (_, true) => 0.5,
569 }),
570 if self.variant == GlassVariant::Lens {
571 10.0
572 } else {
573 22.0
574 },
575 if self.variant == GlassVariant::Lens {
576 3.0
577 } else {
578 8.0
579 },
580 if self.variant == GlassVariant::Lens {
581 -6.0
582 } else {
583 -2.0
584 },
585 )
586 });
587 ResolvedGlass {
588 shape: self.shape,
589 tint: self.tint.unwrap_or(colors.glass_tint),
590 blur_radius_dp: self
591 .blur_radius
592 .unwrap_or_else(|| self.default_blur_radius()),
593 saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
594 refraction_depth: self.refraction_depth,
595 refraction_curve: self.refraction_curve,
596 dispersion: self.dispersion,
597 transmission_refraction: self.transmission_refraction,
598 meniscus_absorption: self.meniscus_absorption,
599 fold_depth: self.fold_depth,
600 optical_zoom: self.optical_zoom,
601 rim_reflection: self.rim_reflection,
602 ink_recolor: self.ink_recolor,
603 highlight: self.highlight,
604 lift,
605 contrast: self.contrast.unwrap_or(match self.variant {
606 GlassVariant::Lens => 1.0,
607 _ => 1.03,
608 }),
609 shadow: self.shadow,
610 clip: self.clip,
611 foreground_luma: 0.2126 * foreground.r()
612 + 0.7152 * foreground.g()
613 + 0.0722 * foreground.b(),
614 adaptive_frost: self.adaptive_frost,
615 rim_style: if self.variant == GlassVariant::Lens {
616 1.0
617 } else {
618 0.0
619 },
620 shadow_color: shadow.color,
621 shadow_radius: shadow.radius,
622 shadow_offset_y: shadow.offset_y,
623 shadow_spread: shadow.spread,
624 }
625 }
626}
627
628impl Default for Glass {
629 fn default() -> Self {
630 Self::regular()
631 }
632}
633
634#[derive(Clone, Debug, PartialEq)]
637pub(crate) struct ResolvedGlass {
638 pub shape: LiquidShape,
639 pub tint: Color,
640 pub blur_radius_dp: f32,
641 pub saturation: f32,
642 pub refraction_depth: f32,
643 pub refraction_curve: f32,
644 pub dispersion: f32,
645 pub transmission_refraction: f32,
646 pub meniscus_absorption: f32,
647 pub fold_depth: f32,
648 pub optical_zoom: f32,
649 pub rim_reflection: f32,
650 pub ink_recolor: Option<(Color, f32)>,
651 pub highlight: f32,
652 pub lift: f32,
653 pub contrast: f32,
654 pub shadow: bool,
655 pub clip: bool,
656 pub rim_style: f32,
659 pub foreground_luma: f32,
660 pub adaptive_frost: f32,
661 pub shadow_color: Color,
662 pub shadow_radius: f32,
665 pub shadow_offset_y: f32,
666 pub shadow_spread: f32,
667}
668
669impl ResolvedGlass {
670 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
674 self.runtime_effect(density, dynamics, false)
675 }
676
677 fn content_mask_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
678 self.runtime_effect(density, dynamics, true)
679 }
680
681 fn runtime_effect(
682 &self,
683 density: f32,
684 dynamics: GlassDynamics,
685 content_mask: bool,
686 ) -> RenderEffect {
687 let density = density.max(f32::EPSILON);
688 let activity = dynamics
689 .activity
690 .filter(|value| value.is_finite())
691 .unwrap_or(1.0)
692 .clamp(0.0, 1.0);
693 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
694 if let Some(morph) = dynamics.morph.as_ref() {
695 let (node_w, node_h) = morph.node_size;
701 let (cx, cy, w, h, radius) = morph.primary;
702 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
703 shader.set_float2(2, cx, cy);
704 shader.set_float2(4, w, h);
705 shader.set_float(6, radius);
706 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
707 shader.set_float(30, count as f32);
708 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
709 let base = 36 + index * 5;
710 shader.set_float(base, *sx);
711 shader.set_float(base + 1, *sy);
712 shader.set_float(base + 2, *sw);
713 shader.set_float(base + 3, *sh);
714 shader.set_float(base + 4, *sr);
715 }
716 shader.set_float(31, morph.glue);
717 shader.set_float(32, morph.wobble_amplitude * activity);
718 shader.set_float(33, morph.wobble_phase);
719 shader.set_float(26, morph.bulge_amplitude * activity);
720 shader.set_float(27, morph.bulge_direction);
721 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
722 if let Some(deformation) = morph.deformation {
723 let axis = deformation.axis();
724 let along = 1.0 + (deformation.along() - 1.0) * activity;
725 shader.set_float2(106, axis.0, axis.1);
726 shader.set_float(108, along);
727 shader.set_float(109, 1.0 / along);
728 } else {
729 shader.set_float2(106, 1.0, 0.0);
730 shader.set_float2(108, 1.0, 1.0);
731 }
732 } else {
734 shader.set_float2(0, 0.0, 0.0);
737 shader.set_float(6, self.shape.shader_radius_px(density));
738 }
739 let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
740 shader.set_float(9, self.refraction_depth * activity * press_depth);
741 shader.set_float(
742 GLASS_REFRACTION_CURVE_UNIFORM,
743 self.refraction_curve * activity,
744 );
745 shader.set_float(
746 GLASS_DISPERSION_UNIFORM,
747 self.dispersion * activity * press_depth,
748 );
749 shader.set_float(
750 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
751 self.transmission_refraction * activity,
752 );
753 shader.set_float(GLASS_MENISCUS_ABSORPTION_UNIFORM, self.meniscus_absorption);
754 shader.set_float(GLASS_FOLD_DEPTH_UNIFORM, self.fold_depth.max(0.0));
758 shader.set_float(
759 GLASS_OPTICAL_ZOOM_UNIFORM,
760 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
761 );
762 let zoom_anchor = dynamics
763 .morph
764 .as_ref()
765 .map(|morph| morph.zoom_anchor)
766 .unwrap_or((0.0, 0.0));
767 shader.set_float2(
768 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
769 zoom_anchor.0,
770 zoom_anchor.1,
771 );
772 shader.set_float(121, self.rim_reflection.max(0.001));
773 let (ink_color, ink_strength) = self
774 .ink_recolor
775 .map(|(color, strength)| (color, strength * activity))
776 .unwrap_or((Color::TRANSPARENT, 0.0));
777 shader.set_float(124, ink_color.r());
778 shader.set_float(125, ink_color.g());
779 shader.set_float(126, ink_color.b());
780 shader.set_float(127, ink_strength);
781 let (light_x, light_y) = glass_light_direction();
782 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
783 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
784 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
785 shader.set_float(118, touch_x);
786 shader.set_float(119, touch_y);
787 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
788 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
789 shader.set_float(
790 11,
791 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
792 );
793 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
794 let dynamic_tint_alpha = (dynamic_tint.a()
795 * dynamics
796 .tint_alpha_multiplier
797 .unwrap_or(1.0)
798 .clamp(0.0, 2.0)
799 * activity)
800 .clamp(0.0, 1.0);
801 shader.set_float4(
802 14,
803 dynamic_tint.r(),
804 dynamic_tint.g(),
805 dynamic_tint.b(),
806 dynamic_tint_alpha,
807 );
808 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
809 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
810 shader.set_float(20, self.lift * activity);
811 shader.set_float(21, 0.5 * activity);
812 shader.set_float2(22, 0.0, 1.0);
813 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
814 shader.set_float(28, self.rim_style * activity);
815 let requested_blur_radius_px = if content_mask {
816 0.0
817 } else {
818 self.blur_radius_dp * density * activity
819 };
820 let wcksrd_blur_radius = requested_blur_radius_px.min(WCKSRD_OPTICAL_BLUR_RADIUS_PX);
821 let gaussian_blur_radius = (requested_blur_radius_px - wcksrd_blur_radius).max(0.0);
822 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
823 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
824 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
825 shader.set_float4(
826 GLASS_RESTING_TINT_UNIFORM,
827 resting_tint.r(),
828 resting_tint.g(),
829 resting_tint.b(),
830 resting_tint.a(),
831 );
832 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
833 shader.set_float(91, self.adaptive_frost * activity);
834 shader.set_float(97, self.foreground_luma);
835 let dynamic_shadow = !self.clip && self.shadow;
836 shader.set_float(
837 102,
838 if dynamic_shadow {
839 self.shadow_color.a() * 0.55 * activity
840 } else {
841 0.0
842 },
843 );
844 shader.set_float(103, self.shadow_radius);
845 shader.set_float(104, self.shadow_offset_y);
846 shader.set_float(105, self.shadow_spread);
847 let morph_pad = dynamics
851 .morph
852 .as_ref()
853 .map(|morph| {
854 let (px, py, pw, ph, _) = morph.primary;
855 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
856 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
857 let mut shape_reach = 0.0f32;
858 for (sx, sy, sw, sh, _) in &morph.shapes {
859 let reach_x = ((sx + sw * 0.5) - right)
860 .max(left - (sx - sw * 0.5))
861 .max(0.0);
862 let reach_y = ((sy + sh * 0.5) - bottom)
863 .max(top - (sy - sh * 0.5))
864 .max(0.0);
865 shape_reach = shape_reach.max(reach_x.max(reach_y));
866 }
867 let glue_pad = if morph.shapes.is_empty() {
868 0.0
869 } else {
870 morph.glue * 2.0
871 };
872 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
873 })
874 .unwrap_or(0.0);
875 shader.set_input_padding(self.input_padding() + morph_pad + wcksrd_blur_radius / density);
878 if dynamics.morph.is_some() {
882 let shadow_reach = if dynamic_shadow {
883 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
884 } else {
885 0.0
886 };
887 shader.set_output_padding(morph_pad + shadow_reach + 4.0);
888 }
889
890 let optical_effect = RenderEffect::runtime_shader(shader);
891 if gaussian_blur_radius > f32::EPSILON {
892 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
899 .then(optical_effect)
900 } else {
901 optical_effect
902 }
903 }
904
905 fn input_padding(&self) -> f32 {
909 2.0
914 }
915}
916
917pub trait LiquidModifierExt {
919 fn glass_effect(self, glass: Glass) -> Modifier;
925
926 fn glass_effect_with(
930 self,
931 glass: Glass,
932 dynamics: impl Fn() -> GlassDynamics + 'static,
933 ) -> Modifier;
934}
935
936impl LiquidModifierExt for Modifier {
937 fn glass_effect(self, glass: Glass) -> Modifier {
938 self.glass_effect_with(glass, GlassDynamics::default)
939 }
940
941 fn glass_effect_with(
942 self,
943 glass: Glass,
944 dynamics: impl Fn() -> GlassDynamics + 'static,
945 ) -> Modifier {
946 let colors = crate::theme::liquid_colors();
947 let resolved = Rc::new(glass.resolve(&colors));
948 let shape = resolved.shape;
949
950 let mut modifier = self;
951 if resolved.shadow && resolved.clip {
952 let shadow_color = resolved.shadow_color;
953 let (radius, offset_y, spread) = (
954 resolved.shadow_radius,
955 resolved.shadow_offset_y,
956 resolved.shadow_spread,
957 );
958 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
959 scope.radius = radius;
960 scope.spread = spread;
961 scope.offset.y = offset_y;
962 scope.color = shadow_color;
963 scope.cutout = true;
966 });
967 }
968
969 let layer_resolved = Rc::clone(&resolved);
970 let clip = resolved.clip;
971 modifier.graphics_layer(move || {
972 let density = current_density();
973 let frame = dynamics();
974 let render_effect = (!clip && frame.morph.is_some())
975 .then(|| layer_resolved.content_mask_effect(density, frame.clone()));
976 GraphicsLayer {
977 backdrop_effect: Some(layer_resolved.backdrop_effect(density, frame)),
978 render_effect,
979 shape: shape.layer_shape(),
980 clip,
981 ..Default::default()
982 }
983 })
984 }
985}
986
987#[cfg(test)]
988mod tests {
989 use super::*;
990
991 fn light_colors() -> LiquidColors {
992 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
993 }
994
995 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
996 match effect {
997 RenderEffect::Shader { shader } => shader,
998 RenderEffect::Chain { second, .. } => terminal_shader(*second),
999 effect => panic!("expected runtime shader, got {effect:?}"),
1000 }
1001 }
1002
1003 #[test]
1004 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1005 assert_eq!(glass_light_direction(), (0.0, 1.0));
1006 let resolved = Glass::regular().resolve(&light_colors());
1007 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1008 let shader = terminal_shader(effect);
1009 let u = shader.uniforms();
1010 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1011 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1012
1013 set_glass_light_direction((1.0, 0.0));
1016 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1017 let u_rotated = terminal_shader(effect);
1018 let u_rotated = u_rotated.uniforms();
1019 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1020 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1021 set_glass_light_direction((0.0, 1.0));
1022 }
1023
1024 #[test]
1025 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1026 for shape in [
1027 LiquidShape::Capsule,
1028 LiquidShape::Circle,
1029 LiquidShape::RoundedRect(12.0),
1030 ] {
1031 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1032 }
1033 }
1034
1035 #[test]
1036 fn glass_shadow_clamps_negative_radius() {
1037 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1038 assert_eq!(shadow.radius, 0.0);
1039 assert_eq!(shadow.offset_y, 3.0);
1040 assert_eq!(shadow.spread, -1.0);
1041 }
1042
1043 #[test]
1044 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1045 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1046 assert_eq!(deformation.axis(), (0.6, 0.8));
1047 assert_eq!(deformation.along(), 1.25);
1048 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1049 assert_eq!(
1050 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1051 (1.0, 0.0)
1052 );
1053 }
1054
1055 #[test]
1056 fn glass_builders_clamp_physical_inputs() {
1057 let glass = Glass::lens()
1058 .shape(LiquidShape::Circle)
1059 .tint(Color::BLACK)
1060 .blur_radius(-2.0)
1061 .saturation(1.2)
1062 .refraction_depth(3.0)
1063 .refraction_curve(2.0)
1064 .dispersion(2.0)
1065 .transmission_refraction(2.0)
1066 .meniscus_absorption(2.0)
1067 .highlight(0.4)
1068 .lift(-0.2)
1069 .adaptive_frost(Color::WHITE, 2.0)
1070 .shadow(false)
1071 .no_clip();
1072 assert_eq!(glass.shape, LiquidShape::Circle);
1073 assert_eq!(glass.tint, Some(Color::BLACK));
1074 assert_eq!(glass.blur_radius, Some(-2.0));
1075 assert_eq!(glass.saturation, Some(1.2));
1076 assert_eq!(glass.refraction_depth, 2.0);
1077 assert_eq!(glass.refraction_curve, 1.0);
1078 assert_eq!(glass.dispersion, 1.0);
1079 assert_eq!(glass.transmission_refraction, 1.0);
1080 assert_eq!(glass.meniscus_absorption, 1.0);
1081 assert_eq!(glass.highlight, 0.4);
1082 assert_eq!(glass.lift, Some(-0.2));
1083 assert_eq!(glass.adaptive_frost, 1.0);
1084 assert!(!glass.shadow);
1085 assert!(!glass.clip);
1086 }
1087
1088 #[test]
1089 fn material_variants_resolve_distinct_frost_levels() {
1090 let regular = Glass::regular().resolve(&light_colors());
1091 let clear = Glass::clear().resolve(&light_colors());
1092 let lens = Glass::lens().resolve(&light_colors());
1093 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
1094 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
1095 assert!(regular.saturation > clear.saturation);
1096 assert_eq!(lens.rim_style, 1.0);
1097 assert_eq!(regular.refraction_curve, 0.25);
1098 assert_eq!(lens.refraction_curve, 1.0);
1099 assert_eq!(regular.dispersion, 0.0);
1100 assert_eq!(lens.dispersion, 0.30);
1101 }
1102
1103 #[test]
1104 fn neutral_surface_helpers_follow_foreground_polarity() {
1105 assert_eq!(
1106 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
1107 Color::BLACK.with_alpha(0.08)
1108 );
1109 assert_eq!(
1110 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
1111 Color::WHITE.with_alpha(0.10)
1112 );
1113 assert_eq!(neutral_surface_lift(Color::BLACK, 0.7, -0.3), 0.7);
1114 assert_eq!(neutral_surface_lift(Color::WHITE, 0.7, -0.3), -0.3);
1115 }
1116
1117 #[test]
1118 fn dynamic_saturation_reaches_the_material_tint() {
1119 let resting = Color::from_rgb_u8(0, 199, 208);
1120 let raised = boost_tint_saturation(resting, 0.55);
1121 assert_eq!(raised.r(), 0.0);
1122 assert!(raised.g() > resting.g());
1123 assert!(raised.b() > resting.b());
1124 assert_eq!(raised.a(), resting.a());
1125 }
1126
1127 #[test]
1128 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
1129 let resolved = Glass::lens()
1130 .refraction_depth(0.72)
1131 .refraction_curve(0.8)
1132 .dispersion(0.42)
1133 .transmission_refraction(0.35)
1134 .meniscus_absorption(0.3)
1135 .blur_radius(3.0)
1136 .tint(Color::BLACK.with_alpha(0.8))
1137 .resolve(&light_colors());
1138 let effect = resolved.backdrop_effect(
1139 2.0,
1140 GlassDynamics {
1141 highlight_boost: 0.2,
1142 saturation_boost: 0.35,
1143 tint_alpha_multiplier: Some(0.25),
1144 ..Default::default()
1145 },
1146 );
1147 let RenderEffect::Chain { first, .. } = &effect else {
1148 panic!("macroscopic frost must precede the wcKSRD optical pass");
1149 };
1150 assert!(matches!(
1151 first.as_ref(),
1152 RenderEffect::Blur {
1153 radius_x: 4.0,
1154 radius_y: 4.0,
1155 ..
1156 }
1157 ));
1158 let shader = terminal_shader(effect);
1159 assert_eq!(shader.uniforms()[9], 0.72);
1160 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1161 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
1162 assert_eq!(
1163 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
1164 0.35
1165 );
1166 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
1167 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
1168 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
1169 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
1170 assert_eq!(
1171 shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
1172 WCKSRD_OPTICAL_BLUR_RADIUS_PX
1173 );
1174 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
1175 }
1176
1177 #[test]
1178 fn raised_tint_density_stays_premultiplied_alpha_safe() {
1179 let effect = Glass::lens()
1180 .tint(Color::WHITE.with_alpha(0.8))
1181 .resolve(&light_colors())
1182 .backdrop_effect(
1183 1.0,
1184 GlassDynamics {
1185 tint_alpha_multiplier: Some(2.0),
1186 ..Default::default()
1187 },
1188 );
1189 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
1190 }
1191
1192 #[test]
1193 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
1194 let resolved = Glass::lens()
1195 .refraction_depth(0.72)
1196 .refraction_curve(0.8)
1197 .dispersion(0.42)
1198 .blur_radius(3.0)
1199 .saturation(1.4)
1200 .highlight(0.6)
1201 .lift(0.2)
1202 .tint(Color::BLACK.with_alpha(0.8))
1203 .no_clip()
1204 .resolve(&light_colors());
1205 let morph = GlassMorph {
1206 node_size: (120.0, 72.0),
1207 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
1208 wobble_amplitude: 3.0,
1209 bulge_amplitude: 4.0,
1210 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
1211 ..Default::default()
1212 };
1213
1214 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
1215 2.0,
1216 GlassDynamics {
1217 activity: Some(0.0),
1218 morph: Some(morph),
1219 ..Default::default()
1220 },
1221 ) else {
1222 panic!("material must resolve to the shared wcKSRD shader");
1223 };
1224 let uniforms = shader.uniforms();
1225 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
1226 assert_eq!(uniforms[9], 0.0);
1227 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
1228 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
1229 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
1230 assert_eq!(uniforms[11], 0.0);
1231 assert_eq!(uniforms[17], 0.0);
1232 assert_eq!(uniforms[18], 1.0);
1233 assert_eq!(uniforms[20], 0.0);
1234 assert_eq!(uniforms[21], 0.0);
1235 assert_eq!(uniforms[24], 1.0);
1236 assert_eq!(uniforms[28], 0.0);
1237 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1238 assert_eq!(uniforms[91], 0.0);
1239 assert_eq!(uniforms[102], 0.0);
1240 assert_eq!(
1241 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1242 &[0.0, 0.0, 0.0, 0.0]
1243 );
1244 assert_eq!(uniforms[32], 0.0);
1245 assert_eq!(uniforms[26], 0.0);
1246 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
1247 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
1248 }
1249
1250 #[test]
1251 fn resting_surface_tint_survives_zero_optical_activity() {
1252 let tint = Color::BLACK.with_alpha(0.11);
1253 let RenderEffect::Shader { shader } = Glass::lens()
1254 .no_clip()
1255 .resolve(&light_colors())
1256 .backdrop_effect(
1257 1.0,
1258 GlassDynamics {
1259 activity: Some(0.0),
1260 resting_tint: Some(tint),
1261 ..Default::default()
1262 },
1263 )
1264 else {
1265 panic!("resting surface must use the shared wcKSRD shader");
1266 };
1267 assert_eq!(
1268 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1269 &[tint.r(), tint.g(), tint.b(), tint.a()]
1270 );
1271 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
1272 }
1273
1274 #[test]
1275 fn full_optical_activity_preserves_the_resolved_material() {
1276 let resolved = Glass::lens()
1277 .refraction_depth(0.72)
1278 .refraction_curve(0.8)
1279 .dispersion(0.42)
1280 .blur_radius(3.0)
1281 .resolve(&light_colors());
1282 let shader = terminal_shader(resolved.backdrop_effect(
1283 2.0,
1284 GlassDynamics {
1285 activity: Some(1.0),
1286 ..Default::default()
1287 },
1288 ));
1289 let uniforms = shader.uniforms();
1290 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
1291 assert_eq!(uniforms[9], 0.72);
1292 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1293 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
1294 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
1295 assert_eq!(
1296 uniforms[GLASS_BLUR_RADIUS_UNIFORM],
1297 WCKSRD_OPTICAL_BLUR_RADIUS_PX
1298 );
1299 }
1300
1301 #[test]
1302 fn morph_geometry_and_incompressible_strain_are_packed() {
1303 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
1304 let morph = GlassMorph {
1305 node_size: (78.0, 59.0),
1306 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
1307 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
1308 glue: 8.0,
1309 wobble_amplitude: 1.0,
1310 wobble_phase: 0.5,
1311 bulge_amplitude: 2.0,
1312 bulge_direction: 0.25,
1313 ellipse_blend: 0.3,
1314 deformation: Some(deformation),
1315 zoom_anchor: (0.0, 0.0),
1316 };
1317 let RenderEffect::Shader { shader } = Glass::lens()
1318 .no_clip()
1319 .resolve(&light_colors())
1320 .backdrop_effect(
1321 1.0,
1322 GlassDynamics {
1323 morph: Some(morph),
1324 ..Default::default()
1325 },
1326 )
1327 else {
1328 panic!("morph must use the shared wcKSRD shader");
1329 };
1330 let uniforms = shader.uniforms();
1331 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
1332 assert_eq!(uniforms[30], 1.0);
1333 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
1334 assert_eq!(uniforms[110], 0.3);
1335 assert!(shader.output_padding() > 0.0);
1336 }
1337}