use crate::theme::LiquidColors;
use cranpose_ui::current_density;
use cranpose_ui::Modifier;
use cranpose_ui_graphics::{
apply_glass_surface_profile, glass_surface_max_slope, Color, GlassSurfaceProfile,
GraphicsLayer, LayerShape, RenderEffect, RoundedCornerShape, RuntimeShader, LIQUID_GLASS_WGSL,
};
use std::rc::Rc;
const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
const CAPSULE_SHADER_RADIUS: f32 = -1.0;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum LiquidShape {
#[default]
Capsule,
RoundedRect(f32),
Circle,
}
impl LiquidShape {
pub fn clip_shape(&self) -> RoundedCornerShape {
match self {
LiquidShape::Capsule | LiquidShape::Circle => {
RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
}
LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
}
}
pub fn layer_shape(&self) -> LayerShape {
LayerShape::Rounded(self.clip_shape())
}
fn shader_radius_px(&self, density: f32) -> f32 {
match self {
LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
LiquidShape::RoundedRect(radius) => radius * density,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
pub enum GlassVariant {
#[default]
Regular,
Clear,
Lens,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlassShadow {
pub color: Color,
pub radius: f32,
pub offset_y: f32,
pub spread: f32,
}
impl GlassShadow {
pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
Self {
color,
radius: radius.max(0.0),
offset_y,
spread,
}
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct GlassDynamics {
pub tilt: (f32, f32),
pub highlight_boost: f32,
pub tint_alpha_multiplier: Option<f32>,
pub surface_depth_boost: f32,
pub optical_strength: f32,
pub morph: Option<GlassMorph>,
}
pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
let foreground_luma =
0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
if foreground_luma < 0.5 {
Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
} else {
Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
}
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct GlassMorph {
pub node_size: (f32, f32),
pub primary: (f32, f32, f32, f32, f32),
pub shapes: Vec<(f32, f32, f32, f32, f32)>,
pub glue: f32,
pub wobble_amplitude: f32,
pub wobble_phase: f32,
pub bulge_amplitude: f32,
pub bulge_direction: f32,
pub ellipse_blend: f32,
pub deformation: Option<GlassDeformation>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GlassDeformation {
axis: (f32, f32),
along: f32,
}
impl GlassDeformation {
pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
let axis = if length > f32::EPSILON {
(axis.0 / length, axis.1 / length)
} else {
(1.0, 0.0)
};
Self {
axis,
along: along.max(f32::EPSILON),
}
}
pub fn axis(self) -> (f32, f32) {
self.axis
}
pub fn along(self) -> f32 {
self.along
}
pub fn across(self) -> f32 {
1.0 / self.along
}
}
impl GlassMorph {
pub const MAX_SHAPES: usize = 8;
}
#[derive(Clone, Debug, PartialEq)]
pub struct Glass {
pub variant: GlassVariant,
pub shape: LiquidShape,
pub tint: Option<Color>,
pub blur_radius: Option<f32>,
pub saturation: Option<f32>,
pub chromatic_aberration: f32,
pub dispersion_axes: (f32, f32),
pub displacement: f32,
pub bezel_width: f32,
pub highlight: f32,
pub surface_profile: GlassSurfaceProfile,
pub sheen: Option<f32>,
pub lift: Option<f32>,
pub shadow: bool,
pub shadow_style: Option<GlassShadow>,
pub clip: bool,
pub foreground: Option<Color>,
pub adaptive_frost: f32,
pub content_recolor: Option<(Color, f32)>,
pub edge_fold: f32,
}
impl Glass {
pub fn regular() -> Self {
Self {
variant: GlassVariant::Regular,
shape: LiquidShape::Capsule,
tint: None,
blur_radius: None,
saturation: None,
chromatic_aberration: 0.5,
dispersion_axes: (1.0, 1.0),
displacement: 22.0,
bezel_width: 12.0,
highlight: 0.9,
surface_profile: GlassSurfaceProfile::regular(),
sheen: None,
lift: None,
shadow: true,
shadow_style: None,
clip: true,
foreground: None,
adaptive_frost: 0.65,
content_recolor: None,
edge_fold: 0.0,
}
}
pub fn clear() -> Self {
Self {
variant: GlassVariant::Clear,
..Self::regular()
}
}
pub fn lens() -> Self {
Self {
variant: GlassVariant::Lens,
shape: LiquidShape::Capsule,
tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
blur_radius: None,
saturation: None,
chromatic_aberration: 3.2,
dispersion_axes: (1.0, 1.0),
displacement: 30.0,
bezel_width: 10.0,
highlight: 1.15,
surface_profile: GlassSurfaceProfile::lens(),
sheen: None,
lift: None,
shadow: true,
shadow_style: None,
clip: true,
foreground: None,
adaptive_frost: 0.0,
content_recolor: None,
edge_fold: 0.0,
}
}
pub fn shape(mut self, shape: LiquidShape) -> Self {
self.shape = shape;
self
}
pub fn tint(mut self, tint: Color) -> Self {
self.tint = Some(tint);
self
}
pub fn blur_radius(mut self, radius_dp: f32) -> Self {
self.blur_radius = Some(radius_dp);
self
}
pub fn saturation(mut self, saturation: f32) -> Self {
self.saturation = Some(saturation);
self
}
pub fn chromatic_aberration(mut self, spread: f32) -> Self {
self.chromatic_aberration = spread;
self
}
pub fn dispersion_axes(mut self, x: f32, y: f32) -> Self {
self.dispersion_axes = (x.clamp(0.0, 4.0), y.clamp(0.0, 4.0));
self
}
pub fn displacement(mut self, displacement_px: f32) -> Self {
self.displacement = displacement_px;
self
}
pub fn bezel_width(mut self, width_dp: f32) -> Self {
self.bezel_width = width_dp.max(0.0);
self
}
pub fn highlight(mut self, highlight: f32) -> Self {
self.highlight = highlight;
self
}
pub fn surface_profile(mut self, profile: GlassSurfaceProfile) -> Self {
self.surface_profile = profile;
self
}
pub fn sheen(mut self, sheen: f32) -> Self {
self.sheen = Some(sheen);
self
}
pub fn lift(mut self, lift: f32) -> Self {
self.lift = Some(lift);
self
}
pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
self.foreground = Some(foreground);
self.adaptive_frost = strength.clamp(0.0, 1.0);
self
}
pub fn content_recolor(mut self, color: Color, strength: f32) -> Self {
self.content_recolor = Some((color, strength.clamp(0.0, 1.0)));
self
}
pub fn edge_fold(mut self, strength: f32) -> Self {
self.edge_fold = strength;
self
}
pub fn shadow(mut self, shadow: bool) -> Self {
self.shadow = shadow;
self
}
pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
self.shadow_style = Some(shadow);
self
}
pub fn no_clip(mut self) -> Self {
self.clip = false;
self
}
fn default_blur_radius(&self) -> f32 {
match self.variant {
GlassVariant::Regular => 18.0,
GlassVariant::Clear => 3.0,
GlassVariant::Lens => 0.0,
}
}
fn default_saturation(&self) -> f32 {
match self.variant {
GlassVariant::Regular => 1.5,
GlassVariant::Clear => 1.25,
GlassVariant::Lens => 1.0,
}
}
pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
(GlassVariant::Regular, false) => 0.42,
(GlassVariant::Regular, true) => -0.38,
(GlassVariant::Clear, false) => 0.12,
(GlassVariant::Clear, true) => -0.12,
(GlassVariant::Lens, false) => 0.10,
(GlassVariant::Lens, true) => -0.08,
});
let foreground = self.foreground.unwrap_or(colors.label);
let shadow = self.shadow_style.unwrap_or_else(|| {
GlassShadow::new(
Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
(GlassVariant::Lens, false) => 0.14,
(GlassVariant::Lens, true) => 0.28,
(_, false) => 0.16,
(_, true) => 0.5,
}),
if self.variant == GlassVariant::Lens {
10.0
} else {
22.0
},
if self.variant == GlassVariant::Lens {
3.0
} else {
8.0
},
if self.variant == GlassVariant::Lens {
-6.0
} else {
-2.0
},
)
});
ResolvedGlass {
shape: self.shape,
tint: self.tint.unwrap_or(colors.glass_tint),
blur_radius_dp: self
.blur_radius
.unwrap_or_else(|| self.default_blur_radius()),
saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
chromatic_aberration: self.chromatic_aberration,
dispersion_axes: self.dispersion_axes,
displacement: self.displacement,
bezel_width_dp: self.bezel_width,
highlight: self.highlight,
surface_profile: self.surface_profile,
lift,
contrast: if self.variant == GlassVariant::Lens {
1.0
} else {
1.03
},
shadow: self.shadow,
clip: self.clip,
foreground_luma: 0.2126 * foreground.r()
+ 0.7152 * foreground.g()
+ 0.0722 * foreground.b(),
adaptive_frost: self.adaptive_frost,
content_recolor: self.content_recolor.unwrap_or((Color::BLACK, 0.0)),
edge_fold: self.edge_fold,
sheen: self.sheen.unwrap_or(if self.variant == GlassVariant::Lens {
0.05
} else {
1.0
}),
rim_style: if self.variant == GlassVariant::Lens {
1.0
} else {
0.0
},
shadow_color: shadow.color,
shadow_radius: shadow.radius,
shadow_offset_y: shadow.offset_y,
shadow_spread: shadow.spread,
}
}
}
impl Default for Glass {
fn default() -> Self {
Self::regular()
}
}
#[derive(Clone, Debug, PartialEq)]
pub(crate) struct ResolvedGlass {
pub shape: LiquidShape,
pub tint: Color,
pub blur_radius_dp: f32,
pub saturation: f32,
pub chromatic_aberration: f32,
pub dispersion_axes: (f32, f32),
pub displacement: f32,
pub bezel_width_dp: f32,
pub highlight: f32,
pub surface_profile: GlassSurfaceProfile,
pub lift: f32,
pub contrast: f32,
pub shadow: bool,
pub clip: bool,
pub sheen: f32,
pub rim_style: f32,
pub foreground_luma: f32,
pub adaptive_frost: f32,
pub content_recolor: (Color, f32),
pub edge_fold: f32,
pub shadow_color: Color,
pub shadow_radius: f32,
pub shadow_offset_y: f32,
pub shadow_spread: f32,
}
impl ResolvedGlass {
pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
self.runtime_effect(density, dynamics, false)
}
fn content_mask_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
self.runtime_effect(density, dynamics, true)
}
fn runtime_effect(
&self,
density: f32,
dynamics: GlassDynamics,
content_mask: bool,
) -> RenderEffect {
let density = density.max(f32::EPSILON);
let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
if let Some(morph) = dynamics.morph.as_ref() {
let (node_w, node_h) = morph.node_size;
let (cx, cy, w, h, radius) = morph.primary;
shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
shader.set_float2(2, cx, cy);
shader.set_float2(4, w, h);
shader.set_float(6, radius);
let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
shader.set_float(30, count as f32);
for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
let base = 36 + index * 5;
shader.set_float(base, *sx);
shader.set_float(base + 1, *sy);
shader.set_float(base + 2, *sw);
shader.set_float(base + 3, *sh);
shader.set_float(base + 4, *sr);
}
shader.set_float(31, morph.glue);
shader.set_float(32, morph.wobble_amplitude);
shader.set_float(33, morph.wobble_phase);
shader.set_float(26, morph.bulge_amplitude);
shader.set_float(27, morph.bulge_direction);
shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0));
if let Some(deformation) = morph.deformation {
let axis = deformation.axis();
shader.set_float2(106, axis.0, axis.1);
shader.set_float(108, deformation.along());
shader.set_float(109, deformation.across());
} else {
shader.set_float2(106, 1.0, 0.0);
shader.set_float2(108, 1.0, 1.0);
}
shader.set_float(7, self.bezel_width_dp);
} else {
shader.set_float2(0, 0.0, 0.0);
shader.set_float(6, self.shape.shader_radius_px(density));
shader.set_float(7, self.bezel_width_dp * density);
}
shader.set_float(8, self.displacement);
shader.set_float(9, 1.5);
shader.set_float(
11,
(self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0),
);
shader.set_float2(12, dynamics.tilt.0, dynamics.tilt.1);
shader.set_float4(
14,
self.tint.r(),
self.tint.g(),
self.tint.b(),
self.tint.a()
* dynamics
.tint_alpha_multiplier
.unwrap_or(1.0)
.clamp(0.0, 1.0),
);
shader.set_float(18, self.saturation);
shader.set_float(19, self.chromatic_aberration);
shader.set_float2(118, self.dispersion_axes.0, self.dispersion_axes.1);
shader.set_float(20, self.lift);
shader.set_float(21, 0.5);
shader.set_float2(22, 0.0, 1.0);
shader.set_float(24, self.contrast);
shader.set_float(29, self.sheen);
shader.set_float(28, self.rim_style);
shader.set_float(111, dynamics.optical_strength.clamp(0.0, 1.0));
shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
let profile_unit_scale = if dynamics.morph.is_some() {
1.0
} else {
density
};
apply_glass_surface_profile(
&mut shader,
self.surface_profile,
profile_unit_scale * (1.0 + dynamics.surface_depth_boost).max(0.0),
);
shader.set_float(91, self.adaptive_frost);
shader.set_float(92, self.edge_fold);
shader.set_float(97, self.foreground_luma);
shader.set_float(98, self.content_recolor.1);
shader.set_float4(
99,
self.content_recolor.0.r(),
self.content_recolor.0.g(),
self.content_recolor.0.b(),
0.0,
);
let dynamic_shadow = !self.clip && self.shadow;
shader.set_float(
102,
if dynamic_shadow {
self.shadow_color.a() * 0.55
} else {
0.0
},
);
shader.set_float(103, self.shadow_radius);
shader.set_float(104, self.shadow_offset_y);
shader.set_float(105, self.shadow_spread);
let morph_pad = dynamics
.morph
.as_ref()
.map(|morph| {
let (px, py, pw, ph, _) = morph.primary;
let (left, top) = (px - pw * 0.5, py - ph * 0.5);
let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
let mut shape_reach = 0.0f32;
for (sx, sy, sw, sh, _) in &morph.shapes {
let reach_x = ((sx + sw * 0.5) - right)
.max(left - (sx - sw * 0.5))
.max(0.0);
let reach_y = ((sy + sh * 0.5) - bottom)
.max(top - (sy - sh * 0.5))
.max(0.0);
shape_reach = shape_reach.max(reach_x.max(reach_y));
}
let glue_pad = if morph.shapes.is_empty() {
0.0
} else {
morph.glue * 2.0
};
morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
})
.unwrap_or(0.0);
shader.set_input_padding(self.input_padding() + morph_pad);
if dynamics.morph.is_some() {
let shadow_reach = if dynamic_shadow {
self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
} else {
0.0
};
shader.set_output_padding(morph_pad + shadow_reach + 4.0);
}
let lens = RenderEffect::runtime_shader(shader);
if content_mask {
return lens;
}
let blur_px = self.blur_radius_dp * density;
if blur_px > 0.5 {
RenderEffect::blur(blur_px).then(lens)
} else {
lens
}
}
fn input_padding(&self) -> f32 {
let bend = 1.0 - 1.0 / 1.5_f32;
let slope = glass_surface_max_slope(self.surface_profile, self.bezel_width_dp.max(1.0));
let reach = slope + std::f32::consts::SQRT_2 * 0.18;
let axis_scale = self.dispersion_axes.0.max(self.dispersion_axes.1);
let spread = 1.0 + axis_scale * self.chromatic_aberration.max(0.0) * 0.5;
let displacement = reach * bend * self.displacement.max(0.0) * slope * spread;
displacement.ceil() + 2.0
}
}
pub trait LiquidModifierExt {
fn glass_effect(self, glass: Glass) -> Modifier;
fn glass_effect_with(
self,
glass: Glass,
dynamics: impl Fn() -> GlassDynamics + 'static,
) -> Modifier;
}
impl LiquidModifierExt for Modifier {
fn glass_effect(self, glass: Glass) -> Modifier {
self.glass_effect_with(glass, GlassDynamics::default)
}
fn glass_effect_with(
self,
glass: Glass,
dynamics: impl Fn() -> GlassDynamics + 'static,
) -> Modifier {
let colors = crate::theme::liquid_colors();
let resolved = Rc::new(glass.resolve(&colors));
let shape = resolved.shape;
let mut modifier = self;
if resolved.shadow && resolved.clip {
let shadow_color = resolved.shadow_color;
let (radius, offset_y, spread) = (
resolved.shadow_radius,
resolved.shadow_offset_y,
resolved.shadow_spread,
);
modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
scope.radius = radius;
scope.spread = spread;
scope.offset.y = offset_y;
scope.color = shadow_color;
scope.cutout = true;
});
}
let layer_resolved = Rc::clone(&resolved);
let clip = resolved.clip;
modifier.graphics_layer(move || {
let density = current_density();
let frame = dynamics();
let render_effect = (!clip && frame.morph.is_some())
.then(|| layer_resolved.content_mask_effect(density, frame.clone()));
GraphicsLayer {
backdrop_effect: Some(layer_resolved.backdrop_effect(density, frame)),
render_effect,
shape: shape.layer_shape(),
clip,
..Default::default()
}
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn light_colors() -> LiquidColors {
LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
}
#[test]
fn regular_glass_resolves_frosted_defaults() {
let colors = light_colors();
let resolved = Glass::regular().resolve(&colors);
assert!(resolved.blur_radius_dp > 5.0, "regular material is frosted");
assert!(resolved.saturation > 1.3, "regular material is vibrant");
assert!(resolved.lift > 0.0, "light scheme lifts toward white");
assert_eq!(resolved.adaptive_frost, 0.65);
let label_luma =
0.2126 * colors.label.r() + 0.7152 * colors.label.g() + 0.0722 * colors.label.b();
assert!((resolved.foreground_luma - label_luma).abs() < 1e-6);
}
#[test]
fn neutral_surface_tint_follows_foreground_polarity() {
let light_surface = neutral_surface_tint(Color::BLACK, 0.08, 0.10);
assert_eq!(light_surface, Color::BLACK.with_alpha(0.08));
let dark_surface = neutral_surface_tint(Color::WHITE, 0.08, 0.10);
assert_eq!(dark_surface, Color::WHITE.with_alpha(0.10));
}
#[test]
fn dynamics_can_clear_a_material_tint_without_switching_bodies() {
let tint = Color::BLACK.with_alpha(0.8);
let resolved = Glass::lens().tint(tint).resolve(&light_colors());
let effect = resolved.backdrop_effect(
1.0,
GlassDynamics {
tint_alpha_multiplier: Some(0.25),
..Default::default()
},
);
let RenderEffect::Shader { shader } = effect else {
panic!("lens glass must be a bare shader");
};
assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
}
#[test]
fn clear_glass_is_barely_frosted() {
let resolved = Glass::clear().resolve(&light_colors());
assert!(resolved.blur_radius_dp < 5.0);
assert!(resolved.saturation < 1.3);
}
#[test]
fn interactive_lens_uses_a_compact_signed_meniscus() {
let glass = Glass::lens();
assert!(
(9.0..=11.0).contains(&glass.bezel_width),
"the target lens has one thin raised-lip/recess section, got {}dp",
glass.bezel_width
);
let resolved = glass.resolve(&light_colors());
let RenderEffect::Shader { shader } =
resolved.backdrop_effect(1.0, GlassDynamics::default())
else {
panic!("lens glass must be a bare shader");
};
assert_eq!(shader.uniforms()[7], glass.bezel_width);
assert_eq!(Glass::lens().bezel_width(7.5).bezel_width, 7.5);
assert_eq!(Glass::lens().bezel_width(-1.0).bezel_width, 0.0);
assert_eq!(
Glass::lens().dispersion_axes(-1.0, 5.0).dispersion_axes,
(0.0, 4.0)
);
}
#[test]
fn dark_scheme_sinks_lift_and_tint() {
let dark = LiquidColors::dark(Color::from_rgb_u8(0, 122, 255));
let resolved = Glass::regular().resolve(&dark);
assert!(resolved.lift < 0.0, "dark scheme sinks toward black");
assert_eq!(resolved.tint, dark.glass_tint);
}
#[test]
fn explicit_tint_overrides_theme() {
let tint = Color::from_rgba_u8(0, 122, 255, 60);
let resolved = Glass::regular().tint(tint).resolve(&light_colors());
assert_eq!(resolved.tint, tint);
}
#[test]
fn explicit_sheen_overrides_the_variant_default() {
let resolved = Glass::lens().sheen(0.85).resolve(&light_colors());
assert_eq!(resolved.sheen, 0.85);
let effect = resolved.backdrop_effect(1.0, GlassDynamics::default());
let RenderEffect::Shader { shader } = effect else {
panic!("lens glass must be a bare shader");
};
assert_eq!(shader.uniforms()[29], 0.85);
}
#[test]
fn physical_surface_profile_is_packed_for_the_shader() {
let x = cranpose_ui_graphics::GlassProfileCurve::from_points(&[
(0.0, 0.1),
(0.5, 0.2),
(1.0, 0.8),
])
.expect("X-Z profile");
let y = cranpose_ui_graphics::GlassProfileCurve::from_points(&[
(0.0, 0.1),
(0.6, 0.3),
(1.0, 0.7),
])
.expect("Y-Z profile");
let profile = GlassSurfaceProfile::new(x, y, 4.0, 3.0)
.and_then(|profile| profile.with_axis_coupling(0.3))
.expect("surface profile");
let resolved = Glass::lens()
.surface_profile(profile)
.dispersion_axes(0.25, 2.0)
.resolve(&light_colors());
assert_eq!(resolved.surface_profile, profile);
assert_eq!(resolved.dispersion_axes, (0.25, 2.0));
let effect = resolved.backdrop_effect(
1.0,
GlassDynamics {
surface_depth_boost: 0.25,
..Default::default()
},
);
let RenderEffect::Shader { shader } = effect else {
panic!("lens glass must be a bare shader");
};
let uniforms = shader.uniforms();
assert_eq!(&uniforms[113..118], &[1.0, 3.0, 3.0, 3.0, 5.0]);
assert_eq!(&uniforms[118..120], &[0.25, 2.0]);
assert_eq!(uniforms[120], 0.0);
assert_eq!(uniforms[121], 0.1);
assert_eq!(uniforms[126], 1.0);
assert_eq!(uniforms[127], 0.8);
assert_eq!(uniforms[140], 0.0);
assert_eq!(uniforms[141], 0.1);
assert_eq!(uniforms[146], 1.0);
assert_eq!(uniforms[147], 0.7);
assert_eq!(uniforms[158], 0.3);
}
#[test]
fn adaptive_frost_and_inside_recolor_are_packed() {
let foreground = Color::WHITE;
let accent = Color::from_rgb_u8(0, 122, 255);
let resolved = Glass::lens()
.adaptive_frost(foreground, 0.75)
.content_recolor(accent, 0.9)
.resolve(&light_colors());
let effect = resolved.backdrop_effect(1.0, GlassDynamics::default());
let RenderEffect::Shader { shader } = effect else {
panic!("lens glass must be a bare shader");
};
let uniforms = shader.uniforms();
assert_eq!(uniforms[91], 0.75);
assert!((uniforms[97] - 1.0).abs() < 1e-6);
assert_eq!(uniforms[98], 0.9);
assert_eq!(&uniforms[99..102], &[accent.r(), accent.g(), accent.b()]);
let clamped = Glass::lens()
.adaptive_frost(foreground, 2.0)
.content_recolor(accent, -1.0)
.resolve(&light_colors());
assert_eq!(clamped.adaptive_frost, 1.0);
assert_eq!(clamped.content_recolor.1, 0.0);
}
#[test]
fn cover_mode_effect_chains_blur_then_lens() {
let resolved = Glass::regular().resolve(&light_colors());
let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
let RenderEffect::Chain { first, second } = effect else {
panic!("regular glass must chain blur → lens");
};
assert!(matches!(*first, RenderEffect::Blur { .. }));
let RenderEffect::Shader { shader } = *second else {
panic!("second stage must be the lens shader");
};
let uniforms = shader.uniforms();
assert_eq!(uniforms[0], 0.0, "cover mode zeroes the container size");
assert_eq!(uniforms[6], CAPSULE_SHADER_RADIUS, "capsule sentinel");
assert!(shader.input_padding() > 0.0);
}
#[test]
fn morph_glass_packs_dp_geometry_with_node_size_container() {
let resolved = Glass::lens().no_clip().resolve(&light_colors());
let dynamics = GlassDynamics {
optical_strength: 0.3,
morph: Some(GlassMorph {
node_size: (78.0, 59.0),
primary: (39.0, 29.5, 58.0, 39.0, -1.0),
shapes: vec![(100.0, 29.5, 44.0, 44.0, -1.0)],
glue: 12.0,
ellipse_blend: 0.75,
deformation: Some(GlassDeformation::incompressible((3.0, 4.0), 1.25)),
..Default::default()
}),
..Default::default()
};
let effect = resolved.backdrop_effect(2.0, dynamics.clone());
let RenderEffect::Shader { shader } = effect else {
panic!("lens glass must be a bare shader (no frost blur)");
};
let uniforms = shader.uniforms();
assert_eq!(&uniforms[0..2], &[78.0, 59.0], "container = node size dp");
assert_eq!(&uniforms[2..6], &[39.0, 29.5, 58.0, 39.0], "primary dp");
assert_eq!(uniforms[6], -1.0, "capsule sentinel unscaled");
assert_eq!(uniforms[31], 12.0, "glue dp");
assert_eq!(&uniforms[36..40], &[100.0, 29.5, 44.0, 44.0], "shape dp");
assert_eq!(&uniforms[106..108], &[0.6, 0.8], "normalized strain axis");
assert_eq!(&uniforms[108..110], &[1.25, 0.8], "reciprocal scales");
assert_eq!(uniforms[110], 0.75, "primary ellipse blend");
assert_eq!(uniforms[111], 0.3, "dynamic optical strength");
assert_eq!(uniforms[112], 0.0, "backdrop mode must preserve the lens");
assert!(uniforms[102] > 0.0, "morph shadow is generated by the SDF");
assert_eq!(uniforms[103], resolved.shadow_radius);
assert_eq!(uniforms[104], resolved.shadow_offset_y);
assert_eq!(uniforms[105], resolved.shadow_spread);
assert!(
shader.output_padding() > resolved.shadow_radius,
"morph output padding must include the dynamic shadow"
);
let mask = resolved.content_mask_effect(2.0, dynamics);
let RenderEffect::Shader {
shader: mask_shader,
} = mask
else {
panic!("the exact morph content mask must be one shader stage");
};
let mask_uniforms = mask_shader.uniforms();
assert_eq!(&mask_uniforms[0..6], &uniforms[0..6]);
assert_eq!(&mask_uniforms[30..40], &uniforms[30..40]);
assert_eq!(&mask_uniforms[106..111], &uniforms[106..111]);
assert_eq!(mask_uniforms[112], 1.0, "content mask mode is explicit");
}
#[test]
fn explicit_shadow_style_controls_static_and_morph_shadow_geometry() {
let style = GlassShadow::new(Color::BLACK.with_alpha(0.07), 32.0, 10.0, 1.5);
let resolved = Glass::regular()
.shadow_style(style)
.no_clip()
.resolve(&light_colors());
assert_eq!(resolved.shadow_color, style.color);
assert_eq!(resolved.shadow_radius, 32.0);
assert_eq!(resolved.shadow_offset_y, 10.0);
assert_eq!(resolved.shadow_spread, 1.5);
assert_eq!(GlassShadow::new(Color::BLACK, -2.0, 0.0, 0.0).radius, 0.0);
}
#[test]
fn clear_variant_skips_heavy_blur() {
let resolved = Glass::clear().blur_radius(0.0).resolve(&light_colors());
let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
assert!(matches!(effect, RenderEffect::Shader { .. }));
}
#[test]
fn rounded_rect_radius_scales_with_density() {
let resolved = Glass::regular()
.shape(LiquidShape::RoundedRect(16.0))
.resolve(&light_colors());
let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
let RenderEffect::Chain { second, .. } = effect else {
panic!("expected chain");
};
let RenderEffect::Shader { shader } = *second else {
panic!("expected lens");
};
assert_eq!(shader.uniforms()[6], 32.0, "16dp at 2x density = 32px");
}
}