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::{cell::RefCell, sync::OnceLock};
7
8use cranpose_ui::{Modifier, current_density};
9use cranpose_ui_graphics::{
10    Color, GLASS_ACTIVITY_UNIFORM, GLASS_ADAPTIVE_FROST_UNIFORM, GLASS_ADAPTIVE_TONE_UNIFORM,
11    GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM, GLASS_EDGE_REFRACTION_REACH_UNIFORM,
12    GLASS_EDGE_RETURN_DEPTH_UNIFORM, GLASS_EDGE_SPECTRUM_UNIFORM, GLASS_EFFECT_DENSITY_UNIFORM,
13    GLASS_FACE_LIGHTING_OFF_UNIFORM, GLASS_FACE_RESPONSE_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM,
14    GLASS_INNER_SHADOW_UNIFORM, GLASS_KEY_FILL_UNIFORM, GLASS_LAYER_CLIPPED_UNIFORM,
15    GLASS_LAYERED_RETURN_UNIFORM, GLASS_LIGHT_DIRECTION_UNIFORM, GLASS_MENISCUS_ABSORPTION_UNIFORM,
16    GLASS_OPTICAL_PROJECTION_UNIFORM, GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
17    GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
18    GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM,
19    GLASS_REFRACTION_MODE_UNIFORM, GLASS_RESTING_EDGE_SHARPNESS_UNIFORM,
20    GLASS_RESTING_TINT_UNIFORM, GLASS_TOUCH_RADIUS_UNIFORM, GLASS_TRANSMISSION_REFRACTION_UNIFORM,
21    GraphicsLayer, LIQUID_GLASS_WGSL, LayerShape, PlaceholderShape, Rect, RenderEffect,
22    RoundedCornerShape, RuntimeShader, TileMode, liquid_glass_placeholder,
23    liquid_glass_runtime_effect, specialize_liquid_glass,
24};
25
26use crate::{appearance::GlassTintAmount, theme::LiquidColors};
27
28/// Sets the ambient glass light return direction (screen space, need not be
29/// normalized — the shader normalizes). Feed device attitude here.
30pub fn set_glass_light_direction(direction: (f32, f32)) {
31    crate::thread_state::with(|state| state.glass_light_return.set(direction));
32}
33
34/// The current ambient glass light return direction.
35pub fn glass_light_direction() -> (f32, f32) {
36    crate::thread_state::with(|state| state.glass_light_return.get())
37}
38
39const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
40
41const CAPSULE_SHADER_RADIUS: f32 = -1.0;
42
43const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
44
45/// The shape of a glass element (also its clip and shadow shape).
46#[derive(Clone, Copy, Debug, PartialEq, Default)]
47pub enum LiquidShape {
48    /// A pill: corner radius follows the smaller half-extent.
49    #[default]
50    Capsule,
51    /// Rounded rectangle with the radius in dp.
52    RoundedRect(f32),
53    /// A circle (capsule of a square node).
54    Circle,
55}
56
57impl LiquidShape {
58    /// The clip shape handed to the graphics layer.
59    pub fn clip_shape(&self) -> RoundedCornerShape {
60        match self {
61            LiquidShape::Capsule | LiquidShape::Circle => {
62                RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
63            }
64            LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
65        }
66    }
67
68    /// The layer shape (clip + shadow geometry).
69    pub fn layer_shape(&self) -> LayerShape {
70        LayerShape::Rounded(self.clip_shape())
71    }
72
73    fn shader_radius_px(&self, density: f32) -> f32 {
74        match self {
75            LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
76            LiquidShape::RoundedRect(radius) => radius * density,
77        }
78    }
79
80    /// The corner radius in dp, the largest one making a capsule.
81    fn radius_dp(&self) -> f32 {
82        match self {
83            LiquidShape::Capsule | LiquidShape::Circle => f32::MAX,
84            LiquidShape::RoundedRect(radius) => *radius,
85        }
86    }
87}
88
89/// Material variant, mirroring SwiftUI's `.regular` / `.clear` glass, plus
90/// the interactive lens.
91#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
92pub enum GlassVariant {
93    /// Frosted: strong backdrop blur, vibrancy boost, scheme-adaptive lift.
94    #[default]
95    Regular,
96    /// Transparent: minimal blur, mild vibrancy — for media-rich backdrops.
97    Clear,
98    /// The interactive magnifying bubble (drag lenses, flying selection):
99    /// no frost, no tone shift, a full-element dome and pronounced rainbow
100    /// dispersion at the rim.
101    Lens,
102}
103
104/// The projection used to transmit the captured backdrop through glass.
105#[derive(Clone, Copy, Debug, PartialEq, Default)]
106pub enum GlassRefraction {
107    /// A radial projection toward the optical axis.
108    #[default]
109    Radial,
110    /// A projection perpendicular to the nearest surface edge.
111    Surface {
112        /// Maximum inward sample displacement in dp, independent of its falloff depth.
113        reach_dp: f32,
114    },
115    /// Successive outward and inward sample displacements with circular falloff profiles.
116    /// The return amplitude is 17.5/9 times the outward amplitude. Its default depth
117    /// is 7/36 of the outer profile; [`GlassDynamics::edge_return_depth_dp`] overrides it.
118    EdgeLens {
119        /// Outward warp amplitude in dp; refraction depth sets its falloff height.
120        reach_dp: f32,
121    },
122    /// An outward backdrop warp followed by the material's inward surface warp.
123    /// Both coordinate maps are composed before sampling, avoiding an intermediate image.
124    /// Amplitudes and depths are absolute; activity controls the resting fill separately.
125    LayeredSurface {
126        /// Outward backdrop amplitude in dp; refraction depth sets its falloff height.
127        reach_dp: f32,
128        /// Inward surface amplitude in dp.
129        return_reach_dp: f32,
130        /// Inward surface falloff height in dp.
131        return_depth_dp: f32,
132    },
133}
134
135impl GlassRefraction {
136    fn shader_parameters(self) -> (f32, f32) {
137        match self {
138            Self::Radial => (0.0, 0.0),
139            Self::Surface { reach_dp } => (1.0, finite_nonnegative(reach_dp)),
140            Self::EdgeLens { reach_dp } => (2.0, finite_nonnegative(reach_dp)),
141            Self::LayeredSurface { reach_dp, .. } => (3.0, finite_nonnegative(reach_dp)),
142        }
143    }
144}
145
146fn finite_nonnegative(reach_dp: f32) -> f32 {
147    if reach_dp.is_finite() {
148        reach_dp.max(0.0)
149    } else {
150        0.0
151    }
152}
153
154/// Shadow owned by a glass surface. Morphing glass evaluates the same live
155/// SDF for this shadow; clipped static glass forwards the values to the layer
156/// shadow primitive.
157#[derive(Clone, Copy, Debug, PartialEq)]
158pub struct GlassShadow {
159    /// The color of the shadow. Its alpha sets the opacity.
160    pub color: Color,
161    /// The blur radius of the shadow, in dp.
162    pub radius: f32,
163    /// The vertical offset of the shadow, in dp. A positive value moves it down.
164    pub offset_y: f32,
165    /// How far the shadow shape grows past the surface outline, in dp.
166    pub spread: f32,
167}
168
169impl GlassShadow {
170    /// Creates a shadow. A negative `radius` becomes zero.
171    pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
172        Self {
173            color,
174            radius: radius.max(0.0),
175            offset_y,
176            spread,
177        }
178    }
179
180    fn uniforms(self) -> Option<[f32; 8]> {
181        let values = [
182            self.color.r(),
183            self.color.g(),
184            self.color.b(),
185            self.color.a(),
186            self.radius,
187            self.offset_y,
188            self.spread,
189            1.0,
190        ];
191        (values.iter().all(|value| value.is_finite())
192            && self.radius >= 0.0
193            && (0.0..=1.0).contains(&self.color.a()))
194        .then_some(values)
195    }
196}
197
198/// Per-frame motion inputs for an interactive glass element, read lazily at
199/// scene-build time (no recomposition per frame).
200#[derive(Clone, Debug, PartialEq, Default)]
201pub struct GlassDynamics {
202    /// Uses a global color transfer and a white blurred contact light in place of touch glow.
203    pub contact_lighting: bool,
204    /// Per-frame source mapping. Layered surfaces use this to animate the backdrop
205    /// and material independently of the opaque resting fill.
206    pub refraction: Option<GlassRefraction>,
207    /// Per-frame shadow of the persistent surface, independent of optical activity.
208    pub shadow: Option<GlassShadow>,
209    /// A shadow cast by the surface contour instead of its filled silhouette.
210    /// The second value is the contour stroke width in dp; the shadow radius
211    /// is its Gaussian standard deviation. Nonfinite or nonpositive widths disable it.
212    pub ring_shadow: Option<(GlassShadow, f32)>,
213    /// Independent inner return depth for an edge lens, in dp. Zero removes its depth.
214    /// Nonfinite or negative values preserve the material's default depth ratio.
215    pub edge_return_depth_dp: Option<f32>,
216    /// Horizontal and vertical projection of the complete glass field about its primary center.
217    /// Refraction, lighting and all scene shapes share the projection. Missing, nonfinite or
218    /// nonpositive scales resolve to identity. This does not magnify the underlying backdrop.
219    pub optical_projection: Option<(f32, f32)>,
220    /// Continuous optical presence. `None` preserves the resolved material;
221    /// `Some(0)` is an exact backdrop identity while retaining the same node,
222    /// SDF, and pointer ride path.
223    pub activity: Option<f32>,
224    /// Base tint of the same persistent SDF surface at zero optical activity.
225    /// Its alpha cross-fades out as the refractive material rises.
226    pub resting_tint: Option<Color>,
227    /// Extra specular intensity (0 = spec default; e.g. press boost).
228    pub highlight_boost: f32,
229    /// Per-frame saturation added to the resolved material. Interactive
230    /// surfaces use this to raise chroma without changing their base tint.
231    pub saturation_boost: f32,
232    /// Optional per-frame multiplier for the material tint alpha. Values
233    /// below one clear a resting wash; values above one densify a raised tint.
234    pub tint_alpha_multiplier: Option<f32>,
235    /// Blends a plain translucent tint into this material's tint, including its
236    /// luminance-dependent transmission. The fraction is zero at the supplied
237    /// tint and one at the material tint; refraction and lighting stay active.
238    pub tint_crossfade: Option<(Color, f32)>,
239    /// Shape morph: when set, the glass geometry is these node-local rects
240    /// instead of the node cover — the shapeshift channel.
241    pub morph: Option<GlassMorph>,
242    /// Touch glow: `(x_dp, y_dp, intensity)` in node-local dp. A pressed
243    /// surface concentrates saturation and a soft light in a radial
244    /// gradient under the finger (never a flat recolor).
245    pub touch: Option<(f32, f32, f32)>,
246    /// Radius of the touch glow in dp; absent, nonpositive and nonfinite values use 58 dp.
247    pub touch_radius_dp: Option<f32>,
248    /// Dome press depth: how squashed the interactive dome is. At 1 the
249    /// pressed dome refracts deep and vivid (wide rim band, strong
250    /// chromatic split — the reference toggle's gray-hold rainbow); toward
251    /// 0 the released bead relaxes shallow and its split fades with it
252    /// (the reference's thin settled ring). `None` = 1.
253    pub press_depth: Option<f32>,
254}
255
256impl GlassDynamics {
257    fn projection(&self) -> (f32, f32) {
258        self.optical_projection
259            .filter(|(x, y)| x.is_finite() && y.is_finite() && *x > 0.0 && *y > 0.0)
260            .unwrap_or((1.0, 1.0))
261    }
262
263    /// The universal touched-up state (user-directed law): a pressed
264    /// liquid surface comes closer to the user while its OWN colors go
265    /// HDR-bright and saturated, with a radial glow following the finger.
266    /// Never a repaint with alternate colors — the boosts amplify whatever
267    /// the surface already is. `press` is 0..1; `finger` is node-local dp
268    /// (falls back to `center`, the surface's own heart).
269    pub fn touched_up(
270        mut self,
271        press: f32,
272        finger: Option<(f32, f32)>,
273        center: (f32, f32),
274    ) -> Self {
275        let press = press.clamp(0.0, 1.0);
276        if press <= 0.0 {
277            return self;
278        }
279        self.highlight_boost += 0.85 * press;
280        self.saturation_boost += 0.45 * press;
281        let (fx, fy) = finger.unwrap_or(center);
282        let carried = self.touch.map_or(0.0, |(_, _, i)| i);
283        self.touch = Some((fx, fy, (carried + press).clamp(0.0, 1.0)));
284        self
285    }
286}
287
288fn foreground_is_dark(foreground: Color) -> bool {
289    let foreground_luma =
290        0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
291    foreground_luma < 0.5
292}
293
294/// The tint a glass shows: `tint`, or, while it crossfades from a previous
295/// tint, the two mixed by the crossfade's progress.
296fn shown_tint(tint: Color, crossfade: Option<(Color, f32)>) -> Color {
297    let Some((previous, progress)) = crossfade.filter(|(_, progress)| progress.is_finite()) else {
298        return tint;
299    };
300    let progress = progress.clamp(0.0, 1.0);
301    let mix = |from: f32, to: f32| from + (to - from) * progress;
302    Color(
303        mix(previous.0, tint.0),
304        mix(previous.1, tint.1),
305        mix(previous.2, tint.2),
306        mix(previous.3, tint.3),
307    )
308}
309
310fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
311    if boost.abs() <= f32::EPSILON {
312        return tint;
313    }
314    let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
315    let saturation = (1.0 + boost).max(0.0);
316    Color::rgba(
317        (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
318        (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
319        (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
320        tint.a(),
321    )
322}
323
324pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
325    if foreground_is_dark(foreground) {
326        Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
327    } else {
328        Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
329    }
330}
331
332/// A liquid shapeshift frame: the primary shape plus any number of nearby
333/// glass shapes, ALL smooth-unioned into one field — liquid glass glues to
334/// whatever glass it passes near (a growing menu necks with a neighboring
335/// button, the drag lens merges with the search circle). Up to
336/// [`GlassMorph::MAX_SHAPES`] extra shapes; an angular wobble makes the
337/// mid-flight field bubble like a droplet. All geometry is node-local dp:
338/// `(center_x, center_y, width, height, corner_radius)`; radius sentinel
339/// `-1` means capsule; `-2` means SUBTRACT capsule — the shape carves a
340/// smooth hole in the field (the growing menu leaves its anchor button
341/// crisp on top until it swallows it).
342#[derive(Clone, Debug, PartialEq, Default)]
343pub struct GlassMorph {
344    /// The glass node's own size in dp. The shader receives all morph
345    /// geometry in dp and derives px-per-dp from the renderer-injected node
346    /// pixel rect divided by this — geometry then lands correctly at ANY
347    /// render scale (live window density, robot captures at 1.0, fractional
348    /// desktop scales). The authoring widget always knows its node size.
349    pub node_size: (f32, f32),
350    /// The primary glass shape as `(center_x, center_y, width, height, corner_radius)`, node-local,
351    /// in dp. A corner radius of `-1` means a capsule.
352    pub primary: (f32, f32, f32, f32, f32),
353    /// Nearby glass shapes participating in the field.
354    pub shapes: Vec<(f32, f32, f32, f32, f32)>,
355    /// Smooth-union glue radius (dp): shapes within it neck together.
356    pub glue: f32,
357    /// Wobble amplitude (dp) and phase (radians).
358    pub wobble_amplitude: f32,
359    /// The phase of the wobble, in radians.
360    pub wobble_phase: f32,
361    /// Viscous leading-edge bulge: while the shape travels or inflates, its
362    /// side facing `bulge_direction` (radians, math convention) swells like a
363    /// pulled droplet. Amplitude in dp, usually driven by morph velocity.
364    pub bulge_amplitude: f32,
365    /// The direction of the bulge, in radians in the math convention. It points toward the swelling
366    /// side.
367    pub bulge_direction: f32,
368    /// Blends the primary rounded-rectangle field toward an ellipse. This is
369    /// used by expanding droplets whose broad phase has continuous curvature
370    /// rather than the straight sides of a capsule.
371    pub ellipse_blend: f32,
372    /// Smooths the primary capsule's straight-to-arc joins over this distance in dp.
373    /// A positive value replaces its corner radius with the maximum capsule radius.
374    /// The span is capped by half the difference between the primary side lengths.
375    /// Zero or a nonfinite value preserves the primary outline.
376    pub capsule_smoothing_dp: f32,
377    /// Area-preserving affine strain applied to the primary shape. Extra
378    /// scene shapes remain fixed so nearby glass can join the travelling
379    /// droplet without being dragged through its local deformation.
380    pub deformation: Option<GlassDeformation>,
381    /// Offset (dp) of the optical-zoom axis from the primary SDF center. A
382    /// leaning droplet's silhouette shifts toward its travel side while its
383    /// curvature apex stays over the content it rides; anchoring the
384    /// magnification there keeps the face filled by that content instead of
385    /// pulling in whatever lies beyond it.
386    pub zoom_anchor: (f32, f32),
387}
388
389/// A normalized motion axis and reciprocal scales for incompressible glass.
390/// Construction derives the cross-axis scale, so callers cannot describe a
391/// deformation that changes the bubble's area.
392#[derive(Clone, Copy, Debug, PartialEq)]
393pub struct GlassDeformation {
394    axis: (f32, f32),
395    along: f32,
396}
397
398impl GlassDeformation {
399    /// Creates a deformation that scales by `along` along `axis` and by the reciprocal of `along`
400    /// across it, so the area stays the same. `axis` is normalized, and a zero axis becomes the x
401    /// axis.
402    pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
403        let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
404        let axis = if length > f32::EPSILON {
405            (axis.0 / length, axis.1 / length)
406        } else {
407            (1.0, 0.0)
408        };
409        Self {
410            axis,
411            along: along.max(f32::EPSILON),
412        }
413    }
414
415    /// The unit direction of the stretch.
416    pub fn axis(self) -> (f32, f32) {
417        self.axis
418    }
419
420    /// The scale along the axis.
421    pub fn along(self) -> f32 {
422        self.along
423    }
424
425    /// The scale across the axis, the reciprocal of `along`.
426    pub fn across(self) -> f32 {
427        1.0 / self.along
428    }
429}
430
431impl GlassMorph {
432    /// Shader budget for extra scene shapes.
433    pub const MAX_SHAPES: usize = 8;
434}
435
436/// Reflection of opposing lights across a narrow band around the surface contour.
437/// The reflected environment is brightest along the light axis and darkest between the lights.
438/// Nonfinite fields or nonpositive height or spread disable this treatment.
439#[derive(Clone, Copy, Debug, PartialEq)]
440pub struct GlassContourHighlight {
441    /// Width of the reflecting band in dp.
442    pub height_dp: f32,
443    /// Signed distance of the band's outer edge; negative values extend outside the surface.
444    pub offset_dp: f32,
445    /// Reflected-light opacity, clamped to zero through one.
446    pub amount: f32,
447    /// Additive brightness of the reflected environment.
448    pub color_bias: f32,
449    /// Direction of the first light in screen-space radians; the second is opposite.
450    pub angle_radians: f32,
451    /// Angular extent of each light in radians.
452    pub spread_radians: f32,
453}
454
455impl GlassContourHighlight {
456    fn uniforms(self) -> Option<[f32; 6]> {
457        let values = [
458            self.height_dp,
459            self.offset_dp,
460            self.amount,
461            self.color_bias,
462            self.angle_radians,
463            self.spread_radians,
464        ];
465        (values.iter().all(|value| value.is_finite())
466            && self.height_dp > 0.0
467            && self.spread_radians > 0.0)
468            .then_some(values)
469    }
470}
471
472/// Backdrop-dependent reflections from opposing lights, with a sharp rim and a softer inward lobe.
473/// Invalid dimensions or nonfinite fields disable the reflection.
474#[derive(Clone, Copy, Debug, PartialEq)]
475pub struct GlassSpecularHighlight {
476    /// Width of the sharp reflection in dp.
477    pub height_dp: f32,
478    /// Direction of the first light in screen-space radians; the second is opposite.
479    pub angle_radians: f32,
480    /// Angular extent of each sharp reflection, in radians.
481    pub spread_radians: f32,
482    /// Exponent of the angular response; must be positive.
483    pub curvature: f32,
484    /// Opacity of the diffuse reflection, clamped to zero through one.
485    pub diffuse_amount: f32,
486    /// Inward extent of the diffuse reflection, in dp.
487    pub diffuse_height_dp: f32,
488    /// Angular extent of each diffuse reflection, in radians.
489    pub diffuse_spread_radians: f32,
490    /// Gain applied to the transmitted surface's chroma in the reflected color.
491    pub saturation: f32,
492    /// Gain applied to the transmitted surface's luminance in the reflected color.
493    pub luma_gain: f32,
494    /// Additive brightness of the reflected color.
495    pub color_bias: f32,
496}
497
498impl GlassSpecularHighlight {
499    fn uniforms(self) -> Option<[f32; 10]> {
500        let values = [
501            self.height_dp,
502            self.angle_radians,
503            self.spread_radians,
504            self.curvature,
505            self.diffuse_amount,
506            self.diffuse_height_dp,
507            self.diffuse_spread_radians,
508            self.saturation,
509            self.luma_gain,
510            self.color_bias,
511        ];
512        (values.iter().all(|value| value.is_finite())
513            && self.height_dp > 0.0
514            && self.spread_radians > 0.0
515            && self.curvature > 0.0
516            && self.diffuse_height_dp > 0.0
517            && self.diffuse_spread_radians > 0.0)
518            .then_some(values)
519    }
520}
521
522/// Opposing edge lights whose color responds to the backdrop.
523/// Nonfinite fields or nonpositive height or curvature disable this treatment.
524#[derive(Clone, Copy, Debug, PartialEq)]
525pub struct GlassKeyFill {
526    /// Inward extent of the highlight, in dp.
527    pub height_dp: f32,
528    /// Linear reduction in brightness across the highlight's inward extent.
529    pub curvature: f32,
530    /// Direction of the first light in screen-space radians; the second is opposite.
531    pub angle_radians: f32,
532    /// Gain applied to the backdrop's chroma in the reflected color.
533    pub saturation: f32,
534    /// Gain applied to the backdrop's luminance in the reflected color.
535    pub luma_gain: f32,
536    /// Additive contribution to each reflected color channel.
537    pub offset: f32,
538    /// Whether the highlight width follows the surface transform or stays fixed on screen.
539    pub scale_with_surface: bool,
540}
541
542impl GlassKeyFill {
543    fn uniforms(self) -> Option<[f32; 7]> {
544        let values = [
545            self.height_dp,
546            self.curvature,
547            self.angle_radians,
548            self.saturation,
549            self.luma_gain,
550            self.offset,
551            f32::from(self.scale_with_surface),
552        ];
553        (values.iter().all(|value| value.is_finite())
554            && self.height_dp > 0.0
555            && self.curvature > 0.0)
556            .then_some(values)
557    }
558}
559
560/// Seven-tap RGB spectrum for an edge lens. Distances follow the surface transform.
561/// Nonfinite values, negative extents, or opacities outside zero to one are ignored.
562#[derive(Clone, Copy, Debug, PartialEq)]
563pub struct GlassSpectrum {
564    /// Angle θ in radians. For outward normal (x, y), the sampling direction is
565    /// (y cosθ + x sinθ, (x cosθ − y sinθ) × vertical_scale).
566    pub angle_radians: f32,
567    /// Signed spacing between adjacent taps, in dp; negative values reverse the colors.
568    pub step_dp: f32,
569    /// Scale of the vertical component of the sampling direction.
570    pub vertical_scale: f32,
571    /// Spectral opacity at the silhouette edge.
572    pub opacity_near: f32,
573    /// Spectral opacity at and beyond the fade depth.
574    pub opacity_far: f32,
575    /// Inward distance in dp over which opacity interpolates linearly.
576    pub fade_depth_dp: f32,
577    /// Inward distance in dp beyond which the spectrum disappears.
578    pub extent_dp: f32,
579}
580
581impl GlassSpectrum {
582    fn uniforms(self) -> Option<[f32; 8]> {
583        let values = [
584            self.angle_radians,
585            self.step_dp,
586            self.vertical_scale,
587            self.opacity_near,
588            self.opacity_far,
589            self.fade_depth_dp,
590            self.extent_dp,
591            1.0,
592        ];
593        (values.iter().all(|value| value.is_finite())
594            && self.vertical_scale >= 0.0
595            && (0.0..=1.0).contains(&self.opacity_near)
596            && (0.0..=1.0).contains(&self.opacity_far)
597            && self.fade_depth_dp >= 0.0
598            && self.extent_dp >= 0.0)
599            .then_some(values)
600    }
601
602    fn capture_reach(self) -> f32 {
603        self.step_dp.abs() * 3.0 * self.vertical_scale.max(1.0)
604    }
605}
606
607/// Face attenuation before edge lighting, followed by additive illumination.
608/// Edge lenses attenuate the inner refraction image before chromatic sampling.
609/// Invalid or nonfinite parameters disable the response.
610#[derive(Clone, Copy, Debug, PartialEq)]
611pub struct GlassFaceResponse {
612    /// Interior color gain, from zero to one.
613    pub gain: f32,
614    /// Screen-space depth in dp where attenuation starts.
615    pub start_dp: f32,
616    /// Screen-space depth in dp where attenuation reaches its interior gain.
617    /// Equal start and end apply the gain uniformly.
618    pub end_dp: f32,
619    /// Light added to each color channel inside the silhouette after edge lighting.
620    pub illumination: f32,
621}
622
623impl GlassFaceResponse {
624    fn uniforms(self) -> Option<[f32; 4]> {
625        let values = [
626            1.0 - self.gain,
627            self.start_dp,
628            self.end_dp,
629            self.illumination,
630        ];
631        (values.iter().all(|value| value.is_finite())
632            && (0.0..=1.0).contains(&self.gain)
633            && self.start_dp >= 0.0
634            && self.end_dp >= self.start_dp)
635            .then_some(values)
636    }
637}
638
639/// A fixed luminance transfer with an independent chroma gain.
640/// Luminance `y` becomes `black + (white - black) * y - (1 - max_luminance) * y²`;
641/// chroma is scaled by `saturation` and the combined RGB is clamped to zero–one.
642#[derive(Clone, Copy, Debug, PartialEq)]
643pub struct GlassFaceTone {
644    /// Black input's output, from zero to one.
645    pub black: f32,
646    /// White endpoint before quadratic highlight compression; greater than black.
647    pub white: f32,
648    /// Chroma gain independent of the luminance transfer; finite and nonnegative.
649    pub saturation: f32,
650    /// Highlight compression endpoint, from zero to one. One disables compression.
651    pub max_luminance: f32,
652}
653
654impl GlassFaceTone {
655    fn uniforms(self) -> Option<[f32; 5]> {
656        let values = [
657            self.black,
658            self.white,
659            self.saturation,
660            self.max_luminance,
661            1.0,
662        ];
663        (values.iter().all(|value| value.is_finite())
664            && (0.0..=1.0).contains(&self.black)
665            && self.white > self.black
666            && self.saturation >= 0.0
667            && (0.0..=1.0).contains(&self.max_luminance))
668        .then_some(values)
669    }
670}
671
672/// Builder describing a glass material. Resolved against the theme at the
673/// composition site, then evaluated per frame for density and dynamics.
674#[derive(Clone, Debug, PartialEq)]
675pub struct Glass {
676    /// Optional fixed face transfer, replacing adaptive tone, saturation, contrast and lift.
677    pub face_tone: Option<GlassFaceTone>,
678    /// The material of the glass: frosted, clear or lens.
679    pub variant: GlassVariant,
680    /// The outline of the glass, also used for its clip and its shadow.
681    pub shape: LiquidShape,
682    /// Tint over the refracted backdrop; defaults to the theme's glass tint.
683    pub tint: Option<Color>,
684    /// Minimum transmitted luminance for a colored tint. `None` uses alpha tinting.
685    pub tint_transmission: Option<f32>,
686    /// Optional sharp/blurred pane blend, separate from the color tint.
687    pub tint_amount: Option<GlassTintAmount>,
688    /// Diffuse pane kernel reach in dp; its Gaussian standard deviation is half this value.
689    pub pane_blur_radius: f32,
690    /// Backdrop blur radius in dp (defaults per variant).
691    pub blur_radius: Option<f32>,
692    /// Radius in dp and opacity of the blurred backdrop mixed with an edge lens’s outer warp.
693    pub backdrop_blur: Option<(f32, f32)>,
694    /// Saturation boost (defaults per variant).
695    pub saturation: Option<f32>,
696    /// wcKSRD refraction depth as a fraction of the shape inradius. Regular
697    /// frosted surfaces default to zero so their backdrop stays motion-stable;
698    /// interactive lenses opt into optical displacement explicitly.
699    pub refraction_depth: f32,
700    /// Optional physical wcKSRD refraction depth in dp. When present, this
701    /// keeps the optical band equally deep across surfaces with different
702    /// inradii instead of scaling it from each surface's height.
703    pub refraction_depth_dp: Option<f32>,
704    /// Backdrop projection model.
705    pub refraction: GlassRefraction,
706    /// Normalized wcKSRD ray-return exponent.
707    pub refraction_curve: f32,
708    /// Normalized wcKSRD spectral ray separation.
709    pub dispersion: f32,
710    /// Optional directional spectrum for the edge-lens projection.
711    pub edge_spectrum: Option<GlassSpectrum>,
712    /// Fraction of the wcKSRD displacement applied to the transmitted
713    /// backdrop. Regular frosted surfaces default to zero. The mirrored
714    /// meniscus follows its own optical path.
715    pub transmission_refraction: f32,
716    /// Energy removed from the transmitted ray at the meniscus. Reflection
717    /// and spectral return follow independent paths.
718    pub meniscus_absorption: f32,
719    /// Depth of the rim fold band in dp: the raised lens rim replays its
720    /// interior mirrored toward the edge (a pure displacement). Zero
721    /// disables the fold.
722    pub fold_depth: f32,
723    /// Uniform face magnification of a riding lens (1.0 = no zoom).
724    /// The selected refraction model controls the surrounding rim band.
725    pub optical_zoom: f32,
726    /// Meniscus rim reflectivity multiplier (1.0 = the reference toggle's
727    /// visible rim line; near 0 = the segmented lens's invisible body).
728    pub rim_reflection: f32,
729    /// Optical ink recolor: the lens recolors dark transmitted ink toward
730    /// this color at the given strength (the reference tab bubble's
731    /// color-mask act). None = off.
732    pub ink_recolor: Option<(Color, f32)>,
733    /// Specular rim intensity.
734    pub highlight: f32,
735    /// Whether the face receives the material's diffuse directional illumination.
736    pub face_lighting: bool,
737    /// Replaces dome lighting with a pair of opposing edge lights.
738    pub key_fill: Option<GlassKeyFill>,
739    /// Contour reflection applied before the foreground's spectral dispersion.
740    pub contour_highlight: Option<GlassContourHighlight>,
741    /// Directional rim reflection applied above the transmitted and tinted surface.
742    pub specular_highlight: Option<GlassSpecularHighlight>,
743    /// Attenuation and illumination applied around the edge-light pass.
744    pub face_response: Option<GlassFaceResponse>,
745    /// Inset shadow on the transmitted face, covered by the chromatic foreground.
746    pub inner_shadow: Option<GlassShadow>,
747    /// Screen-lift override (brightening toward white; negative darkens).
748    /// Defaults per variant.
749    pub lift: Option<f32>,
750    /// Tone-compression override around the shader's mid pivot (<1 pulls
751    /// every backdrop toward mid-luminance — the reference dark menu reads
752    /// bright magenta over deep purple AND dim gray over white through one
753    /// law). Defaults per variant.
754    pub contrast: Option<f32>,
755    /// Drop shadow below the glass.
756    pub shadow: bool,
757    /// Per-surface shadow override.
758    pub shadow_style: Option<GlassShadow>,
759    /// Clip the layer to `shape`. Morphing glass disables this — coverage
760    /// comes entirely from the shader's SDF.
761    pub clip: bool,
762    /// Foreground color whose contrast the frost must protect. `None` uses
763    /// the theme label color.
764    pub foreground: Option<Color>,
765    /// Strength of backdrop+foreground frost adaptation.
766    pub adaptive_frost: f32,
767    /// Adapts the face transfer curve to the mean captured backdrop brightness.
768    /// Foreground polarity selects the preferred light or dark treatment.
769    pub adaptive_tone: bool,
770    /// Resting edge sharpness, from a soft eight-dp fade at zero to pixel antialiasing at one.
771    pub resting_edge_sharpness: f32,
772}
773
774impl Glass {
775    /// A motion-stable frosted surface without optical displacement.
776    pub fn regular() -> Self {
777        Self {
778            variant: GlassVariant::Regular,
779            face_tone: None,
780            shape: LiquidShape::Capsule,
781            tint: None,
782            tint_transmission: None,
783            tint_amount: None,
784            pane_blur_radius: 8.0,
785            blur_radius: None,
786            backdrop_blur: None,
787            saturation: None,
788            refraction_depth: 0.0,
789            refraction_depth_dp: None,
790            refraction: GlassRefraction::Radial,
791            refraction_curve: 0.25,
792            dispersion: 0.0,
793            edge_spectrum: None,
794            transmission_refraction: 0.0,
795            meniscus_absorption: 1.0,
796            fold_depth: 0.0,
797            optical_zoom: 1.0,
798            rim_reflection: 1.0,
799            ink_recolor: None,
800            highlight: 0.9,
801            face_lighting: true,
802            key_fill: None,
803            contour_highlight: None,
804            specular_highlight: None,
805            face_response: None,
806            inner_shadow: None,
807            lift: None,
808            contrast: None,
809            shadow: true,
810            shadow_style: None,
811            clip: true,
812            foreground: None,
813            adaptive_frost: 0.65,
814            adaptive_tone: false,
815            resting_edge_sharpness: 0.0,
816        }
817    }
818
819    /// A transparent surface with minimal blur, for media-rich backdrops. See
820    /// `GlassVariant::Clear`.
821    pub fn clear() -> Self {
822        Self {
823            variant: GlassVariant::Clear,
824            ..Self::regular()
825        }
826    }
827
828    /// An interactive glass lens with a radial projection and spectral rim.
829    /// [`Self::edge_refraction`] selects an outward rim around a uniformly magnified face.
830    pub fn lens() -> Self {
831        Self {
832            variant: GlassVariant::Lens,
833            tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
834            refraction_depth: 0.34,
835            refraction_curve: 1.0,
836            dispersion: 0.30,
837            transmission_refraction: 1.0,
838            highlight: 1.15,
839            adaptive_frost: 0.0,
840            ..Self::regular()
841        }
842    }
843
844    /// Enables or disables diffuse face illumination without changing the specular rim.
845    pub fn face_lighting(mut self, enabled: bool) -> Self {
846        self.face_lighting = enabled;
847        self
848    }
849
850    /// Sets the resting edge sharpness from zero (eight-dp fade) to one (pixel antialiasing).
851    /// Out-of-range values are clamped; nonfinite values preserve the soft default.
852    pub fn resting_edge_sharpness(mut self, sharpness: f32) -> Self {
853        self.resting_edge_sharpness = sharpness;
854        self
855    }
856
857    /// Uses opposing edge lights with a backdrop-dependent reflected color.
858    /// [`Self::highlight`] controls their opacity.
859    pub fn key_fill(mut self, lighting: GlassKeyFill) -> Self {
860        self.key_fill = Some(lighting);
861        self
862    }
863
864    /// Sets the face attenuation and illumination around the edge-light pass.
865    pub fn face_response(mut self, response: GlassFaceResponse) -> Self {
866        self.face_response = Some(response);
867        self
868    }
869
870    /// Adds an inset shadow with a Gaussian falloff from the live surface outline.
871    /// The chromatic foreground covers this shadow without refracting it.
872    /// Nonfinite values, negative radii, and opacity outside zero to one disable it.
873    pub fn inner_shadow(mut self, shadow: GlassShadow) -> Self {
874        self.inner_shadow = shadow.uniforms().map(|_| shadow);
875        self
876    }
877
878    /// Sets the outline of the glass.
879    pub fn shape(mut self, shape: LiquidShape) -> Self {
880        self.shape = shape;
881        self
882    }
883
884    /// Sets the tint over the refracted backdrop, replacing the glass tint of the theme.
885    pub fn tint(mut self, tint: Color) -> Self {
886        self.tint = Some(tint);
887        self
888    }
889
890    /// Colors transmitted light while retaining the backdrop's luminance structure.
891    pub fn transmission_tint(mut self, tint: Color, black: f32) -> Self {
892        if black.is_finite() {
893            self.tint = Some(tint);
894            self.tint_transmission = Some(black.clamp(0.0, 1.0));
895        }
896        self
897    }
898
899    /// Sets the backdrop blur radius, in dp, replacing the default of the variant.
900    pub fn blur_radius(mut self, radius_dp: f32) -> Self {
901        self.blur_radius = Some(radius_dp);
902        self
903    }
904
905    /// Sets the saturation boost, replacing the default of the variant.
906    pub fn saturation(mut self, saturation: f32) -> Self {
907        self.saturation = Some(saturation);
908        self
909    }
910
911    /// Sets the refraction depth as a fraction of the shape inradius, between 0 and 2. This clears
912    /// a depth set by `refraction_depth_dp`.
913    pub fn refraction_depth(mut self, fraction: f32) -> Self {
914        self.refraction_depth = fraction.clamp(0.0, 2.0);
915        self.refraction_depth_dp = None;
916        self
917    }
918
919    /// Uses a density-stable physical refraction depth instead of a fraction
920    /// of the live shape inradius.
921    pub fn refraction_depth_dp(mut self, depth_dp: f32) -> Self {
922        self.refraction_depth_dp = Some(depth_dp.max(0.0));
923        self
924    }
925
926    /// Selects a circular depth profile along the surface normal, preserving
927    /// tangential background coordinates along straight edges. `reach_dp` sets the inward
928    /// displacement independently of depth; both remain fixed under surface scaling.
929    /// Negative and nonfinite reach values become zero.
930    pub fn surface_refraction(mut self, reach_dp: f32) -> Self {
931        self.refraction = GlassRefraction::Surface {
932            reach_dp: finite_nonnegative(reach_dp),
933        };
934        self
935    }
936
937    /// Overrides the edge lens's spectrum; radial and surface projections use `dispersion`.
938    /// Invalid profiles are ignored. The signed spacing supports continuous color reversal.
939    pub fn edge_spectrum(mut self, spectrum: GlassSpectrum) -> Self {
940        self.edge_spectrum = spectrum.uniforms().map(|_| spectrum);
941        self
942    }
943
944    /// Selects a rim that enters inward, crests outward and returns smoothly to the face.
945    /// `reach_dp` controls its maximum outward displacement; negative and nonfinite values become zero.
946    /// Its inward return uses the amplitude and depth ratios in [`GlassRefraction::EdgeLens`].
947    pub fn edge_refraction(mut self, reach_dp: f32) -> Self {
948        self.refraction = GlassRefraction::EdgeLens {
949            reach_dp: finite_nonnegative(reach_dp),
950        };
951        self
952    }
953
954    /// Sets the ray-return exponent of the refraction, between 0.05 and 1.
955    pub fn refraction_curve(mut self, curve: f32) -> Self {
956        self.refraction_curve = curve.clamp(0.05, 1.0);
957        self
958    }
959
960    /// Sets the strength of the spectral ray separation, between 0 and 1.
961    pub fn dispersion(mut self, strength: f32) -> Self {
962        self.dispersion = strength.clamp(0.0, 1.0);
963        self
964    }
965
966    /// Sets the fraction of the refraction displacement that moves the transmitted backdrop,
967    /// between 0 and 1.
968    pub fn transmission_refraction(mut self, strength: f32) -> Self {
969        self.transmission_refraction = strength.clamp(0.0, 1.0);
970        self
971    }
972
973    /// Sets the energy that the meniscus removes from the transmitted ray, between 0 and 1.
974    pub fn meniscus_absorption(mut self, strength: f32) -> Self {
975        self.meniscus_absorption = strength.clamp(0.0, 1.0);
976        self
977    }
978
979    /// Sets the rim fold band depth in dp (zero disables the fold).
980    pub fn fold_depth(mut self, depth_dp: f32) -> Self {
981        self.fold_depth = depth_dp.max(0.0);
982        self
983    }
984
985    /// Sets the uniform face magnification of a riding lens (1.0 = none).
986    pub fn optical_zoom(mut self, zoom: f32) -> Self {
987        self.optical_zoom = zoom.max(1.0);
988        self
989    }
990
991    /// Sets the meniscus rim reflectivity (1.0 = full reference line).
992    pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
993        self.rim_reflection = reflectivity.clamp(0.0, 2.0);
994        self
995    }
996
997    /// The lens recolors dark transmitted ink toward `color` at `strength`
998    /// (0..1) — the reference bubble's color-mask act as a material optic.
999    pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
1000        self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
1001        self
1002    }
1003
1004    /// Sets the intensity of the specular rim.
1005    pub fn highlight(mut self, highlight: f32) -> Self {
1006        self.highlight = highlight;
1007        self
1008    }
1009
1010    /// Overrides the screen-lift (how hard the glass brightens what it
1011    /// shows; the bar lens uses a near-zero lift to stay transmissive).
1012    pub fn lift(mut self, lift: f32) -> Self {
1013        self.lift = Some(lift);
1014        self
1015    }
1016
1017    /// Overrides the tone compression around the shader's mid pivot
1018    /// (values below one pull every backdrop toward mid-luminance).
1019    pub fn contrast(mut self, contrast: f32) -> Self {
1020        self.contrast = Some(contrast.max(0.05));
1021        self
1022    }
1023
1024    /// Sets the foreground color whose contrast the frost protects and how strongly the frost
1025    /// adapts to it, between 0 and 1.
1026    pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
1027        self.foreground = Some(foreground);
1028        self.adaptive_frost = strength.clamp(0.0, 1.0);
1029        self
1030    }
1031
1032    /// Uses the captured backdrop's mean brightness to adapt the face transfer curve.
1033    /// This replaces local frost adaptation, saturation, contrast, lift and
1034    /// the key/fill color response. Tint and edge-light geometry still apply.
1035    /// The foreground chooses the preferred polarity, with dark treatment over black
1036    /// backgrounds and light treatment over white backgrounds.
1037    pub fn adaptive_tone(mut self, foreground: Color) -> Self {
1038        self.face_tone = None;
1039        self.foreground = Some(foreground);
1040        self.adaptive_tone = true;
1041        self.adaptive_frost = 0.0;
1042        self
1043    }
1044
1045    /// Uses a fixed face transfer without reading a mean-backdrop texture.
1046    /// Invalid parameters disable the fixed transfer.
1047    pub fn face_tone(mut self, tone: GlassFaceTone) -> Self {
1048        self.face_tone = tone.uniforms().map(|_| tone);
1049        self.adaptive_tone = false;
1050        self.adaptive_frost = 0.0;
1051        self
1052    }
1053
1054    /// Turns the drop shadow below the glass on or off.
1055    pub fn shadow(mut self, shadow: bool) -> Self {
1056        self.shadow = shadow;
1057        self
1058    }
1059
1060    /// Sets the style of the shadow, replacing the default drop shadow.
1061    pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
1062        self.shadow_style = Some(shadow);
1063        self
1064    }
1065
1066    /// Disables the layer clip: the shader's SDF coverage is the only shape
1067    /// (required while morphing across geometry the clip can't follow).
1068    pub fn no_clip(mut self) -> Self {
1069        self.clip = false;
1070        self
1071    }
1072
1073    /// Uses a sharp/blurred backdrop blend controlled by the tint percentage.
1074    /// This replaces the whole-backdrop blur for this material.
1075    pub fn tint_amount(mut self, amount: GlassTintAmount) -> Self {
1076        self.tint_amount = Some(amount);
1077        self
1078    }
1079
1080    /// Sets the diffuse pane's kernel reach, independently of its sharp transmitted image.
1081    pub fn pane_blur_radius(mut self, radius: f32) -> Self {
1082        if radius.is_finite() && radius >= 0.0 {
1083            self.pane_blur_radius = radius;
1084        }
1085        self
1086    }
1087
1088    /// Crossfades the edge lens’s outer warp with a fixed-radius blurred backdrop.
1089    /// Radius uses the renderer's Gaussian convention: twice the standard deviation, in dp.
1090    /// Nonfinite values disable the blend; finite radius is nonnegative and opacity is clamped to 0–1.
1091    pub fn backdrop_blur(mut self, radius_dp: f32, opacity: f32) -> Self {
1092        self.backdrop_blur = (radius_dp.is_finite() && opacity.is_finite())
1093            .then_some((radius_dp.max(0.0), opacity.clamp(0.0, 1.0)));
1094        self
1095    }
1096
1097    fn default_blur_radius(&self) -> f32 {
1098        match self.variant {
1099            GlassVariant::Regular => 8.0,
1100            GlassVariant::Clear => 3.0,
1101            GlassVariant::Lens => 0.0,
1102        }
1103    }
1104
1105    fn default_saturation(&self) -> f32 {
1106        match self.variant {
1107            GlassVariant::Regular => 1.5,
1108            GlassVariant::Clear => 1.25,
1109            GlassVariant::Lens => 1.0,
1110        }
1111    }
1112
1113    /// The backdrop effect this material produces for a node at `density`
1114    /// under `dynamics`, resolved against `colors`: the render effect the
1115    /// glass modifiers install, for a layer built without them.
1116    pub fn backdrop_effect(
1117        &self,
1118        colors: &LiquidColors,
1119        density: f32,
1120        dynamics: GlassDynamics,
1121    ) -> RenderEffect {
1122        self.resolve(colors).backdrop_effect(density, dynamics)
1123    }
1124
1125    fn preferred_foreground(&self, colors: &LiquidColors) -> Color {
1126        self.foreground
1127            .filter(|color| {
1128                [color.r(), color.g(), color.b(), color.a()]
1129                    .iter()
1130                    .all(|value| value.is_finite())
1131            })
1132            .unwrap_or(colors.label)
1133    }
1134
1135    pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
1136        let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
1137            (GlassVariant::Regular, false) => 0.42,
1138            (GlassVariant::Regular, true) => -0.38,
1139            (GlassVariant::Clear, false) => 0.12,
1140            (GlassVariant::Clear, true) => -0.12,
1141            (GlassVariant::Lens, false) => 0.10,
1142            (GlassVariant::Lens, true) => -0.08,
1143        });
1144        let foreground = self.preferred_foreground(colors);
1145        let shadow = self.shadow_style.unwrap_or_else(|| {
1146            GlassShadow::new(
1147                Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
1148                    (GlassVariant::Lens, false) => 0.14,
1149                    (GlassVariant::Lens, true) => 0.28,
1150                    (_, false) => 0.16,
1151                    (_, true) => 0.5,
1152                }),
1153                if self.variant == GlassVariant::Lens {
1154                    10.0
1155                } else {
1156                    22.0
1157                },
1158                if self.variant == GlassVariant::Lens {
1159                    3.0
1160                } else {
1161                    8.0
1162                },
1163                if self.variant == GlassVariant::Lens {
1164                    -6.0
1165                } else {
1166                    -2.0
1167                },
1168            )
1169        });
1170        ResolvedGlass {
1171            face_tone: self.face_tone,
1172            shape: self.shape,
1173            tint: self.tint.unwrap_or(colors.glass_tint),
1174            tint_transmission: self.tint_transmission,
1175            tint_amount: self.tint_amount,
1176            pane_blur_radius: self.pane_blur_radius,
1177            backdrop_blur: self.backdrop_blur,
1178            blur_radius_dp: self
1179                .blur_radius
1180                .unwrap_or_else(|| self.default_blur_radius()),
1181            saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
1182            refraction_depth: self.refraction_depth,
1183            refraction_depth_dp: self.refraction_depth_dp,
1184            refraction: self.refraction,
1185            refraction_curve: self.refraction_curve,
1186            dispersion: self.dispersion,
1187            edge_spectrum: self.edge_spectrum,
1188            transmission_refraction: self.transmission_refraction,
1189            meniscus_absorption: self.meniscus_absorption,
1190            fold_depth: self.fold_depth,
1191            optical_zoom: self.optical_zoom,
1192            rim_reflection: self.rim_reflection,
1193            ink_recolor: self.ink_recolor,
1194            highlight: self.highlight,
1195            face_lighting: self.face_lighting,
1196            key_fill: self.key_fill,
1197            contour_highlight: self.contour_highlight,
1198            specular_highlight: self.specular_highlight,
1199            face_response: self.face_response,
1200            inner_shadow: self.inner_shadow,
1201            lift,
1202            contrast: self.contrast.unwrap_or(match self.variant {
1203                GlassVariant::Lens => 1.0,
1204                _ => 1.03,
1205            }),
1206            shadow: self.shadow,
1207            clip: self.clip,
1208            foreground_luma: 0.2126 * foreground.r()
1209                + 0.7152 * foreground.g()
1210                + 0.0722 * foreground.b(),
1211            adaptive_frost: self.adaptive_frost,
1212            adaptive_tone: self.adaptive_tone,
1213            resting_edge_sharpness: finite_nonnegative(self.resting_edge_sharpness).min(1.0),
1214            rim_style: if self.variant == GlassVariant::Lens {
1215                1.0
1216            } else {
1217                0.0
1218            },
1219            shadow_color: shadow.color,
1220            shadow_radius: shadow.radius,
1221            shadow_offset_y: shadow.offset_y,
1222            shadow_spread: shadow.spread,
1223        }
1224    }
1225}
1226
1227impl Default for Glass {
1228    fn default() -> Self {
1229        Self::regular()
1230    }
1231}
1232
1233#[derive(Clone, Debug, PartialEq)]
1234pub(crate) struct ResolvedGlass {
1235    specular_highlight: Option<GlassSpecularHighlight>,
1236    face_tone: Option<GlassFaceTone>,
1237    resting_edge_sharpness: f32,
1238    pub shape: LiquidShape,
1239    pub tint: Color,
1240    tint_transmission: Option<f32>,
1241    tint_amount: Option<GlassTintAmount>,
1242    pane_blur_radius: f32,
1243    backdrop_blur: Option<(f32, f32)>,
1244    pub blur_radius_dp: f32,
1245    pub saturation: f32,
1246    pub refraction_depth: f32,
1247    pub refraction_depth_dp: Option<f32>,
1248    pub refraction: GlassRefraction,
1249    pub refraction_curve: f32,
1250    pub dispersion: f32,
1251    pub edge_spectrum: Option<GlassSpectrum>,
1252    pub transmission_refraction: f32,
1253    pub meniscus_absorption: f32,
1254    pub fold_depth: f32,
1255    pub optical_zoom: f32,
1256    pub rim_reflection: f32,
1257    pub ink_recolor: Option<(Color, f32)>,
1258    pub highlight: f32,
1259    pub face_lighting: bool,
1260    pub key_fill: Option<GlassKeyFill>,
1261    /// Contour reflection applied before the foreground's spectral dispersion.
1262    pub contour_highlight: Option<GlassContourHighlight>,
1263    /// Attenuation and illumination applied around the edge-light pass.
1264    pub face_response: Option<GlassFaceResponse>,
1265    pub lift: f32,
1266    /// Inset shadow on the transmitted face, covered by the chromatic foreground.
1267    pub inner_shadow: Option<GlassShadow>,
1268    pub contrast: f32,
1269    pub shadow: bool,
1270    pub clip: bool,
1271    pub rim_style: f32,
1272    pub foreground_luma: f32,
1273    pub adaptive_frost: f32,
1274    pub adaptive_tone: bool,
1275    pub shadow_color: Color,
1276    pub shadow_radius: f32,
1277    pub shadow_offset_y: f32,
1278    pub shadow_spread: f32,
1279}
1280
1281fn set_edge_return_depth(shader: &mut RuntimeShader, depth: Option<f32>) {
1282    if let Some(depth) = depth.filter(|depth| depth.is_finite() && *depth >= 0.0) {
1283        shader.set_float2(GLASS_EDGE_RETURN_DEPTH_UNIFORM, depth, 1.0);
1284    }
1285}
1286
1287impl ResolvedGlass {
1288    /// The rounded rectangle a placeholder fills while this glass compiles:
1289    /// around its morph's shapes, or the whole node in its shape.
1290    fn placeholder_shape(&self, morph: Option<&GlassMorph>) -> PlaceholderShape {
1291        let Some(morph) = morph else {
1292            return PlaceholderShape {
1293                bounds: Rect {
1294                    x: 0.0,
1295                    y: 0.0,
1296                    width: 1.0,
1297                    height: 1.0,
1298                },
1299                corner_radius: self.shape.radius_dp(),
1300            };
1301        };
1302        let (node_width, node_height) = morph.node_size;
1303        let (node_width, node_height) =
1304            (node_width.max(f32::EPSILON), node_height.max(f32::EPSILON));
1305        // A radius of -2 subtracts its shape: it carves a hole in the field.
1306        let added = morph.shapes.iter().filter(|&&(.., radius)| radius > -1.5);
1307        let (left, top, right, bottom) = std::iter::once(&morph.primary).chain(added).fold(
1308            (f32::MAX, f32::MAX, f32::MIN, f32::MIN),
1309            |(left, top, right, bottom), &(center_x, center_y, width, height, _)| {
1310                (
1311                    left.min(center_x - width * 0.5),
1312                    top.min(center_y - height * 0.5),
1313                    right.max(center_x + width * 0.5),
1314                    bottom.max(center_y + height * 0.5),
1315                )
1316            },
1317        );
1318        // A negative radius is the shader's capsule.
1319        let (.., radius) = morph.primary;
1320        PlaceholderShape {
1321            bounds: Rect {
1322                x: left / node_width,
1323                y: top / node_height,
1324                width: (right - left) / node_width,
1325                height: (bottom - top) / node_height,
1326            },
1327            corner_radius: if radius < 0.0 { f32::MAX } else { radius },
1328        }
1329    }
1330
1331    /// The same glass as a flat surface, for a person who asked the system
1332    /// for less transparency: the surface color with no blur, no refraction
1333    /// and no spectrum, the shape and the shadow as they were.
1334    pub(crate) fn without_transparency(mut self, colors: &LiquidColors) -> Self {
1335        self.tint = colors.surface;
1336        self.tint_transmission = None;
1337        self.tint_amount = None;
1338        self.backdrop_blur = None;
1339        self.face_tone = None;
1340        self.contour_highlight = None;
1341        self.specular_highlight = None;
1342        self.adaptive_tone = false;
1343        self.blur_radius_dp = 0.0;
1344        self.saturation = 1.0;
1345        self.refraction_depth = 0.0;
1346        self.refraction_depth_dp = None;
1347        self.dispersion = 0.0;
1348        self.edge_spectrum = None;
1349        self.transmission_refraction = 0.0;
1350        self.meniscus_absorption = 0.0;
1351        self.rim_reflection = 0.0;
1352        self.adaptive_frost = 0.0;
1353        self
1354    }
1355
1356    fn clips_in_shader(&self) -> bool {
1357        self.key_fill.and_then(GlassKeyFill::uniforms).is_some()
1358    }
1359    pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
1360        self.runtime_effect(density, &dynamics, false)
1361    }
1362
1363    fn blur_radii(&self, density: f32, activity: f32, content_mask: bool) -> (f32, f32) {
1364        if content_mask || self.tint_amount.is_some() {
1365            return (0.0, 0.0);
1366        }
1367        let radius = self.blur_radius_dp * density * activity;
1368        if radius > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
1369            (0.0, radius)
1370        } else {
1371            (radius, 0.0)
1372        }
1373    }
1374
1375    fn runtime_effect(
1376        &self,
1377        density: f32,
1378        dynamics: &GlassDynamics,
1379        content_mask: bool,
1380    ) -> RenderEffect {
1381        self.runtime_effect_with_content(density, dynamics, content_mask, false)
1382    }
1383
1384    fn set_refraction_uniforms(
1385        &self,
1386        shader: &mut RuntimeShader,
1387        dynamics: &GlassDynamics,
1388        activity: f32,
1389    ) -> (f32, f32, f32) {
1390        let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
1391        shader.set_float(9, self.refraction_depth * activity * press_depth);
1392        let refraction = dynamics.refraction.unwrap_or(self.refraction);
1393        let (refraction_mode, refraction_reach) = refraction.shader_parameters();
1394        let refraction_weight = if matches!(refraction, GlassRefraction::LayeredSurface { .. }) {
1395            1.0
1396        } else {
1397            activity * press_depth
1398        };
1399        shader.set_float(GLASS_REFRACTION_MODE_UNIFORM, refraction_mode);
1400        if let GlassRefraction::LayeredSurface {
1401            return_reach_dp,
1402            return_depth_dp,
1403            ..
1404        } = refraction
1405        {
1406            shader.set_float2(
1407                GLASS_LAYERED_RETURN_UNIFORM,
1408                finite_nonnegative(return_reach_dp),
1409                finite_nonnegative(return_depth_dp),
1410            );
1411        }
1412        set_edge_return_depth(shader, dynamics.edge_return_depth_dp);
1413        shader.set_float(
1414            GLASS_EDGE_REFRACTION_REACH_UNIFORM,
1415            refraction_reach * refraction_weight,
1416        );
1417        shader.set_float(
1418            GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
1419            self.refraction_depth_dp.unwrap_or(0.0) * refraction_weight,
1420        );
1421        shader.set_float(
1422            GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
1423            if self.refraction_depth_dp.is_some() {
1424                1.0
1425            } else {
1426                0.0
1427            },
1428        );
1429        shader.set_float(
1430            GLASS_REFRACTION_CURVE_UNIFORM,
1431            self.refraction_curve * activity,
1432        );
1433        shader.set_float(
1434            GLASS_DISPERSION_UNIFORM,
1435            self.dispersion * activity * press_depth,
1436        );
1437        let spectrum_reach = self.set_spectrum_uniforms(shader);
1438        shader.set_float(
1439            GLASS_TRANSMISSION_REFRACTION_UNIFORM,
1440            self.transmission_refraction * if refraction_mode > 2.5 { 1.0 } else { activity },
1441        );
1442        shader.set_float(
1443            GLASS_MENISCUS_ABSORPTION_UNIFORM,
1444            self.meniscus_absorption * press_depth,
1445        );
1446        shader.set_float(
1447            GLASS_FOLD_DEPTH_UNIFORM,
1448            self.fold_depth.max(0.0) * press_depth,
1449        );
1450        (refraction_mode, refraction_reach, spectrum_reach)
1451    }
1452
1453    fn set_shadow_uniforms(
1454        &self,
1455        shader: &mut RuntimeShader,
1456        dynamics: &GlassDynamics,
1457        activity: f32,
1458    ) -> (f32, f32) {
1459        let dynamic_shadow = !self.clip && self.shadow;
1460        let shadow = dynamics
1461            .shadow
1462            .filter(|shadow| shadow.uniforms().is_some())
1463            .unwrap_or_else(|| {
1464                GlassShadow::new(
1465                    self.shadow_color
1466                        .with_alpha(self.shadow_color.a() * 0.55 * activity),
1467                    self.shadow_radius,
1468                    self.shadow_offset_y,
1469                    self.shadow_spread,
1470                )
1471            });
1472        shader.set_float(
1473            102,
1474            if dynamic_shadow {
1475                shadow.color.a()
1476            } else {
1477                0.0
1478            },
1479        );
1480        shader.set_float(103, shadow.radius);
1481        shader.set_float(104, shadow.offset_y);
1482        shader.set_float(105, shadow.spread);
1483        shader.set_float4(
1484            cranpose_ui_graphics::GLASS_SHADOW_COLOR_UNIFORM,
1485            shadow.color.r(),
1486            shadow.color.g(),
1487            shadow.color.b(),
1488            0.0,
1489        );
1490        let ring_shadow = dynamics.ring_shadow.filter(|(ring, width)| {
1491            ring.color.a() > 0.0 && ring.uniforms().is_some() && width.is_finite() && *width > 0.0
1492        });
1493        if let Some((ring, width)) = ring_shadow {
1494            shader.set_float4(
1495                cranpose_ui_graphics::GLASS_RING_SHADOW_UNIFORM,
1496                ring.color.r(),
1497                ring.color.g(),
1498                ring.color.b(),
1499                ring.color.a(),
1500            );
1501            shader.set_float4(
1502                cranpose_ui_graphics::GLASS_RING_SHADOW_UNIFORM + 4,
1503                ring.radius,
1504                ring.offset_y,
1505                width,
1506                1.0,
1507            );
1508            shader.set_float(
1509                cranpose_ui_graphics::GLASS_RING_SHADOW_UNIFORM + 8,
1510                ring.spread,
1511            );
1512        }
1513        let ring_reach = ring_shadow.map_or(0.0, |(ring, width)| {
1514            3.0 * ring.radius + ring.offset_y.abs() + width * 0.5 + ring.spread.abs()
1515        });
1516        let contour_reach = self
1517            .contour_highlight
1518            .and_then(GlassContourHighlight::uniforms)
1519            .filter(|values| values[2] > 0.0 && activity > 0.0)
1520            .map_or(0.0, |values| (-values[1]).max(0.0));
1521        let visible_shadow = dynamic_shadow && shadow.color.a() > 0.0;
1522        let shadow_reach = if visible_shadow {
1523            shadow.radius + shadow.offset_y.abs() + shadow.spread.max(0.0)
1524        } else {
1525            0.0
1526        }
1527        .max(ring_reach)
1528        .max(contour_reach);
1529        // The drop shadow falls outside the node: without this reach the
1530        // compositor cuts it at the node's rectangle.
1531        shader.set_output_padding(shadow_reach);
1532        let offset = if visible_shadow || ring_shadow.is_some() {
1533            shadow
1534                .offset_y
1535                .abs()
1536                .max(ring_shadow.map_or(0.0, |(ring, _)| ring.offset_y.abs()))
1537        } else {
1538            0.0
1539        };
1540        (shadow_reach, offset)
1541    }
1542
1543    fn runtime_effect_with_content(
1544        &self,
1545        density: f32,
1546        dynamics: &GlassDynamics,
1547        content_mask: bool,
1548        separate_content: bool,
1549    ) -> RenderEffect {
1550        let density = density.max(f32::EPSILON);
1551        let activity = dynamics
1552            .activity
1553            .filter(|value| value.is_finite())
1554            .unwrap_or(1.0)
1555            .clamp(0.0, 1.0);
1556        static SHADER: OnceLock<RuntimeShader> = OnceLock::new();
1557        let mut shader = SHADER
1558            .get_or_init(|| {
1559                let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
1560                shader.set_float(GLASS_ACTIVITY_UNIFORM, 1.0);
1561                shader.set_float2(GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM, 0.0, 0.0);
1562                specialize_liquid_glass(&mut shader);
1563                shader
1564            })
1565            .clone();
1566        if let Some(morph) = dynamics.morph.as_ref() {
1567            set_morph_uniforms(&mut shader, morph, activity);
1568        } else {
1569            shader.set_float2(0, 0.0, 0.0);
1570            shader.set_float(6, self.shape.shader_radius_px(density));
1571        }
1572        let projection = dynamics.projection();
1573        shader.set_float2(GLASS_OPTICAL_PROJECTION_UNIFORM, projection.0, projection.1);
1574        let (refraction_mode, refraction_reach, spectrum_reach) =
1575            self.set_refraction_uniforms(&mut shader, dynamics, activity);
1576        shader.set_float(
1577            GLASS_OPTICAL_ZOOM_UNIFORM,
1578            1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
1579        );
1580        let zoom_anchor = dynamics
1581            .morph
1582            .as_ref()
1583            .map_or((0.0, 0.0), |morph| morph.zoom_anchor);
1584        shader.set_float2(
1585            GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
1586            zoom_anchor.0,
1587            zoom_anchor.1,
1588        );
1589        shader.set_float(121, self.rim_reflection.max(0.001));
1590        let (ink_color, ink_strength) = self
1591            .ink_recolor
1592            .map_or((Color::TRANSPARENT, 0.0), |(color, strength)| {
1593                (color, strength * activity)
1594            });
1595        shader.set_float(124, ink_color.r());
1596        shader.set_float(125, ink_color.g());
1597        shader.set_float(126, ink_color.b());
1598        shader.set_float(127, ink_strength);
1599        let (light_x, light_y) = glass_light_direction();
1600        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
1601        shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
1602        self.set_lighting_uniforms(&mut shader, dynamics);
1603        shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
1604        shader.set_float(
1605            11,
1606            (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
1607        );
1608        let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
1609        let dynamic_tint_alpha = (dynamic_tint.a()
1610            * dynamics
1611                .tint_alpha_multiplier
1612                .unwrap_or(1.0)
1613                .clamp(0.0, 2.0)
1614            * activity)
1615            .clamp(0.0, 1.0);
1616        shader.set_float4(
1617            14,
1618            dynamic_tint.r(),
1619            dynamic_tint.g(),
1620            dynamic_tint.b(),
1621            dynamic_tint_alpha,
1622        );
1623        if let Some((color, progress)) = dynamics.tint_crossfade.filter(|(_, p)| p.is_finite()) {
1624            let index = cranpose_ui_graphics::liquid_glass::GLASS_TINT_CROSSFADE_UNIFORM;
1625            shader.set_float(index, 1.0 + progress.clamp(0.0, 1.0));
1626            shader.set_float4(index + 1, color.r(), color.g(), color.b(), color.a());
1627        }
1628        let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
1629        shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
1630        shader.set_float(20, self.lift * activity);
1631        shader.set_float(21, 0.5 * activity);
1632        shader.set_float2(22, 0.0, 1.0);
1633        shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
1634        shader.set_float(28, self.rim_style * activity);
1635        let (wcksrd_blur_radius, gaussian_blur_radius) =
1636            self.blur_radii(density, activity, content_mask);
1637        shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
1638        shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
1639        shader.set_float(
1640            GLASS_RESTING_EDGE_SHARPNESS_UNIFORM,
1641            self.resting_edge_sharpness,
1642        );
1643        let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
1644        shader.set_float4(
1645            GLASS_RESTING_TINT_UNIFORM,
1646            resting_tint.r(),
1647            resting_tint.g(),
1648            resting_tint.b(),
1649            resting_tint.a(),
1650        );
1651        shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
1652        shader.set_float(GLASS_ADAPTIVE_FROST_UNIFORM, self.adaptive_frost * activity);
1653        shader.set_float(97, self.foreground_luma);
1654        shader.set_float(GLASS_ADAPTIVE_TONE_UNIFORM, f32::from(self.adaptive_tone));
1655        self.set_backdrop_uniforms(&mut shader, density, content_mask);
1656        shader.set_float(
1657            cranpose_ui_graphics::GLASS_FOREGROUND_CONTENT_UNIFORM,
1658            f32::from(separate_content),
1659        );
1660        let (shadow_reach, shadow_offset) = if content_mask {
1661            (0.0, 0.0)
1662        } else {
1663            self.set_shadow_uniforms(&mut shader, dynamics, activity)
1664        };
1665        let morph_pad = dynamics.morph.as_ref().map_or(0.0, |morph| {
1666            let (px, py, pw, ph, _) = morph.primary;
1667            let (left, top) = (px - pw * 0.5, py - ph * 0.5);
1668            let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
1669            let mut shape_reach = 0.0f32;
1670            for (sx, sy, sw, sh, _) in &morph.shapes {
1671                let reach_x = ((sx + sw * 0.5) - right)
1672                    .max(left - (sx - sw * 0.5))
1673                    .max(0.0);
1674                let reach_y = ((sy + sh * 0.5) - bottom)
1675                    .max(top - (sy - sh * 0.5))
1676                    .max(0.0);
1677                shape_reach = shape_reach.max(reach_x.max(reach_y));
1678            }
1679            let glue_pad = if morph.shapes.is_empty() {
1680                0.0
1681            } else {
1682                morph.glue * 2.0
1683            };
1684            morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
1685        });
1686        shader.set_input_padding(if content_mask {
1687            0.0
1688        } else {
1689            (self.input_padding()
1690                + if refraction_mode > 2.5 {
1691                    refraction_reach + spectrum_reach
1692                } else if refraction_mode >= 1.5 {
1693                    refraction_reach * (26.5 / 9.0) + spectrum_reach
1694                } else {
1695                    refraction_reach
1696                }
1697                + morph_pad
1698                + wcksrd_blur_radius / density)
1699                * projection.0.max(projection.1).max(1.0)
1700        });
1701        if let Some(morph) = dynamics.morph.as_ref() {
1702            let support =
1703                morph_output_support(morph, activity, morph_pad, shadow_reach, shadow_offset);
1704            let (cx, cy, _, _, _) = morph.primary;
1705            let support = Rect {
1706                x: cx + (support.x - cx) * projection.0,
1707                y: cy + (support.y - cy) * projection.1,
1708                width: support.width * projection.0,
1709                height: support.height * projection.1,
1710            };
1711            let (node_w, node_h) = morph.node_size;
1712            let overhang = (-support.x)
1713                .max(-support.y)
1714                .max(support.x + support.width - node_w)
1715                .max(support.y + support.height - node_h)
1716                .max(0.0);
1717            shader.set_output_padding((morph_pad + shadow_reach + 4.0).max(overhang));
1718            shader.set_output_support(Some(support));
1719        }
1720
1721        shader.set_placeholder(Some(liquid_glass_placeholder(
1722            shown_tint(
1723                Color(
1724                    dynamic_tint.r(),
1725                    dynamic_tint.g(),
1726                    dynamic_tint.b(),
1727                    dynamic_tint_alpha,
1728                ),
1729                dynamics.tint_crossfade,
1730            ),
1731            Some(self.placeholder_shape(dynamics.morph.as_ref())),
1732        )));
1733        let optical_effect = liquid_glass_runtime_effect(shader);
1734        if gaussian_blur_radius > f32::EPSILON {
1735            RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
1736                .then(optical_effect)
1737        } else {
1738            optical_effect
1739        }
1740    }
1741
1742    fn set_backdrop_uniforms(&self, shader: &mut RuntimeShader, density: f32, content_mask: bool) {
1743        if let Some(values) = self.face_tone.and_then(GlassFaceTone::uniforms) {
1744            for (index, value) in values.into_iter().enumerate() {
1745                shader.set_float(cranpose_ui_graphics::GLASS_FACE_TONE_UNIFORM + index, value);
1746            }
1747        }
1748        if let Some(amount) = self.tint_amount {
1749            shader.set_float(
1750                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM,
1751                self.pane_blur_radius * density,
1752            );
1753            shader.set_float(
1754                cranpose_ui_graphics::GLASS_PANE_BLEND_UNIFORM + 1,
1755                amount.percent() / 100.0,
1756            );
1757        }
1758        if !content_mask && let Some((radius, opacity)) = self.backdrop_blur {
1759            shader.set_float2(
1760                cranpose_ui_graphics::GLASS_BACKDROP_BLUR_UNIFORM,
1761                radius * density,
1762                opacity,
1763            );
1764        }
1765    }
1766
1767    fn set_spectrum_uniforms(&self, shader: &mut RuntimeShader) -> f32 {
1768        let spectrum = self
1769            .edge_spectrum
1770            .and_then(|spectrum| spectrum.uniforms().map(|values| (spectrum, values)));
1771        if let Some((spectrum, values)) = spectrum {
1772            for (index, value) in values.into_iter().enumerate() {
1773                shader.set_float(GLASS_EDGE_SPECTRUM_UNIFORM + index, value);
1774            }
1775            spectrum.capture_reach()
1776        } else {
1777            40.787 * self.dispersion
1778        }
1779    }
1780
1781    fn set_lighting_uniforms(&self, shader: &mut RuntimeShader, dynamics: &GlassDynamics) {
1782        if let Some(values) = self
1783            .specular_highlight
1784            .and_then(GlassSpecularHighlight::uniforms)
1785        {
1786            for (index, value) in values.into_iter().enumerate() {
1787                shader.set_float(
1788                    cranpose_ui_graphics::GLASS_SPECULAR_HIGHLIGHT_UNIFORM + index,
1789                    value,
1790                );
1791            }
1792        }
1793        shader.set_float(
1794            cranpose_ui_graphics::GLASS_CONTACT_LIGHT_UNIFORM,
1795            f32::from(dynamics.contact_lighting),
1796        );
1797        if let Some(black) = self.tint_transmission.filter(|value| value.is_finite()) {
1798            shader.set_float2(
1799                cranpose_ui_graphics::GLASS_TINT_TRANSMISSION_UNIFORM,
1800                black.clamp(0.0, 1.0),
1801                1.0,
1802            );
1803        }
1804        let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
1805        shader.set_float(118, touch_x);
1806        shader.set_float(119, touch_y);
1807        shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
1808        shader.set_float(
1809            GLASS_FACE_LIGHTING_OFF_UNIFORM,
1810            f32::from(!self.face_lighting),
1811        );
1812        shader.set_float(
1813            GLASS_TOUCH_RADIUS_UNIFORM,
1814            dynamics
1815                .touch_radius_dp
1816                .filter(|radius| radius.is_finite() && *radius > 0.0)
1817                .unwrap_or(58.0),
1818        );
1819        shader.set_float(
1820            GLASS_LAYER_CLIPPED_UNIFORM,
1821            f32::from(self.clip && !self.clips_in_shader()),
1822        );
1823        if let Some(values) = self.key_fill.and_then(GlassKeyFill::uniforms) {
1824            for (index, value) in values.into_iter().enumerate() {
1825                shader.set_float(GLASS_KEY_FILL_UNIFORM + index, value);
1826            }
1827        }
1828        if let Some(values) = self.face_response.and_then(GlassFaceResponse::uniforms) {
1829            for (index, value) in values.into_iter().enumerate() {
1830                shader.set_float(GLASS_FACE_RESPONSE_UNIFORM + index, value);
1831            }
1832        }
1833        if let Some(values) = self
1834            .contour_highlight
1835            .and_then(GlassContourHighlight::uniforms)
1836        {
1837            for (index, value) in values.into_iter().enumerate() {
1838                shader.set_float(
1839                    cranpose_ui_graphics::GLASS_CONTOUR_HIGHLIGHT_UNIFORM + index,
1840                    value,
1841                );
1842            }
1843        }
1844        if let Some(values) = self.inner_shadow.and_then(GlassShadow::uniforms) {
1845            for (index, value) in values.into_iter().enumerate() {
1846                shader.set_float(GLASS_INNER_SHADOW_UNIFORM + index, value);
1847            }
1848        }
1849    }
1850
1851    fn input_padding(&self) -> f32 {
1852        2.0
1853    }
1854}
1855
1856/// Modifier extension installing the Liquid Glass material.
1857pub trait LiquidModifierExt {
1858    /// Applies the glass material to this composable's bounds: backdrop blur +
1859    /// lens shader, clipped to `glass.shape`, with a soft drop shadow.
1860    ///
1861    /// Must be called in composable context (the material resolves theme
1862    /// colors at the call site).
1863    fn glass_effect(self, glass: Glass) -> Modifier;
1864
1865    /// [`glass_effect`](Self::glass_effect) with per-frame motion inputs; the
1866    /// closure is read at scene-build time, so animating tilt or highlight
1867    /// does not recompose.
1868    fn glass_effect_with(
1869        self,
1870        glass: Glass,
1871        dynamics: impl Fn() -> GlassDynamics + 'static,
1872    ) -> Modifier;
1873}
1874
1875impl LiquidModifierExt for Modifier {
1876    fn glass_effect(self, glass: Glass) -> Modifier {
1877        self.glass_effect_with(glass, GlassDynamics::default)
1878    }
1879
1880    fn glass_effect_with(
1881        self,
1882        glass: Glass,
1883        dynamics: impl Fn() -> GlassDynamics + 'static,
1884    ) -> Modifier {
1885        let colors = crate::theme::liquid_colors();
1886        let options = cranpose_services::local_accessibility_options().current();
1887        let resolved = if options.reduce_transparency {
1888            glass.resolve(&colors).without_transparency(&colors)
1889        } else {
1890            glass.resolve(&colors)
1891        };
1892        let shape = resolved.shape;
1893
1894        let mut modifier = self;
1895        if resolved.shadow && resolved.clip {
1896            let shadow_color = resolved.shadow_color;
1897            let (radius, offset_y, spread) = (
1898                resolved.shadow_radius,
1899                resolved.shadow_offset_y,
1900                resolved.shadow_spread,
1901            );
1902            modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
1903                scope.radius = radius;
1904                scope.spread = spread;
1905                scope.offset.y = offset_y;
1906                scope.color = shadow_color;
1907                scope.cutout = true;
1908            });
1909        }
1910
1911        let layer = cached_glass_layer(resolved);
1912        modifier.graphics_layer(move || layer(current_density(), dynamics()))
1913    }
1914}
1915
1916struct CachedGlassLayer<T> {
1917    density: u32,
1918    light: [u32; 2],
1919    dynamics: GlassDynamics,
1920    value: T,
1921}
1922
1923fn cached_glass_layer(resolved: ResolvedGlass) -> impl Fn(f32, GlassDynamics) -> GraphicsLayer {
1924    cached_glass_layers(resolved, false, std::convert::identity)
1925}
1926
1927pub(crate) fn cached_glass_content_layers(
1928    resolved: ResolvedGlass,
1929) -> impl Fn(f32, GlassDynamics) -> [GraphicsLayer; 2] {
1930    cached_glass_layers(resolved, true, |mut layer| {
1931        let Some(RenderEffect::Chain { first, second }) = layer.backdrop_effect.take() else {
1932            panic!("content layers require a sequential edge lens");
1933        };
1934        layer.backdrop_effect = Some((*second).clone());
1935        [
1936            GraphicsLayer {
1937                backdrop_effect: Some((*first).clone()),
1938                ..Default::default()
1939            },
1940            layer,
1941        ]
1942    })
1943}
1944
1945fn cached_glass_layers<T: Clone>(
1946    resolved: ResolvedGlass,
1947    separate_content: bool,
1948    assemble: impl Fn(GraphicsLayer) -> T,
1949) -> impl Fn(f32, GlassDynamics) -> T {
1950    let cached = RefCell::new(None::<CachedGlassLayer<T>>);
1951    move |density, dynamics| {
1952        let direction = glass_light_direction();
1953        let light = [direction.0, direction.1].map(f32::to_bits);
1954        let mut cached = cached.borrow_mut();
1955        if let Some(previous) = &*cached
1956            && previous.density == density.to_bits()
1957            && previous.light == light
1958            && glass_dynamics_match(&previous.dynamics, &dynamics)
1959        {
1960            return previous.value.clone();
1961        }
1962        let render_effect = ((!resolved.clip && dynamics.morph.is_some())
1963            || (resolved.clip && resolved.clips_in_shader()))
1964        .then(|| resolved.runtime_effect(density, &dynamics, true));
1965        let layer = GraphicsLayer {
1966            backdrop_effect: Some(resolved.runtime_effect_with_content(
1967                density,
1968                &dynamics,
1969                false,
1970                separate_content,
1971            )),
1972            render_effect,
1973            shape: resolved.shape.layer_shape(),
1974            clip: resolved.clip && !resolved.clips_in_shader(),
1975            ..Default::default()
1976        };
1977        let value = assemble(layer);
1978        *cached = Some(CachedGlassLayer {
1979            density: density.to_bits(),
1980            light,
1981            dynamics,
1982            value: value.clone(),
1983        });
1984        value
1985    }
1986}
1987
1988fn glass_dynamics_match(a: &GlassDynamics, b: &GlassDynamics) -> bool {
1989    let bits = |d: &GlassDynamics| {
1990        let GlassDynamics {
1991            contact_lighting,
1992            refraction,
1993            shadow,
1994            ring_shadow,
1995            edge_return_depth_dp,
1996            optical_projection,
1997            activity,
1998            resting_tint,
1999            highlight_boost,
2000            saturation_boost,
2001            tint_alpha_multiplier,
2002            tint_crossfade,
2003            morph: _,
2004            touch,
2005            touch_radius_dp,
2006            press_depth,
2007        } = d;
2008        (
2009            (
2010                *contact_lighting,
2011                ring_shadow.map(|(ring, width)| (shadow_bits(ring), width.to_bits())),
2012                tint_crossfade.map(|(Color(r, g, b, a), p)| [r, g, b, a, p].map(f32::to_bits)),
2013            ),
2014            refraction.map(|refraction| {
2015                match refraction {
2016                    GlassRefraction::Radial => [0.0, 0.0, 0.0, 0.0],
2017                    GlassRefraction::Surface { reach_dp } => [1.0, reach_dp, 0.0, 0.0],
2018                    GlassRefraction::EdgeLens { reach_dp } => [2.0, reach_dp, 0.0, 0.0],
2019                    GlassRefraction::LayeredSurface {
2020                        reach_dp,
2021                        return_reach_dp,
2022                        return_depth_dp,
2023                    } => [3.0, reach_dp, return_reach_dp, return_depth_dp],
2024                }
2025                .map(f32::to_bits)
2026            }),
2027            shadow.map(shadow_bits),
2028            edge_return_depth_dp.map(f32::to_bits),
2029            optical_projection.map(|(x, y)| [x, y].map(f32::to_bits)),
2030            activity.map(f32::to_bits),
2031            resting_tint.map(|Color(r, g, b, a)| [r, g, b, a].map(f32::to_bits)),
2032            [*highlight_boost, *saturation_boost].map(f32::to_bits),
2033            tint_alpha_multiplier.map(f32::to_bits),
2034            touch.map(|(x, y, strength)| [x, y, strength].map(f32::to_bits)),
2035            press_depth.map(f32::to_bits),
2036            touch_radius_dp.map(f32::to_bits),
2037        )
2038    };
2039    bits(a) == bits(b)
2040        && match (&a.morph, &b.morph) {
2041            (None, None) => true,
2042            (Some(a), Some(b)) => glass_morphs_match(a, b),
2043            _ => false,
2044        }
2045}
2046
2047fn shadow_bits(shadow: GlassShadow) -> [u32; 7] {
2048    [
2049        shadow.color.r(),
2050        shadow.color.g(),
2051        shadow.color.b(),
2052        shadow.color.a(),
2053        shadow.radius,
2054        shadow.offset_y,
2055        shadow.spread,
2056    ]
2057    .map(f32::to_bits)
2058}
2059
2060fn glass_morphs_match(a: &GlassMorph, b: &GlassMorph) -> bool {
2061    let shape_bits =
2062        |&(x, y, w, h, r): &(f32, f32, f32, f32, f32)| [x, y, w, h, r].map(f32::to_bits);
2063    let bits = |m: &GlassMorph| {
2064        let GlassMorph {
2065            node_size,
2066            primary,
2067            shapes: _,
2068            glue,
2069            wobble_amplitude,
2070            wobble_phase,
2071            bulge_amplitude,
2072            bulge_direction,
2073            ellipse_blend,
2074            capsule_smoothing_dp,
2075            deformation,
2076            zoom_anchor,
2077        } = m;
2078        (
2079            [
2080                node_size.0,
2081                node_size.1,
2082                *glue,
2083                *wobble_amplitude,
2084                *wobble_phase,
2085                *bulge_amplitude,
2086                *bulge_direction,
2087                *ellipse_blend,
2088                *capsule_smoothing_dp,
2089                zoom_anchor.0,
2090                zoom_anchor.1,
2091            ]
2092            .map(f32::to_bits),
2093            shape_bits(primary),
2094            deformation
2095                .map(|GlassDeformation { axis, along }| [axis.0, axis.1, along].map(f32::to_bits)),
2096        )
2097    };
2098    bits(a) == bits(b)
2099        && a.shapes
2100            .iter()
2101            .map(shape_bits)
2102            .eq(b.shapes.iter().map(shape_bits))
2103}
2104
2105fn morph_output_support(
2106    morph: &GlassMorph,
2107    activity: f32,
2108    morph_pad: f32,
2109    shadow_reach: f32,
2110    shadow_offset: f32,
2111) -> Rect {
2112    let (px, py, pw, ph, _) = morph.primary;
2113    let (along, (ax, ay)) = morph.deformation.map_or((1.0, (1.0, 0.0)), |deformation| {
2114        (
2115            1.0 + (deformation.along() - 1.0) * activity,
2116            deformation.axis(),
2117        )
2118    });
2119    let across = 1.0 / along;
2120    let smallest_strain = along.min(across);
2121    let reach = (morph_pad + shadow_reach + 4.0) / smallest_strain;
2122    let rows = [
2123        (
2124            (along * ax * ax + across * ay * ay).abs(),
2125            ((along - across) * ax * ay).abs(),
2126        ),
2127        (
2128            ((along - across) * ax * ay).abs(),
2129            (along * ay * ay + across * ax * ax).abs(),
2130        ),
2131    ];
2132    let half = (pw * 0.5 + reach, ph * 0.5 + reach);
2133    let extent_x = rows[0].0 * half.0 + rows[0].1 * half.1;
2134    let extent_y = rows[1].0 * half.0 + rows[1].1 * half.1;
2135    let (mut left, mut top) = (px - extent_x, py - extent_y);
2136    let (mut right, mut bottom) = (px + extent_x, py + extent_y);
2137    for (sx, sy, sw, sh, _) in &morph.shapes {
2138        left = left.min(sx - sw * 0.5 - reach);
2139        top = top.min(sy - sh * 0.5 - reach);
2140        right = right.max(sx + sw * 0.5 + reach);
2141        bottom = bottom.max(sy + sh * 0.5 + reach);
2142    }
2143    Rect {
2144        x: left,
2145        y: top - shadow_offset,
2146        width: right - left,
2147        height: bottom - top + 2.0 * shadow_offset,
2148    }
2149}
2150
2151fn set_morph_uniforms(shader: &mut RuntimeShader, morph: &GlassMorph, activity: f32) {
2152    let (node_w, node_h) = morph.node_size;
2153    let (cx, cy, w, h, radius) = morph.primary;
2154    shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
2155    shader.set_float2(2, cx, cy);
2156    shader.set_float2(4, w, h);
2157    shader.set_float(6, radius);
2158    let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
2159    shader.set_float(30, count as f32);
2160    for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
2161        let base = 36 + index * 5;
2162        shader.set_float(base, *sx);
2163        shader.set_float(base + 1, *sy);
2164        shader.set_float(base + 2, *sw);
2165        shader.set_float(base + 3, *sh);
2166        shader.set_float(base + 4, *sr);
2167    }
2168    shader.set_float(31, morph.glue);
2169    shader.set_float(32, morph.wobble_amplitude * activity);
2170    shader.set_float(33, morph.wobble_phase);
2171    shader.set_float(26, morph.bulge_amplitude * activity);
2172    shader.set_float(27, morph.bulge_direction);
2173    shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
2174    shader.set_float(
2175        cranpose_ui_graphics::liquid_glass::GLASS_CAPSULE_SMOOTHING_UNIFORM,
2176        if morph.capsule_smoothing_dp.is_finite() {
2177            morph.capsule_smoothing_dp.max(0.0) * activity
2178        } else {
2179            0.0
2180        },
2181    );
2182    if let Some(deformation) = morph.deformation {
2183        let axis = deformation.axis();
2184        let along = 1.0 + (deformation.along() - 1.0) * activity;
2185        shader.set_float2(106, axis.0, axis.1);
2186        shader.set_float(108, along);
2187        shader.set_float(109, 1.0 / along);
2188    } else {
2189        shader.set_float2(106, 1.0, 0.0);
2190        shader.set_float2(108, 1.0, 1.0);
2191    }
2192}
2193
2194#[cfg(test)]
2195#[path = "tests/material_tests.rs"]
2196mod tests;
2197
2198#[cfg(test)]
2199#[path = "tests/material_transparency_tests.rs"]
2200mod transparency_tests;