Skip to main content

cranpose_liquid/
material.rs

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