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