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    /// The same glass as a flat surface, for a person who asked the system
1047    /// for less transparency: the surface color with no blur, no refraction
1048    /// and no spectrum, the shape and the shadow as they were.
1049    pub(crate) fn without_transparency(mut self, colors: &LiquidColors) -> Self {
1050        self.tint = colors.surface;
1051        self.tint_amount = None;
1052        self.backdrop_blur = None;
1053        self.blur_radius_dp = 0.0;
1054        self.saturation = 1.0;
1055        self.refraction_depth = 0.0;
1056        self.refraction_depth_dp = None;
1057        self.dispersion = 0.0;
1058        self.edge_spectrum = None;
1059        self.transmission_refraction = 0.0;
1060        self.meniscus_absorption = 0.0;
1061        self.rim_reflection = 0.0;
1062        self.adaptive_frost = 0.0;
1063        self
1064    }
1065
1066    fn clips_in_shader(&self) -> bool {
1067        self.key_fill.and_then(GlassKeyFill::uniforms).is_some()
1068    }
1069    pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
1070        self.runtime_effect(density, &dynamics, false)
1071    }
1072
1073    fn blur_radii(&self, density: f32, activity: f32, content_mask: bool) -> (f32, f32) {
1074        if content_mask || self.tint_amount.is_some() {
1075            return (0.0, 0.0);
1076        }
1077        let radius = self.blur_radius_dp * density * activity;
1078        if radius > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
1079            (0.0, radius)
1080        } else {
1081            (radius, 0.0)
1082        }
1083    }
1084
1085    fn runtime_effect(
1086        &self,
1087        density: f32,
1088        dynamics: &GlassDynamics,
1089        content_mask: bool,
1090    ) -> RenderEffect {
1091        self.runtime_effect_with_content(density, dynamics, content_mask, false)
1092    }
1093
1094    fn runtime_effect_with_content(
1095        &self,
1096        density: f32,
1097        dynamics: &GlassDynamics,
1098        content_mask: bool,
1099        separate_content: bool,
1100    ) -> RenderEffect {
1101        let density = density.max(f32::EPSILON);
1102        let activity = dynamics
1103            .activity
1104            .filter(|value| value.is_finite())
1105            .unwrap_or(1.0)
1106            .clamp(0.0, 1.0);
1107        static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
1108        let mut shader = SHADER
1109            .get_or_init(|| {
1110                let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
1111                shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
1112                shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
1113                specialize_liquid_glass(&mut shader);
1114                shader
1115            })
1116            .clone();
1117        if let Some(morph) = dynamics.morph.as_ref() {
1118            set_morph_uniforms(&mut shader, morph, activity);
1119        } else {
1120            shader.set_float2(0, 0.0, 0.0);
1121            shader.set_float(6, self.shape.shader_radius_px(density));
1122        }
1123        let projection = dynamics.projection();
1124        shader.set_float2(GLASS_OPTICAL_PROJECTION_UNIFORM, projection.0, projection.1);
1125        let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
1126        shader.set_float(9, self.refraction_depth * activity * press_depth);
1127        let (refraction_mode, refraction_reach) = self.refraction.shader_parameters();
1128        shader.set_float(GLASS_REFRACTION_MODE_UNIFORM, refraction_mode);
1129        set_edge_return_depth(&mut shader, dynamics.edge_return_depth_dp);
1130        shader.set_float(
1131            GLASS_EDGE_REFRACTION_REACH_UNIFORM,
1132            refraction_reach * activity * press_depth,
1133        );
1134        shader.set_float(
1135            GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
1136            self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
1137        );
1138        shader.set_float(
1139            GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
1140            if self.refraction_depth_dp.is_some() {
1141                1.0
1142            } else {
1143                0.0
1144            },
1145        );
1146        shader.set_float(
1147            GLASS_REFRACTION_CURVE_UNIFORM,
1148            self.refraction_curve * activity,
1149        );
1150        shader.set_float(
1151            GLASS_DISPERSION_UNIFORM,
1152            self.dispersion * activity * press_depth,
1153        );
1154        let spectrum_reach = self.set_spectrum_uniforms(&mut shader);
1155        shader.set_float(
1156            GLASS_TRANSMISSION_REFRACTION_UNIFORM,
1157            self.transmission_refraction * activity,
1158        );
1159        shader.set_float(
1160            GLASS_MENISCUS_ABSORPTION_UNIFORM,
1161            self.meniscus_absorption * press_depth,
1162        );
1163        shader.set_float(
1164            GLASS_FOLD_DEPTH_UNIFORM,
1165            self.fold_depth.max(0.0) * press_depth,
1166        );
1167        shader.set_float(
1168            GLASS_OPTICAL_ZOOM_UNIFORM,
1169            1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
1170        );
1171        let zoom_anchor = dynamics
1172            .morph
1173            .as_ref()
1174            .map(|morph| morph.zoom_anchor)
1175            .unwrap_or((0.0, 0.0));
1176        shader.set_float2(
1177            GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1178            zoom_anchor.0,
1179            zoom_anchor.1,
1180        );
1181        shader.set_float(121, self.rim_reflection.max(0.001));
1182        let (ink_color, ink_strength) = self
1183            .ink_recolor
1184            .map(|(color, strength)| (color, strength * activity))
1185            .unwrap_or((Color::TRANSPARENT, 0.0));
1186        shader.set_float(124, ink_color.r());
1187        shader.set_float(125, ink_color.g());
1188        shader.set_float(126, ink_color.b());
1189        shader.set_float(127, ink_strength);
1190        let (light_x, light_y) = glass_light_direction();
1191        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
1192        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
1193        self.set_lighting_uniforms(&mut shader, dynamics);
1194        shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
1195        shader.set_float(
1196            11,
1197            (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
1198        );
1199        let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
1200        let dynamic_tint_alpha = (dynamic_tint.a()
1201            * dynamics
1202                .tint_alpha_multiplier
1203                .unwrap_or(1.0)
1204                .clamp(0.0, 2.0)
1205            * activity)
1206            .clamp(0.0, 1.0);
1207        shader.set_float4(
1208            14,
1209            dynamic_tint.r(),
1210            dynamic_tint.g(),
1211            dynamic_tint.b(),
1212            dynamic_tint_alpha,
1213        );
1214        let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
1215        shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
1216        shader.set_float(20, self.lift * activity);
1217        shader.set_float(21, 0.5 * activity);
1218        shader.set_float2(22, 0.0, 1.0);
1219        shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
1220        shader.set_float(28, self.rim_style * activity);
1221        let (wcksrd_blur_radius, gaussian_blur_radius) =
1222            self.blur_radii(density, activity, content_mask);
1223        shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
1224        shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
1225        let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
1226        shader.set_float4(
1227            GLASS_RESTING_TINT_UNIFORM,
1228            resting_tint.r(),
1229            resting_tint.g(),
1230            resting_tint.b(),
1231            resting_tint.a(),
1232        );
1233        shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
1234        shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
1235        shader.set_float(97, self.foreground_luma);
1236        shader.set_float(GLASS_ADAPTIVE_TONE_UNIFORM, f32::from(self.adaptive_tone));
1237        self.set_backdrop_uniforms(&mut shader, density, content_mask);
1238        shader.set_float(
1239            cranpose_ui_graphics::GLASS_FOREGROUND_CONTENT_UNIFORM,
1240            f32::from(separate_content),
1241        );
1242        let dynamic_shadow = !self.clip && self.shadow;
1243        shader.set_float(
1244            102,
1245            if dynamic_shadow {
1246                self.shadow_color.a() * 0.55 * activity
1247            } else {
1248                0.0
1249            },
1250        );
1251        shader.set_float(103, self.shadow_radius);
1252        shader.set_float(104, self.shadow_offset_y);
1253        shader.set_float(105, self.shadow_spread);
1254        let morph_pad = dynamics
1255            .morph
1256            .as_ref()
1257            .map(|morph| {
1258                let (px, py, pw, ph, _) = morph.primary;
1259                let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1260                let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1261                let mut shape_reach = 0.0f32;
1262                for (sx, sy, sw, sh, _) in &morph.shapes {
1263                    let reach_x = ((sx + sw * 0.5) - right)
1264                        .max(left - (sx - sw * 0.5))
1265                        .max(0.0);
1266                    let reach_y = ((sy + sh * 0.5) - bottom)
1267                        .max(top - (sy - sh * 0.5))
1268                        .max(0.0);
1269                    shape_reach = shape_reach.max(reach_x.max(reach_y));
1270                }
1271                let glue_pad = if morph.shapes.is_empty() {
1272                    0.0
1273                } else {
1274                    morph.glue * 2.0
1275                };
1276                morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1277            })
1278            .unwrap_or(0.0);
1279        shader.set_input_padding(
1280            (self.input_padding()
1281                + if refraction_mode >= 1.5 {
1282                    refraction_reach * (26.5 / 9.0) + spectrum_reach
1283                } else {
1284                    refraction_reach
1285                }
1286                + morph_pad
1287                + wcksrd_blur_radius / density)
1288                * projection.0.max(projection.1).max(1.0),
1289        );
1290        if let Some(morph) = dynamics.morph.as_ref() {
1291            let shadow_reach = if dynamic_shadow {
1292                self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
1293            } else {
1294                0.0
1295            };
1296            let support = morph_output_support(
1297                morph,
1298                activity,
1299                morph_pad,
1300                shadow_reach,
1301                if dynamic_shadow {
1302                    self.shadow_offset_y.abs()
1303                } else {
1304                    0.0
1305                },
1306            );
1307            let (cx, cy, _, _, _) = morph.primary;
1308            let support = Rect {
1309                x: cx + (support.x - cx) * projection.0,
1310                y: cy + (support.y - cy) * projection.1,
1311                width: support.width * projection.0,
1312                height: support.height * projection.1,
1313            };
1314            let (node_w, node_h) = morph.node_size;
1315            let overhang = (-support.x)
1316                .max(-support.y)
1317                .max(support.x + support.width - node_w)
1318                .max(support.y + support.height - node_h)
1319                .max(0.0);
1320            shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1321            shader.set_output_support(Some(support));
1322        }
1323
1324        let optical_effect = liquid_glass_runtime_effect(shader);
1325        if gaussian_blur_radius > f32::EPSILON {
1326            RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1327                .then(optical_effect)
1328        } else {
1329            optical_effect
1330        }
1331    }
1332
1333    fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1334        if let Some(amount) = self.tint_amount {
1335            shader.set_float(
1336                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1337                8.0 * density,
1338            );
1339            shader.set_float(
1340                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1341                amount.percent() / 100.0,
1342            );
1343        }
1344        if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1345            shader.set_float2(
1346                cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1347                radius * density,
1348                opacity,
1349            );
1350        }
1351    }
1352
1353    fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1354        let spectrum = self
1355            .edge_spectrum
1356            .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1357        if let Some((spectrum, values)) = spectrum {
1358            for (index, value) in values.into_iter().enumerate() {
1359                shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1360            }
1361            spectrum.capture_reach()
1362        } else {
1363            40.787 * self.dispersion
1364        }
1365    }
1366
1367    fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1368        let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1369        shader.set_float(118, touch_x);
1370        shader.set_float(119, touch_y);
1371        shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1372        shader.set_float(
1373            GLASS_FACE_LIGHTING_OFF_UNIFORM,
1374            f32::from(!self.face_lighting),
1375        );
1376        shader.set_float(
1377            GLASS_TOUCH_RADIUS_UNIFORM,
1378            dynamics
1379                .touch_radius_dp
1380                .filter(|radius| radius.is_finite() && *radius > 0.0)
1381                .unwrap_or(58.0),
1382        );
1383        shader.set_float(
1384            GLASS_LAYER_CLIPPED_UNIFORM,
1385            f32::from(self.clip && !self.clips_in_shader()),
1386        );
1387        if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1388            for (index, value) in values.into_iter().enumerate() {
1389                shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1390            }
1391        }
1392        if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1393            for (index, value) in values.into_iter().enumerate() {
1394                shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1395            }
1396        }
1397        if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1398            for (index, value) in values.into_iter().enumerate() {
1399                shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1400            }
1401        }
1402    }
1403
1404    fn input_padding(&self) -> f32 {
1405        2.0
1406    }
1407}
1408
1409/// Modifier extension installing the Liquid Glass material.
1410pub trait LiquidModifierExt {
1411    /// Applies the glass material to this composable's bounds: backdrop blur +
1412    /// lens shader, clipped to `glass.shape`, with a soft drop shadow.
1413    ///
1414    /// Must be called in composable context (the material resolves theme
1415    /// colors at the call site).
1416    fn glass_effect(self, glass: Glass) -> Modifier;
1417
1418    /// [`glass_effect`](Self::glass_effect) with per-frame motion inputs; the
1419    /// closure is read at scene-build time, so animating tilt or highlight
1420    /// does not recompose.
1421    fn glass_effect_with(
1422        self,
1423        glass: Glass,
1424        dynamics: impl Fn() -> GlassDynamics + 'static,
1425    ) -> Modifier;
1426}
1427
1428impl LiquidModifierExt for Modifier {
1429    fn glass_effect(self, glass: Glass) -> Modifier {
1430        self.glass_effect_with(glass, GlassDynamics::default)
1431    }
1432
1433    fn glass_effect_with(
1434        self,
1435        glass: Glass,
1436        dynamics: impl Fn() -> GlassDynamics + 'static,
1437    ) -> Modifier {
1438        let colors = crate::theme::liquid_colors();
1439        let options = cranpose_services::local_accessibility_options().current();
1440        let resolved = if options.reduce_transparency {
1441            glass.resolve(&colors).without_transparency(&colors)
1442        } else {
1443            glass.resolve(&colors)
1444        };
1445        let shape = resolved.shape;
1446
1447        let mut modifier = self;
1448        if resolved.shadow && resolved.clip {
1449            let shadow_color = resolved.shadow_color;
1450            let (radius, offset_y, spread) = (
1451                resolved.shadow_radius,
1452                resolved.shadow_offset_y,
1453                resolved.shadow_spread,
1454            );
1455            modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1456                scope.radius = radius;
1457                scope.spread = spread;
1458                scope.offset.y = offset_y;
1459                scope.color = shadow_color;
1460                scope.cutout = true;
1461            });
1462        }
1463
1464        let layer = cached_glass_layer(resolved);
1465        modifier.graphics_layer(move || layer(current_density(), dynamics()))
1466    }
1467}
1468
1469struct CachedGlassLayer<T> {
1470    density: u32,
1471    light: [u32; 2],
1472    dynamics: GlassDynamics,
1473    value: T,
1474}
1475
1476fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1477    cached_glass_layers(resolved, false, std::convert::identity)
1478}
1479
1480pub(crate) fn cached_glass_content_layers(
1481    resolved: ResolvedGlass,
1482) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1483    cached_glass_layers(resolved, true, |mut layer| {
1484        let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1485            panic!("content layers require a sequential edge lens");
1486        };
1487        layer.backdrop_effect = Some((*second).clone());
1488        [
1489            GraphicsLayer {
1490                backdrop_effect: Some((*first).clone()),
1491                ..Default::default()
1492            },
1493            layer,
1494        ]
1495    })
1496}
1497
1498fn cached_glass_layers<T: Clone>(
1499    resolved: ResolvedGlass,
1500    separate_content: bool,
1501    assemble: impl Fn(GraphicsLayer) -> T,
1502) -> impl Fn(f32, GlassDynamics) -> T {
1503    let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1504    move |density, dynamics| {
1505        let direction = glass_light_direction();
1506        let light = [direction.0, direction.1].map(f32::to_bits);
1507        let mut cached = cached.borrow_mut();
1508        if let Some(previous) = &*cached
1509            && previous.density == density.to_bits()
1510            && previous.light == light
1511            && glass_dynamics_match(&previous.dynamics, &dynamics)
1512        {
1513            return previous.value.clone();
1514        }
1515        let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1516            || (resolved.clip && resolved.clips_in_shader()))
1517        .then(|| resolved.runtime_effect(density, &dynamics, true));
1518        let layer = GraphicsLayer {
1519            backdrop_effect: Some(resolved.runtime_effect_with_content(
1520                density,
1521                &dynamics,
1522                false,
1523                separate_content,
1524            )),
1525            render_effect,
1526            shape: resolved.shape.layer_shape(),
1527            clip: resolved.clip && !resolved.clips_in_shader(),
1528            ..Default::default()
1529        };
1530        let value = assemble(layer);
1531        *cached = Some(CachedGlassLayer {
1532            density: density.to_bits(),
1533            light,
1534            dynamics,
1535            value: value.clone(),
1536        });
1537        value
1538    }
1539}
1540
1541fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1542    let bits = |d: &GlassDynamics| {
1543        let GlassDynamics {
1544            edge_return_depth_dp,
1545            optical_projection,
1546            activity,
1547            resting_tint,
1548            highlight_boost,
1549            saturation_boost,
1550            tint_alpha_multiplier,
1551            morph: _,
1552            touch,
1553            touch_radius_dp,
1554            press_depth,
1555        } = d;
1556        (
1557            edge_return_depth_dp.map(f32::to_bits),
1558            optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
1559            activity.map(f32::to_bits),
1560            resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1561            [*highlight_boost, *saturation_boost].map(f32::to_bits),
1562            tint_alpha_multiplier.map(f32::to_bits),
1563            touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1564            press_depth.map(f32::to_bits),
1565            touch_radius_dp.map(f32::to_bits),
1566        )
1567    };
1568    bits(a) == bits(b)
1569        && match (&a.morph, &b.morph) {
1570            (None, None) => true,
1571            (Some(a), Some(b)) => glass_morphs_match(a, b),
1572            _ => false,
1573        }
1574}
1575
1576fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1577    let shape_bits =
1578        |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1579    let bits = |m: &GlassMorph| {
1580        let GlassMorph {
1581            node_size,
1582            primary,
1583            shapes: _,
1584            glue,
1585            wobble_amplitude,
1586            wobble_phase,
1587            bulge_amplitude,
1588            bulge_direction,
1589            ellipse_blend,
1590            capsule_smoothing_dp,
1591            deformation,
1592            zoom_anchor,
1593        } = m;
1594        (
1595            [
1596                node_size.0,
1597                node_size.1,
1598                *glue,
1599                *wobble_amplitude,
1600                *wobble_phase,
1601                *bulge_amplitude,
1602                *bulge_direction,
1603                *ellipse_blend,
1604                *capsule_smoothing_dp,
1605                zoom_anchor.0,
1606                zoom_anchor.1,
1607            ]
1608            .map(f32::to_bits),
1609            shape_bits(primary),
1610            deformation
1611                .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1612        )
1613    };
1614    bits(a) == bits(b)
1615        && a.shapes
1616            .iter()
1617            .map(shape_bits)
1618            .eq(b.shapes.iter().map(shape_bits))
1619}
1620
1621fn morph_output_support(
1622    morph: &GlassMorph,
1623    activity: f32,
1624    morph_pad: f32,
1625    shadow_reach: f32,
1626    shadow_offset: f32,
1627) -> Rect {
1628    let (px, py, pw, ph, _) = morph.primary;
1629    let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1630        (
1631            1.0 + (deformation.along() - 1.0) * activity,
1632            deformation.axis(),
1633        )
1634    });
1635    let across = 1.0 / along;
1636    let smallest_strain = along.min(across);
1637    let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1638    let rows = [
1639        (
1640            (along * ax * ax + across * ay * ay).abs(),
1641            ((along - across) * ax * ay).abs(),
1642        ),
1643        (
1644            ((along - across) * ax * ay).abs(),
1645            (along * ay * ay + across * ax * ax).abs(),
1646        ),
1647    ];
1648    let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1649    let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1650    let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1651    let (mut left, mut top) = (px - extent_x, py - extent_y);
1652    let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1653    for (sx, sy, sw, sh, _) in &morph.shapes {
1654        left = left.min(sx - sw * 0.5 - reach);
1655        top = top.min(sy - sh * 0.5 - reach);
1656        right = right.max(sx + sw * 0.5 + reach);
1657        bottom = bottom.max(sy + sh * 0.5 + reach);
1658    }
1659    Rect {
1660        x: left,
1661        y: top - shadow_offset,
1662        width: right - left,
1663        height: bottom - top + 2.0 * shadow_offset,
1664    }
1665}
1666
1667fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1668    let (node_w, node_h) = morph.node_size;
1669    let (cx, cy, w, h, radius) = morph.primary;
1670    shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1671    shader.set_float2(2, cx, cy);
1672    shader.set_float2(4, w, h);
1673    shader.set_float(6, radius);
1674    let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1675    shader.set_float(30, count as f32);
1676    for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1677        let base = 36 + index * 5;
1678        shader.set_float(base, *sx);
1679        shader.set_float(base + 1, *sy);
1680        shader.set_float(base + 2, *sw);
1681        shader.set_float(base + 3, *sh);
1682        shader.set_float(base + 4, *sr);
1683    }
1684    shader.set_float(31, morph.glue);
1685    shader.set_float(32, morph.wobble_amplitude * activity);
1686    shader.set_float(33, morph.wobble_phase);
1687    shader.set_float(26, morph.bulge_amplitude * activity);
1688    shader.set_float(27, morph.bulge_direction);
1689    shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1690    shader.set_float(
1691        cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
1692        if morph.capsule_smoothing_dp.is_finite() {
1693            morph.capsule_smoothing_dp.max(0.0) * activity
1694        } else {
1695            0.0
1696        },
1697    );
1698    if let Some(deformation) = morph.deformation {
1699        let axis = deformation.axis();
1700        let along = 1.0 + (deformation.along() - 1.0) * activity;
1701        shader.set_float2(106, axis.0, axis.1);
1702        shader.set_float(108, along);
1703        shader.set_float(109, 1.0 / along);
1704    } else {
1705        shader.set_float2(106, 1.0, 0.0);
1706        shader.set_float2(108, 1.0, 1.0);
1707    }
1708}
1709
1710#[cfg(test)]
1711mod tests {
1712    use super::*;
1713
1714    #[test]
1715    fn backdrop_blur_validates_radius_and_opacity() {
1716        for (radius, opacity, expected) in [
1717            (2.0, 0.5, Some((2.0, 0.5))),
1718            (-2.0, 3.0, Some((0.0, 1.0))),
1719            (2.0, -1.0, Some((2.0, 0.0))),
1720            (f32::NAN, 1.0, None),
1721            (2.0, f32::INFINITY, None),
1722        ] {
1723            assert_eq!(
1724                Glass::lens().backdrop_blur(radius, opacity).backdrop_blur,
1725                expected
1726            );
1727        }
1728        let glass = Glass::lens()
1729            .backdrop_blur(2.0, 0.75)
1730            .edge_refraction(0.0)
1731            .resolve(&light_colors());
1732        let shader = terminal_shader(glass.backdrop_effect(3.0, GlassDynamics::default()));
1733        assert_eq!(shader.uniforms()[172..174], [6.0, 0.75]);
1734        let mask = terminal_shader(glass.runtime_effect(3.0, &GlassDynamics::default(), true));
1735        assert_eq!(
1736            mask.uniforms().get(172..174).unwrap_or(&[0.0, 0.0]),
1737            [0.0, 0.0]
1738        );
1739    }
1740
1741    #[test]
1742    fn pane_tint_preserves_the_sharp_source_at_every_percentage() {
1743        for percent in [0.0, 25.0, 50.0, 100.0] {
1744            let amount = GlassTintAmount::from_percent(percent).unwrap();
1745            let glass = Glass::regular()
1746                .adaptive_frost(Color::BLACK, 0.0)
1747                .tint_amount(amount);
1748            assert_eq!(glass.tint_amount, Some(amount));
1749            let effect = glass.backdrop_effect(&light_colors(), 3.0, GlassDynamics::default());
1750            let RenderEffect::Shader { shader } = effect else {
1751                panic!("the sharp backdrop must not be destroyed by a preceding blur");
1752            };
1753            assert_eq!(
1754                shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM],
1755                24.0
1756            );
1757            assert_eq!(
1758                shader.uniforms()[cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1],
1759                percent / 100.0
1760            );
1761            assert_eq!(
1762                shader.substrates(),
1763                &[cranpose_ui_graphics::SubstrateSpec::Blur { radius_px: 24.0 }]
1764            );
1765        }
1766    }
1767
1768    fn light_colors() -> LiquidColors {
1769        LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
1770    }
1771
1772    fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
1773        match effect {
1774            RenderEffect::Shader { shader } => std::sync::Arc::unwrap_or_clone(shader),
1775            RenderEffect::Chain { second, .. } => {
1776                terminal_shader(std::sync::Arc::unwrap_or_clone(second))
1777            }
1778            effect => panic!("expected runtime shader, got {effect:?}"),
1779        }
1780    }
1781
1782    #[test]
1783    fn optical_projection_validates_scales_and_projects_output_support() {
1784        let glass = Glass::lens().resolve(&light_colors());
1785        let mut dynamics = GlassDynamics {
1786            morph: Some(GlassMorph {
1787                node_size: (200.0, 120.0),
1788                primary: (100.0, 60.0, 111.0, 70.0, 35.0),
1789                ..Default::default()
1790            }),
1791            ..Default::default()
1792        };
1793        let base = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1794        for projection in [
1795            None,
1796            Some((0.0, 1.0)),
1797            Some((-1.0, 1.0)),
1798            Some((1.0, f32::NAN)),
1799            Some((f32::INFINITY, 1.0)),
1800        ] {
1801            dynamics.optical_projection = projection;
1802            let shader = terminal_shader(glass.backdrop_effect(3.0, dynamics.clone()));
1803            assert_eq!(
1804                &shader.uniforms()
1805                    [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1806                &[1.0, 1.0]
1807            );
1808            assert_eq!(shader.output_support(), base.output_support());
1809        }
1810        dynamics.optical_projection = Some((1.2, 0.8));
1811        let projected = terminal_shader(glass.backdrop_effect(3.0, dynamics));
1812        assert_eq!(
1813            &projected.uniforms()
1814                [GLASS_OPTICAL_PROJECTION_UNIFORM..GLASS_OPTICAL_PROJECTION_UNIFORM + 2],
1815            &[1.2, 0.8]
1816        );
1817        let a = base.output_support().unwrap();
1818        let b = projected.output_support().unwrap();
1819        assert!((b.width - a.width * 1.2).abs() < 0.001);
1820        assert!((b.height - a.height * 0.8).abs() < 0.001);
1821    }
1822
1823    #[test]
1824    fn content_layers_preserve_the_complete_material_effect_and_cache() {
1825        let resolved = Glass::lens()
1826            .edge_refraction(9.0)
1827            .no_clip()
1828            .resolve(&light_colors());
1829        let layers = cached_glass_content_layers(resolved.clone());
1830        for density in [1.0, 3.0] {
1831            for activity in [0.0, 0.5, 1.0] {
1832                let dynamics = GlassDynamics {
1833                    activity: Some(activity),
1834                    ..Default::default()
1835                };
1836                let actual = layers(density, dynamics.clone());
1837                let joined = actual[0]
1838                    .backdrop_effect
1839                    .clone()
1840                    .unwrap()
1841                    .then(actual[1].backdrop_effect.clone().unwrap());
1842                assert_eq!(
1843                    joined,
1844                    resolved.runtime_effect_with_content(density, &dynamics, false, true)
1845                );
1846                assert!(actual[0].render_effect.is_none());
1847                assert!(!actual[0].clip);
1848                assert_eq!(
1849                    terminal_shader(actual[1].backdrop_effect.clone().unwrap()).uniforms()[147],
1850                    3.0
1851                );
1852                let mut repeated = layers(density, dynamics.clone());
1853                assert_eq!(actual, repeated);
1854                repeated[0].backdrop_effect = None;
1855                assert_eq!(actual, layers(density, dynamics));
1856            }
1857        }
1858    }
1859
1860    #[test]
1861    fn cached_glass_layers_refresh_all_live_inputs_and_keep_clones_independent() {
1862        let resolved = Glass::regular().no_clip().resolve(&light_colors());
1863        let layer = cached_glass_layer(resolved.clone());
1864        let mut dynamics = GlassDynamics::default();
1865        let mut density = 2.0;
1866        let mut previous = None;
1867        for step in 0..16 {
1868            match step {
1869                1 => density = 3.0,
1870                2 => set_glass_light_direction((0.5, -0.5)),
1871                3 => dynamics.highlight_boost = 0.25,
1872                4 => dynamics.saturation_boost = 0.4,
1873                5 => dynamics.activity = Some(0.5),
1874                6 => dynamics.resting_tint = Some(Color::RED),
1875                7 => dynamics.tint_alpha_multiplier = Some(0.3),
1876                8 => dynamics.touch = Some((5.0, 8.0, 0.7)),
1877                9 => dynamics.press_depth = Some(0.4),
1878                10 => {
1879                    dynamics.morph = Some(GlassMorph {
1880                        node_size: (60.0, 30.0),
1881                        primary: (30.0, 15.0, 50.0, 20.0, 4.0),
1882                        shapes: vec![(10.0, 15.0, 8.0, 8.0, -1.0)],
1883                        ..Default::default()
1884                    })
1885                }
1886                11 => {
1887                    let morph = dynamics.morph.as_mut().unwrap();
1888                    morph.primary.0 += 3.0;
1889                    morph.shapes[0].0 += 2.0;
1890                    morph.deformation = Some(GlassDeformation::incompressible((1.0, 1.0), 1.3));
1891                }
1892                12 => dynamics.morph = None,
1893                13 => dynamics.touch_radius_dp = Some(144.0),
1894                14 => dynamics.edge_return_depth_dp = Some(3.5),
1895                15 => dynamics.optical_projection = Some((1.2, 0.8)),
1896                _ => {}
1897            }
1898            let actual = layer(density, dynamics.clone());
1899            let expected = GraphicsLayer {
1900                backdrop_effect: Some(resolved.backdrop_effect(density, dynamics.clone())),
1901                render_effect: dynamics
1902                    .morph
1903                    .as_ref()
1904                    .map(|_| resolved.runtime_effect(density, &dynamics, true)),
1905                shape: resolved.shape.layer_shape(),
1906                clip: resolved.clip,
1907                ..Default::default()
1908            };
1909            assert_eq!(actual, expected, "live input step {step}");
1910            assert_ne!(
1911                previous.as_ref(),
1912                Some(&actual),
1913                "input change must reach the layer at step {step}"
1914            );
1915            let mut repeated = layer(density, dynamics.clone());
1916            assert_eq!(repeated, actual);
1917            repeated.backdrop_effect = None;
1918            assert_eq!(layer(density, dynamics.clone()), actual);
1919            previous = Some(actual);
1920        }
1921        set_glass_light_direction((0.0, 1.0));
1922        let replacement = cached_glass_layer(
1923            Glass::clear()
1924                .shape(LiquidShape::RoundedRect(9.0))
1925                .resolve(&light_colors()),
1926        );
1927        assert_ne!(
1928            replacement(density, dynamics.clone()),
1929            layer(density, dynamics)
1930        );
1931    }
1932
1933    #[test]
1934    fn glass_cache_input_equality_preserves_signed_zero_and_nan_bits() {
1935        let mut a = GlassDynamics {
1936            touch: Some((0.0, 1.0, 0.5)),
1937            ..Default::default()
1938        };
1939        let mut b = a.clone();
1940        b.touch.as_mut().unwrap().0 = -0.0;
1941        assert!(!glass_dynamics_match(&a, &b));
1942        a.highlight_boost = f32::NAN;
1943        assert!(glass_dynamics_match(&a, &a));
1944        a.morph = Some(GlassMorph::default());
1945        b = a.clone();
1946        b.morph.as_mut().unwrap().zoom_anchor.0 = -0.0;
1947        assert!(!glass_dynamics_match(&a, &b));
1948        b = a.clone();
1949        b.morph
1950            .as_mut()
1951            .unwrap()
1952            .shapes
1953            .push((0.0, 1.0, 2.0, 3.0, 4.0));
1954        assert!(!glass_dynamics_match(&a, &b));
1955    }
1956
1957    #[test]
1958    fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1959        assert_eq!(glass_light_direction(), (0.0, 1.0));
1960        let resolved = Glass::regular().resolve(&light_colors());
1961        let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1962        let shader = terminal_shader(effect);
1963        let u = shader.uniforms();
1964        assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1965        assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1966
1967        set_glass_light_direction((1.0, 0.0));
1968        let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1969        let u_rotated = terminal_shader(effect);
1970        let u_rotated = u_rotated.uniforms();
1971        assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1972        assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1973        set_glass_light_direction((0.0, 1.0));
1974    }
1975
1976    #[test]
1977    fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1978        for shape in [
1979            LiquidShape::Capsule,
1980            LiquidShape::Circle,
1981            LiquidShape::RoundedRect(12.0),
1982        ] {
1983            assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1984        }
1985    }
1986
1987    #[test]
1988    fn glass_shadow_clamps_negative_radius() {
1989        let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1990        assert_eq!(shadow.radius, 0.0);
1991        assert_eq!(shadow.offset_y, 3.0);
1992        assert_eq!(shadow.spread, -1.0);
1993    }
1994
1995    #[test]
1996    fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1997        let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1998        assert_eq!(deformation.axis(), (0.6, 0.8));
1999        assert_eq!(deformation.along(), 1.25);
2000        assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
2001        assert_eq!(
2002            GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
2003            (1.0, 0.0)
2004        );
2005    }
2006
2007    #[test]
2008    fn glass_builders_clamp_physical_inputs() {
2009        let glass = Glass::lens()
2010            .shape(LiquidShape::Circle)
2011            .tint(Color::BLACK)
2012            .blur_radius(-2.0)
2013            .saturation(1.2)
2014            .refraction_depth(3.0)
2015            .refraction_depth_dp(-3.0)
2016            .refraction_curve(2.0)
2017            .dispersion(2.0)
2018            .transmission_refraction(2.0)
2019            .meniscus_absorption(2.0)
2020            .highlight(0.4)
2021            .lift(-0.2)
2022            .adaptive_frost(Color::WHITE, 2.0)
2023            .shadow(false)
2024            .no_clip();
2025        assert_eq!(glass.shape, LiquidShape::Circle);
2026        assert_eq!(glass.tint, Some(Color::BLACK));
2027        assert_eq!(glass.blur_radius, Some(-2.0));
2028        assert_eq!(glass.saturation, Some(1.2));
2029        assert_eq!(glass.refraction_depth, 2.0);
2030        assert_eq!(glass.refraction_depth_dp, Some(0.0));
2031        assert_eq!(glass.refraction_curve, 1.0);
2032        assert_eq!(glass.dispersion, 1.0);
2033        assert_eq!(glass.transmission_refraction, 1.0);
2034        assert_eq!(glass.meniscus_absorption, 1.0);
2035        assert_eq!(glass.highlight, 0.4);
2036        assert_eq!(glass.lift, Some(-0.2));
2037        assert_eq!(glass.adaptive_frost, 1.0);
2038        assert!(!glass.shadow);
2039        assert!(!glass.clip);
2040    }
2041
2042    #[test]
2043    fn material_variants_resolve_distinct_frost_levels() {
2044        let regular = Glass::regular().resolve(&light_colors());
2045        let clear = Glass::clear().resolve(&light_colors());
2046        let lens = Glass::lens().resolve(&light_colors());
2047        assert!(regular.blur_radius_dp > clear.blur_radius_dp);
2048        assert!(clear.blur_radius_dp > lens.blur_radius_dp);
2049        assert!(regular.saturation > clear.saturation);
2050        assert_eq!(lens.rim_style, 1.0);
2051        assert_eq!(regular.refraction_depth, 0.0);
2052        assert_eq!(regular.transmission_refraction, 0.0);
2053        assert_eq!(regular.refraction_curve, 0.25);
2054        assert_eq!(lens.refraction_curve, 1.0);
2055        assert_eq!(regular.dispersion, 0.0);
2056        assert_eq!(lens.dispersion, 0.30);
2057    }
2058
2059    #[test]
2060    fn neutral_surface_helpers_follow_foreground_polarity() {
2061        assert_eq!(
2062            neutral_surface_tint(Color::BLACK, 0.08, 0.10),
2063            Color::BLACK.with_alpha(0.08)
2064        );
2065        assert_eq!(
2066            neutral_surface_tint(Color::WHITE, 0.08, 0.10),
2067            Color::WHITE.with_alpha(0.10)
2068        );
2069    }
2070
2071    #[test]
2072    fn dynamic_saturation_reaches_the_material_tint() {
2073        let resting = Color::from_rgb_u8(0, 199, 208);
2074        let raised = boost_tint_saturation(resting, 0.55);
2075        assert_eq!(raised.r(), 0.0);
2076        assert!(raised.g() > resting.g());
2077        assert!(raised.b() > resting.b());
2078        assert_eq!(raised.a(), resting.a());
2079    }
2080
2081    #[test]
2082    fn template_specialization_follows_each_materials_dispersion_and_activity() {
2083        for (dispersion, activity) in [(0.6, 0.0), (0.0, 0.5), (0.3, 1.0), (0.0, 0.0)] {
2084            let resolved = Glass::regular()
2085                .dispersion(dispersion)
2086                .resolve(&light_colors());
2087            let mut shader = terminal_shader(resolved.backdrop_effect(
2088                1.0,
2089                GlassDynamics {
2090                    activity: Some(activity),
2091                    ..GlassDynamics::default()
2092                },
2093            ));
2094            cranpose_ui_graphics::specialize_liquid_glass_with_folds(&mut shader, true);
2095            assert_eq!(
2096                shader.uniforms()[GLASS_DISPERSION_UNIFORM],
2097                dispersion * activity
2098            );
2099            assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], activity);
2100            assert_eq!(
2101                shader
2102                    .overrides()
2103                    .iter()
2104                    .any(|(name, _)| { *name == cranpose_ui_graphics::GLASS_DISPERSION_OFF_FLAG }),
2105                dispersion * activity == 0.0
2106            );
2107        }
2108    }
2109
2110    fn spectrum_fixture() -> GlassSpectrum {
2111        GlassSpectrum {
2112            angle_radians: 0.7,
2113            step_dp: -1.5,
2114            vertical_scale: 1.25,
2115            opacity_near: 0.25,
2116            opacity_far: 0.75,
2117            fade_depth_dp: 3.5,
2118            extent_dp: 12.0,
2119        }
2120    }
2121
2122    #[test]
2123    fn edge_spectrum_preserves_signed_taps_and_captures_every_ray() {
2124        let spectrum = spectrum_fixture();
2125        let material = Glass::lens()
2126            .edge_refraction(9.0)
2127            .dispersion(0.0)
2128            .edge_spectrum(spectrum);
2129        assert_eq!(material.edge_spectrum, Some(spectrum));
2130        let resolved = material.resolve(&light_colors());
2131        let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2132        assert_eq!(
2133            &shader.uniforms()[GLASS_EDGE_SPECTRUM_UNIFORM..GLASS_EDGE_SPECTRUM_UNIFORM + 8],
2134            &spectrum.uniforms().unwrap()
2135        );
2136        let ordinary = Glass::lens()
2137            .edge_refraction(9.0)
2138            .dispersion(0.0)
2139            .resolve(&light_colors());
2140        let base = terminal_shader(ordinary.backdrop_effect(3.0, GlassDynamics::default()));
2141        assert!((shader.input_padding() - base.input_padding() - 5.625).abs() < 0.001);
2142    }
2143
2144    #[test]
2145    fn edge_spectrum_rejects_invalid_fields_at_both_entry_points() {
2146        for index in 0..7 {
2147            for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2148                let mut spectrum = spectrum_fixture();
2149                let fields = [
2150                    &mut spectrum.angle_radians,
2151                    &mut spectrum.step_dp,
2152                    &mut spectrum.vertical_scale,
2153                    &mut spectrum.opacity_near,
2154                    &mut spectrum.opacity_far,
2155                    &mut spectrum.fade_depth_dp,
2156                    &mut spectrum.extent_dp,
2157                ];
2158                *fields.into_iter().nth(index).unwrap() = invalid;
2159                assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2160                let material = Glass {
2161                    edge_spectrum: Some(spectrum),
2162                    ..Glass::lens()
2163                };
2164                let shader = terminal_shader(
2165                    material
2166                        .resolve(&light_colors())
2167                        .backdrop_effect(1.0, GlassDynamics::default()),
2168                );
2169                assert_eq!(
2170                    shader
2171                        .uniforms()
2172                        .get(GLASS_EDGE_SPECTRUM_UNIFORM + 7)
2173                        .copied()
2174                        .unwrap_or(0.0),
2175                    0.0
2176                );
2177            }
2178        }
2179        for index in 2..7 {
2180            let mut spectrum = spectrum_fixture();
2181            let fields = [
2182                &mut spectrum.vertical_scale,
2183                &mut spectrum.opacity_near,
2184                &mut spectrum.opacity_far,
2185                &mut spectrum.fade_depth_dp,
2186                &mut spectrum.extent_dp,
2187            ];
2188            *fields.into_iter().nth(index - 2).unwrap() = -0.1;
2189            assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2190        }
2191        for near in [true, false] {
2192            let mut spectrum = spectrum_fixture();
2193            if near {
2194                spectrum.opacity_near = 1.1;
2195            } else {
2196                spectrum.opacity_far = 1.1;
2197            }
2198            assert_eq!(Glass::lens().edge_spectrum(spectrum).edge_spectrum, None);
2199        }
2200        let zero = GlassSpectrum {
2201            angle_radians: 0.0,
2202            step_dp: 0.0,
2203            vertical_scale: 0.0,
2204            opacity_near: 0.0,
2205            opacity_far: 1.0,
2206            fade_depth_dp: 0.0,
2207            extent_dp: 0.0,
2208        };
2209        assert_eq!(Glass::lens().edge_spectrum(zero).edge_spectrum, Some(zero));
2210    }
2211
2212    #[test]
2213    fn material_template_preserves_each_instances_optical_zoom_anchor() {
2214        let resolved = Glass::lens().resolve(&light_colors());
2215        for anchor in [(3.5, -2.25), (0.0, 0.0), (-7.0, 11.0)] {
2216            let shader = terminal_shader(resolved.backdrop_effect(
2217                1.5,
2218                GlassDynamics {
2219                    morph: Some(GlassMorph {
2220                        node_size: (120.0, 72.0),
2221                        primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2222                        zoom_anchor: anchor,
2223                        ..Default::default()
2224                    }),
2225                    ..Default::default()
2226                },
2227            ));
2228            assert_eq!(
2229                &shader.uniforms()
2230                    [GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM..GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM + 2],
2231                &[anchor.0, anchor.1]
2232            );
2233        }
2234    }
2235
2236    #[test]
2237    fn key_fill_preserves_valid_parameters_and_rejects_invalid_fields() {
2238        let lighting = GlassKeyFill {
2239            height_dp: 1.0,
2240            curvature: 0.8,
2241            angle_radians: -std::f32::consts::FRAC_PI_4,
2242            saturation: 1.5,
2243            luma_gain: 0.4367,
2244            offset: 1.125,
2245            scale_with_surface: true,
2246        };
2247        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2248            assert!(glass.key_fill.is_none());
2249            let material = glass.key_fill(lighting);
2250            assert_eq!(material.key_fill, Some(lighting));
2251            let shader = terminal_shader(material.backdrop_effect(
2252                &light_colors(),
2253                3.0,
2254                GlassDynamics::default(),
2255            ));
2256            assert_eq!(
2257                &shader.uniforms()[GLASS_KEY_FILL_UNIFORM..GLASS_KEY_FILL_UNIFORM + 7],
2258                &lighting.uniforms().unwrap()
2259            );
2260        }
2261        for scale_with_surface in [false, true] {
2262            let value = GlassKeyFill {
2263                scale_with_surface,
2264                ..lighting
2265            };
2266            assert_eq!(value.uniforms().unwrap()[6], f32::from(scale_with_surface));
2267            let layer = cached_glass_layer(Glass::clear().key_fill(value).resolve(&light_colors()))(
2268                3.0,
2269                GlassDynamics::default(),
2270            );
2271            assert!(!layer.clip);
2272            assert!(layer.render_effect.is_some());
2273            assert!(layer.backdrop_effect.is_some());
2274        }
2275        for index in 0..6 {
2276            for invalid in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2277                let mut value = lighting;
2278                match index {
2279                    0 => value.height_dp = invalid,
2280                    1 => value.curvature = invalid,
2281                    2 => value.angle_radians = invalid,
2282                    3 => value.saturation = invalid,
2283                    4 => value.luma_gain = invalid,
2284                    _ => value.offset = invalid,
2285                }
2286                let shader = terminal_shader(Glass::clear().key_fill(value).backdrop_effect(
2287                    &light_colors(),
2288                    3.0,
2289                    GlassDynamics::default(),
2290                ));
2291                assert_eq!(
2292                    shader
2293                        .uniforms()
2294                        .get(GLASS_KEY_FILL_UNIFORM)
2295                        .copied()
2296                        .unwrap_or_default(),
2297                    0.0
2298                );
2299            }
2300        }
2301        for invalid in [0.0, -1.0] {
2302            assert!(
2303                GlassKeyFill {
2304                    height_dp: invalid,
2305                    ..lighting
2306                }
2307                .uniforms()
2308                .is_none()
2309            );
2310            assert!(
2311                GlassKeyFill {
2312                    curvature: invalid,
2313                    ..lighting
2314                }
2315                .uniforms()
2316                .is_none()
2317            );
2318        }
2319    }
2320
2321    #[test]
2322    fn independent_edge_return_depth_preserves_zero_and_rejects_invalid_values() {
2323        for depth in [
2324            None,
2325            Some(0.0),
2326            Some(3.5),
2327            Some(-1.0),
2328            Some(f32::NAN),
2329            Some(f32::INFINITY),
2330        ] {
2331            let shader = terminal_shader(
2332                Glass::lens()
2333                    .edge_refraction(9.0)
2334                    .refraction_depth_dp(36.0)
2335                    .backdrop_effect(
2336                        &light_colors(),
2337                        3.0,
2338                        GlassDynamics {
2339                            edge_return_depth_dp: depth,
2340                            ..GlassDynamics::default()
2341                        },
2342                    ),
2343            );
2344            let valid = depth.filter(|value| value.is_finite() && *value >= 0.0);
2345            assert_eq!(
2346                shader
2347                    .uniforms()
2348                    .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM)
2349                    .copied()
2350                    .unwrap_or(0.0),
2351                valid.unwrap_or(0.0)
2352            );
2353            assert_eq!(
2354                shader
2355                    .uniforms()
2356                    .get(GLASS_EDGE_RETURN_DEPTH_UNIFORM + 1)
2357                    .copied()
2358                    .unwrap_or(0.0),
2359                f32::from(valid.is_some())
2360            );
2361            assert_eq!(
2362                shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2363                36.0
2364            );
2365        }
2366    }
2367
2368    #[test]
2369    fn inner_shadow_validates_and_resolves_its_gaussian_profile() {
2370        let shadow = GlassShadow::new(Color::BLACK.with_alpha(0.12), 3.0, 7.0, 0.0);
2371        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2372            assert_eq!(glass.inner_shadow, None);
2373            let material = glass.inner_shadow(shadow);
2374            assert_eq!(material.inner_shadow, Some(shadow));
2375            let shader = terminal_shader(material.backdrop_effect(
2376                &light_colors(),
2377                3.0,
2378                GlassDynamics::default(),
2379            ));
2380            assert_eq!(
2381                &shader.uniforms()[GLASS_INNER_SHADOW_UNIFORM..GLASS_INNER_SHADOW_UNIFORM + 8],
2382                &shadow.uniforms().unwrap()
2383            );
2384        }
2385        for alpha in [0.0, 1.0] {
2386            assert!(
2387                Glass::lens()
2388                    .inner_shadow(GlassShadow::new(
2389                        Color::BLACK.with_alpha(alpha),
2390                        0.0,
2391                        -2.0,
2392                        -3.0
2393                    ))
2394                    .inner_shadow
2395                    .is_some()
2396            );
2397        }
2398        for index in 0..7 {
2399            for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2400                let mut values = shadow.uniforms().unwrap();
2401                values[index] = value;
2402                let invalid = GlassShadow {
2403                    color: Color(values[0], values[1], values[2], values[3]),
2404                    radius: values[4],
2405                    offset_y: values[5],
2406                    spread: values[6],
2407                };
2408                assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2409                let material = Glass {
2410                    inner_shadow: Some(invalid),
2411                    ..Glass::lens()
2412                };
2413                let shader = terminal_shader(material.backdrop_effect(
2414                    &light_colors(),
2415                    3.0,
2416                    GlassDynamics::default(),
2417                ));
2418                assert_eq!(
2419                    shader
2420                        .uniforms()
2421                        .get(GLASS_INNER_SHADOW_UNIFORM + 7)
2422                        .copied()
2423                        .unwrap_or(0.0),
2424                    0.0
2425                );
2426            }
2427        }
2428        for invalid in [
2429            GlassShadow {
2430                radius: -1.0,
2431                ..shadow
2432            },
2433            GlassShadow {
2434                color: Color(0.0, 0.0, 0.0, -0.1),
2435                ..shadow
2436            },
2437            GlassShadow {
2438                color: Color(0.0, 0.0, 0.0, 1.1),
2439                ..shadow
2440            },
2441        ] {
2442            assert_eq!(Glass::lens().inner_shadow(invalid).inner_shadow, None);
2443        }
2444    }
2445
2446    #[test]
2447    fn adaptive_tone_resolves_foreground_and_declares_the_shared_mean() {
2448        use cranpose_ui_graphics::SubstrateSpec;
2449        for foreground in [Color::BLACK, Color::WHITE, Color(f32::NAN, 0.0, 0.0, 1.0)] {
2450            let material = Glass::clear().adaptive_tone(foreground);
2451            assert!(material.adaptive_tone);
2452            let resolved = material.resolve(&light_colors());
2453            assert!(resolved.foreground_luma.is_finite());
2454            let shader = terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2455            assert_eq!(shader.uniforms()[GLASS_ADAPTIVE_TONE_UNIFORM], 1.0);
2456            assert_eq!(shader.substrates(), &[SubstrateSpec::Mean]);
2457            let expected = if foreground.r().is_finite() {
2458                foreground.r()
2459            } else {
2460                0.2126 * light_colors().label.r()
2461                    + 0.7152 * light_colors().label.g()
2462                    + 0.0722 * light_colors().label.b()
2463            };
2464            assert!((resolved.foreground_luma - expected).abs() < 0.0001);
2465        }
2466        assert!(!Glass::clear().adaptive_tone);
2467        assert!(!Glass::regular().adaptive_tone);
2468    }
2469
2470    #[test]
2471    fn face_response_preserves_valid_ranges_and_rejects_invalid_values() {
2472        let response = GlassFaceResponse {
2473            gain: 0.97,
2474            start_dp: 1.0 / 3.0,
2475            end_dp: 2.0 / 3.0,
2476            illumination: 0.05,
2477        };
2478        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2479            assert!(glass.face_response.is_none());
2480            let material = glass.face_response(response);
2481            assert_eq!(material.face_response, Some(response));
2482            let shader = terminal_shader(material.backdrop_effect(
2483                &light_colors(),
2484                3.0,
2485                GlassDynamics::default(),
2486            ));
2487            assert_eq!(
2488                &shader.uniforms()[GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4],
2489                &response.uniforms().unwrap()
2490            );
2491        }
2492        for gain in [0.0, 1.0] {
2493            assert!(
2494                GlassFaceResponse {
2495                    gain,
2496                    start_dp: 0.0,
2497                    end_dp: 0.0,
2498                    ..response
2499                }
2500                .uniforms()
2501                .is_some()
2502            );
2503        }
2504        let mut invalid = vec![
2505            GlassFaceResponse {
2506                gain: -0.1,
2507                ..response
2508            },
2509            GlassFaceResponse {
2510                gain: 1.1,
2511                ..response
2512            },
2513            GlassFaceResponse {
2514                start_dp: -1.0,
2515                ..response
2516            },
2517            GlassFaceResponse {
2518                end_dp: 0.0,
2519                ..response
2520            },
2521        ];
2522        for value in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
2523            invalid.extend([
2524                GlassFaceResponse {
2525                    gain: value,
2526                    ..response
2527                },
2528                GlassFaceResponse {
2529                    start_dp: value,
2530                    ..response
2531                },
2532                GlassFaceResponse {
2533                    end_dp: value,
2534                    ..response
2535                },
2536                GlassFaceResponse {
2537                    illumination: value,
2538                    ..response
2539                },
2540            ]);
2541        }
2542        for response in invalid {
2543            assert!(response.uniforms().is_none());
2544            let shader = terminal_shader(Glass::clear().face_response(response).backdrop_effect(
2545                &light_colors(),
2546                3.0,
2547                GlassDynamics::default(),
2548            ));
2549            assert!(
2550                shader
2551                    .uniforms()
2552                    .get(GLASS_FACE_RESPONSE_UNIFORM..GLASS_FACE_RESPONSE_UNIFORM + 4)
2553                    .is_none_or(|values| values.iter().all(|value| *value == 0.0))
2554            );
2555        }
2556    }
2557
2558    #[test]
2559    fn face_lighting_is_explicit_and_preserves_each_material_default() {
2560        for glass in [Glass::regular(), Glass::clear(), Glass::lens()] {
2561            assert!(glass.face_lighting);
2562            for enabled in [false, true] {
2563                let material = glass.clone().face_lighting(enabled);
2564                assert_eq!(material.face_lighting, enabled);
2565                let resolved = material.resolve(&light_colors());
2566                assert_eq!(resolved.face_lighting, enabled);
2567                let shader =
2568                    terminal_shader(resolved.backdrop_effect(3.0, GlassDynamics::default()));
2569                assert_eq!(
2570                    shader.uniforms()[GLASS_FACE_LIGHTING_OFF_UNIFORM],
2571                    f32::from(!enabled)
2572                );
2573            }
2574        }
2575    }
2576
2577    #[test]
2578    fn layer_clipping_has_one_owner_for_material_coverage() {
2579        for clipped in [false, true] {
2580            let mut glass = Glass::regular();
2581            if !clipped {
2582                glass = glass.no_clip();
2583            }
2584            let shader = terminal_shader(glass.backdrop_effect(
2585                &light_colors(),
2586                3.0,
2587                GlassDynamics::default(),
2588            ));
2589            assert_eq!(
2590                shader.uniforms()[GLASS_LAYER_CLIPPED_UNIFORM],
2591                f32::from(clipped)
2592            );
2593        }
2594    }
2595
2596    #[test]
2597    fn touch_radius_preserves_valid_values_and_normalizes_invalid_values() {
2598        for radius in [
2599            None,
2600            Some(-1.0),
2601            Some(0.0),
2602            Some(f32::NAN),
2603            Some(f32::INFINITY),
2604            Some(f32::NEG_INFINITY),
2605            Some(0.5),
2606            Some(144.0),
2607        ] {
2608            let expected = radius
2609                .filter(|value| value.is_finite() && *value > 0.0)
2610                .unwrap_or(58.0);
2611            for density in [1.0, 3.0] {
2612                let shader = terminal_shader(Glass::regular().backdrop_effect(
2613                    &light_colors(),
2614                    density,
2615                    GlassDynamics {
2616                        touch_radius_dp: radius,
2617                        ..Default::default()
2618                    },
2619                ));
2620                assert_eq!(shader.uniforms()[GLASS_TOUCH_RADIUS_UNIFORM], expected);
2621            }
2622        }
2623    }
2624
2625    #[test]
2626    fn material_instances_share_source_without_sharing_uniforms_or_overrides() {
2627        let resolved = Glass::regular().resolve(&light_colors());
2628        let mut first = terminal_shader(resolved.backdrop_effect(2.0, GlassDynamics::default()));
2629        let untouched = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2630        let unused_slot = RuntimeShader::MAX_USER_UNIFORMS - 1;
2631        first.set_float(unused_slot, 17.0);
2632        first.set_override("INSTANCE_ONLY", 1.0);
2633        let second = terminal_shader(resolved.backdrop_effect(1.0, GlassDynamics::default()));
2634        assert_eq!(second.uniforms(), untouched.uniforms());
2635        assert_eq!(second.overrides(), untouched.overrides());
2636        assert_eq!(
2637            second.uniforms().get(unused_slot).copied().unwrap_or(0.0),
2638            0.0
2639        );
2640        assert!(
2641            !second
2642                .overrides()
2643                .iter()
2644                .any(|(name, _)| *name == "INSTANCE_ONLY")
2645        );
2646        assert_eq!(first.uniforms()[unused_slot], 17.0);
2647        assert_eq!(first.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2648        assert_eq!(second.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 1.0);
2649        assert_eq!(first.source().as_ptr(), second.source().as_ptr());
2650    }
2651
2652    #[test]
2653    fn resolved_material_packs_wcksrd_and_dynamic_tint() {
2654        let resolved = Glass::lens()
2655            .refraction_depth(0.72)
2656            .refraction_depth_dp(21.0)
2657            .refraction_curve(0.8)
2658            .dispersion(0.42)
2659            .transmission_refraction(0.35)
2660            .meniscus_absorption(0.3)
2661            .blur_radius(3.0)
2662            .tint(Color::BLACK.with_alpha(0.8))
2663            .resolve(&light_colors());
2664        let effect = resolved.backdrop_effect(
2665            2.0,
2666            GlassDynamics {
2667                highlight_boost: 0.2,
2668                saturation_boost: 0.35,
2669                tint_alpha_multiplier: Some(0.25),
2670                ..Default::default()
2671            },
2672        );
2673        let RenderEffect::Chain { first, .. } = &effect else {
2674            panic!("macroscopic frost must precede the wcKSRD optical pass");
2675        };
2676        assert!(matches!(
2677            first.as_ref(),
2678            RenderEffect::Blur {
2679                radius_x: 6.0,
2680                radius_y: 6.0,
2681                ..
2682            }
2683        ));
2684        let shader = terminal_shader(effect);
2685        assert_eq!(shader.uniforms()[9], 0.72);
2686        assert_eq!(
2687            shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2688            21.0
2689        );
2690        assert_eq!(
2691            shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
2692            1.0
2693        );
2694        assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
2695        assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
2696        assert_eq!(
2697            shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
2698            0.35
2699        );
2700        assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
2701        assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
2702        assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
2703        assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
2704        assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2705        assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
2706    }
2707
2708    #[test]
2709    fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
2710        let effect = Glass::regular()
2711            .resolve(&light_colors())
2712            .backdrop_effect(3.0, GlassDynamics::default());
2713        let RenderEffect::Chain { first, .. } = &effect else {
2714            panic!("heavy blur must ride the separable Gaussian pre-pass");
2715        };
2716        assert!(matches!(
2717            first.as_ref(),
2718            RenderEffect::Blur {
2719                radius_x: 24.0,
2720                radius_y: 24.0,
2721                ..
2722            }
2723        ));
2724        assert_eq!(
2725            terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
2726            0.0
2727        );
2728    }
2729
2730    #[test]
2731    fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
2732        let RenderEffect::Shader { shader } = Glass::regular()
2733            .blur_radius(0.5)
2734            .resolve(&light_colors())
2735            .backdrop_effect(3.0, GlassDynamics::default())
2736        else {
2737            panic!("a sub-cap blur must not build a Gaussian chain");
2738        };
2739        assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
2740    }
2741
2742    #[test]
2743    fn raised_tint_density_stays_premultiplied_alpha_safe() {
2744        let effect = Glass::lens()
2745            .tint(Color::WHITE.with_alpha(0.8))
2746            .resolve(&light_colors())
2747            .backdrop_effect(
2748                1.0,
2749                GlassDynamics {
2750                    tint_alpha_multiplier: Some(2.0),
2751                    ..Default::default()
2752                },
2753            );
2754        assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
2755    }
2756
2757    #[test]
2758    fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
2759        let resolved = Glass::lens()
2760            .refraction_depth(0.72)
2761            .refraction_curve(0.8)
2762            .dispersion(0.42)
2763            .blur_radius(3.0)
2764            .saturation(1.4)
2765            .highlight(0.6)
2766            .lift(0.2)
2767            .tint(Color::BLACK.with_alpha(0.8))
2768            .no_clip()
2769            .resolve(&light_colors());
2770        let morph = GlassMorph {
2771            node_size: (120.0, 72.0),
2772            primary: (60.0, 36.0, 96.0, 52.0, -1.0),
2773            wobble_amplitude: 3.0,
2774            bulge_amplitude: 4.0,
2775            deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
2776            ..Default::default()
2777        };
2778
2779        let RenderEffect::Shader { shader } = resolved.backdrop_effect(
2780            2.0,
2781            GlassDynamics {
2782                activity: Some(0.0),
2783                morph: Some(morph),
2784                ..Default::default()
2785            },
2786        ) else {
2787            panic!("material must resolve to the shared wcKSRD shader");
2788        };
2789        let uniforms = shader.uniforms();
2790        assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
2791        assert_eq!(uniforms[9], 0.0);
2792        assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
2793        assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
2794        assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
2795        assert_eq!(uniforms[11], 0.0);
2796        assert_eq!(uniforms[17], 0.0);
2797        assert_eq!(uniforms[18], 1.0);
2798        assert_eq!(uniforms[20], 0.0);
2799        assert_eq!(uniforms[21], 0.0);
2800        assert_eq!(uniforms[24], 1.0);
2801        assert_eq!(uniforms[28], 0.0);
2802        assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
2803        assert_eq!(uniforms[91], 0.0);
2804        assert_eq!(uniforms[102], 0.0);
2805        assert_eq!(
2806            &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2807            &[0.0, 0.0, 0.0, 0.0]
2808        );
2809        assert_eq!(uniforms[32], 0.0);
2810        assert_eq!(uniforms[26], 0.0);
2811        assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
2812        assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
2813    }
2814
2815    #[test]
2816    fn surface_refraction_is_explicit_and_reaches_the_shader() {
2817        for glass in [Glass::regular(), Glass::lens()] {
2818            assert_eq!(glass.refraction, GlassRefraction::Radial);
2819            let surface = glass.surface_refraction(31.0);
2820            assert_eq!(
2821                surface.refraction,
2822                GlassRefraction::Surface { reach_dp: 31.0 }
2823            );
2824            let shader = terminal_shader(surface.backdrop_effect(
2825                &light_colors(),
2826                3.0,
2827                GlassDynamics::default(),
2828            ));
2829            assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 1.0);
2830            assert_eq!(shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM], 31.0);
2831            assert!(
2832                !shader
2833                    .overrides()
2834                    .iter()
2835                    .any(|(name, value)| *name == "GLASS_DIRECTIONAL_REFRACTION_OFF"
2836                        && *value != 0.0)
2837            );
2838        }
2839    }
2840
2841    #[test]
2842    fn edge_refraction_preserves_face_zoom_and_scales_outward_reach_with_contact() {
2843        assert_eq!(GlassRefraction::default(), GlassRefraction::Radial);
2844        let glass = Glass::lens()
2845            .surface_refraction(31.0)
2846            .edge_refraction(12.0)
2847            .optical_zoom(1.2);
2848        assert_eq!(
2849            glass.refraction,
2850            GlassRefraction::EdgeLens { reach_dp: 12.0 }
2851        );
2852        for (activity, press, reach) in [(1.0, 1.0, 12.0), (0.5, 0.5, 3.0), (0.0, 1.0, 0.0)] {
2853            let shader = terminal_shader(glass.backdrop_effect(
2854                &light_colors(),
2855                3.0,
2856                GlassDynamics {
2857                    activity: Some(activity),
2858                    press_depth: Some(press),
2859                    ..Default::default()
2860                },
2861            ));
2862            assert_eq!(shader.uniforms()[GLASS_REFRACTION_MODE_UNIFORM], 2.0);
2863            assert_eq!(
2864                shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2865                reach
2866            );
2867            assert_eq!(
2868                shader.uniforms()[GLASS_OPTICAL_ZOOM_UNIFORM],
2869                1.0 + 0.2 * activity
2870            );
2871        }
2872        assert_eq!(
2873            glass.surface_refraction(31.0).refraction,
2874            GlassRefraction::Surface { reach_dp: 31.0 }
2875        );
2876    }
2877
2878    #[test]
2879    fn edge_refraction_captures_the_farthest_spectral_ray() {
2880        for (reach, dispersion, required) in
2881            [(20.0, 1.0, 101.67), (0.0, 1.0, 42.78), (20.0, 0.0, 60.88)]
2882        {
2883            let shader = terminal_shader(
2884                Glass::lens()
2885                    .edge_refraction(reach)
2886                    .dispersion(dispersion)
2887                    .backdrop_effect(&light_colors(), 3.0, GlassDynamics::default()),
2888            );
2889            assert!(shader.input_padding() >= required);
2890        }
2891    }
2892
2893    #[test]
2894    fn directional_refraction_rejects_invalid_reach_at_both_public_entry_points() {
2895        for input in [-1.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY, 0.0, 12.0] {
2896            let expected = if input.is_finite() {
2897                input.max(0.0)
2898            } else {
2899                0.0
2900            };
2901            for (built, direct, normalized) in [
2902                (
2903                    Glass::lens().edge_refraction(input),
2904                    GlassRefraction::EdgeLens { reach_dp: input },
2905                    GlassRefraction::EdgeLens { reach_dp: expected },
2906                ),
2907                (
2908                    Glass::regular().surface_refraction(input),
2909                    GlassRefraction::Surface { reach_dp: input },
2910                    GlassRefraction::Surface { reach_dp: expected },
2911                ),
2912            ] {
2913                assert_eq!(built.refraction, normalized);
2914                let direct = Glass {
2915                    refraction: direct,
2916                    ..built
2917                };
2918                let shader = terminal_shader(direct.backdrop_effect(
2919                    &light_colors(),
2920                    1.0,
2921                    GlassDynamics::default(),
2922                ));
2923                assert_eq!(
2924                    shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2925                    expected
2926                );
2927            }
2928        }
2929    }
2930
2931    #[test]
2932    fn surface_reach_is_independent_of_depth_and_tracks_contact() {
2933        for depth in [4.0, 15.5, 30.0] {
2934            for (activity, press, reach) in [(1.0, 1.0, 31.0), (0.5, 0.5, 7.75), (0.0, 1.0, 0.0)] {
2935                let shader = terminal_shader(
2936                    Glass::regular()
2937                        .surface_refraction(31.0)
2938                        .refraction_depth_dp(depth)
2939                        .backdrop_effect(
2940                            &light_colors(),
2941                            3.0,
2942                            GlassDynamics {
2943                                activity: Some(activity),
2944                                press_depth: Some(press),
2945                                ..Default::default()
2946                            },
2947                        ),
2948                );
2949                assert_eq!(
2950                    shader.uniforms()[GLASS_EDGE_REFRACTION_REACH_UNIFORM],
2951                    reach
2952                );
2953                assert_eq!(
2954                    shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
2955                    depth * activity * press
2956                );
2957                assert!(shader.input_padding() >= 31.0);
2958            }
2959        }
2960    }
2961
2962    #[test]
2963    fn resting_surface_tint_survives_zero_optical_activity() {
2964        let tint = Color::BLACK.with_alpha(0.11);
2965        let RenderEffect::Shader { shader } = Glass::lens()
2966            .no_clip()
2967            .resolve(&light_colors())
2968            .backdrop_effect(
2969                1.0,
2970                GlassDynamics {
2971                    activity: Some(0.0),
2972                    resting_tint: Some(tint),
2973                    ..Default::default()
2974                },
2975            )
2976        else {
2977            panic!("resting surface must use the shared wcKSRD shader");
2978        };
2979        assert_eq!(
2980            &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
2981            &[tint.r(), tint.g(), tint.b(), tint.a()]
2982        );
2983        assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
2984    }
2985
2986    #[test]
2987    fn full_optical_activity_preserves_the_resolved_material() {
2988        let resolved = Glass::lens()
2989            .refraction_depth(0.72)
2990            .refraction_curve(0.8)
2991            .dispersion(0.42)
2992            .blur_radius(3.0)
2993            .resolve(&light_colors());
2994        let shader = terminal_shader(resolved.backdrop_effect(
2995            2.0,
2996            GlassDynamics {
2997                activity: Some(1.0),
2998                ..Default::default()
2999            },
3000        ));
3001        let uniforms = shader.uniforms();
3002        assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
3003        assert_eq!(uniforms[9], 0.72);
3004        assert_eq!(
3005            uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
3006            0.0
3007        );
3008        assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
3009        assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
3010        assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
3011        assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
3012    }
3013
3014    #[test]
3015    fn morph_geometry_and_incompressible_strain_are_packed() {
3016        let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
3017        let morph = GlassMorph {
3018            node_size: (78.0, 59.0),
3019            primary: (39.0, 29.5, 58.0, 39.0, -1.0),
3020            shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
3021            glue: 8.0,
3022            wobble_amplitude: 1.0,
3023            wobble_phase: 0.5,
3024            bulge_amplitude: 2.0,
3025            bulge_direction: 0.25,
3026            ellipse_blend: 0.3,
3027            capsule_smoothing_dp: 4.0,
3028            deformation: Some(deformation),
3029            zoom_anchor: (0.0, 0.0),
3030        };
3031        let RenderEffect::Shader { shader } = Glass::lens()
3032            .no_clip()
3033            .resolve(&light_colors())
3034            .backdrop_effect(
3035                1.0,
3036                GlassDynamics {
3037                    morph: Some(morph),
3038                    ..Default::default()
3039                },
3040            )
3041        else {
3042            panic!("morph must use the shared wcKSRD shader");
3043        };
3044        let uniforms = shader.uniforms();
3045        assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
3046        assert_eq!(uniforms[30], 1.0);
3047        assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
3048        assert_eq!(uniforms[110], 0.3);
3049        assert_eq!(
3050            uniforms[cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3051            4.0
3052        );
3053        assert!(shader.output_padding() > 0.0);
3054    }
3055
3056    #[test]
3057    fn capsule_smoothing_is_finite_nonnegative_and_invalidates_the_morph() {
3058        for value in [-1.0, 0.0, 4.0, f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
3059            let morph = GlassMorph {
3060                node_size: (120.0, 80.0),
3061                primary: (60.0, 40.0, 111.0, 70.0, -1.0),
3062                capsule_smoothing_dp: value,
3063                ..Default::default()
3064            };
3065            let expected = if value.is_finite() {
3066                value.max(0.0)
3067            } else {
3068                0.0
3069            };
3070            let shader = terminal_shader(Glass::lens().backdrop_effect(
3071                &light_colors(),
3072                3.0,
3073                GlassDynamics {
3074                    morph: Some(morph.clone()),
3075                    ..Default::default()
3076                },
3077            ));
3078            assert_eq!(
3079                shader.uniforms()[cranpose_ui_graphics::GLASS_CAPSULE_SMOOTHING_UNIFORM],
3080                expected
3081            );
3082            assert!(glass_morphs_match(&morph, &morph));
3083            let changed = GlassMorph {
3084                capsule_smoothing_dp: 5.0,
3085                ..morph.clone()
3086            };
3087            assert!(!glass_morphs_match(&morph, &changed));
3088        }
3089    }
3090
3091    #[test]
3092    fn optical_builders_hold_their_own_legal_range() {
3093        let glass = Glass::regular()
3094            .fold_depth(-4.0)
3095            .optical_zoom(0.5)
3096            .rim_reflection(9.0);
3097        assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
3098        assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
3099        assert_eq!(
3100            glass.rim_reflection, 2.0,
3101            "the rim tops out at twice the line"
3102        );
3103
3104        let glass = Glass::regular()
3105            .fold_depth(6.0)
3106            .optical_zoom(1.4)
3107            .rim_reflection(0.25);
3108        assert_eq!(glass.fold_depth, 6.0);
3109        assert_eq!(glass.optical_zoom, 1.4);
3110        assert_eq!(glass.rim_reflection, 0.25);
3111    }
3112
3113    #[test]
3114    fn ink_recolor_records_its_color_at_a_clamped_strength() {
3115        let red = Color::from_rgb_u8(255, 0, 0);
3116        let glass = Glass::regular().ink_recolor(red, 3.0);
3117        assert_eq!(glass.ink_recolor, Some((red, 1.0)));
3118
3119        let glass = Glass::regular().ink_recolor(red, -1.0);
3120        assert_eq!(glass.ink_recolor, Some((red, 0.0)));
3121        assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
3122    }
3123
3124    #[test]
3125    fn a_touched_up_surface_amplifies_what_it_already_is() {
3126        let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
3127        assert!(
3128            dynamics.highlight_boost > 0.0,
3129            "a pressed surface goes brighter"
3130        );
3131        assert!(dynamics.saturation_boost > 0.0, "and more saturated");
3132        assert_eq!(
3133            dynamics.touch,
3134            Some((10.0, 4.0, 1.0)),
3135            "with no finger the glow sits at the surface's own heart"
3136        );
3137        assert_eq!(
3138            dynamics.resting_tint, None,
3139            "a touch never repaints the surface with another color"
3140        );
3141    }
3142
3143    #[test]
3144    fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
3145        let dynamics = GlassDynamics::default()
3146            .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
3147            .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
3148        let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
3149        assert_eq!(
3150            (x, y),
3151            (3.0, 7.0),
3152            "the glow tracks the finger, not the center"
3153        );
3154        assert_eq!(intensity, 1.0, "carried press saturates at full");
3155    }
3156
3157    #[test]
3158    fn an_unpressed_surface_is_left_exactly_as_it_was() {
3159        let resting = GlassDynamics::default();
3160        let untouched = resting
3161            .clone()
3162            .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
3163        assert_eq!(untouched.highlight_boost, resting.highlight_boost);
3164        assert_eq!(untouched.saturation_boost, resting.saturation_boost);
3165        assert_eq!(untouched.touch, None, "no press, no glow");
3166    }
3167
3168    #[test]
3169    fn an_off_axis_strain_widens_the_support_by_the_affine_rows_times_the_half_extents() {
3170        let resolved = Glass::regular()
3171            .shape(LiquidShape::RoundedRect(0.0))
3172            .blur_radius(0.0)
3173            .shadow(false)
3174            .no_clip()
3175            .resolve(&light_colors());
3176        let angle = 22.5f32.to_radians();
3177        let morph = GlassMorph {
3178            node_size: (600.0, 600.0),
3179            primary: (300.0, 300.0, 200.0, 200.0, 0.0),
3180            deformation: Some(GlassDeformation::incompressible(
3181                (angle.cos(), angle.sin()),
3182                2.0,
3183            )),
3184            ..Default::default()
3185        };
3186        let RenderEffect::Shader { shader } = resolved.backdrop_effect(
3187            1.0,
3188            GlassDynamics {
3189                activity: Some(1.0),
3190                morph: Some(morph),
3191                ..Default::default()
3192            },
3193        ) else {
3194            panic!("an unblurred glass is one shader");
3195        };
3196        let support = shader
3197            .output_support()
3198            .expect("a morphing glass declares its support");
3199        let half_x = 100.0 * (2.0 * angle.cos().powi(2) + 0.5 * angle.sin().powi(2))
3200            + 100.0 * (1.5 * angle.sin() * angle.cos());
3201        assert!(
3202            (half_x - 231.066).abs() < 0.01,
3203            "the example's half extent is {half_x}"
3204        );
3205        assert!(
3206            support.x <= 300.0 - half_x && support.x + support.width >= 300.0 + half_x,
3207            "the support {support:?} must hold the strained square's x extent {half_x}"
3208        );
3209        let half_y = 100.0 * (1.5 * angle.sin() * angle.cos())
3210            + 100.0 * (2.0 * angle.sin().powi(2) + 0.5 * angle.cos().powi(2));
3211        assert!(
3212            (half_y - 125.0).abs() < 0.01,
3213            "the example's y half extent is {half_y}"
3214        );
3215        assert!(
3216            support.y <= 300.0 - half_y && support.y + support.height >= 300.0 + half_y,
3217            "the support {support:?} must hold the strained square's y extent {half_y}"
3218        );
3219    }
3220
3221    #[test]
3222    fn a_morphing_glass_declares_its_output_support_around_its_shapes_and_their_reach() {
3223        let resolved = Glass::regular()
3224            .shape(LiquidShape::RoundedRect(12.0))
3225            .blur_radius(0.0)
3226            .shadow(true)
3227            .no_clip()
3228            .resolve(&light_colors());
3229        let morph = GlassMorph {
3230            node_size: (280.0, 160.0),
3231            primary: (140.0, 80.0, 120.0, 40.0, -1.0),
3232            shapes: vec![(230.0, 80.0, 40.0, 40.0, -1.0)],
3233            glue: 20.0,
3234            wobble_amplitude: 3.0,
3235            bulge_amplitude: 8.0,
3236            deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
3237            ..Default::default()
3238        };
3239        let dynamics = GlassDynamics {
3240            activity: Some(1.0),
3241            morph: Some(morph.clone()),
3242            ..Default::default()
3243        };
3244        let RenderEffect::Shader { shader } = resolved.backdrop_effect(2.0, dynamics) else {
3245            panic!("an unblurred glass is one shader");
3246        };
3247        let support = shader
3248            .output_support()
3249            .expect("a morphing glass declares its support");
3250        let shape_reach = (230.0 + 20.0) - (140.0 + 60.0);
3251        let shadow_reach = resolved.shadow_radius
3252            + resolved.shadow_offset_y.abs()
3253            + resolved.shadow_spread.max(0.0);
3254        let field_reach = 3.0 * 2.0 + 8.0 + shape_reach + 40.0 + shadow_reach + 4.0;
3255        let smallest_strain = 0.8;
3256        let reach = field_reach / smallest_strain;
3257        let contains = |x: f32, y: f32| {
3258            support.x <= x
3259                && support.y <= y
3260                && support.x + support.width >= x
3261                && support.y + support.height >= y
3262        };
3263        for (cx, cy) in [(-1.0, -1.0), (1.0, -1.0), (-1.0, 1.0), (1.0, 1.0)] {
3264            let corner = ((60.0 + reach) * cx, (20.0 + reach) * cy);
3265            let stretched = (140.0 + corner.0 * 1.25, 80.0 + corner.1 * 0.8);
3266            assert!(
3267                contains(stretched.0, stretched.1 - resolved.shadow_offset_y.abs())
3268                    && contains(stretched.0, stretched.1 + resolved.shadow_offset_y.abs()),
3269                "{support:?} must hold the strained, dilated corner {stretched:?}"
3270            );
3271        }
3272        assert!(
3273            contains(230.0 + 20.0 + reach, 80.0),
3274            "the glued shape's reach is inside"
3275        );
3276        assert!(shader.output_padding() >= field_reach);
3277
3278        let RenderEffect::Shader { shader } =
3279            resolved.backdrop_effect(2.0, GlassDynamics::default())
3280        else {
3281            panic!("an unblurred glass is one shader");
3282        };
3283        assert_eq!(
3284            shader.output_support(),
3285            None,
3286            "cover-mode glass fills its rect"
3287        );
3288    }
3289}
3290
3291#[cfg(test)]
3292mod transparency_tests {
3293    use super::*;
3294
3295    #[test]
3296    fn less_transparency_makes_glass_a_flat_surface() {
3297        let colors = crate::theme::LiquidColors::light(Color::from_rgb_u8(0, 122, 255));
3298        let flat = Glass::regular()
3299            .resolve(&colors)
3300            .without_transparency(&colors);
3301        assert_eq!(flat.tint, colors.surface);
3302        assert_eq!(flat.blur_radius_dp, 0.0);
3303        assert_eq!(flat.refraction_depth, 0.0);
3304        assert_eq!(flat.saturation, 1.0);
3305        assert!(flat.edge_spectrum.is_none());
3306        let clear = Glass::regular().resolve(&colors);
3307        assert!(clear.blur_radius_dp > 0.0, "plain glass keeps its blur");
3308    }
3309}