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