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 shadow: bool,
322 pub shadow_style: Option<GlassShadow>,
324 pub clip: bool,
327 pub foreground: Option<Color>,
330 pub adaptive_frost: f32,
332}
333
334impl Glass {
335 pub fn regular() -> Self {
336 Self {
337 variant: GlassVariant::Regular,
338 shape: LiquidShape::Capsule,
339 tint: None,
340 blur_radius: None,
341 saturation: None,
342 refraction_depth: 0.34,
343 refraction_curve: 0.25,
344 dispersion: 0.0,
345 transmission_refraction: 1.0,
346 meniscus_absorption: 1.0,
347 fold_depth: 0.0,
348 optical_zoom: 1.0,
349 rim_reflection: 1.0,
350 ink_recolor: None,
351 highlight: 0.9,
352 lift: None,
353 shadow: true,
354 shadow_style: None,
355 clip: true,
356 foreground: None,
357 adaptive_frost: 0.65,
358 }
359 }
360
361 pub fn clear() -> Self {
362 Self {
363 variant: GlassVariant::Clear,
364 ..Self::regular()
365 }
366 }
367
368 pub fn lens() -> Self {
372 Self {
373 variant: GlassVariant::Lens,
374 shape: LiquidShape::Capsule,
375 tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
376 blur_radius: None,
377 saturation: None,
378 refraction_depth: 0.34,
379 refraction_curve: 1.0,
380 dispersion: 0.30,
381 transmission_refraction: 1.0,
382 meniscus_absorption: 1.0,
383 fold_depth: 0.0,
384 optical_zoom: 1.0,
385 rim_reflection: 1.0,
386 ink_recolor: None,
387 highlight: 1.15,
388 lift: None,
389 shadow: true,
390 shadow_style: None,
391 clip: true,
392 foreground: None,
393 adaptive_frost: 0.0,
394 }
395 }
396
397 pub fn shape(mut self, shape: LiquidShape) -> Self {
398 self.shape = shape;
399 self
400 }
401
402 pub fn tint(mut self, tint: Color) -> Self {
403 self.tint = Some(tint);
404 self
405 }
406
407 pub fn blur_radius(mut self, radius_dp: f32) -> Self {
408 self.blur_radius = Some(radius_dp);
409 self
410 }
411
412 pub fn saturation(mut self, saturation: f32) -> Self {
413 self.saturation = Some(saturation);
414 self
415 }
416
417 pub fn refraction_depth(mut self, fraction: f32) -> Self {
418 self.refraction_depth = fraction.clamp(0.0, 2.0);
419 self
420 }
421
422 pub fn refraction_curve(mut self, curve: f32) -> Self {
423 self.refraction_curve = curve.clamp(0.05, 1.0);
424 self
425 }
426
427 pub fn dispersion(mut self, strength: f32) -> Self {
428 self.dispersion = strength.clamp(0.0, 1.0);
429 self
430 }
431
432 pub fn transmission_refraction(mut self, strength: f32) -> Self {
433 self.transmission_refraction = strength.clamp(0.0, 1.0);
434 self
435 }
436
437 pub fn meniscus_absorption(mut self, strength: f32) -> Self {
438 self.meniscus_absorption = strength.clamp(0.0, 1.0);
439 self
440 }
441
442 pub fn fold_depth(mut self, depth_dp: f32) -> Self {
444 self.fold_depth = depth_dp.max(0.0);
445 self
446 }
447
448 pub fn optical_zoom(mut self, zoom: f32) -> Self {
450 self.optical_zoom = zoom.max(1.0);
451 self
452 }
453
454 pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
456 self.rim_reflection = reflectivity.clamp(0.0, 2.0);
457 self
458 }
459
460 pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
463 self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
464 self
465 }
466
467 pub fn highlight(mut self, highlight: f32) -> Self {
468 self.highlight = highlight;
469 self
470 }
471
472 pub fn lift(mut self, lift: f32) -> Self {
475 self.lift = Some(lift);
476 self
477 }
478
479 pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
480 self.foreground = Some(foreground);
481 self.adaptive_frost = strength.clamp(0.0, 1.0);
482 self
483 }
484
485 pub fn shadow(mut self, shadow: bool) -> Self {
486 self.shadow = shadow;
487 self
488 }
489
490 pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
491 self.shadow_style = Some(shadow);
492 self
493 }
494
495 pub fn no_clip(mut self) -> Self {
498 self.clip = false;
499 self
500 }
501
502 fn default_blur_radius(&self) -> f32 {
503 match self.variant {
504 GlassVariant::Regular => 8.0,
505 GlassVariant::Clear => 3.0,
506 GlassVariant::Lens => 0.0,
507 }
508 }
509
510 fn default_saturation(&self) -> f32 {
511 match self.variant {
512 GlassVariant::Regular => 1.5,
513 GlassVariant::Clear => 1.25,
514 GlassVariant::Lens => 1.0,
515 }
516 }
517
518 pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
520 let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
527 (GlassVariant::Regular, false) => 0.42,
528 (GlassVariant::Regular, true) => -0.38,
529 (GlassVariant::Clear, false) => 0.12,
530 (GlassVariant::Clear, true) => -0.12,
531 (GlassVariant::Lens, false) => 0.10,
532 (GlassVariant::Lens, true) => -0.08,
533 });
534 let foreground = self.foreground.unwrap_or(colors.label);
535 let shadow = self.shadow_style.unwrap_or_else(|| {
536 GlassShadow::new(
537 Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
538 (GlassVariant::Lens, false) => 0.14,
539 (GlassVariant::Lens, true) => 0.28,
540 (_, false) => 0.16,
541 (_, true) => 0.5,
542 }),
543 if self.variant == GlassVariant::Lens {
544 10.0
545 } else {
546 22.0
547 },
548 if self.variant == GlassVariant::Lens {
549 3.0
550 } else {
551 8.0
552 },
553 if self.variant == GlassVariant::Lens {
554 -6.0
555 } else {
556 -2.0
557 },
558 )
559 });
560 ResolvedGlass {
561 shape: self.shape,
562 tint: self.tint.unwrap_or(colors.glass_tint),
563 blur_radius_dp: self
564 .blur_radius
565 .unwrap_or_else(|| self.default_blur_radius()),
566 saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
567 refraction_depth: self.refraction_depth,
568 refraction_curve: self.refraction_curve,
569 dispersion: self.dispersion,
570 transmission_refraction: self.transmission_refraction,
571 meniscus_absorption: self.meniscus_absorption,
572 fold_depth: self.fold_depth,
573 optical_zoom: self.optical_zoom,
574 rim_reflection: self.rim_reflection,
575 ink_recolor: self.ink_recolor,
576 highlight: self.highlight,
577 lift,
578 contrast: if self.variant == GlassVariant::Lens {
579 1.0
580 } else {
581 1.03
582 },
583 shadow: self.shadow,
584 clip: self.clip,
585 foreground_luma: 0.2126 * foreground.r()
586 + 0.7152 * foreground.g()
587 + 0.0722 * foreground.b(),
588 adaptive_frost: self.adaptive_frost,
589 rim_style: if self.variant == GlassVariant::Lens {
590 1.0
591 } else {
592 0.0
593 },
594 shadow_color: shadow.color,
595 shadow_radius: shadow.radius,
596 shadow_offset_y: shadow.offset_y,
597 shadow_spread: shadow.spread,
598 }
599 }
600}
601
602impl Default for Glass {
603 fn default() -> Self {
604 Self::regular()
605 }
606}
607
608#[derive(Clone, Debug, PartialEq)]
611pub(crate) struct ResolvedGlass {
612 pub shape: LiquidShape,
613 pub tint: Color,
614 pub blur_radius_dp: f32,
615 pub saturation: f32,
616 pub refraction_depth: f32,
617 pub refraction_curve: f32,
618 pub dispersion: f32,
619 pub transmission_refraction: f32,
620 pub meniscus_absorption: f32,
621 pub fold_depth: f32,
622 pub optical_zoom: f32,
623 pub rim_reflection: f32,
624 pub ink_recolor: Option<(Color, f32)>,
625 pub highlight: f32,
626 pub lift: f32,
627 pub contrast: f32,
628 pub shadow: bool,
629 pub clip: bool,
630 pub rim_style: f32,
633 pub foreground_luma: f32,
634 pub adaptive_frost: f32,
635 pub shadow_color: Color,
636 pub shadow_radius: f32,
639 pub shadow_offset_y: f32,
640 pub shadow_spread: f32,
641}
642
643impl ResolvedGlass {
644 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
648 self.runtime_effect(density, dynamics, false)
649 }
650
651 fn content_mask_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
652 self.runtime_effect(density, dynamics, true)
653 }
654
655 fn runtime_effect(
656 &self,
657 density: f32,
658 dynamics: GlassDynamics,
659 content_mask: bool,
660 ) -> RenderEffect {
661 let density = density.max(f32::EPSILON);
662 let activity = dynamics
663 .activity
664 .filter(|value| value.is_finite())
665 .unwrap_or(1.0)
666 .clamp(0.0, 1.0);
667 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
668 if let Some(morph) = dynamics.morph.as_ref() {
669 let (node_w, node_h) = morph.node_size;
675 let (cx, cy, w, h, radius) = morph.primary;
676 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
677 shader.set_float2(2, cx, cy);
678 shader.set_float2(4, w, h);
679 shader.set_float(6, radius);
680 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
681 shader.set_float(30, count as f32);
682 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
683 let base = 36 + index * 5;
684 shader.set_float(base, *sx);
685 shader.set_float(base + 1, *sy);
686 shader.set_float(base + 2, *sw);
687 shader.set_float(base + 3, *sh);
688 shader.set_float(base + 4, *sr);
689 }
690 shader.set_float(31, morph.glue);
691 shader.set_float(32, morph.wobble_amplitude * activity);
692 shader.set_float(33, morph.wobble_phase);
693 shader.set_float(26, morph.bulge_amplitude * activity);
694 shader.set_float(27, morph.bulge_direction);
695 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
696 if let Some(deformation) = morph.deformation {
697 let axis = deformation.axis();
698 let along = 1.0 + (deformation.along() - 1.0) * activity;
699 shader.set_float2(106, axis.0, axis.1);
700 shader.set_float(108, along);
701 shader.set_float(109, 1.0 / along);
702 } else {
703 shader.set_float2(106, 1.0, 0.0);
704 shader.set_float2(108, 1.0, 1.0);
705 }
706 } else {
708 shader.set_float2(0, 0.0, 0.0);
711 shader.set_float(6, self.shape.shader_radius_px(density));
712 }
713 shader.set_float(9, self.refraction_depth * activity);
714 shader.set_float(
715 GLASS_REFRACTION_CURVE_UNIFORM,
716 self.refraction_curve * activity,
717 );
718 shader.set_float(GLASS_DISPERSION_UNIFORM, self.dispersion * activity);
719 shader.set_float(
720 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
721 self.transmission_refraction * activity,
722 );
723 shader.set_float(GLASS_MENISCUS_ABSORPTION_UNIFORM, self.meniscus_absorption);
724 shader.set_float(GLASS_FOLD_DEPTH_UNIFORM, self.fold_depth.max(0.0));
728 shader.set_float(
729 GLASS_OPTICAL_ZOOM_UNIFORM,
730 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
731 );
732 shader.set_float(121, self.rim_reflection.max(0.001));
733 let (ink_color, ink_strength) = self
734 .ink_recolor
735 .map(|(color, strength)| (color, strength * activity))
736 .unwrap_or((Color::TRANSPARENT, 0.0));
737 shader.set_float(124, ink_color.r());
738 shader.set_float(125, ink_color.g());
739 shader.set_float(126, ink_color.b());
740 shader.set_float(127, ink_strength);
741 let (light_x, light_y) = glass_light_direction();
742 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
743 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
744 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
745 shader.set_float(118, touch_x);
746 shader.set_float(119, touch_y);
747 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
748 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
749 shader.set_float(
750 11,
751 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
752 );
753 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
754 let dynamic_tint_alpha = (dynamic_tint.a()
755 * dynamics
756 .tint_alpha_multiplier
757 .unwrap_or(1.0)
758 .clamp(0.0, 2.0)
759 * activity)
760 .clamp(0.0, 1.0);
761 shader.set_float4(
762 14,
763 dynamic_tint.r(),
764 dynamic_tint.g(),
765 dynamic_tint.b(),
766 dynamic_tint_alpha,
767 );
768 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
769 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
770 shader.set_float(20, self.lift * activity);
771 shader.set_float(21, 0.5 * activity);
772 shader.set_float2(22, 0.0, 1.0);
773 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
774 shader.set_float(28, self.rim_style * activity);
775 let requested_blur_radius_px = if content_mask {
776 0.0
777 } else {
778 self.blur_radius_dp * density * activity
779 };
780 let wcksrd_blur_radius = requested_blur_radius_px.min(WCKSRD_OPTICAL_BLUR_RADIUS_PX);
781 let gaussian_blur_radius = (requested_blur_radius_px - wcksrd_blur_radius).max(0.0);
782 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
783 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
784 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
785 shader.set_float4(
786 GLASS_RESTING_TINT_UNIFORM,
787 resting_tint.r(),
788 resting_tint.g(),
789 resting_tint.b(),
790 resting_tint.a(),
791 );
792 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
793 shader.set_float(91, self.adaptive_frost * activity);
794 shader.set_float(97, self.foreground_luma);
795 let dynamic_shadow = !self.clip && self.shadow;
796 shader.set_float(
797 102,
798 if dynamic_shadow {
799 self.shadow_color.a() * 0.55 * activity
800 } else {
801 0.0
802 },
803 );
804 shader.set_float(103, self.shadow_radius);
805 shader.set_float(104, self.shadow_offset_y);
806 shader.set_float(105, self.shadow_spread);
807 let morph_pad = dynamics
811 .morph
812 .as_ref()
813 .map(|morph| {
814 let (px, py, pw, ph, _) = morph.primary;
815 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
816 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
817 let mut shape_reach = 0.0f32;
818 for (sx, sy, sw, sh, _) in &morph.shapes {
819 let reach_x = ((sx + sw * 0.5) - right)
820 .max(left - (sx - sw * 0.5))
821 .max(0.0);
822 let reach_y = ((sy + sh * 0.5) - bottom)
823 .max(top - (sy - sh * 0.5))
824 .max(0.0);
825 shape_reach = shape_reach.max(reach_x.max(reach_y));
826 }
827 let glue_pad = if morph.shapes.is_empty() {
828 0.0
829 } else {
830 morph.glue * 2.0
831 };
832 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
833 })
834 .unwrap_or(0.0);
835 shader.set_input_padding(self.input_padding() + morph_pad + wcksrd_blur_radius / density);
838 if dynamics.morph.is_some() {
842 let shadow_reach = if dynamic_shadow {
843 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
844 } else {
845 0.0
846 };
847 shader.set_output_padding(morph_pad + shadow_reach + 4.0);
848 }
849
850 let optical_effect = RenderEffect::runtime_shader(shader);
851 if gaussian_blur_radius > f32::EPSILON {
852 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
859 .then(optical_effect)
860 } else {
861 optical_effect
862 }
863 }
864
865 fn input_padding(&self) -> f32 {
869 2.0
874 }
875}
876
877pub trait LiquidModifierExt {
879 fn glass_effect(self, glass: Glass) -> Modifier;
885
886 fn glass_effect_with(
890 self,
891 glass: Glass,
892 dynamics: impl Fn() -> GlassDynamics + 'static,
893 ) -> Modifier;
894}
895
896impl LiquidModifierExt for Modifier {
897 fn glass_effect(self, glass: Glass) -> Modifier {
898 self.glass_effect_with(glass, GlassDynamics::default)
899 }
900
901 fn glass_effect_with(
902 self,
903 glass: Glass,
904 dynamics: impl Fn() -> GlassDynamics + 'static,
905 ) -> Modifier {
906 let colors = crate::theme::liquid_colors();
907 let resolved = Rc::new(glass.resolve(&colors));
908 let shape = resolved.shape;
909
910 let mut modifier = self;
911 if resolved.shadow && resolved.clip {
912 let shadow_color = resolved.shadow_color;
913 let (radius, offset_y, spread) = (
914 resolved.shadow_radius,
915 resolved.shadow_offset_y,
916 resolved.shadow_spread,
917 );
918 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
919 scope.radius = radius;
920 scope.spread = spread;
921 scope.offset.y = offset_y;
922 scope.color = shadow_color;
923 scope.cutout = true;
926 });
927 }
928
929 let layer_resolved = Rc::clone(&resolved);
930 let clip = resolved.clip;
931 modifier.graphics_layer(move || {
932 let density = current_density();
933 let frame = dynamics();
934 let render_effect = (!clip && frame.morph.is_some())
935 .then(|| layer_resolved.content_mask_effect(density, frame.clone()));
936 GraphicsLayer {
937 backdrop_effect: Some(layer_resolved.backdrop_effect(density, frame)),
938 render_effect,
939 shape: shape.layer_shape(),
940 clip,
941 ..Default::default()
942 }
943 })
944 }
945}
946
947#[cfg(test)]
948mod tests {
949 use super::*;
950
951 fn light_colors() -> LiquidColors {
952 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
953 }
954
955 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
956 match effect {
957 RenderEffect::Shader { shader } => shader,
958 RenderEffect::Chain { second, .. } => terminal_shader(*second),
959 effect => panic!("expected runtime shader, got {effect:?}"),
960 }
961 }
962
963 #[test]
964 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
965 assert_eq!(glass_light_direction(), (0.0, 1.0));
966 let resolved = Glass::regular().resolve(&light_colors());
967 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
968 let shader = terminal_shader(effect);
969 let u = shader.uniforms();
970 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
971 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
972
973 set_glass_light_direction((1.0, 0.0));
976 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
977 let u_rotated = terminal_shader(effect);
978 let u_rotated = u_rotated.uniforms();
979 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
980 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
981 set_glass_light_direction((0.0, 1.0));
982 }
983
984 #[test]
985 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
986 for shape in [
987 LiquidShape::Capsule,
988 LiquidShape::Circle,
989 LiquidShape::RoundedRect(12.0),
990 ] {
991 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
992 }
993 }
994
995 #[test]
996 fn glass_shadow_clamps_negative_radius() {
997 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
998 assert_eq!(shadow.radius, 0.0);
999 assert_eq!(shadow.offset_y, 3.0);
1000 assert_eq!(shadow.spread, -1.0);
1001 }
1002
1003 #[test]
1004 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1005 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1006 assert_eq!(deformation.axis(), (0.6, 0.8));
1007 assert_eq!(deformation.along(), 1.25);
1008 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1009 assert_eq!(
1010 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1011 (1.0, 0.0)
1012 );
1013 }
1014
1015 #[test]
1016 fn glass_builders_clamp_physical_inputs() {
1017 let glass = Glass::lens()
1018 .shape(LiquidShape::Circle)
1019 .tint(Color::BLACK)
1020 .blur_radius(-2.0)
1021 .saturation(1.2)
1022 .refraction_depth(3.0)
1023 .refraction_curve(2.0)
1024 .dispersion(2.0)
1025 .transmission_refraction(2.0)
1026 .meniscus_absorption(2.0)
1027 .highlight(0.4)
1028 .lift(-0.2)
1029 .adaptive_frost(Color::WHITE, 2.0)
1030 .shadow(false)
1031 .no_clip();
1032 assert_eq!(glass.shape, LiquidShape::Circle);
1033 assert_eq!(glass.tint, Some(Color::BLACK));
1034 assert_eq!(glass.blur_radius, Some(-2.0));
1035 assert_eq!(glass.saturation, Some(1.2));
1036 assert_eq!(glass.refraction_depth, 2.0);
1037 assert_eq!(glass.refraction_curve, 1.0);
1038 assert_eq!(glass.dispersion, 1.0);
1039 assert_eq!(glass.transmission_refraction, 1.0);
1040 assert_eq!(glass.meniscus_absorption, 1.0);
1041 assert_eq!(glass.highlight, 0.4);
1042 assert_eq!(glass.lift, Some(-0.2));
1043 assert_eq!(glass.adaptive_frost, 1.0);
1044 assert!(!glass.shadow);
1045 assert!(!glass.clip);
1046 }
1047
1048 #[test]
1049 fn material_variants_resolve_distinct_frost_levels() {
1050 let regular = Glass::regular().resolve(&light_colors());
1051 let clear = Glass::clear().resolve(&light_colors());
1052 let lens = Glass::lens().resolve(&light_colors());
1053 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
1054 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
1055 assert!(regular.saturation > clear.saturation);
1056 assert_eq!(lens.rim_style, 1.0);
1057 assert_eq!(regular.refraction_curve, 0.25);
1058 assert_eq!(lens.refraction_curve, 1.0);
1059 assert_eq!(regular.dispersion, 0.0);
1060 assert_eq!(lens.dispersion, 0.30);
1061 }
1062
1063 #[test]
1064 fn neutral_surface_helpers_follow_foreground_polarity() {
1065 assert_eq!(
1066 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
1067 Color::BLACK.with_alpha(0.08)
1068 );
1069 assert_eq!(
1070 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
1071 Color::WHITE.with_alpha(0.10)
1072 );
1073 assert_eq!(neutral_surface_lift(Color::BLACK, 0.7, -0.3), 0.7);
1074 assert_eq!(neutral_surface_lift(Color::WHITE, 0.7, -0.3), -0.3);
1075 }
1076
1077 #[test]
1078 fn dynamic_saturation_reaches_the_material_tint() {
1079 let resting = Color::from_rgb_u8(0, 199, 208);
1080 let raised = boost_tint_saturation(resting, 0.55);
1081 assert_eq!(raised.r(), 0.0);
1082 assert!(raised.g() > resting.g());
1083 assert!(raised.b() > resting.b());
1084 assert_eq!(raised.a(), resting.a());
1085 }
1086
1087 #[test]
1088 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
1089 let resolved = Glass::lens()
1090 .refraction_depth(0.72)
1091 .refraction_curve(0.8)
1092 .dispersion(0.42)
1093 .transmission_refraction(0.35)
1094 .meniscus_absorption(0.3)
1095 .blur_radius(3.0)
1096 .tint(Color::BLACK.with_alpha(0.8))
1097 .resolve(&light_colors());
1098 let effect = resolved.backdrop_effect(
1099 2.0,
1100 GlassDynamics {
1101 highlight_boost: 0.2,
1102 saturation_boost: 0.35,
1103 tint_alpha_multiplier: Some(0.25),
1104 ..Default::default()
1105 },
1106 );
1107 let RenderEffect::Chain { first, .. } = &effect else {
1108 panic!("macroscopic frost must precede the wcKSRD optical pass");
1109 };
1110 assert!(matches!(
1111 first.as_ref(),
1112 RenderEffect::Blur {
1113 radius_x: 4.0,
1114 radius_y: 4.0,
1115 ..
1116 }
1117 ));
1118 let shader = terminal_shader(effect);
1119 assert_eq!(shader.uniforms()[9], 0.72);
1120 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1121 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
1122 assert_eq!(
1123 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
1124 0.35
1125 );
1126 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
1127 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
1128 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
1129 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
1130 assert_eq!(
1131 shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
1132 WCKSRD_OPTICAL_BLUR_RADIUS_PX
1133 );
1134 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
1135 }
1136
1137 #[test]
1138 fn raised_tint_density_stays_premultiplied_alpha_safe() {
1139 let effect = Glass::lens()
1140 .tint(Color::WHITE.with_alpha(0.8))
1141 .resolve(&light_colors())
1142 .backdrop_effect(
1143 1.0,
1144 GlassDynamics {
1145 tint_alpha_multiplier: Some(2.0),
1146 ..Default::default()
1147 },
1148 );
1149 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
1150 }
1151
1152 #[test]
1153 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
1154 let resolved = Glass::lens()
1155 .refraction_depth(0.72)
1156 .refraction_curve(0.8)
1157 .dispersion(0.42)
1158 .blur_radius(3.0)
1159 .saturation(1.4)
1160 .highlight(0.6)
1161 .lift(0.2)
1162 .tint(Color::BLACK.with_alpha(0.8))
1163 .no_clip()
1164 .resolve(&light_colors());
1165 let morph = GlassMorph {
1166 node_size: (120.0, 72.0),
1167 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
1168 wobble_amplitude: 3.0,
1169 bulge_amplitude: 4.0,
1170 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
1171 ..Default::default()
1172 };
1173
1174 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
1175 2.0,
1176 GlassDynamics {
1177 activity: Some(0.0),
1178 morph: Some(morph),
1179 ..Default::default()
1180 },
1181 ) else {
1182 panic!("material must resolve to the shared wcKSRD shader");
1183 };
1184 let uniforms = shader.uniforms();
1185 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
1186 assert_eq!(uniforms[9], 0.0);
1187 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
1188 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
1189 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
1190 assert_eq!(uniforms[11], 0.0);
1191 assert_eq!(uniforms[17], 0.0);
1192 assert_eq!(uniforms[18], 1.0);
1193 assert_eq!(uniforms[20], 0.0);
1194 assert_eq!(uniforms[21], 0.0);
1195 assert_eq!(uniforms[24], 1.0);
1196 assert_eq!(uniforms[28], 0.0);
1197 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1198 assert_eq!(uniforms[91], 0.0);
1199 assert_eq!(uniforms[102], 0.0);
1200 assert_eq!(
1201 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1202 &[0.0, 0.0, 0.0, 0.0]
1203 );
1204 assert_eq!(uniforms[32], 0.0);
1205 assert_eq!(uniforms[26], 0.0);
1206 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
1207 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
1208 }
1209
1210 #[test]
1211 fn resting_surface_tint_survives_zero_optical_activity() {
1212 let tint = Color::BLACK.with_alpha(0.11);
1213 let RenderEffect::Shader { shader } = Glass::lens()
1214 .no_clip()
1215 .resolve(&light_colors())
1216 .backdrop_effect(
1217 1.0,
1218 GlassDynamics {
1219 activity: Some(0.0),
1220 resting_tint: Some(tint),
1221 ..Default::default()
1222 },
1223 )
1224 else {
1225 panic!("resting surface must use the shared wcKSRD shader");
1226 };
1227 assert_eq!(
1228 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1229 &[tint.r(), tint.g(), tint.b(), tint.a()]
1230 );
1231 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
1232 }
1233
1234 #[test]
1235 fn full_optical_activity_preserves_the_resolved_material() {
1236 let resolved = Glass::lens()
1237 .refraction_depth(0.72)
1238 .refraction_curve(0.8)
1239 .dispersion(0.42)
1240 .blur_radius(3.0)
1241 .resolve(&light_colors());
1242 let shader = terminal_shader(resolved.backdrop_effect(
1243 2.0,
1244 GlassDynamics {
1245 activity: Some(1.0),
1246 ..Default::default()
1247 },
1248 ));
1249 let uniforms = shader.uniforms();
1250 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
1251 assert_eq!(uniforms[9], 0.72);
1252 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1253 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
1254 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
1255 assert_eq!(
1256 uniforms[GLASS_BLUR_RADIUS_UNIFORM],
1257 WCKSRD_OPTICAL_BLUR_RADIUS_PX
1258 );
1259 }
1260
1261 #[test]
1262 fn morph_geometry_and_incompressible_strain_are_packed() {
1263 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
1264 let morph = GlassMorph {
1265 node_size: (78.0, 59.0),
1266 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
1267 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
1268 glue: 8.0,
1269 wobble_amplitude: 1.0,
1270 wobble_phase: 0.5,
1271 bulge_amplitude: 2.0,
1272 bulge_direction: 0.25,
1273 ellipse_blend: 0.3,
1274 deformation: Some(deformation),
1275 };
1276 let RenderEffect::Shader { shader } = Glass::lens()
1277 .no_clip()
1278 .resolve(&light_colors())
1279 .backdrop_effect(
1280 1.0,
1281 GlassDynamics {
1282 morph: Some(morph),
1283 ..Default::default()
1284 },
1285 )
1286 else {
1287 panic!("morph must use the shared wcKSRD shader");
1288 };
1289 let uniforms = shader.uniforms();
1290 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
1291 assert_eq!(uniforms[30], 1.0);
1292 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
1293 assert_eq!(uniforms[110], 0.3);
1294 assert!(shader.output_padding() > 0.0);
1295 }
1296}