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