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::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_or(0.0, |(_, _, i)| i);
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_or((0.0, 0.0), |morph| morph.zoom_anchor);
1175        shader.set_float2(
1176            GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1177            zoom_anchor.0,
1178            zoom_anchor.1,
1179        );
1180        shader.set_float(121, self.rim_reflection.max(0.001));
1181        let (ink_color, ink_strength) = self
1182            .ink_recolor
1183            .map_or((Color::TRANSPARENT, 0.0), |(color, strength)| {
1184                (color, strength * activity)
1185            });
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.morph.as_ref().map_or(0.0, |morph| {
1255            let (px, py, pw, ph, _) = morph.primary;
1256            let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1257            let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1258            let mut shape_reach = 0.0f32;
1259            for (sx, sy, sw, sh, _) in &morph.shapes {
1260                let reach_x = ((sx + sw * 0.5) - right)
1261                    .max(left - (sx - sw * 0.5))
1262                    .max(0.0);
1263                let reach_y = ((sy + sh * 0.5) - bottom)
1264                    .max(top - (sy - sh * 0.5))
1265                    .max(0.0);
1266                shape_reach = shape_reach.max(reach_x.max(reach_y));
1267            }
1268            let glue_pad = if morph.shapes.is_empty() {
1269                0.0
1270            } else {
1271                morph.glue * 2.0
1272            };
1273            morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1274        });
1275        shader.set_input_padding(
1276            (self.input_padding()
1277                + if refraction_mode >= 1.5 {
1278                    refraction_reach * (26.5 / 9.0) + spectrum_reach
1279                } else {
1280                    refraction_reach
1281                }
1282                + morph_pad
1283                + wcksrd_blur_radius / density)
1284                * projection.0.max(projection.1).max(1.0),
1285        );
1286        if let Some(morph) = dynamics.morph.as_ref() {
1287            let shadow_reach = if dynamic_shadow {
1288                self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
1289            } else {
1290                0.0
1291            };
1292            let support = morph_output_support(
1293                morph,
1294                activity,
1295                morph_pad,
1296                shadow_reach,
1297                if dynamic_shadow {
1298                    self.shadow_offset_y.abs()
1299                } else {
1300                    0.0
1301                },
1302            );
1303            let (cx, cy, _, _, _) = morph.primary;
1304            let support = Rect {
1305                x: cx + (support.x - cx) * projection.0,
1306                y: cy + (support.y - cy) * projection.1,
1307                width: support.width * projection.0,
1308                height: support.height * projection.1,
1309            };
1310            let (node_w, node_h) = morph.node_size;
1311            let overhang = (-support.x)
1312                .max(-support.y)
1313                .max(support.x + support.width - node_w)
1314                .max(support.y + support.height - node_h)
1315                .max(0.0);
1316            shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1317            shader.set_output_support(Some(support));
1318        }
1319
1320        let optical_effect = liquid_glass_runtime_effect(shader);
1321        if gaussian_blur_radius > f32::EPSILON {
1322            RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1323                .then(optical_effect)
1324        } else {
1325            optical_effect
1326        }
1327    }
1328
1329    fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1330        if let Some(amount) = self.tint_amount {
1331            shader.set_float(
1332                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1333                8.0 * density,
1334            );
1335            shader.set_float(
1336                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1337                amount.percent() / 100.0,
1338            );
1339        }
1340        if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1341            shader.set_float2(
1342                cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1343                radius * density,
1344                opacity,
1345            );
1346        }
1347    }
1348
1349    fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1350        let spectrum = self
1351            .edge_spectrum
1352            .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1353        if let Some((spectrum, values)) = spectrum {
1354            for (index, value) in values.into_iter().enumerate() {
1355                shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1356            }
1357            spectrum.capture_reach()
1358        } else {
1359            40.787 * self.dispersion
1360        }
1361    }
1362
1363    fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1364        let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1365        shader.set_float(118, touch_x);
1366        shader.set_float(119, touch_y);
1367        shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1368        shader.set_float(
1369            GLASS_FACE_LIGHTING_OFF_UNIFORM,
1370            f32::from(!self.face_lighting),
1371        );
1372        shader.set_float(
1373            GLASS_TOUCH_RADIUS_UNIFORM,
1374            dynamics
1375                .touch_radius_dp
1376                .filter(|radius| radius.is_finite() && *radius > 0.0)
1377                .unwrap_or(58.0),
1378        );
1379        shader.set_float(
1380            GLASS_LAYER_CLIPPED_UNIFORM,
1381            f32::from(self.clip && !self.clips_in_shader()),
1382        );
1383        if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1384            for (index, value) in values.into_iter().enumerate() {
1385                shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1386            }
1387        }
1388        if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1389            for (index, value) in values.into_iter().enumerate() {
1390                shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1391            }
1392        }
1393        if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1394            for (index, value) in values.into_iter().enumerate() {
1395                shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1396            }
1397        }
1398    }
1399
1400    fn input_padding(&self) -> f32 {
1401        2.0
1402    }
1403}
1404
1405/// Modifier extension installing the Liquid Glass material.
1406pub trait LiquidModifierExt {
1407    /// Applies the glass material to this composable's bounds: backdrop blur +
1408    /// lens shader, clipped to `glass.shape`, with a soft drop shadow.
1409    ///
1410    /// Must be called in composable context (the material resolves theme
1411    /// colors at the call site).
1412    fn glass_effect(self, glass: Glass) -> Modifier;
1413
1414    /// [`glass_effect`](Self::glass_effect) with per-frame motion inputs; the
1415    /// closure is read at scene-build time, so animating tilt or highlight
1416    /// does not recompose.
1417    fn glass_effect_with(
1418        self,
1419        glass: Glass,
1420        dynamics: impl Fn() -> GlassDynamics + 'static,
1421    ) -> Modifier;
1422}
1423
1424impl LiquidModifierExt for Modifier {
1425    fn glass_effect(self, glass: Glass) -> Modifier {
1426        self.glass_effect_with(glass, GlassDynamics::default)
1427    }
1428
1429    fn glass_effect_with(
1430        self,
1431        glass: Glass,
1432        dynamics: impl Fn() -> GlassDynamics + 'static,
1433    ) -> Modifier {
1434        let colors = crate::theme::liquid_colors();
1435        let options = cranpose_services::local_accessibility_options().current();
1436        let resolved = if options.reduce_transparency {
1437            glass.resolve(&colors).without_transparency(&colors)
1438        } else {
1439            glass.resolve(&colors)
1440        };
1441        let shape = resolved.shape;
1442
1443        let mut modifier = self;
1444        if resolved.shadow && resolved.clip {
1445            let shadow_color = resolved.shadow_color;
1446            let (radius, offset_y, spread) = (
1447                resolved.shadow_radius,
1448                resolved.shadow_offset_y,
1449                resolved.shadow_spread,
1450            );
1451            modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1452                scope.radius = radius;
1453                scope.spread = spread;
1454                scope.offset.y = offset_y;
1455                scope.color = shadow_color;
1456                scope.cutout = true;
1457            });
1458        }
1459
1460        let layer = cached_glass_layer(resolved);
1461        modifier.graphics_layer(move || layer(current_density(), dynamics()))
1462    }
1463}
1464
1465struct CachedGlassLayer<T> {
1466    density: u32,
1467    light: [u32; 2],
1468    dynamics: GlassDynamics,
1469    value: T,
1470}
1471
1472fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1473    cached_glass_layers(resolved, false, std::convert::identity)
1474}
1475
1476pub(crate) fn cached_glass_content_layers(
1477    resolved: ResolvedGlass,
1478) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1479    cached_glass_layers(resolved, true, |mut layer| {
1480        let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1481            panic!("content layers require a sequential edge lens");
1482        };
1483        layer.backdrop_effect = Some((*second).clone());
1484        [
1485            GraphicsLayer {
1486                backdrop_effect: Some((*first).clone()),
1487                ..Default::default()
1488            },
1489            layer,
1490        ]
1491    })
1492}
1493
1494fn cached_glass_layers<T: Clone>(
1495    resolved: ResolvedGlass,
1496    separate_content: bool,
1497    assemble: impl Fn(GraphicsLayer) -> T,
1498) -> impl Fn(f32, GlassDynamics) -> T {
1499    let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1500    move |density, dynamics| {
1501        let direction = glass_light_direction();
1502        let light = [direction.0, direction.1].map(f32::to_bits);
1503        let mut cached = cached.borrow_mut();
1504        if let Some(previous) = &*cached
1505            && previous.density == density.to_bits()
1506            && previous.light == light
1507            && glass_dynamics_match(&previous.dynamics, &dynamics)
1508        {
1509            return previous.value.clone();
1510        }
1511        let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1512            || (resolved.clip && resolved.clips_in_shader()))
1513        .then(|| resolved.runtime_effect(density, &dynamics, true));
1514        let layer = GraphicsLayer {
1515            backdrop_effect: Some(resolved.runtime_effect_with_content(
1516                density,
1517                &dynamics,
1518                false,
1519                separate_content,
1520            )),
1521            render_effect,
1522            shape: resolved.shape.layer_shape(),
1523            clip: resolved.clip && !resolved.clips_in_shader(),
1524            ..Default::default()
1525        };
1526        let value = assemble(layer);
1527        *cached = Some(CachedGlassLayer {
1528            density: density.to_bits(),
1529            light,
1530            dynamics,
1531            value: value.clone(),
1532        });
1533        value
1534    }
1535}
1536
1537fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1538    let bits = |d: &GlassDynamics| {
1539        let GlassDynamics {
1540            edge_return_depth_dp,
1541            optical_projection,
1542            activity,
1543            resting_tint,
1544            highlight_boost,
1545            saturation_boost,
1546            tint_alpha_multiplier,
1547            morph: _,
1548            touch,
1549            touch_radius_dp,
1550            press_depth,
1551        } = d;
1552        (
1553            edge_return_depth_dp.map(f32::to_bits),
1554            optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
1555            activity.map(f32::to_bits),
1556            resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
1557            [*highlight_boost, *saturation_boost].map(f32::to_bits),
1558            tint_alpha_multiplier.map(f32::to_bits),
1559            touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
1560            press_depth.map(f32::to_bits),
1561            touch_radius_dp.map(f32::to_bits),
1562        )
1563    };
1564    bits(a) == bits(b)
1565        && match (&a.morph, &b.morph) {
1566            (None, None) => true,
1567            (Some(a), Some(b)) => glass_morphs_match(a, b),
1568            _ => false,
1569        }
1570}
1571
1572fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
1573    let shape_bits =
1574        |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
1575    let bits = |m: &GlassMorph| {
1576        let GlassMorph {
1577            node_size,
1578            primary,
1579            shapes: _,
1580            glue,
1581            wobble_amplitude,
1582            wobble_phase,
1583            bulge_amplitude,
1584            bulge_direction,
1585            ellipse_blend,
1586            capsule_smoothing_dp,
1587            deformation,
1588            zoom_anchor,
1589        } = m;
1590        (
1591            [
1592                node_size.0,
1593                node_size.1,
1594                *glue,
1595                *wobble_amplitude,
1596                *wobble_phase,
1597                *bulge_amplitude,
1598                *bulge_direction,
1599                *ellipse_blend,
1600                *capsule_smoothing_dp,
1601                zoom_anchor.0,
1602                zoom_anchor.1,
1603            ]
1604            .map(f32::to_bits),
1605            shape_bits(primary),
1606            deformation
1607                .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
1608        )
1609    };
1610    bits(a) == bits(b)
1611        && a.shapes
1612            .iter()
1613            .map(shape_bits)
1614            .eq(b.shapes.iter().map(shape_bits))
1615}
1616
1617fn morph_output_support(
1618    morph: &GlassMorph,
1619    activity: f32,
1620    morph_pad: f32,
1621    shadow_reach: f32,
1622    shadow_offset: f32,
1623) -> Rect {
1624    let (px, py, pw, ph, _) = morph.primary;
1625    let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
1626        (
1627            1.0 + (deformation.along() - 1.0) * activity,
1628            deformation.axis(),
1629        )
1630    });
1631    let across = 1.0 / along;
1632    let smallest_strain = along.min(across);
1633    let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
1634    let rows = [
1635        (
1636            (along * ax * ax + across * ay * ay).abs(),
1637            ((along - across) * ax * ay).abs(),
1638        ),
1639        (
1640            ((along - across) * ax * ay).abs(),
1641            (along * ay * ay + across * ax * ax).abs(),
1642        ),
1643    ];
1644    let half = (pw * 0.5 + reach, ph * 0.5 + reach);
1645    let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
1646    let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
1647    let (mut left, mut top) = (px - extent_x, py - extent_y);
1648    let (mut right, mut bottom) = (px + extent_x, py + extent_y);
1649    for (sx, sy, sw, sh, _) in &morph.shapes {
1650        left = left.min(sx - sw * 0.5 - reach);
1651        top = top.min(sy - sh * 0.5 - reach);
1652        right = right.max(sx + sw * 0.5 + reach);
1653        bottom = bottom.max(sy + sh * 0.5 + reach);
1654    }
1655    Rect {
1656        x: left,
1657        y: top - shadow_offset,
1658        width: right - left,
1659        height: bottom - top + 2.0 * shadow_offset,
1660    }
1661}
1662
1663fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
1664    let (node_w, node_h) = morph.node_size;
1665    let (cx, cy, w, h, radius) = morph.primary;
1666    shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
1667    shader.set_float2(2, cx, cy);
1668    shader.set_float2(4, w, h);
1669    shader.set_float(6, radius);
1670    let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
1671    shader.set_float(30, count as f32);
1672    for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
1673        let base = 36 + index * 5;
1674        shader.set_float(base, *sx);
1675        shader.set_float(base + 1, *sy);
1676        shader.set_float(base + 2, *sw);
1677        shader.set_float(base + 3, *sh);
1678        shader.set_float(base + 4, *sr);
1679    }
1680    shader.set_float(31, morph.glue);
1681    shader.set_float(32, morph.wobble_amplitude * activity);
1682    shader.set_float(33, morph.wobble_phase);
1683    shader.set_float(26, morph.bulge_amplitude * activity);
1684    shader.set_float(27, morph.bulge_direction);
1685    shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
1686    shader.set_float(
1687        cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
1688        if morph.capsule_smoothing_dp.is_finite() {
1689            morph.capsule_smoothing_dp.max(0.0) * activity
1690        } else {
1691            0.0
1692        },
1693    );
1694    if let Some(deformation) = morph.deformation {
1695        let axis = deformation.axis();
1696        let along = 1.0 + (deformation.along() - 1.0) * activity;
1697        shader.set_float2(106, axis.0, axis.1);
1698        shader.set_float(108, along);
1699        shader.set_float(109, 1.0 / along);
1700    } else {
1701        shader.set_float2(106, 1.0, 0.0);
1702        shader.set_float2(108, 1.0, 1.0);
1703    }
1704}
1705
1706#[cfg(test)]
1707#[path = "tests/material_tests.rs"]
1708mod tests;
1709
1710#[cfg(test)]
1711#[path = "tests/material_transparency_tests.rs"]
1712mod transparency_tests;