Skip to main content

cranpose_liquid/
material.rs

1//! The Liquid Glass material: Gaussian backdrop frost followed by wcKSRD
2//! refraction applied to a composable's own bounds through
3//! [`LiquidModifierExt::glass_effect`] — the analogue of SwiftUI's
4//! `.glassEffect(_:in:)`.
5
6use std::{
7    cell::{Cell, RefCell},
8    sync::OnceLock,
9};
10
11use cranpose_ui::{Modifier, current_density};
12use cranpose_ui_graphics::{
13    Color, GLASS_ACTIVITY_UNIFORM, GLASS_ADAPTIVE_FROST_UNIFORM, GLASS_ADAPTIVE_TONE_UNIFORM,
14    GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM, GLASS_EDGE_REFRACTION_REACH_UNIFORM,
15    GLASS_EDGE_RETURN_DEPTH_UNIFORM, GLASS_EDGE_SPECTRUM_UNIFORM, GLASS_EFFECT_DENSITY_UNIFORM,
16    GLASS_FACE_LIGHTING_OFF_UNIFORM, GLASS_FACE_RESPONSE_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM,
17    GLASS_INNER_SHADOW_UNIFORM, GLASS_KEY_FILL_UNIFORM, GLASS_LAYER_CLIPPED_UNIFORM,
18    GLASS_LIGHT_DIRECTION_UNIFORM, GLASS_MENISCUS_ABSORPTION_UNIFORM,
19    GLASS_OPTICAL_PROJECTION_UNIFORM, GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
20    GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
21    GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM,
22    GLASS_REFRACTION_MODE_UNIFORM, GLASS_RESTING_TINT_UNIFORM, GLASS_TOUCH_RADIUS_UNIFORM,
23    GLASS_TRANSMISSION_REFRACTION_UNIFORM, GraphicsLayer, LIQUID_GLASS_WGSL, LayerShape, Rect,
24    RenderEffect, RoundedCornerShape, RuntimeShader, TileMode, liquid_glass_runtime_effect,
25    specialize_liquid_glass,
26};
27
28use crate::{appearance::GlassTintAmount, theme::LiquidColors};
29
30thread_local! {
31    static GLASS_LIGHT_RETURN: Cell<(f32, f32)> = const { Cell::new((0.0, 1.0)) };
32}
33
34/// Sets the ambient glass light return direction (screen space, need not be
35/// normalized — the shader normalizes). Feed device attitude here.
36pub fn set_glass_light_direction(direction: (f32, f32)) {
37    GLASS_LIGHT_RETURN.with(|cell| cell.set(direction));
38}
39
40/// The current ambient glass light return direction.
41pub fn glass_light_direction() -> (f32, f32) {
42    GLASS_LIGHT_RETURN.with(|cell| cell.get())
43}
44
45const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
46
47const CAPSULE_SHADER_RADIUS: f32 = -1.0;
48
49const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
50
51/// The shape of a glass element (also its clip and shadow shape).
52#[derive(Clone, Copy, Debug, PartialEq, Default)]
53pub enum LiquidShape {
54    /// A pill: corner radius follows the smaller half-extent.
55    #[default]
56    Capsule,
57    /// Rounded rectangle with the radius in dp.
58    RoundedRect(f32),
59    /// A circle (capsule of a square node).
60    Circle,
61}
62
63impl LiquidShape {
64    /// The clip shape handed to the graphics layer.
65    pub fn clip_shape(&self) -> RoundedCornerShape {
66        match self {
67            LiquidShape::Capsule | LiquidShape::Circle => {
68                RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
69            }
70            LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
71        }
72    }
73
74    /// The layer shape (clip + shadow geometry).
75    pub fn layer_shape(&self) -> LayerShape {
76        LayerShape::Rounded(self.clip_shape())
77    }
78
79    fn shader_radius_px(&self, density: f32) -> f32 {
80        match self {
81            LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
82            LiquidShape::RoundedRect(radius) => radius * density,
83        }
84    }
85}
86
87/// Material variant, mirroring SwiftUI's `.regular` / `.clear` glass, plus
88/// the interactive lens.
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
90pub enum GlassVariant {
91    /// Frosted: strong backdrop blur, vibrancy boost, scheme-adaptive lift.
92    #[default]
93    Regular,
94    /// Transparent: minimal blur, mild vibrancy — for media-rich backdrops.
95    Clear,
96    /// The interactive magnifying bubble (drag lenses, flying selection):
97    /// no frost, no tone shift, a full-element dome and pronounced rainbow
98    /// dispersion at the rim.
99    Lens,
100}
101
102/// The projection used to transmit the captured backdrop through glass.
103#[derive(Clone, Copy, Debug, PartialEq, Default)]
104pub enum GlassRefraction {
105    /// A radial projection toward the optical axis.
106    #[default]
107    Radial,
108    /// A projection perpendicular to the nearest surface edge.
109    Surface {
110        /// Maximum inward sample displacement in dp, independent of its falloff depth.
111        reach_dp: f32,
112    },
113    /// Successive outward and inward sample displacements with circular falloff profiles.
114    /// The return amplitude is 17.5/9 times the outward amplitude. Its default depth
115    /// is 7/36 of the outer profile; [`GlassDynamics::edge_return_depth_dp`] overrides it.
116    EdgeLens {
117        /// Outward warp amplitude in dp; refraction depth sets its falloff height.
118        reach_dp: f32,
119    },
120}
121
122impl GlassRefraction {
123    fn shader_parameters(self) -> (f32, f32) {
124        match self {
125            Self::Radial => (0.0, 0.0),
126            Self::Surface { reach_dp } => (1.0, normalized_refraction_reach(reach_dp)),
127            Self::EdgeLens { reach_dp } => (2.0, normalized_refraction_reach(reach_dp)),
128        }
129    }
130}
131
132fn normalized_refraction_reach(reach_dp: f32) -> f32 {
133    if reach_dp.is_finite() {
134        reach_dp.max(0.0)
135    } else {
136        0.0
137    }
138}
139
140/// Shadow owned by a glass surface. Morphing glass evaluates the same live
141/// SDF for this shadow; clipped static glass forwards the values to the layer
142/// shadow primitive.
143#[derive(Clone, Copy, Debug, PartialEq)]
144pub struct GlassShadow {
145    pub color: Color,
146    pub radius: f32,
147    pub offset_y: f32,
148    pub spread: f32,
149}
150
151impl GlassShadow {
152    pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
153        Self {
154            color,
155            radius: radius.max(0.0),
156            offset_y,
157            spread,
158        }
159    }
160
161    fn uniforms(self) -> Option<[f32; 8]> {
162        let values = [
163            self.color.r(),
164            self.color.g(),
165            self.color.b(),
166            self.color.a(),
167            self.radius,
168            self.offset_y,
169            self.spread,
170            1.0,
171        ];
172        (values.iter().all(|value| value.is_finite())
173            && self.radius >= 0.0
174            && (0.0..=1.0).contains(&self.color.a()))
175        .then_some(values)
176    }
177}
178
179/// Per-frame motion inputs for an interactive glass element, read lazily at
180/// scene-build time (no recomposition per frame).
181#[derive(Clone, Debug, PartialEq, Default)]
182pub struct GlassDynamics {
183    /// Independent inner return depth for an edge lens, in dp. Zero removes its depth.
184    /// Nonfinite or negative values preserve the material's default depth ratio.
185    pub edge_return_depth_dp: Option<f32>,
186    /// Horizontal and vertical projection of the complete glass field about its primary center.
187    /// Refraction, lighting and all scene shapes share the projection. Missing, nonfinite or
188    /// nonpositive scales resolve to identity. This does not magnify the underlying backdrop.
189    pub optical_projection: Option<(f32, f32)>,
190    /// Continuous optical presence. `None` preserves the resolved material;
191    /// `Some(0)` is an exact backdrop identity while retaining the same node,
192    /// SDF, and pointer ride path.
193    pub activity: Option<f32>,
194    /// Base tint of the same persistent SDF surface at zero optical activity.
195    /// Its alpha cross-fades out as the refractive material rises.
196    pub resting_tint: Option<Color>,
197    /// Extra specular intensity (0 = spec default; e.g. press boost).
198    pub highlight_boost: f32,
199    /// Per-frame saturation added to the resolved material. Interactive
200    /// surfaces use this to raise chroma without changing their base tint.
201    pub saturation_boost: f32,
202    /// Optional per-frame multiplier for the material tint alpha. Values
203    /// below one clear a resting wash; values above one densify a raised tint.
204    pub tint_alpha_multiplier: Option<f32>,
205    /// Shape morph: when set, the glass geometry is these node-local rects
206    /// instead of the node cover — the shapeshift channel.
207    pub morph: Option<GlassMorph>,
208    /// Touch glow: `(x_dp, y_dp, intensity)` in node-local dp. A pressed
209    /// surface concentrates saturation and a soft light in a radial
210    /// gradient under the finger (never a flat recolor).
211    pub touch: Option<(f32, f32, f32)>,
212    /// Radius of the touch glow in dp; absent, nonpositive and nonfinite values use 58 dp.
213    pub touch_radius_dp: Option<f32>,
214    /// Dome press depth: how squashed the interactive dome is. At 1 the
215    /// pressed dome refracts deep and vivid (wide rim band, strong
216    /// chromatic split — the reference toggle's gray-hold rainbow); toward
217    /// 0 the released bead relaxes shallow and its split fades with it
218    /// (the reference's thin settled ring). `None` = 1.
219    pub press_depth: Option<f32>,
220}
221
222impl GlassDynamics {
223    fn projection(&self) -> (f32, f32) {
224        self.optical_projection
225            .filter(|(x, y)| x.is_finite() && y.is_finite() && *x > 0.0 && *y > 0.0)
226            .unwrap_or((1.0, 1.0))
227    }
228
229    /// The universal touched-up state (user-directed law): a pressed
230    /// liquid surface comes closer to the user while its OWN colors go
231    /// HDR-bright and saturated, with a radial glow following the finger.
232    /// Never a repaint with alternate colors — the boosts amplify whatever
233    /// the surface already is. `press` is 0..1; `finger` is node-local dp
234    /// (falls back to `center`, the surface's own heart).
235    pub fn touched_up(
236        mut self,
237        press: f32,
238        finger: Option<(f32, f32)>,
239        center: (f32, f32),
240    ) -> Self {
241        let press = press.clamp(0.0, 1.0);
242        if press <= 0.0 {
243            return self;
244        }
245        self.highlight_boost += 0.85 * press;
246        self.saturation_boost += 0.45 * press;
247        let (fx, fy) = finger.unwrap_or(center);
248        let carried = self.touch.map(|(_, _, i)| i).unwrap_or(0.0);
249        self.touch = Some((fx, fy, (carried + press).clamp(0.0, 1.0)));
250        self
251    }
252}
253
254fn foreground_is_dark(foreground: Color) -> bool {
255    let foreground_luma =
256        0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
257    foreground_luma < 0.5
258}
259
260fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
261    if boost.abs() <= f32::EPSILON {
262        return tint;
263    }
264    let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
265    let saturation = (1.0 + boost).max(0.0);
266    Color::rgba(
267        (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
268        (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
269        (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
270        tint.a(),
271    )
272}
273
274pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
275    if foreground_is_dark(foreground) {
276        Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
277    } else {
278        Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
279    }
280}
281
282/// A liquid shapeshift frame: the primary shape plus any number of nearby
283/// glass shapes, ALL smooth-unioned into one field — liquid glass glues to
284/// whatever glass it passes near (a growing menu necks with a neighboring
285/// button, the drag lens merges with the search circle). Up to
286/// [`GlassMorph::MAX_SHAPES`] extra shapes; an angular wobble makes the
287/// mid-flight field bubble like a droplet. All geometry is node-local dp:
288/// `(center_x, center_y, width, height, corner_radius)`; radius sentinel
289/// `-1` means capsule; `-2` means SUBTRACT capsule — the shape carves a
290/// smooth hole in the field (the growing menu leaves its anchor button
291/// crisp on top until it swallows it).
292#[derive(Clone, Debug, PartialEq, Default)]
293pub struct GlassMorph {
294    /// The glass node's own size in dp. The shader receives all morph
295    /// geometry in dp and derives px-per-dp from the renderer-injected node
296    /// pixel rect divided by this — geometry then lands correctly at ANY
297    /// render scale (live window density, robot captures at 1.0, fractional
298    /// desktop scales). The authoring widget always knows its node size.
299    pub node_size: (f32, f32),
300    pub primary: (f32, f32, f32, f32, f32),
301    /// Nearby glass shapes participating in the field.
302    pub shapes: Vec<(f32, f32, f32, f32, f32)>,
303    /// Smooth-union glue radius (dp): shapes within it neck together.
304    pub glue: f32,
305    /// Wobble amplitude (dp) and phase (radians).
306    pub wobble_amplitude: f32,
307    pub wobble_phase: f32,
308    /// Viscous leading-edge bulge: while the shape travels or inflates, its
309    /// side facing `bulge_direction` (radians, math convention) swells like a
310    /// pulled droplet. Amplitude in dp, usually driven by morph velocity.
311    pub bulge_amplitude: f32,
312    pub bulge_direction: f32,
313    /// Blends the primary rounded-rectangle field toward an ellipse. This is
314    /// used by expanding droplets whose broad phase has continuous curvature
315    /// rather than the straight sides of a capsule.
316    pub ellipse_blend: f32,
317    /// Smooths the primary capsule's straight-to-arc joins over this distance in dp.
318    /// A positive value replaces its corner radius with the maximum capsule radius.
319    /// The span is capped by half the difference between the primary side lengths.
320    /// Zero or a nonfinite value preserves the primary outline.
321    pub capsule_smoothing_dp: f32,
322    /// Area-preserving affine strain applied to the primary shape. Extra
323    /// scene shapes remain fixed so nearby glass can join the travelling
324    /// droplet without being dragged through its local deformation.
325    pub deformation: Option<GlassDeformation>,
326    /// Offset (dp) of the optical-zoom axis from the primary SDF center. A
327    /// leaning droplet's silhouette shifts toward its travel side while its
328    /// curvature apex stays over the content it rides; anchoring the
329    /// magnification there keeps the face filled by that content instead of
330    /// pulling in whatever lies beyond it.
331    pub zoom_anchor: (f32, f32),
332}
333
334/// A normalized motion axis and reciprocal scales for incompressible glass.
335/// Construction derives the cross-axis scale, so callers cannot describe a
336/// deformation that changes the bubble's area.
337#[derive(Clone, Copy, Debug, PartialEq)]
338pub struct GlassDeformation {
339    axis: (f32, f32),
340    along: f32,
341}
342
343impl GlassDeformation {
344    pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
345        let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
346        let axis = if length > f32::EPSILON {
347            (axis.0 / length, axis.1 / length)
348        } else {
349            (1.0, 0.0)
350        };
351        Self {
352            axis,
353            along: along.max(f32::EPSILON),
354        }
355    }
356
357    pub fn axis(self) -> (f32, f32) {
358        self.axis
359    }
360
361    pub fn along(self) -> f32 {
362        self.along
363    }
364
365    pub fn across(self) -> f32 {
366        1.0 / self.along
367    }
368}
369
370impl GlassMorph {
371    /// Shader budget for extra scene shapes.
372    pub const MAX_SHAPES: usize = 8;
373}
374
375/// Opposing edge lights whose color responds to the backdrop.
376/// Nonfinite fields or nonpositive height or curvature disable this treatment.
377#[derive(Clone, Copy, Debug, PartialEq)]
378pub struct GlassKeyFill {
379    /// Inward extent of the highlight, in dp.
380    pub height_dp: f32,
381    /// Linear reduction in brightness across the highlight's inward extent.
382    pub curvature: f32,
383    /// Direction of the first light in screen-space radians; the second is opposite.
384    pub angle_radians: f32,
385    /// Gain applied to the backdrop's chroma in the reflected color.
386    pub saturation: f32,
387    /// Gain applied to the backdrop's luminance in the reflected color.
388    pub luma_gain: f32,
389    /// Additive contribution to each reflected color channel.
390    pub offset: f32,
391    /// Whether the highlight width follows the surface transform or stays fixed on screen.
392    pub scale_with_surface: bool,
393}
394
395impl GlassKeyFill {
396    fn uniforms(self) -> Option<[f32; 7]> {
397        let values = [
398            self.height_dp,
399            self.curvature,
400            self.angle_radians,
401            self.saturation,
402            self.luma_gain,
403            self.offset,
404            f32::from(self.scale_with_surface),
405        ];
406        (values.iter().all(|value| value.is_finite())
407            && self.height_dp > 0.0
408            && self.curvature > 0.0)
409            .then_some(values)
410    }
411}
412
413/// Seven-tap RGB spectrum for an edge lens. Distances follow the surface transform.
414/// Nonfinite values, negative extents, or opacities outside zero to one are ignored.
415#[derive(Clone, Copy, Debug, PartialEq)]
416pub struct GlassSpectrum {
417    /// Angle θ in radians. For outward normal (x, y), the sampling direction is
418    /// (y cosθ + x sinθ, (x cosθ − y sinθ) × vertical_scale).
419    pub angle_radians: f32,
420    /// Signed spacing between adjacent taps, in dp; negative values reverse the colors.
421    pub step_dp: f32,
422    /// Scale of the vertical component of the sampling direction.
423    pub vertical_scale: f32,
424    /// Spectral opacity at the silhouette edge.
425    pub opacity_near: f32,
426    /// Spectral opacity at and beyond the fade depth.
427    pub opacity_far: f32,
428    /// Inward distance in dp over which opacity interpolates linearly.
429    pub fade_depth_dp: f32,
430    /// Inward distance in dp beyond which the spectrum disappears.
431    pub extent_dp: f32,
432}
433
434impl GlassSpectrum {
435    fn uniforms(self) -> Option<[f32; 8]> {
436        let values = [
437            self.angle_radians,
438            self.step_dp,
439            self.vertical_scale,
440            self.opacity_near,
441            self.opacity_far,
442            self.fade_depth_dp,
443            self.extent_dp,
444            1.0,
445        ];
446        (values.iter().all(|value| value.is_finite())
447            && self.vertical_scale >= 0.0
448            && (0.0..=1.0).contains(&self.opacity_near)
449            && (0.0..=1.0).contains(&self.opacity_far)
450            && self.fade_depth_dp >= 0.0
451            && self.extent_dp >= 0.0)
452            .then_some(values)
453    }
454
455    fn capture_reach(self) -> f32 {
456        self.step_dp.abs() * 3.0 * self.vertical_scale.max(1.0)
457    }
458}
459
460/// Face attenuation before edge lighting, followed by additive illumination.
461/// Edge lenses attenuate the inner refraction image before chromatic sampling.
462/// Invalid or nonfinite parameters disable the response.
463#[derive(Clone, Copy, Debug, PartialEq)]
464pub struct GlassFaceResponse {
465    /// Interior color gain, from zero to one.
466    pub gain: f32,
467    /// Screen-space depth in dp where attenuation starts.
468    pub start_dp: f32,
469    /// Screen-space depth in dp where attenuation reaches its interior gain.
470    /// Equal start and end apply the gain uniformly.
471    pub end_dp: f32,
472    /// Light added to each color channel inside the silhouette after edge lighting.
473    pub illumination: f32,
474}
475
476impl GlassFaceResponse {
477    fn uniforms(self) -> Option<[f32; 4]> {
478        let values = [
479            1.0 - self.gain,
480            self.start_dp,
481            self.end_dp,
482            self.illumination,
483        ];
484        (values.iter().all(|value| value.is_finite())
485            && (0.0..=1.0).contains(&self.gain)
486            && self.start_dp >= 0.0
487            && self.end_dp >= self.start_dp)
488            .then_some(values)
489    }
490}
491
492/// Builder describing a glass material. Resolved against the theme at the
493/// composition site, then evaluated per frame for density and dynamics.
494#[derive(Clone, Debug, PartialEq)]
495pub struct Glass {
496    pub variant: GlassVariant,
497    pub shape: LiquidShape,
498    /// Tint over the refracted backdrop; defaults to the theme's glass tint.
499    pub tint: Option<Color>,
500    /// Optional sharp/blurred pane blend, separate from the color tint.
501    pub tint_amount: Option<GlassTintAmount>,
502    /// Backdrop blur radius in dp (defaults per variant).
503    pub blur_radius: Option<f32>,
504    /// Radius in dp and opacity of the blurred backdrop mixed with an edge lens’s outer warp.
505    pub backdrop_blur: Option<(f32, f32)>,
506    /// Saturation boost (defaults per variant).
507    pub saturation: Option<f32>,
508    /// wcKSRD refraction depth as a fraction of the shape inradius. Regular
509    /// frosted surfaces default to zero so their backdrop stays motion-stable;
510    /// interactive lenses opt into optical displacement explicitly.
511    pub refraction_depth: f32,
512    /// Optional physical wcKSRD refraction depth in dp. When present, this
513    /// keeps the optical band equally deep across surfaces with different
514    /// inradii instead of scaling it from each surface's height.
515    pub refraction_depth_dp: Option<f32>,
516    /// Backdrop projection model.
517    pub refraction: GlassRefraction,
518    /// Normalized wcKSRD ray-return exponent.
519    pub refraction_curve: f32,
520    /// Normalized wcKSRD spectral ray separation.
521    pub dispersion: f32,
522    /// Optional directional spectrum for the edge-lens projection.
523    pub edge_spectrum: Option<GlassSpectrum>,
524    /// Fraction of the wcKSRD displacement applied to the transmitted
525    /// backdrop. Regular frosted surfaces default to zero. The mirrored
526    /// meniscus follows its own optical path.
527    pub transmission_refraction: f32,
528    /// Energy removed from the transmitted ray at the meniscus. Reflection
529    /// and spectral return follow independent paths.
530    pub meniscus_absorption: f32,
531    /// Depth of the rim fold band in dp: the raised lens rim replays its
532    /// interior mirrored toward the edge (a pure displacement). Zero
533    /// disables the fold.
534    pub fold_depth: f32,
535    /// Uniform face magnification of a riding lens (1.0 = no zoom).
536    /// The selected refraction model controls the surrounding rim band.
537    pub optical_zoom: f32,
538    /// Meniscus rim reflectivity multiplier (1.0 = the reference toggle's
539    /// visible rim line; near 0 = the segmented lens's invisible body).
540    pub rim_reflection: f32,
541    /// Optical ink recolor: the lens recolors dark transmitted ink toward
542    /// this color at the given strength (the reference tab bubble's
543    /// color-mask act). None = off.
544    pub ink_recolor: Option<(Color, f32)>,
545    /// Specular rim intensity.
546    pub highlight: f32,
547    /// Whether the face receives the material's diffuse directional illumination.
548    pub face_lighting: bool,
549    /// Replaces dome lighting with a pair of opposing edge lights.
550    pub key_fill: Option<GlassKeyFill>,
551    /// Attenuation and illumination applied around the edge-light pass.
552    pub face_response: Option<GlassFaceResponse>,
553    /// Inset shadow on the transmitted face, covered by the chromatic foreground.
554    pub inner_shadow: Option<GlassShadow>,
555    /// Screen-lift override (brightening toward white; negative darkens).
556    /// Defaults per variant.
557    pub lift: Option<f32>,
558    /// Tone-compression override around the shader's mid pivot (<1 pulls
559    /// every backdrop toward mid-luminance — the reference dark menu reads
560    /// bright magenta over deep purple AND dim gray over white through one
561    /// law). Defaults per variant.
562    pub contrast: Option<f32>,
563    /// Drop shadow below the glass.
564    pub shadow: bool,
565    /// Per-surface shadow override.
566    pub shadow_style: Option<GlassShadow>,
567    /// Clip the layer to `shape`. Morphing glass disables this — coverage
568    /// comes entirely from the shader's SDF.
569    pub clip: bool,
570    /// Foreground color whose contrast the frost must protect. `None` uses
571    /// the theme label color.
572    pub foreground: Option<Color>,
573    /// Strength of backdrop+foreground frost adaptation.
574    pub adaptive_frost: f32,
575    /// Adapts the face transfer curve to the mean captured backdrop brightness.
576    /// Foreground polarity selects the preferred light or dark treatment.
577    pub adaptive_tone: bool,
578}
579
580impl Glass {
581    /// A motion-stable frosted surface without optical displacement.
582    pub fn regular() -> Self {
583        Self {
584            variant: GlassVariant::Regular,
585            shape: LiquidShape::Capsule,
586            tint: None,
587            tint_amount: None,
588            blur_radius: None,
589            backdrop_blur: None,
590            saturation: None,
591            refraction_depth: 0.0,
592            refraction_depth_dp: None,
593            refraction: GlassRefraction::Radial,
594            refraction_curve: 0.25,
595            dispersion: 0.0,
596            edge_spectrum: None,
597            transmission_refraction: 0.0,
598            meniscus_absorption: 1.0,
599            fold_depth: 0.0,
600            optical_zoom: 1.0,
601            rim_reflection: 1.0,
602            ink_recolor: None,
603            highlight: 0.9,
604            face_lighting: true,
605            key_fill: None,
606            face_response: None,
607            inner_shadow: None,
608            lift: None,
609            contrast: None,
610            shadow: true,
611            shadow_style: None,
612            clip: true,
613            foreground: None,
614            adaptive_frost: 0.65,
615            adaptive_tone: false,
616        }
617    }
618
619    pub fn clear() -> Self {
620        Self {
621            variant: GlassVariant::Clear,
622            ..Self::regular()
623        }
624    }
625
626    /// An interactive glass lens with a radial projection and spectral rim.
627    /// [`Self::edge_refraction`] selects an outward rim around a uniformly magnified face.
628    pub fn lens() -> Self {
629        Self {
630            variant: GlassVariant::Lens,
631            shape: LiquidShape::Capsule,
632            tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
633            tint_amount: None,
634            blur_radius: None,
635            backdrop_blur: None,
636            saturation: None,
637            refraction_depth: 0.34,
638            refraction_depth_dp: None,
639            refraction: GlassRefraction::Radial,
640            refraction_curve: 1.0,
641            dispersion: 0.30,
642            edge_spectrum: None,
643            transmission_refraction: 1.0,
644            meniscus_absorption: 1.0,
645            fold_depth: 0.0,
646            optical_zoom: 1.0,
647            rim_reflection: 1.0,
648            ink_recolor: None,
649            highlight: 1.15,
650            face_lighting: true,
651            key_fill: None,
652            face_response: None,
653            inner_shadow: None,
654            lift: None,
655            contrast: None,
656            shadow: true,
657            shadow_style: None,
658            clip: true,
659            foreground: None,
660            adaptive_frost: 0.0,
661            adaptive_tone: false,
662        }
663    }
664
665    /// Enables or disables diffuse face illumination without changing the specular rim.
666    pub fn face_lighting(mut self, enabled: bool) -> Self {
667        self.face_lighting = enabled;
668        self
669    }
670
671    /// Uses opposing edge lights with a backdrop-dependent reflected color.
672    /// [`Self::highlight`] controls their opacity.
673    pub fn key_fill(mut self, lighting: GlassKeyFill) -> Self {
674        self.key_fill = Some(lighting);
675        self
676    }
677
678    /// Sets the face attenuation and illumination around the edge-light pass.
679    pub fn face_response(mut self, response: GlassFaceResponse) -> Self {
680        self.face_response = Some(response);
681        self
682    }
683
684    /// Adds an inset shadow with a Gaussian falloff from the live surface outline.
685    /// The chromatic foreground covers this shadow without refracting it.
686    /// Nonfinite values, negative radii, and opacity outside zero to one disable it.
687    pub fn inner_shadow(mut self, shadow: GlassShadow) -> Self {
688        self.inner_shadow = shadow.uniforms().map(|_| shadow);
689        self
690    }
691
692    pub fn shape(mut self, shape: LiquidShape) -> Self {
693        self.shape = shape;
694        self
695    }
696
697    pub fn tint(mut self, tint: Color) -> Self {
698        self.tint = Some(tint);
699        self
700    }
701
702    pub fn blur_radius(mut self, radius_dp: f32) -> Self {
703        self.blur_radius = Some(radius_dp);
704        self
705    }
706
707    pub fn saturation(mut self, saturation: f32) -> Self {
708        self.saturation = Some(saturation);
709        self
710    }
711
712    pub fn refraction_depth(mut self, fraction: f32) -> Self {
713        self.refraction_depth = fraction.clamp(0.0, 2.0);
714        self.refraction_depth_dp = None;
715        self
716    }
717
718    /// Uses a density-stable physical refraction depth instead of a fraction
719    /// of the live shape inradius.
720    pub fn refraction_depth_dp(mut self, depth_dp: f32) -> Self {
721        self.refraction_depth_dp = Some(depth_dp.max(0.0));
722        self
723    }
724
725    /// Selects a circular depth profile along the surface normal, preserving
726    /// tangential background coordinates along straight edges. `reach_dp` sets the inward
727    /// displacement independently of depth; both remain fixed under surface scaling.
728    /// Negative and nonfinite reach values become zero.
729    pub fn surface_refraction(mut self, reach_dp: f32) -> Self {
730        self.refraction = GlassRefraction::Surface {
731            reach_dp: normalized_refraction_reach(reach_dp),
732        };
733        self
734    }
735
736    /// Overrides the edge lens's spectrum; radial and surface projections use `dispersion`.
737    /// Invalid profiles are ignored. The signed spacing supports continuous color reversal.
738    pub fn edge_spectrum(mut self, spectrum: GlassSpectrum) -> Self {
739        self.edge_spectrum = spectrum.uniforms().map(|_| spectrum);
740        self
741    }
742
743    /// Selects a rim that enters inward, crests outward and returns smoothly to the face.
744    /// `reach_dp` controls its maximum outward displacement; negative and nonfinite values become zero.
745    /// Its inward return uses the amplitude and depth ratios in [`GlassRefraction::EdgeLens`].
746    pub fn edge_refraction(mut self, reach_dp: f32) -> Self {
747        self.refraction = GlassRefraction::EdgeLens {
748            reach_dp: normalized_refraction_reach(reach_dp),
749        };
750        self
751    }
752
753    pub fn refraction_curve(mut self, curve: f32) -> Self {
754        self.refraction_curve = curve.clamp(0.05, 1.0);
755        self
756    }
757
758    pub fn dispersion(mut self, strength: f32) -> Self {
759        self.dispersion = strength.clamp(0.0, 1.0);
760        self
761    }
762
763    pub fn transmission_refraction(mut self, strength: f32) -> Self {
764        self.transmission_refraction = strength.clamp(0.0, 1.0);
765        self
766    }
767
768    pub fn meniscus_absorption(mut self, strength: f32) -> Self {
769        self.meniscus_absorption = strength.clamp(0.0, 1.0);
770        self
771    }
772
773    /// Sets the rim fold band depth in dp (zero disables the fold).
774    pub fn fold_depth(mut self, depth_dp: f32) -> Self {
775        self.fold_depth = depth_dp.max(0.0);
776        self
777    }
778
779    /// Sets the uniform face magnification of a riding lens (1.0 = none).
780    pub fn optical_zoom(mut self, zoom: f32) -> Self {
781        self.optical_zoom = zoom.max(1.0);
782        self
783    }
784
785    /// Sets the meniscus rim reflectivity (1.0 = full reference line).
786    pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
787        self.rim_reflection = reflectivity.clamp(0.0, 2.0);
788        self
789    }
790
791    /// The lens recolors dark transmitted ink toward `color` at `strength`
792    /// (0..1) — the reference bubble's color-mask act as a material optic.
793    pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
794        self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
795        self
796    }
797
798    pub fn highlight(mut self, highlight: f32) -> Self {
799        self.highlight = highlight;
800        self
801    }
802
803    /// Overrides the screen-lift (how hard the glass brightens what it
804    /// shows; the bar lens uses a near-zero lift to stay transmissive).
805    pub fn lift(mut self, lift: f32) -> Self {
806        self.lift = Some(lift);
807        self
808    }
809
810    /// Overrides the tone compression around the shader's mid pivot
811    /// (values below one pull every backdrop toward mid-luminance).
812    pub fn contrast(mut self, contrast: f32) -> Self {
813        self.contrast = Some(contrast.max(0.05));
814        self
815    }
816
817    pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
818        self.foreground = Some(foreground);
819        self.adaptive_frost = strength.clamp(0.0, 1.0);
820        self
821    }
822
823    /// Uses the captured backdrop's mean brightness to adapt the face transfer curve.
824    /// This replaces local frost adaptation, saturation, contrast, lift and
825    /// the key/fill color response. Tint and edge-light geometry still apply.
826    /// The foreground chooses the preferred polarity, with dark treatment over black
827    /// backgrounds and light treatment over white backgrounds.
828    pub fn adaptive_tone(mut self, foreground: Color) -> Self {
829        self.foreground = Some(foreground);
830        self.adaptive_tone = true;
831        self.adaptive_frost = 0.0;
832        self
833    }
834
835    pub fn shadow(mut self, shadow: bool) -> Self {
836        self.shadow = shadow;
837        self
838    }
839
840    pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
841        self.shadow_style = Some(shadow);
842        self
843    }
844
845    /// Disables the layer clip: the shader's SDF coverage is the only shape
846    /// (required while morphing across geometry the clip can't follow).
847    pub fn no_clip(mut self) -> Self {
848        self.clip = false;
849        self
850    }
851
852    /// Uses a sharp/blurred backdrop blend controlled by the tint percentage.
853    /// This replaces the whole-backdrop blur for this material.
854    pub fn tint_amount(mut self, amount: GlassTintAmount) -> Self {
855        self.tint_amount = Some(amount);
856        self
857    }
858
859    /// Crossfades the edge lens’s outer warp with a fixed-radius blurred backdrop.
860    /// Radius uses the renderer's Gaussian convention: twice the standard deviation, in dp.
861    /// Nonfinite values disable the blend; finite radius is nonnegative and opacity is clamped to 0–1.
862    pub fn backdrop_blur(mut self, radius_dp: f32, opacity: f32) -> Self {
863        self.backdrop_blur = (radius_dp.is_finite() && opacity.is_finite())
864            .then_some((radius_dp.max(0.0), opacity.clamp(0.0, 1.0)));
865        self
866    }
867
868    fn default_blur_radius(&self) -> f32 {
869        match self.variant {
870            GlassVariant::Regular => 8.0,
871            GlassVariant::Clear => 3.0,
872            GlassVariant::Lens => 0.0,
873        }
874    }
875
876    fn default_saturation(&self) -> f32 {
877        match self.variant {
878            GlassVariant::Regular => 1.5,
879            GlassVariant::Clear => 1.25,
880            GlassVariant::Lens => 1.0,
881        }
882    }
883
884    /// The backdrop effect this material produces for a node at `density`
885    /// under `dynamics`, resolved against `colors`: the render effect the
886    /// glass modifiers install, for a layer built without them.
887    pub fn backdrop_effect(
888        &self,
889        colors: &LiquidColors,
890        density: f32,
891        dynamics: GlassDynamics,
892    ) -> RenderEffect {
893        self.resolve(colors).backdrop_effect(density, dynamics)
894    }
895
896    fn preferred_foreground(&self, colors: &LiquidColors) -> Color {
897        self.foreground
898            .filter(|color| {
899                [color.r(), color.g(), color.b(), color.a()]
900                    .iter()
901                    .all(|value| value.is_finite())
902            })
903            .unwrap_or(colors.label)
904    }
905
906    pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
907        let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
908            (GlassVariant::Regular, false) => 0.42,
909            (GlassVariant::Regular, true) => -0.38,
910            (GlassVariant::Clear, false) => 0.12,
911            (GlassVariant::Clear, true) => -0.12,
912            (GlassVariant::Lens, false) => 0.10,
913            (GlassVariant::Lens, true) => -0.08,
914        });
915        let foreground = self.preferred_foreground(colors);
916        let shadow = self.shadow_style.unwrap_or_else(|| {
917            GlassShadow::new(
918                Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
919                    (GlassVariant::Lens, false) => 0.14,
920                    (GlassVariant::Lens, true) => 0.28,
921                    (_, false) => 0.16,
922                    (_, true) => 0.5,
923                }),
924                if self.variant == GlassVariant::Lens {
925                    10.0
926                } else {
927                    22.0
928                },
929                if self.variant == GlassVariant::Lens {
930                    3.0
931                } else {
932                    8.0
933                },
934                if self.variant == GlassVariant::Lens {
935                    -6.0
936                } else {
937                    -2.0
938                },
939            )
940        });
941        ResolvedGlass {
942            shape: self.shape,
943            tint: self.tint.unwrap_or(colors.glass_tint),
944            tint_amount: self.tint_amount,
945            backdrop_blur: self.backdrop_blur,
946            blur_radius_dp: self
947                .blur_radius
948                .unwrap_or_else(|| self.default_blur_radius()),
949            saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
950            refraction_depth: self.refraction_depth,
951            refraction_depth_dp: self.refraction_depth_dp,
952            refraction: self.refraction,
953            refraction_curve: self.refraction_curve,
954            dispersion: self.dispersion,
955            edge_spectrum: self.edge_spectrum,
956            transmission_refraction: self.transmission_refraction,
957            meniscus_absorption: self.meniscus_absorption,
958            fold_depth: self.fold_depth,
959            optical_zoom: self.optical_zoom,
960            rim_reflection: self.rim_reflection,
961            ink_recolor: self.ink_recolor,
962            highlight: self.highlight,
963            face_lighting: self.face_lighting,
964            key_fill: self.key_fill,
965            face_response: self.face_response,
966            inner_shadow: self.inner_shadow,
967            lift,
968            contrast: self.contrast.unwrap_or(match self.variant {
969                GlassVariant::Lens => 1.0,
970                _ => 1.03,
971            }),
972            shadow: self.shadow,
973            clip: self.clip,
974            foreground_luma: 0.2126 * foreground.r()
975                + 0.7152 * foreground.g()
976                + 0.0722 * foreground.b(),
977            adaptive_frost: self.adaptive_frost,
978            adaptive_tone: self.adaptive_tone,
979            rim_style: if self.variant == GlassVariant::Lens {
980                1.0
981            } else {
982                0.0
983            },
984            shadow_color: shadow.color,
985            shadow_radius: shadow.radius,
986            shadow_offset_y: shadow.offset_y,
987            shadow_spread: shadow.spread,
988        }
989    }
990}
991
992impl Default for Glass {
993    fn default() -> Self {
994        Self::regular()
995    }
996}
997
998#[derive(Clone, Debug, PartialEq)]
999pub(crate) struct ResolvedGlass {
1000    pub shape: LiquidShape,
1001    pub tint: Color,
1002    tint_amount: Option<GlassTintAmount>,
1003    backdrop_blur: Option<(f32, f32)>,
1004    pub blur_radius_dp: f32,
1005    pub saturation: f32,
1006    pub refraction_depth: f32,
1007    pub refraction_depth_dp: Option<f32>,
1008    pub refraction: GlassRefraction,
1009    pub refraction_curve: f32,
1010    pub dispersion: f32,
1011    pub edge_spectrum: Option<GlassSpectrum>,
1012    pub transmission_refraction: f32,
1013    pub meniscus_absorption: f32,
1014    pub fold_depth: f32,
1015    pub optical_zoom: f32,
1016    pub rim_reflection: f32,
1017    pub ink_recolor: Option<(Color, f32)>,
1018    pub highlight: f32,
1019    pub face_lighting: bool,
1020    pub key_fill: Option<GlassKeyFill>,
1021    /// Attenuation and illumination applied around the edge-light pass.
1022    pub face_response: Option<GlassFaceResponse>,
1023    pub lift: f32,
1024    /// Inset shadow on the transmitted face, covered by the chromatic foreground.
1025    pub inner_shadow: Option<GlassShadow>,
1026    pub contrast: f32,
1027    pub shadow: bool,
1028    pub clip: bool,
1029    pub rim_style: f32,
1030    pub foreground_luma: f32,
1031    pub adaptive_frost: f32,
1032    pub adaptive_tone: bool,
1033    pub shadow_color: Color,
1034    pub shadow_radius: f32,
1035    pub shadow_offset_y: f32,
1036    pub shadow_spread: f32,
1037}
1038
1039fn set_edge_return_depth(shader: &mut RuntimeShader, depth: Option<f32>) {
1040    if let Some(depth) = depth.filter(|depth| depth.is_finite() && *depth >= 0.0) {
1041        shader.set_float2(GLASS_EDGE_RETURN_DEPTH_UNIFORM, depth, 1.0);
1042    }
1043}
1044
1045impl ResolvedGlass {
1046    fn clips_in_shader(&self) -> bool {
1047        self.key_fill.and_then(GlassKeyFill::uniforms).is_some()
1048    }
1049    pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
1050        self.runtime_effect(density, &dynamics, false)
1051    }
1052
1053    fn blur_radii(&self, density: f32, activity: f32, content_mask: bool) -> (f32, f32) {
1054        if content_mask || self.tint_amount.is_some() {
1055            return (0.0, 0.0);
1056        }
1057        let radius = self.blur_radius_dp * density * activity;
1058        if radius > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
1059            (0.0, radius)
1060        } else {
1061            (radius, 0.0)
1062        }
1063    }
1064
1065    fn runtime_effect(
1066        &self,
1067        density: f32,
1068        dynamics: &GlassDynamics,
1069        content_mask: bool,
1070    ) -> RenderEffect {
1071        self.runtime_effect_with_content(density, dynamics, content_mask, false)
1072    }
1073
1074    fn runtime_effect_with_content(
1075        &self,
1076        density: f32,
1077        dynamics: &GlassDynamics,
1078        content_mask: bool,
1079        separate_content: bool,
1080    ) -> RenderEffect {
1081        let density = density.max(f32::EPSILON);
1082        let activity = dynamics
1083            .activity
1084            .filter(|value| value.is_finite())
1085            .unwrap_or(1.0)
1086            .clamp(0.0, 1.0);
1087        static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
1088        let mut shader = SHADER
1089            .get_or_init(|| {
1090                let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
1091                shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
1092                shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
1093                specialize_liquid_glass(&mut shader);
1094                shader
1095            })
1096            .clone();
1097        if let Some(morph) = dynamics.morph.as_ref() {
1098            set_morph_uniforms(&mut shader, morph, activity);
1099        } else {
1100            shader.set_float2(0, 0.0, 0.0);
1101            shader.set_float(6, self.shape.shader_radius_px(density));
1102        }
1103        let projection = dynamics.projection();
1104        shader.set_float2(GLASS_OPTICAL_PROJECTION_UNIFORM, projection.0, projection.1);
1105        let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
1106        shader.set_float(9, self.refraction_depth * activity * press_depth);
1107        let (refraction_mode, refraction_reach) = self.refraction.shader_parameters();
1108        shader.set_float(GLASS_REFRACTION_MODE_UNIFORM, refraction_mode);
1109        set_edge_return_depth(&mut shader, dynamics.edge_return_depth_dp);
1110        shader.set_float(
1111            GLASS_EDGE_REFRACTION_REACH_UNIFORM,
1112            refraction_reach * activity * press_depth,
1113        );
1114        shader.set_float(
1115            GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
1116            self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
1117        );
1118        shader.set_float(
1119            GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
1120            if self.refraction_depth_dp.is_some() {
1121                1.0
1122            } else {
1123                0.0
1124            },
1125        );
1126        shader.set_float(
1127            GLASS_REFRACTION_CURVE_UNIFORM,
1128            self.refraction_curve * activity,
1129        );
1130        shader.set_float(
1131            GLASS_DISPERSION_UNIFORM,
1132            self.dispersion * activity * press_depth,
1133        );
1134        let spectrum_reach = self.set_spectrum_uniforms(&mut shader);
1135        shader.set_float(
1136            GLASS_TRANSMISSION_REFRACTION_UNIFORM,
1137            self.transmission_refraction * activity,
1138        );
1139        shader.set_float(
1140            GLASS_MENISCUS_ABSORPTION_UNIFORM,
1141            self.meniscus_absorption * press_depth,
1142        );
1143        shader.set_float(
1144            GLASS_FOLD_DEPTH_UNIFORM,
1145            self.fold_depth.max(0.0) * press_depth,
1146        );
1147        shader.set_float(
1148            GLASS_OPTICAL_ZOOM_UNIFORM,
1149            1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
1150        );
1151        let zoom_anchor = dynamics
1152            .morph
1153            .as_ref()
1154            .map(|morph| morph.zoom_anchor)
1155            .unwrap_or((0.0, 0.0));
1156        shader.set_float2(
1157            GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1158            zoom_anchor.0,
1159            zoom_anchor.1,
1160        );
1161        shader.set_float(121, self.rim_reflection.max(0.001));
1162        let (ink_color, ink_strength) = self
1163            .ink_recolor
1164            .map(|(color, strength)| (color, strength * activity))
1165            .unwrap_or((Color::TRANSPARENT, 0.0));
1166        shader.set_float(124, ink_color.r());
1167        shader.set_float(125, ink_color.g());
1168        shader.set_float(126, ink_color.b());
1169        shader.set_float(127, ink_strength);
1170        let (light_x, light_y) = glass_light_direction();
1171        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
1172        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
1173        self.set_lighting_uniforms(&mut shader, dynamics);
1174        shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
1175        shader.set_float(
1176            11,
1177            (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
1178        );
1179        let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
1180        let dynamic_tint_alpha = (dynamic_tint.a()
1181            * dynamics
1182                .tint_alpha_multiplier
1183                .unwrap_or(1.0)
1184                .clamp(0.0, 2.0)
1185            * activity)
1186            .clamp(0.0, 1.0);
1187        shader.set_float4(
1188            14,
1189            dynamic_tint.r(),
1190            dynamic_tint.g(),
1191            dynamic_tint.b(),
1192            dynamic_tint_alpha,
1193        );
1194        let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
1195        shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
1196        shader.set_float(20, self.lift * activity);
1197        shader.set_float(21, 0.5 * activity);
1198        shader.set_float2(22, 0.0, 1.0);
1199        shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
1200        shader.set_float(28, self.rim_style * activity);
1201        let (wcksrd_blur_radius, gaussian_blur_radius) =
1202            self.blur_radii(density, activity, content_mask);
1203        shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
1204        shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
1205        let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
1206        shader.set_float4(
1207            GLASS_RESTING_TINT_UNIFORM,
1208            resting_tint.r(),
1209            resting_tint.g(),
1210            resting_tint.b(),
1211            resting_tint.a(),
1212        );
1213        shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
1214        shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
1215        shader.set_float(97, self.foreground_luma);
1216        shader.set_float(GLASS_ADAPTIVE_TONE_UNIFORM, f32::from(self.adaptive_tone));
1217        self.set_backdrop_uniforms(&mut shader, density, content_mask);
1218        shader.set_float(
1219            cranpose_ui_graphics::GLASS_FOREGROUND_CONTENT_UNIFORM,
1220            f32::from(separate_content),
1221        );
1222        let dynamic_shadow = !self.clip && self.shadow;
1223        shader.set_float(
1224            102,
1225            if dynamic_shadow {
1226                self.shadow_color.a() * 0.55 * activity
1227            } else {
1228                0.0
1229            },
1230        );
1231        shader.set_float(103, self.shadow_radius);
1232        shader.set_float(104, self.shadow_offset_y);
1233        shader.set_float(105, self.shadow_spread);
1234        let morph_pad = dynamics
1235            .morph
1236            .as_ref()
1237            .map(|morph| {
1238                let (px, py, pw, ph, _) = morph.primary;
1239                let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1240                let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1241                let mut shape_reach = 0.0f32;
1242                for (sx, sy, sw, sh, _) in &morph.shapes {
1243                    let reach_x = ((sx + sw * 0.5) - right)
1244                        .max(left - (sx - sw * 0.5))
1245                        .max(0.0);
1246                    let reach_y = ((sy + sh * 0.5) - bottom)
1247                        .max(top - (sy - sh * 0.5))
1248                        .max(0.0);
1249                    shape_reach = shape_reach.max(reach_x.max(reach_y));
1250                }
1251                let glue_pad = if morph.shapes.is_empty() {
1252                    0.0
1253                } else {
1254                    morph.glue * 2.0
1255                };
1256                morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1257            })
1258            .unwrap_or(0.0);
1259        shader.set_input_padding(
1260            (self.input_padding()
1261                + if refraction_mode >= 1.5 {
1262                    refraction_reach * (26.5 / 9.0) + spectrum_reach
1263                } else {
1264                    refraction_reach
1265                }
1266                + morph_pad
1267                + wcksrd_blur_radius / density)
1268                * projection.0.max(projection.1).max(1.0),
1269        );
1270        if let Some(morph) = dynamics.morph.as_ref() {
1271            let shadow_reach = if dynamic_shadow {
1272                self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
1273            } else {
1274                0.0
1275            };
1276            let support = morph_output_support(
1277                morph,
1278                activity,
1279                morph_pad,
1280                shadow_reach,
1281                if dynamic_shadow {
1282                    self.shadow_offset_y.abs()
1283                } else {
1284                    0.0
1285                },
1286            );
1287            let (cx, cy, _, _, _) = morph.primary;
1288            let support = Rect {
1289                x: cx + (support.x - cx) * projection.0,
1290                y: cy + (support.y - cy) * projection.1,
1291                width: support.width * projection.0,
1292                height: support.height * projection.1,
1293            };
1294            let (node_w, node_h) = morph.node_size;
1295            let overhang = (-support.x)
1296                .max(-support.y)
1297                .max(support.x + support.width - node_w)
1298                .max(support.y + support.height - node_h)
1299                .max(0.0);
1300            shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1301            shader.set_output_support(Some(support));
1302        }
1303
1304        let optical_effect = liquid_glass_runtime_effect(shader);
1305        if gaussian_blur_radius > f32::EPSILON {
1306            RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1307                .then(optical_effect)
1308        } else {
1309            optical_effect
1310        }
1311    }
1312
1313    fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1314        if let Some(amount) = self.tint_amount {
1315            shader.set_float(
1316                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1317                8.0 * density,
1318            );
1319            shader.set_float(
1320                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1321                amount.percent() / 100.0,
1322            );
1323        }
1324        if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1325            shader.set_float2(
1326                cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1327                radius * density,
1328                opacity,
1329            );
1330        }
1331    }
1332
1333    fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1334        let spectrum = self
1335            .edge_spectrum
1336            .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1337        if let Some((spectrum, values)) = spectrum {
1338            for (index, value) in values.into_iter().enumerate() {
1339                shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1340            }
1341            spectrum.capture_reach()
1342        } else {
1343            40.787 * self.dispersion
1344        }
1345    }
1346
1347    fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1348        let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1349        shader.set_float(118, touch_x);
1350        shader.set_float(119, touch_y);
1351        shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1352        shader.set_float(
1353            GLASS_FACE_LIGHTING_OFF_UNIFORM,
1354            f32::from(!self.face_lighting),
1355        );
1356        shader.set_float(
1357            GLASS_TOUCH_RADIUS_UNIFORM,
1358            dynamics
1359                .touch_radius_dp
1360                .filter(|radius| radius.is_finite() && *radius > 0.0)
1361                .unwrap_or(58.0),
1362        );
1363        shader.set_float(
1364            GLASS_LAYER_CLIPPED_UNIFORM,
1365            f32::from(self.clip && !self.clips_in_shader()),
1366        );
1367        if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1368            for (index, value) in values.into_iter().enumerate() {
1369                shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1370            }
1371        }
1372        if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1373            for (index, value) in values.into_iter().enumerate() {
1374                shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1375            }
1376        }
1377        if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1378            for (index, value) in values.into_iter().enumerate() {
1379                shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1380            }
1381        }
1382    }
1383
1384    fn input_padding(&self) -> f32 {
1385        2.0
1386    }
1387}
1388
1389/// Modifier extension installing the Liquid Glass material.
1390pub trait LiquidModifierExt {
1391    /// Applies the glass material to this composable's bounds: backdrop blur +
1392    /// lens shader, clipped to `glass.shape`, with a soft drop shadow.
1393    ///
1394    /// Must be called in composable context (the material resolves theme
1395    /// colors at the call site).
1396    fn glass_effect(self, glass: Glass) -> Modifier;
1397
1398    /// [`glass_effect`](Self::glass_effect) with per-frame motion inputs; the
1399    /// closure is read at scene-build time, so animating tilt or highlight
1400    /// does not recompose.
1401    fn glass_effect_with(
1402        self,
1403        glass: Glass,
1404        dynamics: impl Fn() -> GlassDynamics + 'static,
1405    ) -> Modifier;
1406}
1407
1408impl LiquidModifierExt for Modifier {
1409    fn glass_effect(self, glass: Glass) -> Modifier {
1410        self.glass_effect_with(glass, GlassDynamics::default)
1411    }
1412
1413    fn glass_effect_with(
1414        self,
1415        glass: Glass,
1416        dynamics: impl Fn() -> GlassDynamics + 'static,
1417    ) -> Modifier {
1418        let colors = crate::theme::liquid_colors();
1419        let resolved = glass.resolve(&colors);
1420        let shape = resolved.shape;
1421
1422        let mut modifier = self;
1423        if resolved.shadow && resolved.clip {
1424            let shadow_color = resolved.shadow_color;
1425            let (radius, offset_y, spread) = (
1426                resolved.shadow_radius,
1427                resolved.shadow_offset_y,
1428                resolved.shadow_spread,
1429            );
1430            modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1431                scope.radius = radius;
1432                scope.spread = spread;
1433                scope.offset.y = offset_y;
1434                scope.color = shadow_color;
1435                scope.cutout = true;
1436            });
1437        }
1438
1439        let layer = cached_glass_layer(resolved);
1440        modifier.graphics_layer(move || layer(current_density(), dynamics()))
1441    }
1442}
1443
1444struct CachedGlassLayer<T> {
1445    density: u32,
1446    light: [u32; 2],
1447    dynamics: GlassDynamics,
1448    value: T,
1449}
1450
1451fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1452    cached_glass_layers(resolved, false, std::convert::identity)
1453}
1454
1455pub(crate) fn cached_glass_content_layers(
1456    resolved: ResolvedGlass,
1457) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1458    cached_glass_layers(resolved, true, |mut layer| {
1459        let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1460            panic!("content layers require a sequential edge lens");
1461        };
1462        layer.backdrop_effect = Some((*second).clone());
1463        [
1464            GraphicsLayer {
1465                backdrop_effect: Some((*first).clone()),
1466                ..Default::default()
1467            },
1468            layer,
1469        ]
1470    })
1471}
1472
1473fn cached_glass_layers<T: Clone>(
1474    resolved: ResolvedGlass,
1475    separate_content: bool,
1476    assemble: impl Fn(GraphicsLayer) -> T,
1477) -> impl Fn(f32, GlassDynamics) -> T {
1478    let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1479    move |density, dynamics| {
1480        let direction = glass_light_direction();
1481        let light = [direction.0, direction.1].map(f32::to_bits);
1482        let mut cached = cached.borrow_mut();
1483        if let Some(previous) = &*cached
1484            && previous.density == density.to_bits()
1485            && previous.light == light
1486            && glass_dynamics_match(&previous.dynamics, &dynamics)
1487        {
1488            return previous.value.clone();
1489        }
1490        let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1491            || (resolved.clip && resolved.clips_in_shader()))
1492        .then(|| resolved.runtime_effect(density, &dynamics, true));
1493        let layer = GraphicsLayer {
1494            backdrop_effect: Some(resolved.runtime_effect_with_content(
1495                density,
1496                &dynamics,
1497                false,
1498                separate_content,
1499            )),
1500            render_effect,
1501            shape: resolved.shape.layer_shape(),
1502            clip: resolved.clip && !resolved.clips_in_shader(),
1503            ..Default::default()
1504        };
1505        let value = assemble(layer);
1506        *cached = Some(CachedGlassLayer {
1507            density: density.to_bits(),
1508            light,
1509            dynamics,
1510            value: value.clone(),
1511        });
1512        value
1513    }
1514}
1515
1516fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1517    let bits = |d: &GlassDynamics| {
1518        let GlassDynamics {
1519            edge_return_depth_dp,
1520            optical_projection,
1521            activity,
1522            resting_tint,
1523            highlight_boost,
1524            saturation_boost,
1525            tint_alpha_multiplier,
1526            morph: _,
1527            touch,
1528            touch_radius_dp,
1529            press_depth,
1530        } = d;
1531        (
1532            edge_return_depth_dp.map(f32::to_bits),
1533            optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
1534            activity.map(f32::to_bits),
1535            resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1536            [*highlight_boost, *saturation_boost].map(f32::to_bits),
1537            tint_alpha_multiplier.map(f32::to_bits),
1538            touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1539            press_depth.map(f32::to_bits),
1540            touch_radius_dp.map(f32::to_bits),
1541        )
1542    };
1543    bits(a) == bits(b)
1544        && match (&a.morph, &b.morph) {
1545            (None, None) => true,
1546            (Some(a), Some(b)) => glass_morphs_match(a, b),
1547            _ => false,
1548        }
1549}
1550
1551fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1552    let shape_bits =
1553        |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1554    let bits = |m: &GlassMorph| {
1555        let GlassMorph {
1556            node_size,
1557            primary,
1558            shapes: _,
1559            glue,
1560            wobble_amplitude,
1561            wobble_phase,
1562            bulge_amplitude,
1563            bulge_direction,
1564            ellipse_blend,
1565            capsule_smoothing_dp,
1566            deformation,
1567            zoom_anchor,
1568        } = m;
1569        (
1570            [
1571                node_size.0,
1572                node_size.1,
1573                *glue,
1574                *wobble_amplitude,
1575                *wobble_phase,
1576                *bulge_amplitude,
1577                *bulge_direction,
1578                *ellipse_blend,
1579                *capsule_smoothing_dp,
1580                zoom_anchor.0,
1581                zoom_anchor.1,
1582            ]
1583            .map(f32::to_bits),
1584            shape_bits(primary),
1585            deformation
1586                .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1587        )
1588    };
1589    bits(a) == bits(b)
1590        && a.shapes
1591            .iter()
1592            .map(shape_bits)
1593            .eq(b.shapes.iter().map(shape_bits))
1594}
1595
1596fn morph_output_support(
1597    morph: &GlassMorph,
1598    activity: f32,
1599    morph_pad: f32,
1600    shadow_reach: f32,
1601    shadow_offset: f32,
1602) -> Rect {
1603    let (px, py, pw, ph, _) = morph.primary;
1604    let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1605        (
1606            1.0 + (deformation.along() - 1.0) * activity,
1607            deformation.axis(),
1608        )
1609    });
1610    let across = 1.0 / along;
1611    let smallest_strain = along.min(across);
1612    let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1613    let rows = [
1614        (
1615            (along * ax * ax + across * ay * ay).abs(),
1616            ((along - across) * ax * ay).abs(),
1617        ),
1618        (
1619            ((along - across) * ax * ay).abs(),
1620            (along * ay * ay + across * ax * ax).abs(),
1621        ),
1622    ];
1623    let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1624    let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1625    let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1626    let (mut left, mut top) = (px - extent_x, py - extent_y);
1627    let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1628    for (sx, sy, sw, sh, _) in &morph.shapes {
1629        left = left.min(sx - sw * 0.5 - reach);
1630        top = top.min(sy - sh * 0.5 - reach);
1631        right = right.max(sx + sw * 0.5 + reach);
1632        bottom = bottom.max(sy + sh * 0.5 + reach);
1633    }
1634    Rect {
1635        x: left,
1636        y: top - shadow_offset,
1637        width: right - left,
1638        height: bottom - top + 2.0 * shadow_offset,
1639    }
1640}
1641
1642fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1643    let (node_w, node_h) = morph.node_size;
1644    let (cx, cy, w, h, radius) = morph.primary;
1645    shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1646    shader.set_float2(2, cx, cy);
1647    shader.set_float2(4, w, h);
1648    shader.set_float(6, radius);
1649    let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1650    shader.set_float(30, count as f32);
1651    for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1652        let base = 36 + index * 5;
1653        shader.set_float(base, *sx);
1654        shader.set_float(base + 1, *sy);
1655        shader.set_float(base + 2, *sw);
1656        shader.set_float(base + 3, *sh);
1657        shader.set_float(base + 4, *sr);
1658    }
1659    shader.set_float(31, morph.glue);
1660    shader.set_float(32, morph.wobble_amplitude * activity);
1661    shader.set_float(33, morph.wobble_phase);
1662    shader.set_float(26, morph.bulge_amplitude * activity);
1663    shader.set_float(27, morph.bulge_direction);
1664    shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1665    shader.set_float(
1666        cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
1667        if morph.capsule_smoothing_dp.is_finite() {
1668            morph.capsule_smoothing_dp.max(0.0) * activity
1669        } else {
1670            0.0
1671        },
1672    );
1673    if let Some(deformation) = morph.deformation {
1674        let axis = deformation.axis();
1675        let along = 1.0 + (deformation.along() - 1.0) * activity;
1676        shader.set_float2(106, axis.0, axis.1);
1677        shader.set_float(108, along);
1678        shader.set_float(109, 1.0 / along);
1679    } else {
1680        shader.set_float2(106, 1.0, 0.0);
1681        shader.set_float2(108, 1.0, 1.0);
1682    }
1683}
1684
1685#[cfg(test)]
1686mod tests {
1687    use super::*;
1688
1689    #[test]
1690    fn backdrop_blur_validates_radius_and_opacity() {
1691        for (radius, opacity, expected) in [
1692            (2.0, 0.5, Some((2.0, 0.5))),
1693            (-2.0, 3.0, Some((0.0, 1.0))),
1694            (2.0, -1.0, Some((2.0, 0.0))),
1695            (f32::NAN, 1.0, None),
1696            (2.0, f32::INFINITY, None),
1697        ] {
1698            assert_eq!(
1699                Glass::lens().backdrop_blur(radius, opacity).backdrop_blur,
1700                expected
1701            );
1702        }
1703        let glass = Glass::lens()
1704            .backdrop_blur(2.0, 0.75)
1705            .edge_refraction(0.0)
1706            .resolve(&light_colors());
1707        let shader = terminal_shader(glass.backdrop_effect(3.0, GlassDynamics::default()));
1708        assert_eq!(shader.uniforms()[172..174], [6.0, 0.75]);
1709        let mask = terminal_shader(glass.runtime_effect(3.0, &GlassDynamics::default(), true));
1710        assert_eq!(
1711            mask.uniforms().get(172..174).unwrap_or(&[0.0, 0.0]),
1712            [0.0, 0.0]
1713        );
1714    }
1715
1716    #[test]
1717    fn pane_tint_preserves_the_sharp_source_at_every_percentage() {
1718        for percent in [0.0, 25.0, 50.0, 100.0] {
1719            let amount = GlassTintAmount::from_percent(percent).unwrap();
1720            let glass = Glass::regular()
1721                .adaptive_frost(Color::BLACK, 0.0)
1722                .tint_amount(amount);
1723            assert_eq!(glass.tint_amount, Some(amount));
1724            let effect = glass.backdrop_effect(&light_colors(), 3.0, GlassDynamics::default());
1725            let RenderEffect::Shader { shader } = effect else {
1726                panic!("the sharp backdrop must not be destroyed by a preceding blur");
1727            };
1728            assert_eq!(
1729                shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM],
1730                24.0
1731            );
1732            assert_eq!(
1733                shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1],
1734                percent / 100.0
1735            );
1736            assert_eq!(
1737                shader.substrates(),
1738                &[cranpose_ui_graphics::SubstrateSpec::Blur { radius_px: 24.0 }]
1739            );
1740        }
1741    }
1742
1743    fn light_colors() -> LiquidColors {
1744        LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
1745    }
1746
1747    fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
1748        match effect {
1749            RenderEffect::Shader { shader } => std::sync::Arc::unwrap_or_clone(shader),
1750            RenderEffect::Chain { second, .. } => {
1751                terminal_shader(std::sync::Arc::unwrap_or_clone(second))
1752            }
1753            effect => panic!("expected runtime shader, got {effect:?}"),
1754        }
1755    }
1756
1757    #[test]
1758    fn optical_projection_validates_scales_and_projects_output_support() {
1759        let glass = Glass::lens().resolve(&light_colors());
1760        let mut dynamics = GlassDynamics {
1761            morph: Some(GlassMorph {
1762                node_size: (200.0, 120.0),
1763                primary: (100.0, 60.0, 111.0, 70.0, 35.0),
1764                ..Default::default()
1765            }),
1766            ..Default::default()
1767        };
1768        let base = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1769        for projection in [
1770            None,
1771            Some((0.0, 1.0)),
1772            Some((-1.0, 1.0)),
1773            Some((1.0, f32::NAN)),
1774            Some((f32::INFINITY, 1.0)),
1775        ] {
1776            dynamics.optical_projection = projection;
1777            let shader = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1778            assert_eq!(
1779                &shader.uniforms()
1780                    [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1781                &[1.0, 1.0]
1782            );
1783            assert_eq!(shader.output_support(), base.output_support());
1784        }
1785        dynamics.optical_projection = Some((1.2, 0.8));
1786        let projected = terminal_shader(glass.backdrop_effect(3.0, dynamics));
1787        assert_eq!(
1788            &projected.uniforms()
1789                [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1790            &[1.2, 0.8]
1791        );
1792        let a = base.output_support().unwrap();
1793        let b = projected.output_support().unwrap();
1794        assert!((b.width - a.width * 1.2).abs() < 0.001);
1795        assert!((b.height - a.height * 0.8).abs() < 0.001);
1796    }
1797
1798    #[test]
1799    fn content_layers_preserve_the_complete_material_effect_and_cache() {
1800        let resolved = Glass::lens()
1801            .edge_refraction(9.0)
1802            .no_clip()
1803            .resolve(&light_colors());
1804        let layers = cached_glass_content_layers(resolved.clone());
1805        for density in [1.0, 3.0] {
1806            for activity in [0.0, 0.5, 1.0] {
1807                let dynamics = GlassDynamics {
1808                    activity: Some(activity),
1809                    ..Default::default()
1810                };
1811                let actual = layers(density, dynamics.clone());
1812                let joined = actual[0]
1813                    .backdrop_effect
1814                    .clone()
1815                    .unwrap()
1816                    .then(actual[1].backdrop_effect.clone().unwrap());
1817                assert_eq!(
1818                    joined,
1819                    resolved.runtime_effect_with_content(density, &dynamics, false, true)
1820                );
1821                assert!(actual[0].render_effect.is_none());
1822                assert!(!actual[0].clip);
1823                assert_eq!(
1824                    terminal_shader(actual[1].backdrop_effect.clone().unwrap()).uniforms()[147],
1825                    3.0
1826                );
1827                let mut repeated = layers(density, dynamics.clone());
1828                assert_eq!(actual, repeated);
1829                repeated[0].backdrop_effect = None;
1830                assert_eq!(actual, layers(density, dynamics));
1831            }
1832        }
1833    }
1834
1835    #[test]
1836    fn cached_glass_layers_refresh_all_live_inputs_and_keep_clones_independent() {
1837        let resolved = Glass::regular().no_clip().resolve(&light_colors());
1838        let layer = cached_glass_layer(resolved.clone());
1839        let mut dynamics = GlassDynamics::default();
1840        let mut density = 2.0;
1841        let mut previous = None;
1842        for step in 0..16 {
1843            match step {
1844                1 => density = 3.0,
1845                2 => set_glass_light_direction((0.5, -0.5)),
1846                3 => dynamics.highlight_boost = 0.25,
1847                4 => dynamics.saturation_boost = 0.4,
1848                5 => dynamics.activity = Some(0.5),
1849                6 => dynamics.resting_tint = Some(Color::RED),
1850                7 => dynamics.tint_alpha_multiplier = Some(0.3),
1851                8 => dynamics.touch = Some((5.0, 8.0, 0.7)),
1852                9 => dynamics.press_depth = Some(0.4),
1853                10 => {
1854                    dynamics.morph = Some(GlassMorph {
1855                        node_size: (60.0, 30.0),
1856                        primary: (30.0, 15.0, 50.0, 20.0, 4.0),
1857                        shapes: vec![(10.0, 15.0, 8.0, 8.0, -1.0)],
1858                        ..Default::default()
1859                    })
1860                }
1861                11 => {
1862                    let morph = dynamics.morph.as_mut().unwrap();
1863                    morph.primary.0 += 3.0;
1864                    morph.shapes[0].0 += 2.0;
1865                    morph.deformation = Some(GlassDeformation::incompressible((1.0, 1.0), 1.3));
1866                }
1867                12 => dynamics.morph = None,
1868                13 => dynamics.touch_radius_dp = Some(144.0),
1869                14 => dynamics.edge_return_depth_dp = Some(3.5),
1870                15 => dynamics.optical_projection = Some((1.2, 0.8)),
1871                _ => {}
1872            }
1873            let actual = layer(density, dynamics.clone());
1874            let expected = GraphicsLayer {
1875                backdrop_effect: Some(resolved.backdrop_effect(density, dynamics.clone())),
1876                render_effect: dynamics
1877                    .morph
1878                    .as_ref()
1879                    .map(|_| resolved.runtime_effect(density, &dynamics, true)),
1880                shape: resolved.shape.layer_shape(),
1881                clip: resolved.clip,
1882                ..Default::default()
1883            };
1884            assert_eq!(actual, expected, "live input step {step}");
1885            assert_ne!(
1886                previous.as_ref(),
1887                Some(&actual),
1888                "input change must reach the layer at step {step}"
1889            );
1890            let mut repeated = layer(density, dynamics.clone());
1891            assert_eq!(repeated, actual);
1892            repeated.backdrop_effect = None;
1893            assert_eq!(layer(density, dynamics.clone()), actual);
1894            previous = Some(actual);
1895        }
1896        set_glass_light_direction((0.0, 1.0));
1897        let replacement = cached_glass_layer(
1898            Glass::clear()
1899                .shape(LiquidShape::RoundedRect(9.0))
1900                .resolve(&light_colors()),
1901        );
1902        assert_ne!(
1903            replacement(density, dynamics.clone()),
1904            layer(density, dynamics)
1905        );
1906    }
1907
1908    #[test]
1909    fn glass_cache_input_equality_preserves_signed_zero_and_nan_bits() {
1910        let mut a = GlassDynamics {
1911            touch: Some((0.0, 1.0, 0.5)),
1912            ..Default::default()
1913        };
1914        let mut b = a.clone();
1915        b.touch.as_mut().unwrap().0 = -0.0;
1916        assert!(!glass_dynamics_match(&a, &b));
1917        a.highlight_boost = f32::NAN;
1918        assert!(glass_dynamics_match(&a, &a));
1919        a.morph = Some(GlassMorph::default());
1920        b = a.clone();
1921        b.morph.as_mut().unwrap().zoom_anchor.0 = -0.0;
1922        assert!(!glass_dynamics_match(&a, &b));
1923        b = a.clone();
1924        b.morph
1925            .as_mut()
1926            .unwrap()
1927            .shapes
1928            .push((0.0, 1.0, 2.0, 3.0, 4.0));
1929        assert!(!glass_dynamics_match(&a, &b));
1930    }
1931
1932    #[test]
1933    fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1934        assert_eq!(glass_light_direction(), (0.0, 1.0));
1935        let resolved = Glass::regular().resolve(&light_colors());
1936        let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1937        let shader = terminal_shader(effect);
1938        let u = shader.uniforms();
1939        assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1940        assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1941
1942        set_glass_light_direction((1.0, 0.0));
1943        let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1944        let u_rotated = terminal_shader(effect);
1945        let u_rotated = u_rotated.uniforms();
1946        assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1947        assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1948        set_glass_light_direction((0.0, 1.0));
1949    }
1950
1951    #[test]
1952    fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1953        for shape in [
1954            LiquidShape::Capsule,
1955            LiquidShape::Circle,
1956            LiquidShape::RoundedRect(12.0),
1957        ] {
1958            assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1959        }
1960    }
1961
1962    #[test]
1963    fn glass_shadow_clamps_negative_radius() {
1964        let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1965        assert_eq!(shadow.radius, 0.0);
1966        assert_eq!(shadow.offset_y, 3.0);
1967        assert_eq!(shadow.spread, -1.0);
1968    }
1969
1970    #[test]
1971    fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1972        let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1973        assert_eq!(deformation.axis(), (0.6, 0.8));
1974        assert_eq!(deformation.along(), 1.25);
1975        assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1976        assert_eq!(
1977            GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1978            (1.0, 0.0)
1979        );
1980    }
1981
1982    #[test]
1983    fn glass_builders_clamp_physical_inputs() {
1984        let glass = Glass::lens()
1985            .shape(LiquidShape::Circle)
1986            .tint(Color::BLACK)
1987            .blur_radius(-2.0)
1988            .saturation(1.2)
1989            .refraction_depth(3.0)
1990            .refraction_depth_dp(-3.0)
1991            .refraction_curve(2.0)
1992            .dispersion(2.0)
1993            .transmission_refraction(2.0)
1994            .meniscus_absorption(2.0)
1995            .highlight(0.4)
1996            .lift(-0.2)
1997            .adaptive_frost(Color::WHITE, 2.0)
1998            .shadow(false)
1999            .no_clip();
2000        assert_eq!(glass.shape, LiquidShape::Circle);
2001        assert_eq!(glass.tint, Some(Color::BLACK));
2002        assert_eq!(glass.blur_radius, Some(-2.0));
2003        assert_eq!(glass.saturation, Some(1.2));
2004        assert_eq!(glass.refraction_depth, 2.0);
2005        assert_eq!(glass.refraction_depth_dp, Some(0.0));
2006        assert_eq!(glass.refraction_curve, 1.0);
2007        assert_eq!(glass.dispersion, 1.0);
2008        assert_eq!(glass.transmission_refraction, 1.0);
2009        assert_eq!(glass.meniscus_absorption, 1.0);
2010        assert_eq!(glass.highlight, 0.4);
2011        assert_eq!(glass.lift, Some(-0.2));
2012        assert_eq!(glass.adaptive_frost, 1.0);
2013        assert!(!glass.shadow);
2014        assert!(!glass.clip);
2015    }
2016
2017    #[test]
2018    fn material_variants_resolve_distinct_frost_levels() {
2019        let regular = Glass::regular().resolve(&light_colors());
2020        let clear = Glass::clear().resolve(&light_colors());
2021        let lens = Glass::lens().resolve(&light_colors());
2022        assert!(regular.blur_radius_dp > clear.blur_radius_dp);
2023        assert!(clear.blur_radius_dp > lens.blur_radius_dp);
2024        assert!(regular.saturation > clear.saturation);
2025        assert_eq!(lens.rim_style, 1.0);
2026        assert_eq!(regular.refraction_depth, 0.0);
2027        assert_eq!(regular.transmission_refraction, 0.0);
2028        assert_eq!(regular.refraction_curve, 0.25);
2029        assert_eq!(lens.refraction_curve, 1.0);
2030        assert_eq!(regular.dispersion, 0.0);
2031        assert_eq!(lens.dispersion, 0.30);
2032    }
2033
2034    #[test]
2035    fn neutral_surface_helpers_follow_foreground_polarity() {
2036        assert_eq!(
2037            neutral_surface_tint(Color::BLACK, 0.08, 0.10),
2038            Color::BLACK.with_alpha(0.08)
2039        );
2040        assert_eq!(
2041            neutral_surface_tint(Color::WHITE, 0.08, 0.10),
2042            Color::WHITE.with_alpha(0.10)
2043        );
2044    }
2045
2046    #[test]
2047    fn dynamic_saturation_reaches_the_material_tint() {
2048        let resting = Color::from_rgb_u8(0, 199, 208);
2049        let raised = boost_tint_saturation(resting, 0.55);
2050        assert_eq!(raised.r(), 0.0);
2051        assert!(raised.g() > resting.g());
2052        assert!(raised.b() > resting.b());
2053        assert_eq!(raised.a(), resting.a());
2054    }
2055
2056    #[test]
2057    fn template_specialization_follows_each_materials_dispersion_and_activity() {
2058        for (dispersion, activity) in [(0.6, 0.0), (0.0, 0.5), (0.3, 1.0), (0.0, 0.0)] {
2059            let resolved = Glass::regular()
2060                .dispersion(dispersion)
2061                .resolve(&light_colors());
2062            let mut shader = terminal_shader(resolved.backdrop_effect(
2063                1.0,
2064                GlassDynamics {
2065                    activity: Some(activity),
2066                    ..GlassDynamics::default()
2067                },
2068            ));
2069            cranpose_ui_graphics::specialize_liquid_glass_with_folds(&mut shader, true);
2070            assert_eq!(
2071                shader.uniforms()[GLASS_DISPERSION_UNIFORM],
2072                dispersion * activity
2073            );
2074            assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], activity);
2075            assert_eq!(
2076                shader
2077                    .overrides()
2078                    .iter()
2079                    .any(|(name, _)| { *name == cranpose_ui_graphics::GLASS_DISPERSION_OFF_FLAG }),
2080                dispersion * activity == 0.0
2081            );
2082        }
2083    }
2084
2085    fn spectrum_fixture() -> GlassSpectrum {
2086        GlassSpectrum {
2087            angle_radians: 0.7,
2088            step_dp: -1.5,
2089            vertical_scale: 1.25,
2090            opacity_near: 0.25,
2091            opacity_far: 0.75,
2092            fade_depth_dp: 3.5,
2093            extent_dp: 12.0,
2094        }
2095    }
2096
2097    #[test]
2098    fn edge_spectrum_preserves_signed_taps_and_captures_every_ray() {
2099        let spectrum = spectrum_fixture();
2100        let material = Glass::lens()
2101            .edge_refraction(9.0)
2102            .dispersion(0.0)
2103            .edge_spectrum(spectrum);
2104        assert_eq!(material.edge_spectrum, Some(spectrum));
2105        let resolved = material.resolve(&light_colors());
2106        let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2107        assert_eq!(
2108            &shader.uniforms()[GLASS_EDGE_SPECTRUM_UNIFORM..GLASS_EDGE_SPECTRUM_UNIFORM + 8],
2109            &spectrum.uniforms().unwrap()
2110        );
2111        let ordinary = Glass::lens()
2112            .edge_refraction(9.0)
2113            .dispersion(0.0)
2114            .resolve(&light_colors());
2115        let base = terminal_shader(ordinary.backdrop_effect(3.0, GlassDynamics::default()));
2116        assert!((shader.input_padding() - base.input_padding() - 5.625).abs() < 0.001);
2117    }
2118
2119    #[test]
2120    fn edge_spectrum_rejects_invalid_fields_at_both_entry_points() {
2121        for index in 0..7 {
2122            for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2123                let mut spectrum = spectrum_fixture();
2124                let fields = [
2125                    &mut spectrum.angle_radians,
2126                    &mut spectrum.step_dp,
2127                    &mut spectrum.vertical_scale,
2128                    &mut spectrum.opacity_near,
2129                    &mut spectrum.opacity_far,
2130                    &mut spectrum.fade_depth_dp,
2131                    &mut spectrum.extent_dp,
2132                ];
2133                *fields.into_iter().nth(index).unwrap() = invalid;
2134                assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2135                let material = Glass {
2136                    edge_spectrum: Some(spectrum),
2137                    ..Glass::lens()
2138                };
2139                let shader = terminal_shader(
2140                    material
2141                        .resolve(&light_colors())
2142                        .backdrop_effect(1.0, GlassDynamics::default()),
2143                );
2144                assert_eq!(
2145                    shader
2146                        .uniforms()
2147                        .get(GLASS_EDGE_SPECTRUM_UNIFORM + 7)
2148                        .copied()
2149                        .unwrap_or(0.0),
2150                    0.0
2151                );
2152            }
2153        }
2154        for index in 2..7 {
2155            let mut spectrum = spectrum_fixture();
2156            let fields = [
2157                &mut spectrum.vertical_scale,
2158                &mut spectrum.opacity_near,
2159                &mut spectrum.opacity_far,
2160                &mut spectrum.fade_depth_dp,
2161                &mut spectrum.extent_dp,
2162            ];
2163            *fields.into_iter().nth(index - 2).unwrap() = -0.1;
2164            assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2165        }
2166        for near in [true, false] {
2167            let mut spectrum = spectrum_fixture();
2168            if near {
2169                spectrum.opacity_near = 1.1;
2170            } else {
2171                spectrum.opacity_far = 1.1;
2172            }
2173            assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2174        }
2175        let zero = GlassSpectrum {
2176            angle_radians: 0.0,
2177            step_dp: 0.0,
2178            vertical_scale: 0.0,
2179            opacity_near: 0.0,
2180            opacity_far: 1.0,
2181            fade_depth_dp: 0.0,
2182            extent_dp: 0.0,
2183        };
2184        assert_eq!(Glass::lens().edge_spectrum(zero).edge_spectrum, Some(zero));
2185    }
2186
2187    #[test]
2188    fn material_template_preserves_each_instances_optical_zoom_anchor() {
2189        let resolved = Glass::lens().resolve(&light_colors());
2190        for anchor in [(3.5, -2.25), (0.0, 0.0), (-7.0, 11.0)] {
2191            let shader = terminal_shader(resolved.backdrop_effect(
2192                1.5,
2193                GlassDynamics {
2194                    morph: Some(GlassMorph {
2195                        node_size: (120.0, 72.0),
2196                        primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2197                        zoom_anchor: anchor,
2198                        ..Default::default()
2199                    }),
2200                    ..Default::default()
2201                },
2202            ));
2203            assert_eq!(
2204                &shader.uniforms()
2205                    [GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM..GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM + 2],
2206                &[anchor.0, anchor.1]
2207            );
2208        }
2209    }
2210
2211    #[test]
2212    fn key_fill_preserves_valid_parameters_and_rejects_invalid_fields() {
2213        let lighting = GlassKeyFill {
2214            height_dp: 1.0,
2215            curvature: 0.8,
2216            angle_radians: -std::f32::consts::FRAC_PI_4,
2217            saturation: 1.5,
2218            luma_gain: 0.4367,
2219            offset: 1.125,
2220            scale_with_surface: true,
2221        };
2222        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2223            assert!(glass.key_fill.is_none());
2224            let material = glass.key_fill(lighting);
2225            assert_eq!(material.key_fill, Some(lighting));
2226            let shader = terminal_shader(material.backdrop_effect(
2227                &light_colors(),
2228                3.0,
2229                GlassDynamics::default(),
2230            ));
2231            assert_eq!(
2232                &shader.uniforms()[GLASS_KEY_FILL_UNIFORM..GLASS_KEY_FILL_UNIFORM + 7],
2233                &lighting.uniforms().unwrap()
2234            );
2235        }
2236        for scale_with_surface in [false, true] {
2237            let value = GlassKeyFill {
2238                scale_with_surface,
2239                ..lighting
2240            };
2241            assert_eq!(value.uniforms().unwrap()[6], f32::from(scale_with_surface));
2242            let layer = cached_glass_layer(Glass::clear().key_fill(value).resolve(&light_colors()))(
2243                3.0,
2244                GlassDynamics::default(),
2245            );
2246            assert!(!layer.clip);
2247            assert!(layer.render_effect.is_some());
2248            assert!(layer.backdrop_effect.is_some());
2249        }
2250        for index in 0..6 {
2251            for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2252                let mut value = lighting;
2253                match index {
2254                    0 => value.height_dp = invalid,
2255                    1 => value.curvature = invalid,
2256                    2 => value.angle_radians = invalid,
2257                    3 => value.saturation = invalid,
2258                    4 => value.luma_gain = invalid,
2259                    _ => value.offset = invalid,
2260                }
2261                let shader = terminal_shader(Glass::clear().key_fill(value).backdrop_effect(
2262                    &light_colors(),
2263                    3.0,
2264                    GlassDynamics::default(),
2265                ));
2266                assert_eq!(
2267                    shader
2268                        .uniforms()
2269                        .get(GLASS_KEY_FILL_UNIFORM)
2270                        .copied()
2271                        .unwrap_or_default(),
2272                    0.0
2273                );
2274            }
2275        }
2276        for invalid in [0.0, -1.0] {
2277            assert!(
2278                GlassKeyFill {
2279                    height_dp: invalid,
2280                    ..lighting
2281                }
2282                .uniforms()
2283                .is_none()
2284            );
2285            assert!(
2286                GlassKeyFill {
2287                    curvature: invalid,
2288                    ..lighting
2289                }
2290                .uniforms()
2291                .is_none()
2292            );
2293        }
2294    }
2295
2296    #[test]
2297    fn independent_edge_return_depth_preserves_zero_and_rejects_invalid_values() {
2298        for depth in [
2299            None,
2300            Some(0.0),
2301            Some(3.5),
2302            Some(-1.0),
2303            Some(f32::NAN),
2304            Some(f32::INFINITY),
2305        ] {
2306            let shader = terminal_shader(
2307                Glass::lens()
2308                    .edge_refraction(9.0)
2309                    .refraction_depth_dp(36.0)
2310                    .backdrop_effect(
2311                        &light_colors(),
2312                        3.0,
2313                        GlassDynamics {
2314                            edge_return_depth_dp: depth,
2315                            ..GlassDynamics::default()
2316                        },
2317                    ),
2318            );
2319            let valid = depth.filter(|value| value.is_finite() && *value >= 0.0);
2320            assert_eq!(
2321                shader
2322                    .uniforms()
2323                    .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM)
2324                    .copied()
2325                    .unwrap_or(0.0),
2326                valid.unwrap_or(0.0)
2327            );
2328            assert_eq!(
2329                shader
2330                    .uniforms()
2331                    .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM + 1)
2332                    .copied()
2333                    .unwrap_or(0.0),
2334                f32::from(valid.is_some())
2335            );
2336            assert_eq!(
2337                shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2338                36.0
2339            );
2340        }
2341    }
2342
2343    #[test]
2344    fn inner_shadow_validates_and_resolves_its_gaussian_profile() {
2345        let shadow = GlassShadow::new(Color::BLACK.with_alpha(0.12), 3.0, 7.0, 0.0);
2346        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2347            assert_eq!(glass.inner_shadow, None);
2348            let material = glass.inner_shadow(shadow);
2349            assert_eq!(material.inner_shadow, Some(shadow));
2350            let shader = terminal_shader(material.backdrop_effect(
2351                &light_colors(),
2352                3.0,
2353                GlassDynamics::default(),
2354            ));
2355            assert_eq!(
2356                &shader.uniforms()[GLASS_INNER_SHADOW_UNIFORM..GLASS_INNER_SHADOW_UNIFORM + 8],
2357                &shadow.uniforms().unwrap()
2358            );
2359        }
2360        for alpha in [0.0, 1.0] {
2361            assert!(
2362                Glass::lens()
2363                    .inner_shadow(GlassShadow::new(
2364                        Color::BLACK.with_alpha(alpha),
2365                        0.0,
2366                        -2.0,
2367                        -3.0
2368                    ))
2369                    .inner_shadow
2370                    .is_some()
2371            );
2372        }
2373        for index in 0..7 {
2374            for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2375                let mut values = shadow.uniforms().unwrap();
2376                values[index] = value;
2377                let invalid = GlassShadow {
2378                    color: Color(values[0], values[1], values[2], values[3]),
2379                    radius: values[4],
2380                    offset_y: values[5],
2381                    spread: values[6],
2382                };
2383                assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2384                let material = Glass {
2385                    inner_shadow: Some(invalid),
2386                    ..Glass::lens()
2387                };
2388                let shader = terminal_shader(material.backdrop_effect(
2389                    &light_colors(),
2390                    3.0,
2391                    GlassDynamics::default(),
2392                ));
2393                assert_eq!(
2394                    shader
2395                        .uniforms()
2396                        .get(GLASS_INNER_SHADOW_UNIFORM + 7)
2397                        .copied()
2398                        .unwrap_or(0.0),
2399                    0.0
2400                );
2401            }
2402        }
2403        for invalid in [
2404            GlassShadow {
2405                radius: -1.0,
2406                ..shadow
2407            },
2408            GlassShadow {
2409                color: Color(0.0, 0.0, 0.0, -0.1),
2410                ..shadow
2411            },
2412            GlassShadow {
2413                color: Color(0.0, 0.0, 0.0, 1.1),
2414                ..shadow
2415            },
2416        ] {
2417            assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2418        }
2419    }
2420
2421    #[test]
2422    fn adaptive_tone_resolves_foreground_and_declares_the_shared_mean() {
2423        use cranpose_ui_graphics::SubstrateSpec;
2424        for foreground in [Color::BLACK, Color::WHITE, Color(f32::NAN, 0.0, 0.0, 1.0)] {
2425            let material = Glass::clear().adaptive_tone(foreground);
2426            assert!(material.adaptive_tone);
2427            let resolved = material.resolve(&light_colors());
2428            assert!(resolved.foreground_luma.is_finite());
2429            let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2430            assert_eq!(shader.uniforms()[GLASS_ADAPTIVE_TONE_UNIFORM], 1.0);
2431            assert_eq!(shader.substrates(), &[SubstrateSpec::Mean]);
2432            let expected = if foreground.r().is_finite() {
2433                foreground.r()
2434            } else {
2435                0.2126 * light_colors().label.r()
2436                    + 0.7152 * light_colors().label.g()
2437                    + 0.0722 * light_colors().label.b()
2438            };
2439            assert!((resolved.foreground_luma - expected).abs() < 0.0001);
2440        }
2441        assert!(!Glass::clear().adaptive_tone);
2442        assert!(!Glass::regular().adaptive_tone);
2443    }
2444
2445    #[test]
2446    fn face_response_preserves_valid_ranges_and_rejects_invalid_values() {
2447        let response = GlassFaceResponse {
2448            gain: 0.97,
2449            start_dp: 1.0 / 3.0,
2450            end_dp: 2.0 / 3.0,
2451            illumination: 0.05,
2452        };
2453        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2454            assert!(glass.face_response.is_none());
2455            let material = glass.face_response(response);
2456            assert_eq!(material.face_response, Some(response));
2457            let shader = terminal_shader(material.backdrop_effect(
2458                &light_colors(),
2459                3.0,
2460                GlassDynamics::default(),
2461            ));
2462            assert_eq!(
2463                &shader.uniforms()[GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4],
2464                &response.uniforms().unwrap()
2465            );
2466        }
2467        for gain in [0.0, 1.0] {
2468            assert!(
2469                GlassFaceResponse {
2470                    gain,
2471                    start_dp: 0.0,
2472                    end_dp: 0.0,
2473                    ..response
2474                }
2475                .uniforms()
2476                .is_some()
2477            );
2478        }
2479        let mut invalid = vec![
2480            GlassFaceResponse {
2481                gain: -0.1,
2482                ..response
2483            },
2484            GlassFaceResponse {
2485                gain: 1.1,
2486                ..response
2487            },
2488            GlassFaceResponse {
2489                start_dp: -1.0,
2490                ..response
2491            },
2492            GlassFaceResponse {
2493                end_dp: 0.0,
2494                ..response
2495            },
2496        ];
2497        for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2498            invalid.extend([
2499                GlassFaceResponse {
2500                    gain: value,
2501                    ..response
2502                },
2503                GlassFaceResponse {
2504                    start_dp: value,
2505                    ..response
2506                },
2507                GlassFaceResponse {
2508                    end_dp: value,
2509                    ..response
2510                },
2511                GlassFaceResponse {
2512                    illumination: value,
2513                    ..response
2514                },
2515            ]);
2516        }
2517        for response in invalid {
2518            assert!(response.uniforms().is_none());
2519            let shader = terminal_shader(Glass::clear().face_response(response).backdrop_effect(
2520                &light_colors(),
2521                3.0,
2522                GlassDynamics::default(),
2523            ));
2524            assert!(
2525                shader
2526                    .uniforms()
2527                    .get(GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4)
2528                    .is_none_or(|values| values.iter().all(|value| *value == 0.0))
2529            );
2530        }
2531    }
2532
2533    #[test]
2534    fn face_lighting_is_explicit_and_preserves_each_material_default() {
2535        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2536            assert!(glass.face_lighting);
2537            for enabled in [false, true] {
2538                let material = glass.clone().face_lighting(enabled);
2539                assert_eq!(material.face_lighting, enabled);
2540                let resolved = material.resolve(&light_colors());
2541                assert_eq!(resolved.face_lighting, enabled);
2542                let shader =
2543                    terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2544                assert_eq!(
2545                    shader.uniforms()[GLASS_FACE_LIGHTING_OFF_UNIFORM],
2546                    f32::from(!enabled)
2547                );
2548            }
2549        }
2550    }
2551
2552    #[test]
2553    fn layer_clipping_has_one_owner_for_material_coverage() {
2554        for clipped in [false, true] {
2555            let mut glass = Glass::regular();
2556            if !clipped {
2557                glass = glass.no_clip();
2558            }
2559            let shader = terminal_shader(glass.backdrop_effect(
2560                &light_colors(),
2561                3.0,
2562                GlassDynamics::default(),
2563            ));
2564            assert_eq!(
2565                shader.uniforms()[GLASS_LAYER_CLIPPED_UNIFORM],
2566                f32::from(clipped)
2567            );
2568        }
2569    }
2570
2571    #[test]
2572    fn touch_radius_preserves_valid_values_and_normalizes_invalid_values() {
2573        for radius in [
2574            None,
2575            Some(-1.0),
2576            Some(0.0),
2577            Some(f32::NAN),
2578            Some(f32::INFINITY),
2579            Some(f32::NEG_INFINITY),
2580            Some(0.5),
2581            Some(144.0),
2582        ] {
2583            let expected = radius
2584                .filter(|value| value.is_finite() && *value > 0.0)
2585                .unwrap_or(58.0);
2586            for density in [1.0, 3.0] {
2587                let shader = terminal_shader(Glass::regular().backdrop_effect(
2588                    &light_colors(),
2589                    density,
2590                    GlassDynamics {
2591                        touch_radius_dp: radius,
2592                        ..Default::default()
2593                    },
2594                ));
2595                assert_eq!(shader.uniforms()[GLASS_TOUCH_RADIUS_UNIFORM], expected);
2596            }
2597        }
2598    }
2599
2600    #[test]
2601    fn material_instances_share_source_without_sharing_uniforms_or_overrides() {
2602        let resolved = Glass::regular().resolve(&light_colors());
2603        let mut first = terminal_shader(resolved.backdrop_effect(2.0, GlassDynamics::default()));
2604        let untouched = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2605        let unused_slot = RuntimeShader::MAX_USER_UNIFORMS - 1;
2606        first.set_float(unused_slot, 17.0);
2607        first.set_override("INSTANCE_ONLY", 1.0);
2608        let second = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2609        assert_eq!(second.uniforms(), untouched.uniforms());
2610        assert_eq!(second.overrides(), untouched.overrides());
2611        assert_eq!(
2612            second.uniforms().get(unused_slot).copied().unwrap_or(0.0),
2613            0.0
2614        );
2615        assert!(
2616            !second
2617                .overrides()
2618                .iter()
2619                .any(|(name, _)| *name == "INSTANCE_ONLY")
2620        );
2621        assert_eq!(first.uniforms()[unused_slot], 17.0);
2622        assert_eq!(first.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2623        assert_eq!(second.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
2624        assert_eq!(first.source().as_ptr(), second.source().as_ptr());
2625    }
2626
2627    #[test]
2628    fn resolved_material_packs_wcksrd_and_dynamic_tint() {
2629        let resolved = Glass::lens()
2630            .refraction_depth(0.72)
2631            .refraction_depth_dp(21.0)
2632            .refraction_curve(0.8)
2633            .dispersion(0.42)
2634            .transmission_refraction(0.35)
2635            .meniscus_absorption(0.3)
2636            .blur_radius(3.0)
2637            .tint(Color::BLACK.with_alpha(0.8))
2638            .resolve(&light_colors());
2639        let effect = resolved.backdrop_effect(
2640            2.0,
2641            GlassDynamics {
2642                highlight_boost: 0.2,
2643                saturation_boost: 0.35,
2644                tint_alpha_multiplier: Some(0.25),
2645                ..Default::default()
2646            },
2647        );
2648        let RenderEffect::Chain { first, .. } = &effect else {
2649            panic!("macroscopic frost must precede the wcKSRD optical pass");
2650        };
2651        assert!(matches!(
2652            first.as_ref(),
2653            RenderEffect::Blur {
2654                radius_x: 6.0,
2655                radius_y: 6.0,
2656                ..
2657            }
2658        ));
2659        let shader = terminal_shader(effect);
2660        assert_eq!(shader.uniforms()[9], 0.72);
2661        assert_eq!(
2662            shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2663            21.0
2664        );
2665        assert_eq!(
2666            shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2667            1.0
2668        );
2669        assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2670        assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
2671        assert_eq!(
2672            shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
2673            0.35
2674        );
2675        assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
2676        assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
2677        assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
2678        assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
2679        assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2680        assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2681    }
2682
2683    #[test]
2684    fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
2685        let effect = Glass::regular()
2686            .resolve(&light_colors())
2687            .backdrop_effect(3.0, GlassDynamics::default());
2688        let RenderEffect::Chain { first, .. } = &effect else {
2689            panic!("heavy blur must ride the separable Gaussian pre-pass");
2690        };
2691        assert!(matches!(
2692            first.as_ref(),
2693            RenderEffect::Blur {
2694                radius_x: 24.0,
2695                radius_y: 24.0,
2696                ..
2697            }
2698        ));
2699        assert_eq!(
2700            terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
2701            0.0
2702        );
2703    }
2704
2705    #[test]
2706    fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
2707        let RenderEffect::Shader { shader } = Glass::regular()
2708            .blur_radius(0.5)
2709            .resolve(&light_colors())
2710            .backdrop_effect(3.0, GlassDynamics::default())
2711        else {
2712            panic!("a sub-cap blur must not build a Gaussian chain");
2713        };
2714        assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
2715    }
2716
2717    #[test]
2718    fn raised_tint_density_stays_premultiplied_alpha_safe() {
2719        let effect = Glass::lens()
2720            .tint(Color::WHITE.with_alpha(0.8))
2721            .resolve(&light_colors())
2722            .backdrop_effect(
2723                1.0,
2724                GlassDynamics {
2725                    tint_alpha_multiplier: Some(2.0),
2726                    ..Default::default()
2727                },
2728            );
2729        assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
2730    }
2731
2732    #[test]
2733    fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
2734        let resolved = Glass::lens()
2735            .refraction_depth(0.72)
2736            .refraction_curve(0.8)
2737            .dispersion(0.42)
2738            .blur_radius(3.0)
2739            .saturation(1.4)
2740            .highlight(0.6)
2741            .lift(0.2)
2742            .tint(Color::BLACK.with_alpha(0.8))
2743            .no_clip()
2744            .resolve(&light_colors());
2745        let morph = GlassMorph {
2746            node_size: (120.0, 72.0),
2747            primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2748            wobble_amplitude: 3.0,
2749            bulge_amplitude: 4.0,
2750            deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
2751            ..Default::default()
2752        };
2753
2754        let RenderEffect::Shader { shader } = resolved.backdrop_effect(
2755            2.0,
2756            GlassDynamics {
2757                activity: Some(0.0),
2758                morph: Some(morph),
2759                ..Default::default()
2760            },
2761        ) else {
2762            panic!("material must resolve to the shared wcKSRD shader");
2763        };
2764        let uniforms = shader.uniforms();
2765        assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
2766        assert_eq!(uniforms[9], 0.0);
2767        assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
2768        assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
2769        assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
2770        assert_eq!(uniforms[11], 0.0);
2771        assert_eq!(uniforms[17], 0.0);
2772        assert_eq!(uniforms[18], 1.0);
2773        assert_eq!(uniforms[20], 0.0);
2774        assert_eq!(uniforms[21], 0.0);
2775        assert_eq!(uniforms[24], 1.0);
2776        assert_eq!(uniforms[28], 0.0);
2777        assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2778        assert_eq!(uniforms[91], 0.0);
2779        assert_eq!(uniforms[102], 0.0);
2780        assert_eq!(
2781            &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2782            &[0.0, 0.0, 0.0, 0.0]
2783        );
2784        assert_eq!(uniforms[32], 0.0);
2785        assert_eq!(uniforms[26], 0.0);
2786        assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
2787        assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
2788    }
2789
2790    #[test]
2791    fn surface_refraction_is_explicit_and_reaches_the_shader() {
2792        for glass in [Glass::regular(), Glass::lens()] {
2793            assert_eq!(glass.refraction, GlassRefraction::Radial);
2794            let surface = glass.surface_refraction(31.0);
2795            assert_eq!(
2796                surface.refraction,
2797                GlassRefraction::Surface { reach_dp: 31.0 }
2798            );
2799            let shader = terminal_shader(surface.backdrop_effect(
2800                &light_colors(),
2801                3.0,
2802                GlassDynamics::default(),
2803            ));
2804            assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 1.0);
2805            assert_eq!(shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM], 31.0);
2806            assert!(
2807                !shader
2808                    .overrides()
2809                    .iter()
2810                    .any(|(name, value)| *name == "GLASS_DIRECTIONAL_REFRACTION_OFF"
2811                        && *value != 0.0)
2812            );
2813        }
2814    }
2815
2816    #[test]
2817    fn edge_refraction_preserves_face_zoom_and_scales_outward_reach_with_contact() {
2818        assert_eq!(GlassRefraction::default(), GlassRefraction::Radial);
2819        let glass = Glass::lens()
2820            .surface_refraction(31.0)
2821            .edge_refraction(12.0)
2822            .optical_zoom(1.2);
2823        assert_eq!(
2824            glass.refraction,
2825            GlassRefraction::EdgeLens { reach_dp: 12.0 }
2826        );
2827        for (activity, press, reach) in [(1.0, 1.0, 12.0), (0.5, 0.5, 3.0), (0.0, 1.0, 0.0)] {
2828            let shader = terminal_shader(glass.backdrop_effect(
2829                &light_colors(),
2830                3.0,
2831                GlassDynamics {
2832                    activity: Some(activity),
2833                    press_depth: Some(press),
2834                    ..Default::default()
2835                },
2836            ));
2837            assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 2.0);
2838            assert_eq!(
2839                shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2840                reach
2841            );
2842            assert_eq!(
2843                shader.uniforms()[GLASS_OPTICAL_ZOOM_UNIFORM],
2844                1.0 + 0.2 * activity
2845            );
2846        }
2847        assert_eq!(
2848            glass.surface_refraction(31.0).refraction,
2849            GlassRefraction::Surface { reach_dp: 31.0 }
2850        );
2851    }
2852
2853    #[test]
2854    fn edge_refraction_captures_the_farthest_spectral_ray() {
2855        for (reach, dispersion, required) in
2856            [(20.0, 1.0, 101.67), (0.0, 1.0, 42.78), (20.0, 0.0, 60.88)]
2857        {
2858            let shader = terminal_shader(
2859                Glass::lens()
2860                    .edge_refraction(reach)
2861                    .dispersion(dispersion)
2862                    .backdrop_effect(&light_colors(), 3.0, GlassDynamics::default()),
2863            );
2864            assert!(shader.input_padding() >= required);
2865        }
2866    }
2867
2868    #[test]
2869    fn directional_refraction_rejects_invalid_reach_at_both_public_entry_points() {
2870        for input in [-1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, 12.0] {
2871            let expected = if input.is_finite() {
2872                input.max(0.0)
2873            } else {
2874                0.0
2875            };
2876            for (built, direct, normalized) in [
2877                (
2878                    Glass::lens().edge_refraction(input),
2879                    GlassRefraction::EdgeLens { reach_dp: input },
2880                    GlassRefraction::EdgeLens { reach_dp: expected },
2881                ),
2882                (
2883                    Glass::regular().surface_refraction(input),
2884                    GlassRefraction::Surface { reach_dp: input },
2885                    GlassRefraction::Surface { reach_dp: expected },
2886                ),
2887            ] {
2888                assert_eq!(built.refraction, normalized);
2889                let direct = Glass {
2890                    refraction: direct,
2891                    ..built
2892                };
2893                let shader = terminal_shader(direct.backdrop_effect(
2894                    &light_colors(),
2895                    1.0,
2896                    GlassDynamics::default(),
2897                ));
2898                assert_eq!(
2899                    shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2900                    expected
2901                );
2902            }
2903        }
2904    }
2905
2906    #[test]
2907    fn surface_reach_is_independent_of_depth_and_tracks_contact() {
2908        for depth in [4.0, 15.5, 30.0] {
2909            for (activity, press, reach) in [(1.0, 1.0, 31.0), (0.5, 0.5, 7.75), (0.0, 1.0, 0.0)] {
2910                let shader = terminal_shader(
2911                    Glass::regular()
2912                        .surface_refraction(31.0)
2913                        .refraction_depth_dp(depth)
2914                        .backdrop_effect(
2915                            &light_colors(),
2916                            3.0,
2917                            GlassDynamics {
2918                                activity: Some(activity),
2919                                press_depth: Some(press),
2920                                ..Default::default()
2921                            },
2922                        ),
2923                );
2924                assert_eq!(
2925                    shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2926                    reach
2927                );
2928                assert_eq!(
2929                    shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2930                    depth * activity * press
2931                );
2932                assert!(shader.input_padding() >= 31.0);
2933            }
2934        }
2935    }
2936
2937    #[test]
2938    fn resting_surface_tint_survives_zero_optical_activity() {
2939        let tint = Color::BLACK.with_alpha(0.11);
2940        let RenderEffect::Shader { shader } = Glass::lens()
2941            .no_clip()
2942            .resolve(&light_colors())
2943            .backdrop_effect(
2944                1.0,
2945                GlassDynamics {
2946                    activity: Some(0.0),
2947                    resting_tint: Some(tint),
2948                    ..Default::default()
2949                },
2950            )
2951        else {
2952            panic!("resting surface must use the shared wcKSRD shader");
2953        };
2954        assert_eq!(
2955            &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2956            &[tint.r(), tint.g(), tint.b(), tint.a()]
2957        );
2958        assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
2959    }
2960
2961    #[test]
2962    fn full_optical_activity_preserves_the_resolved_material() {
2963        let resolved = Glass::lens()
2964            .refraction_depth(0.72)
2965            .refraction_curve(0.8)
2966            .dispersion(0.42)
2967            .blur_radius(3.0)
2968            .resolve(&light_colors());
2969        let shader = terminal_shader(resolved.backdrop_effect(
2970            2.0,
2971            GlassDynamics {
2972                activity: Some(1.0),
2973                ..Default::default()
2974            },
2975        ));
2976        let uniforms = shader.uniforms();
2977        assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
2978        assert_eq!(uniforms[9], 0.72);
2979        assert_eq!(
2980            uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2981            0.0
2982        );
2983        assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2984        assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
2985        assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
2986        assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2987    }
2988
2989    #[test]
2990    fn morph_geometry_and_incompressible_strain_are_packed() {
2991        let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
2992        let morph = GlassMorph {
2993            node_size: (78.0, 59.0),
2994            primary: (39.0, 29.5, 58.0, 39.0, -1.0),
2995            shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
2996            glue: 8.0,
2997            wobble_amplitude: 1.0,
2998            wobble_phase: 0.5,
2999            bulge_amplitude: 2.0,
3000            bulge_direction: 0.25,
3001            ellipse_blend: 0.3,
3002            capsule_smoothing_dp: 4.0,
3003            deformation: Some(deformation),
3004            zoom_anchor: (0.0, 0.0),
3005        };
3006        let RenderEffect::Shader { shader } = Glass::lens()
3007            .no_clip()
3008            .resolve(&light_colors())
3009            .backdrop_effect(
3010                1.0,
3011                GlassDynamics {
3012                    morph: Some(morph),
3013                    ..Default::default()
3014                },
3015            )
3016        else {
3017            panic!("morph must use the shared wcKSRD shader");
3018        };
3019        let uniforms = shader.uniforms();
3020        assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
3021        assert_eq!(uniforms[30], 1.0);
3022        assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
3023        assert_eq!(uniforms[110], 0.3);
3024        assert_eq!(
3025            uniforms[cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3026            4.0
3027        );
3028        assert!(shader.output_padding() > 0.0);
3029    }
3030
3031    #[test]
3032    fn capsule_smoothing_is_finite_nonnegative_and_invalidates_the_morph() {
3033        for value in [-1.0, 0.0, 4.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
3034            let morph = GlassMorph {
3035                node_size: (120.0, 80.0),
3036                primary: (60.0, 40.0, 111.0, 70.0, -1.0),
3037                capsule_smoothing_dp: value,
3038                ..Default::default()
3039            };
3040            let expected = if value.is_finite() {
3041                value.max(0.0)
3042            } else {
3043                0.0
3044            };
3045            let shader = terminal_shader(Glass::lens().backdrop_effect(
3046                &light_colors(),
3047                3.0,
3048                GlassDynamics {
3049                    morph: Some(morph.clone()),
3050                    ..Default::default()
3051                },
3052            ));
3053            assert_eq!(
3054                shader.uniforms()[cranpose_ui_graphics::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3055                expected
3056            );
3057            assert!(glass_morphs_match(&morph, &morph));
3058            let changed = GlassMorph {
3059                capsule_smoothing_dp: 5.0,
3060                ..morph.clone()
3061            };
3062            assert!(!glass_morphs_match(&morph, &changed));
3063        }
3064    }
3065
3066    #[test]
3067    fn optical_builders_hold_their_own_legal_range() {
3068        let glass = Glass::regular()
3069            .fold_depth(-4.0)
3070            .optical_zoom(0.5)
3071            .rim_reflection(9.0);
3072        assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
3073        assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
3074        assert_eq!(
3075            glass.rim_reflection, 2.0,
3076            "the rim tops out at twice the line"
3077        );
3078
3079        let glass = Glass::regular()
3080            .fold_depth(6.0)
3081            .optical_zoom(1.4)
3082            .rim_reflection(0.25);
3083        assert_eq!(glass.fold_depth, 6.0);
3084        assert_eq!(glass.optical_zoom, 1.4);
3085        assert_eq!(glass.rim_reflection, 0.25);
3086    }
3087
3088    #[test]
3089    fn ink_recolor_records_its_color_at_a_clamped_strength() {
3090        let red = Color::from_rgb_u8(255, 0, 0);
3091        let glass = Glass::regular().ink_recolor(red, 3.0);
3092        assert_eq!(glass.ink_recolor, Some((red, 1.0)));
3093
3094        let glass = Glass::regular().ink_recolor(red, -1.0);
3095        assert_eq!(glass.ink_recolor, Some((red, 0.0)));
3096        assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
3097    }
3098
3099    #[test]
3100    fn a_touched_up_surface_amplifies_what_it_already_is() {
3101        let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
3102        assert!(
3103            dynamics.highlight_boost > 0.0,
3104            "a pressed surface goes brighter"
3105        );
3106        assert!(dynamics.saturation_boost > 0.0, "and more saturated");
3107        assert_eq!(
3108            dynamics.touch,
3109            Some((10.0, 4.0, 1.0)),
3110            "with no finger the glow sits at the surface's own heart"
3111        );
3112        assert_eq!(
3113            dynamics.resting_tint, None,
3114            "a touch never repaints the surface with another color"
3115        );
3116    }
3117
3118    #[test]
3119    fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
3120        let dynamics = GlassDynamics::default()
3121            .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
3122            .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
3123        let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
3124        assert_eq!(
3125            (x, y),
3126            (3.0, 7.0),
3127            "the glow tracks the finger, not the center"
3128        );
3129        assert_eq!(intensity, 1.0, "carried press saturates at full");
3130    }
3131
3132    #[test]
3133    fn an_unpressed_surface_is_left_exactly_as_it_was() {
3134        let resting = GlassDynamics::default();
3135        let untouched = resting
3136            .clone()
3137            .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
3138        assert_eq!(untouched.highlight_boost, resting.highlight_boost);
3139        assert_eq!(untouched.saturation_boost, resting.saturation_boost);
3140        assert_eq!(untouched.touch, None, "no press, no glow");
3141    }
3142
3143    #[test]
3144    fn an_off_axis_strain_widens_the_support_by_the_affine_rows_times_the_half_extents() {
3145        let resolved = Glass::regular()
3146            .shape(LiquidShape::RoundedRect(0.0))
3147            .blur_radius(0.0)
3148            .shadow(false)
3149            .no_clip()
3150            .resolve(&light_colors());
3151        let angle = 22.5f32.to_radians();
3152        let morph = GlassMorph {
3153            node_size: (600.0, 600.0),
3154            primary: (300.0, 300.0, 200.0, 200.0, 0.0),
3155            deformation: Some(GlassDeformation::incompressible(
3156                (angle.cos(), angle.sin()),
3157                2.0,
3158            )),
3159            ..Default::default()
3160        };
3161        let RenderEffect::Shader { shader } = resolved.backdrop_effect(
3162            1.0,
3163            GlassDynamics {
3164                activity: Some(1.0),
3165                morph: Some(morph),
3166                ..Default::default()
3167            },
3168        ) else {
3169            panic!("an unblurred glass is one shader");
3170        };
3171        let support = shader
3172            .output_support()
3173            .expect("a morphing glass declares its support");
3174        let half_x = 100.0 * (2.0 * angle.cos().powi(2) + 0.5 * angle.sin().powi(2))
3175            + 100.0 * (1.5 * angle.sin() * angle.cos());
3176        assert!(
3177            (half_x - 231.066).abs() < 0.01,
3178            "the example's half extent is {half_x}"
3179        );
3180        assert!(
3181            support.x <= 300.0 - half_x && support.x + support.width >= 300.0 + half_x,
3182            "the support {support:?} must hold the strained square's x extent {half_x}"
3183        );
3184        let half_y = 100.0 * (1.5 * angle.sin() * angle.cos())
3185            + 100.0 * (2.0 * angle.sin().powi(2) + 0.5 * angle.cos().powi(2));
3186        assert!(
3187            (half_y - 125.0).abs() < 0.01,
3188            "the example's y half extent is {half_y}"
3189        );
3190        assert!(
3191            support.y <= 300.0 - half_y && support.y + support.height >= 300.0 + half_y,
3192            "the support {support:?} must hold the strained square's y extent {half_y}"
3193        );
3194    }
3195
3196    #[test]
3197    fn a_morphing_glass_declares_its_output_support_around_its_shapes_and_their_reach() {
3198        let resolved = Glass::regular()
3199            .shape(LiquidShape::RoundedRect(12.0))
3200            .blur_radius(0.0)
3201            .shadow(true)
3202            .no_clip()
3203            .resolve(&light_colors());
3204        let morph = GlassMorph {
3205            node_size: (280.0, 160.0),
3206            primary: (140.0, 80.0, 120.0, 40.0, -1.0),
3207            shapes: vec![(230.0, 80.0, 40.0, 40.0, -1.0)],
3208            glue: 20.0,
3209            wobble_amplitude: 3.0,
3210            bulge_amplitude: 8.0,
3211            deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
3212            ..Default::default()
3213        };
3214        let dynamics = GlassDynamics {
3215            activity: Some(1.0),
3216            morph: Some(morph.clone()),
3217            ..Default::default()
3218        };
3219        let RenderEffect::Shader { shader } = resolved.backdrop_effect(2.0, dynamics) else {
3220            panic!("an unblurred glass is one shader");
3221        };
3222        let support = shader
3223            .output_support()
3224            .expect("a morphing glass declares its support");
3225        let shape_reach = (230.0 + 20.0) - (140.0 + 60.0);
3226        let shadow_reach = resolved.shadow_radius
3227            + resolved.shadow_offset_y.abs()
3228            + resolved.shadow_spread.max(0.0);
3229        let field_reach = 3.0 * 2.0 + 8.0 + shape_reach + 40.0 + shadow_reach + 4.0;
3230        let smallest_strain = 0.8;
3231        let reach = field_reach / smallest_strain;
3232        let contains = |x: f32, y: f32| {
3233            support.x <= x
3234                && support.y <= y
3235                && support.x + support.width >= x
3236                && support.y + support.height >= y
3237        };
3238        for (cx, cy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
3239            let corner = ((60.0 + reach) * cx, (20.0 + reach) * cy);
3240            let stretched = (140.0 + corner.0 * 1.25, 80.0 + corner.1 * 0.8);
3241            assert!(
3242                contains(stretched.0, stretched.1 - resolved.shadow_offset_y.abs())
3243                    && contains(stretched.0, stretched.1 + resolved.shadow_offset_y.abs()),
3244                "{support:?} must hold the strained, dilated corner {stretched:?}"
3245            );
3246        }
3247        assert!(
3248            contains(230.0 + 20.0 + reach, 80.0),
3249            "the glued shape's reach is inside"
3250        );
3251        assert!(shader.output_padding() >= field_reach);
3252
3253        let RenderEffect::Shader { shader } =
3254            resolved.backdrop_effect(2.0, GlassDynamics::default())
3255        else {
3256            panic!("an unblurred glass is one shader");
3257        };
3258        assert_eq!(
3259            shader.output_support(),
3260            None,
3261            "cover-mode glass fills its rect"
3262        );
3263    }
3264}