1use std::{cell::Cell, rc::Rc};
7
8use cranpose_ui::{Modifier, current_density};
9use cranpose_ui_graphics::{
10 Color, GLASS_ACTIVITY_UNIFORM, GLASS_BLUR_RADIUS_UNIFORM, GLASS_DISPERSION_UNIFORM,
11 GLASS_EFFECT_DENSITY_UNIFORM, GLASS_FOLD_DEPTH_UNIFORM, GLASS_LIGHT_DIRECTION_UNIFORM,
12 GLASS_MENISCUS_ABSORPTION_UNIFORM, GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
13 GLASS_OPTICAL_ZOOM_UNIFORM, GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
14 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM, GLASS_REFRACTION_CURVE_UNIFORM,
15 GLASS_RESTING_TINT_UNIFORM, GLASS_TRANSMISSION_REFRACTION_UNIFORM, GraphicsLayer,
16 LIQUID_GLASS_WGSL, LayerShape, RenderEffect, RoundedCornerShape, RuntimeShader, TileMode,
17 liquid_glass_runtime_effect,
18};
19
20use crate::theme::LiquidColors;
21
22thread_local! {
23 static GLASS_LIGHT_RETURN: Cell<(f32, f32)> = const { Cell::new((0.0, 1.0)) };
24}
25
26pub fn set_glass_light_direction(direction: (f32, f32)) {
29 GLASS_LIGHT_RETURN.with(|cell| cell.set(direction));
30}
31
32pub fn glass_light_direction() -> (f32, f32) {
34 GLASS_LIGHT_RETURN.with(|cell| cell.get())
35}
36
37const CAPSULE_CLIP_RADIUS: f32 = 1.0e6;
38
39const CAPSULE_SHADER_RADIUS: f32 = -1.0;
40
41const WCKSRD_OPTICAL_BLUR_RADIUS_PX: f32 = 2.0;
42
43#[derive(Clone, Copy, Debug, PartialEq, Default)]
45pub enum LiquidShape {
46 #[default]
48 Capsule,
49 RoundedRect(f32),
51 Circle,
53}
54
55impl LiquidShape {
56 pub fn clip_shape(&self) -> RoundedCornerShape {
58 match self {
59 LiquidShape::Capsule | LiquidShape::Circle => {
60 RoundedCornerShape::uniform(CAPSULE_CLIP_RADIUS)
61 }
62 LiquidShape::RoundedRect(radius) => RoundedCornerShape::uniform(*radius),
63 }
64 }
65
66 pub fn layer_shape(&self) -> LayerShape {
68 LayerShape::Rounded(self.clip_shape())
69 }
70
71 fn shader_radius_px(&self, density: f32) -> f32 {
72 match self {
73 LiquidShape::Capsule | LiquidShape::Circle => CAPSULE_SHADER_RADIUS,
74 LiquidShape::RoundedRect(radius) => radius * density,
75 }
76 }
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
82pub enum GlassVariant {
83 #[default]
85 Regular,
86 Clear,
88 Lens,
92}
93
94#[derive(Clone, Copy, Debug, PartialEq)]
98pub struct GlassShadow {
99 pub color: Color,
100 pub radius: f32,
101 pub offset_y: f32,
102 pub spread: f32,
103}
104
105impl GlassShadow {
106 pub fn new(color: Color, radius: f32, offset_y: f32, spread: f32) -> Self {
107 Self {
108 color,
109 radius: radius.max(0.0),
110 offset_y,
111 spread,
112 }
113 }
114}
115
116#[derive(Clone, Debug, PartialEq, Default)]
119pub struct GlassDynamics {
120 pub activity: Option<f32>,
124 pub resting_tint: Option<Color>,
127 pub highlight_boost: f32,
129 pub saturation_boost: f32,
132 pub tint_alpha_multiplier: Option<f32>,
135 pub morph: Option<GlassMorph>,
138 pub touch: Option<(f32, f32, f32)>,
142 pub press_depth: Option<f32>,
148}
149
150impl GlassDynamics {
151 pub fn touched_up(
158 mut self,
159 press: f32,
160 finger: Option<(f32, f32)>,
161 center: (f32, f32),
162 ) -> Self {
163 let press = press.clamp(0.0, 1.0);
164 if press <= 0.0 {
165 return self;
166 }
167 self.highlight_boost += 0.85 * press;
168 self.saturation_boost += 0.45 * press;
169 let (fx, fy) = finger.unwrap_or(center);
170 let carried = self.touch.map(|(_, _, i)| i).unwrap_or(0.0);
171 self.touch = Some((fx, fy, (carried + press).clamp(0.0, 1.0)));
172 self
173 }
174}
175
176fn foreground_is_dark(foreground: Color) -> bool {
177 let foreground_luma =
178 0.2126 * foreground.r() + 0.7152 * foreground.g() + 0.0722 * foreground.b();
179 foreground_luma < 0.5
180}
181
182fn boost_tint_saturation(tint: Color, boost: f32) -> Color {
183 if boost.abs() <= f32::EPSILON {
184 return tint;
185 }
186 let luma = 0.2126 * tint.r() + 0.7152 * tint.g() + 0.0722 * tint.b();
187 let saturation = (1.0 + boost).max(0.0);
188 Color::rgba(
189 (luma + (tint.r() - luma) * saturation).clamp(0.0, 1.0),
190 (luma + (tint.g() - luma) * saturation).clamp(0.0, 1.0),
191 (luma + (tint.b() - luma) * saturation).clamp(0.0, 1.0),
192 tint.a(),
193 )
194}
195
196pub(crate) fn neutral_surface_tint(foreground: Color, light_alpha: f32, dark_alpha: f32) -> Color {
197 if foreground_is_dark(foreground) {
198 Color::BLACK.with_alpha(light_alpha.clamp(0.0, 1.0))
199 } else {
200 Color::WHITE.with_alpha(dark_alpha.clamp(0.0, 1.0))
201 }
202}
203
204pub(crate) fn neutral_surface_lift(foreground: Color, light_lift: f32, dark_lift: f32) -> f32 {
205 if foreground_is_dark(foreground) {
206 light_lift
207 } else {
208 dark_lift
209 }
210}
211
212#[derive(Clone, Debug, PartialEq, Default)]
223pub struct GlassMorph {
224 pub node_size: (f32, f32),
230 pub primary: (f32, f32, f32, f32, f32),
231 pub shapes: Vec<(f32, f32, f32, f32, f32)>,
233 pub glue: f32,
235 pub wobble_amplitude: f32,
237 pub wobble_phase: f32,
238 pub bulge_amplitude: f32,
242 pub bulge_direction: f32,
243 pub ellipse_blend: f32,
247 pub deformation: Option<GlassDeformation>,
251 pub zoom_anchor: (f32, f32),
257}
258
259#[derive(Clone, Copy, Debug, PartialEq)]
263pub struct GlassDeformation {
264 axis: (f32, f32),
265 along: f32,
266}
267
268impl GlassDeformation {
269 pub fn incompressible(axis: (f32, f32), along: f32) -> Self {
270 let length = (axis.0 * axis.0 + axis.1 * axis.1).sqrt();
271 let axis = if length > f32::EPSILON {
272 (axis.0 / length, axis.1 / length)
273 } else {
274 (1.0, 0.0)
275 };
276 Self {
277 axis,
278 along: along.max(f32::EPSILON),
279 }
280 }
281
282 pub fn axis(self) -> (f32, f32) {
283 self.axis
284 }
285
286 pub fn along(self) -> f32 {
287 self.along
288 }
289
290 pub fn across(self) -> f32 {
291 1.0 / self.along
292 }
293}
294
295impl GlassMorph {
296 pub const MAX_SHAPES: usize = 8;
298}
299
300#[derive(Clone, Debug, PartialEq)]
303pub struct Glass {
304 pub variant: GlassVariant,
305 pub shape: LiquidShape,
306 pub tint: Option<Color>,
308 pub blur_radius: Option<f32>,
310 pub saturation: Option<f32>,
312 pub refraction_depth: f32,
316 pub refraction_depth_dp: Option<f32>,
320 pub refraction_curve: f32,
322 pub dispersion: f32,
324 pub transmission_refraction: f32,
328 pub meniscus_absorption: f32,
331 pub fold_depth: f32,
335 pub optical_zoom: f32,
339 pub rim_reflection: f32,
342 pub ink_recolor: Option<(Color, f32)>,
346 pub highlight: f32,
348 pub lift: Option<f32>,
351 pub contrast: Option<f32>,
356 pub shadow: bool,
358 pub shadow_style: Option<GlassShadow>,
360 pub clip: bool,
363 pub foreground: Option<Color>,
366 pub adaptive_frost: f32,
368}
369
370impl Glass {
371 pub fn regular() -> Self {
373 Self {
374 variant: GlassVariant::Regular,
375 shape: LiquidShape::Capsule,
376 tint: None,
377 blur_radius: None,
378 saturation: None,
379 refraction_depth: 0.0,
380 refraction_depth_dp: None,
381 refraction_curve: 0.25,
382 dispersion: 0.0,
383 transmission_refraction: 0.0,
384 meniscus_absorption: 1.0,
385 fold_depth: 0.0,
386 optical_zoom: 1.0,
387 rim_reflection: 1.0,
388 ink_recolor: None,
389 highlight: 0.9,
390 lift: None,
391 contrast: None,
392 shadow: true,
393 shadow_style: None,
394 clip: true,
395 foreground: None,
396 adaptive_frost: 0.65,
397 }
398 }
399
400 pub fn clear() -> Self {
401 Self {
402 variant: GlassVariant::Clear,
403 ..Self::regular()
404 }
405 }
406
407 pub fn lens() -> Self {
411 Self {
412 variant: GlassVariant::Lens,
413 shape: LiquidShape::Capsule,
414 tint: Some(Color::rgba(1.0, 1.0, 1.0, 0.07)),
415 blur_radius: None,
416 saturation: None,
417 refraction_depth: 0.34,
418 refraction_depth_dp: None,
419 refraction_curve: 1.0,
420 dispersion: 0.30,
421 transmission_refraction: 1.0,
422 meniscus_absorption: 1.0,
423 fold_depth: 0.0,
424 optical_zoom: 1.0,
425 rim_reflection: 1.0,
426 ink_recolor: None,
427 highlight: 1.15,
428 lift: None,
429 contrast: None,
430 shadow: true,
431 shadow_style: None,
432 clip: true,
433 foreground: None,
434 adaptive_frost: 0.0,
435 }
436 }
437
438 pub fn shape(mut self, shape: LiquidShape) -> Self {
439 self.shape = shape;
440 self
441 }
442
443 pub fn tint(mut self, tint: Color) -> Self {
444 self.tint = Some(tint);
445 self
446 }
447
448 pub fn blur_radius(mut self, radius_dp: f32) -> Self {
449 self.blur_radius = Some(radius_dp);
450 self
451 }
452
453 pub fn saturation(mut self, saturation: f32) -> Self {
454 self.saturation = Some(saturation);
455 self
456 }
457
458 pub fn refraction_depth(mut self, fraction: f32) -> Self {
459 self.refraction_depth = fraction.clamp(0.0, 2.0);
460 self.refraction_depth_dp = None;
461 self
462 }
463
464 pub fn refraction_depth_dp(mut self, depth_dp: f32) -> Self {
467 self.refraction_depth_dp = Some(depth_dp.max(0.0));
468 self
469 }
470
471 pub fn refraction_curve(mut self, curve: f32) -> Self {
472 self.refraction_curve = curve.clamp(0.05, 1.0);
473 self
474 }
475
476 pub fn dispersion(mut self, strength: f32) -> Self {
477 self.dispersion = strength.clamp(0.0, 1.0);
478 self
479 }
480
481 pub fn transmission_refraction(mut self, strength: f32) -> Self {
482 self.transmission_refraction = strength.clamp(0.0, 1.0);
483 self
484 }
485
486 pub fn meniscus_absorption(mut self, strength: f32) -> Self {
487 self.meniscus_absorption = strength.clamp(0.0, 1.0);
488 self
489 }
490
491 pub fn fold_depth(mut self, depth_dp: f32) -> Self {
493 self.fold_depth = depth_dp.max(0.0);
494 self
495 }
496
497 pub fn optical_zoom(mut self, zoom: f32) -> Self {
499 self.optical_zoom = zoom.max(1.0);
500 self
501 }
502
503 pub fn rim_reflection(mut self, reflectivity: f32) -> Self {
505 self.rim_reflection = reflectivity.clamp(0.0, 2.0);
506 self
507 }
508
509 pub fn ink_recolor(mut self, color: Color, strength: f32) -> Self {
512 self.ink_recolor = Some((color, strength.clamp(0.0, 1.0)));
513 self
514 }
515
516 pub fn highlight(mut self, highlight: f32) -> Self {
517 self.highlight = highlight;
518 self
519 }
520
521 pub fn lift(mut self, lift: f32) -> Self {
524 self.lift = Some(lift);
525 self
526 }
527
528 pub fn contrast(mut self, contrast: f32) -> Self {
531 self.contrast = Some(contrast.max(0.05));
532 self
533 }
534
535 pub fn adaptive_frost(mut self, foreground: Color, strength: f32) -> Self {
536 self.foreground = Some(foreground);
537 self.adaptive_frost = strength.clamp(0.0, 1.0);
538 self
539 }
540
541 pub fn shadow(mut self, shadow: bool) -> Self {
542 self.shadow = shadow;
543 self
544 }
545
546 pub fn shadow_style(mut self, shadow: GlassShadow) -> Self {
547 self.shadow_style = Some(shadow);
548 self
549 }
550
551 pub fn no_clip(mut self) -> Self {
554 self.clip = false;
555 self
556 }
557
558 fn default_blur_radius(&self) -> f32 {
559 match self.variant {
560 GlassVariant::Regular => 8.0,
561 GlassVariant::Clear => 3.0,
562 GlassVariant::Lens => 0.0,
563 }
564 }
565
566 fn default_saturation(&self) -> f32 {
567 match self.variant {
568 GlassVariant::Regular => 1.5,
569 GlassVariant::Clear => 1.25,
570 GlassVariant::Lens => 1.0,
571 }
572 }
573
574 pub(crate) fn resolve(&self, colors: &LiquidColors) -> ResolvedGlass {
575 let lift = self.lift.unwrap_or(match (self.variant, colors.is_dark) {
576 (GlassVariant::Regular, false) => 0.42,
577 (GlassVariant::Regular, true) => -0.38,
578 (GlassVariant::Clear, false) => 0.12,
579 (GlassVariant::Clear, true) => -0.12,
580 (GlassVariant::Lens, false) => 0.10,
581 (GlassVariant::Lens, true) => -0.08,
582 });
583 let foreground = self.foreground.unwrap_or(colors.label);
584 let shadow = self.shadow_style.unwrap_or_else(|| {
585 GlassShadow::new(
586 Color::BLACK.with_alpha(match (self.variant, colors.is_dark) {
587 (GlassVariant::Lens, false) => 0.14,
588 (GlassVariant::Lens, true) => 0.28,
589 (_, false) => 0.16,
590 (_, true) => 0.5,
591 }),
592 if self.variant == GlassVariant::Lens {
593 10.0
594 } else {
595 22.0
596 },
597 if self.variant == GlassVariant::Lens {
598 3.0
599 } else {
600 8.0
601 },
602 if self.variant == GlassVariant::Lens {
603 -6.0
604 } else {
605 -2.0
606 },
607 )
608 });
609 ResolvedGlass {
610 shape: self.shape,
611 tint: self.tint.unwrap_or(colors.glass_tint),
612 blur_radius_dp: self
613 .blur_radius
614 .unwrap_or_else(|| self.default_blur_radius()),
615 saturation: self.saturation.unwrap_or_else(|| self.default_saturation()),
616 refraction_depth: self.refraction_depth,
617 refraction_depth_dp: self.refraction_depth_dp,
618 refraction_curve: self.refraction_curve,
619 dispersion: self.dispersion,
620 transmission_refraction: self.transmission_refraction,
621 meniscus_absorption: self.meniscus_absorption,
622 fold_depth: self.fold_depth,
623 optical_zoom: self.optical_zoom,
624 rim_reflection: self.rim_reflection,
625 ink_recolor: self.ink_recolor,
626 highlight: self.highlight,
627 lift,
628 contrast: self.contrast.unwrap_or(match self.variant {
629 GlassVariant::Lens => 1.0,
630 _ => 1.03,
631 }),
632 shadow: self.shadow,
633 clip: self.clip,
634 foreground_luma: 0.2126 * foreground.r()
635 + 0.7152 * foreground.g()
636 + 0.0722 * foreground.b(),
637 adaptive_frost: self.adaptive_frost,
638 rim_style: if self.variant == GlassVariant::Lens {
639 1.0
640 } else {
641 0.0
642 },
643 shadow_color: shadow.color,
644 shadow_radius: shadow.radius,
645 shadow_offset_y: shadow.offset_y,
646 shadow_spread: shadow.spread,
647 }
648 }
649}
650
651impl Default for Glass {
652 fn default() -> Self {
653 Self::regular()
654 }
655}
656
657#[derive(Clone, Debug, PartialEq)]
658pub(crate) struct ResolvedGlass {
659 pub shape: LiquidShape,
660 pub tint: Color,
661 pub blur_radius_dp: f32,
662 pub saturation: f32,
663 pub refraction_depth: f32,
664 pub refraction_depth_dp: Option<f32>,
665 pub refraction_curve: f32,
666 pub dispersion: f32,
667 pub transmission_refraction: f32,
668 pub meniscus_absorption: f32,
669 pub fold_depth: f32,
670 pub optical_zoom: f32,
671 pub rim_reflection: f32,
672 pub ink_recolor: Option<(Color, f32)>,
673 pub highlight: f32,
674 pub lift: f32,
675 pub contrast: f32,
676 pub shadow: bool,
677 pub clip: bool,
678 pub rim_style: f32,
679 pub foreground_luma: f32,
680 pub adaptive_frost: f32,
681 pub shadow_color: Color,
682 pub shadow_radius: f32,
683 pub shadow_offset_y: f32,
684 pub shadow_spread: f32,
685}
686
687impl ResolvedGlass {
688 pub(crate) fn backdrop_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
689 self.runtime_effect(density, dynamics, false)
690 }
691
692 fn content_mask_effect(&self, density: f32, dynamics: GlassDynamics) -> RenderEffect {
693 self.runtime_effect(density, dynamics, true)
694 }
695
696 fn runtime_effect(
697 &self,
698 density: f32,
699 dynamics: GlassDynamics,
700 content_mask: bool,
701 ) -> RenderEffect {
702 let density = density.max(f32::EPSILON);
703 let activity = dynamics
704 .activity
705 .filter(|value| value.is_finite())
706 .unwrap_or(1.0)
707 .clamp(0.0, 1.0);
708 let mut shader = RuntimeShader::new(LIQUID_GLASS_WGSL);
709 if let Some(morph) = dynamics.morph.as_ref() {
710 let (node_w, node_h) = morph.node_size;
711 let (cx, cy, w, h, radius) = morph.primary;
712 shader.set_float2(0, node_w.max(1.0), node_h.max(1.0));
713 shader.set_float2(2, cx, cy);
714 shader.set_float2(4, w, h);
715 shader.set_float(6, radius);
716 let count = morph.shapes.len().min(GlassMorph::MAX_SHAPES);
717 shader.set_float(30, count as f32);
718 for (index, (sx, sy, sw, sh, sr)) in morph.shapes.iter().take(count).enumerate() {
719 let base = 36 + index * 5;
720 shader.set_float(base, *sx);
721 shader.set_float(base + 1, *sy);
722 shader.set_float(base + 2, *sw);
723 shader.set_float(base + 3, *sh);
724 shader.set_float(base + 4, *sr);
725 }
726 shader.set_float(31, morph.glue);
727 shader.set_float(32, morph.wobble_amplitude * activity);
728 shader.set_float(33, morph.wobble_phase);
729 shader.set_float(26, morph.bulge_amplitude * activity);
730 shader.set_float(27, morph.bulge_direction);
731 shader.set_float(110, morph.ellipse_blend.clamp(0.0, 1.0) * activity);
732 if let Some(deformation) = morph.deformation {
733 let axis = deformation.axis();
734 let along = 1.0 + (deformation.along() - 1.0) * activity;
735 shader.set_float2(106, axis.0, axis.1);
736 shader.set_float(108, along);
737 shader.set_float(109, 1.0 / along);
738 } else {
739 shader.set_float2(106, 1.0, 0.0);
740 shader.set_float2(108, 1.0, 1.0);
741 }
742 } else {
743 shader.set_float2(0, 0.0, 0.0);
744 shader.set_float(6, self.shape.shader_radius_px(density));
745 }
746 let press_depth = dynamics.press_depth.unwrap_or(1.0).clamp(0.0, 1.0);
747 shader.set_float(9, self.refraction_depth * activity * press_depth);
748 shader.set_float(
749 GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM,
750 self.refraction_depth_dp.unwrap_or(0.0) * activity * press_depth,
751 );
752 shader.set_float(
753 GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM,
754 if self.refraction_depth_dp.is_some() {
755 1.0
756 } else {
757 0.0
758 },
759 );
760 shader.set_float(
761 GLASS_REFRACTION_CURVE_UNIFORM,
762 self.refraction_curve * activity,
763 );
764 shader.set_float(
765 GLASS_DISPERSION_UNIFORM,
766 self.dispersion * activity * press_depth,
767 );
768 shader.set_float(
769 GLASS_TRANSMISSION_REFRACTION_UNIFORM,
770 self.transmission_refraction * activity,
771 );
772 shader.set_float(
773 GLASS_MENISCUS_ABSORPTION_UNIFORM,
774 self.meniscus_absorption * press_depth,
775 );
776 shader.set_float(
777 GLASS_FOLD_DEPTH_UNIFORM,
778 self.fold_depth.max(0.0) * press_depth,
779 );
780 shader.set_float(
781 GLASS_OPTICAL_ZOOM_UNIFORM,
782 1.0 + (self.optical_zoom - 1.0).max(0.0) * activity,
783 );
784 let zoom_anchor = dynamics
785 .morph
786 .as_ref()
787 .map(|morph| morph.zoom_anchor)
788 .unwrap_or((0.0, 0.0));
789 shader.set_float2(
790 GLASS_OPTICAL_ZOOM_ANCHOR_UNIFORM,
791 zoom_anchor.0,
792 zoom_anchor.1,
793 );
794 shader.set_float(121, self.rim_reflection.max(0.001));
795 let (ink_color, ink_strength) = self
796 .ink_recolor
797 .map(|(color, strength)| (color, strength * activity))
798 .unwrap_or((Color::TRANSPARENT, 0.0));
799 shader.set_float(124, ink_color.r());
800 shader.set_float(125, ink_color.g());
801 shader.set_float(126, ink_color.b());
802 shader.set_float(127, ink_strength);
803 let (light_x, light_y) = glass_light_direction();
804 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM, light_x);
805 shader.set_float(GLASS_LIGHT_DIRECTION_UNIFORM + 1, light_y);
806 let (touch_x, touch_y, touch_intensity) = dynamics.touch.unwrap_or((0.0, 0.0, 0.0));
807 shader.set_float(118, touch_x);
808 shader.set_float(119, touch_y);
809 shader.set_float(120, touch_intensity.clamp(0.0, 1.0));
810 shader.set_float(GLASS_EFFECT_DENSITY_UNIFORM, density);
811 shader.set_float(
812 11,
813 (self.highlight + dynamics.highlight_boost).clamp(0.0, 2.0) * activity,
814 );
815 let dynamic_tint = boost_tint_saturation(self.tint, dynamics.saturation_boost);
816 let dynamic_tint_alpha = (dynamic_tint.a()
817 * dynamics
818 .tint_alpha_multiplier
819 .unwrap_or(1.0)
820 .clamp(0.0, 2.0)
821 * activity)
822 .clamp(0.0, 1.0);
823 shader.set_float4(
824 14,
825 dynamic_tint.r(),
826 dynamic_tint.g(),
827 dynamic_tint.b(),
828 dynamic_tint_alpha,
829 );
830 let saturation = (self.saturation + dynamics.saturation_boost).max(0.0);
831 shader.set_float(18, 1.0 + (saturation - 1.0) * activity);
832 shader.set_float(20, self.lift * activity);
833 shader.set_float(21, 0.5 * activity);
834 shader.set_float2(22, 0.0, 1.0);
835 shader.set_float(24, 1.0 + (self.contrast - 1.0) * activity);
836 shader.set_float(28, self.rim_style * activity);
837 let requested_blur_radius_px = if content_mask {
838 0.0
839 } else {
840 self.blur_radius_dp * density * activity
841 };
842 let (wcksrd_blur_radius, gaussian_blur_radius) =
843 if requested_blur_radius_px > WCKSRD_OPTICAL_BLUR_RADIUS_PX {
844 (0.0, requested_blur_radius_px)
845 } else {
846 (requested_blur_radius_px, 0.0)
847 };
848 shader.set_float(GLASS_BLUR_RADIUS_UNIFORM, wcksrd_blur_radius);
849 shader.set_float(GLASS_ACTIVITY_UNIFORM, activity);
850 let resting_tint = dynamics.resting_tint.unwrap_or(Color::TRANSPARENT);
851 shader.set_float4(
852 GLASS_RESTING_TINT_UNIFORM,
853 resting_tint.r(),
854 resting_tint.g(),
855 resting_tint.b(),
856 resting_tint.a(),
857 );
858 shader.set_float(112, if content_mask { 1.0 } else { 0.0 });
859 shader.set_float(91, self.adaptive_frost * activity);
860 shader.set_float(97, self.foreground_luma);
861 let dynamic_shadow = !self.clip && self.shadow;
862 shader.set_float(
863 102,
864 if dynamic_shadow {
865 self.shadow_color.a() * 0.55 * activity
866 } else {
867 0.0
868 },
869 );
870 shader.set_float(103, self.shadow_radius);
871 shader.set_float(104, self.shadow_offset_y);
872 shader.set_float(105, self.shadow_spread);
873 let morph_pad = dynamics
874 .morph
875 .as_ref()
876 .map(|morph| {
877 let (px, py, pw, ph, _) = morph.primary;
878 let (left, top) = (px - pw * 0.5, py - ph * 0.5);
879 let (right, bottom) = (px + pw * 0.5, py + ph * 0.5);
880 let mut shape_reach = 0.0f32;
881 for (sx, sy, sw, sh, _) in &morph.shapes {
882 let reach_x = ((sx + sw * 0.5) - right)
883 .max(left - (sx - sw * 0.5))
884 .max(0.0);
885 let reach_y = ((sy + sh * 0.5) - bottom)
886 .max(top - (sy - sh * 0.5))
887 .max(0.0);
888 shape_reach = shape_reach.max(reach_x.max(reach_y));
889 }
890 let glue_pad = if morph.shapes.is_empty() {
891 0.0
892 } else {
893 morph.glue * 2.0
894 };
895 morph.wobble_amplitude * 2.0 + morph.bulge_amplitude + shape_reach + glue_pad
896 })
897 .unwrap_or(0.0);
898 shader.set_input_padding(self.input_padding() + morph_pad + wcksrd_blur_radius / density);
899 if dynamics.morph.is_some() {
900 let shadow_reach = if dynamic_shadow {
901 self.shadow_radius + self.shadow_offset_y.abs() + self.shadow_spread.max(0.0)
902 } else {
903 0.0
904 };
905 shader.set_output_padding(morph_pad + shadow_reach + 4.0);
906 }
907
908 let optical_effect = liquid_glass_runtime_effect(shader);
909 if gaussian_blur_radius > f32::EPSILON {
910 RenderEffect::blur_with_edge_treatment(gaussian_blur_radius, TileMode::Mirror)
911 .then(optical_effect)
912 } else {
913 optical_effect
914 }
915 }
916
917 fn input_padding(&self) -> f32 {
918 2.0
919 }
920}
921
922pub trait LiquidModifierExt {
924 fn glass_effect(self, glass: Glass) -> Modifier;
930
931 fn glass_effect_with(
935 self,
936 glass: Glass,
937 dynamics: impl Fn() -> GlassDynamics + 'static,
938 ) -> Modifier;
939}
940
941impl LiquidModifierExt for Modifier {
942 fn glass_effect(self, glass: Glass) -> Modifier {
943 self.glass_effect_with(glass, GlassDynamics::default)
944 }
945
946 fn glass_effect_with(
947 self,
948 glass: Glass,
949 dynamics: impl Fn() -> GlassDynamics + 'static,
950 ) -> Modifier {
951 let colors = crate::theme::liquid_colors();
952 let resolved = Rc::new(glass.resolve(&colors));
953 let shape = resolved.shape;
954
955 let mut modifier = self;
956 if resolved.shadow && resolved.clip {
957 let shadow_color = resolved.shadow_color;
958 let (radius, offset_y, spread) = (
959 resolved.shadow_radius,
960 resolved.shadow_offset_y,
961 resolved.shadow_spread,
962 );
963 modifier = modifier.drop_shadow(shape.layer_shape(), move |scope| {
964 scope.radius = radius;
965 scope.spread = spread;
966 scope.offset.y = offset_y;
967 scope.color = shadow_color;
968 scope.cutout = true;
969 });
970 }
971
972 let layer_resolved = Rc::clone(&resolved);
973 let clip = resolved.clip;
974 modifier.graphics_layer(move || {
975 let density = current_density();
976 let frame = dynamics();
977 let render_effect = (!clip && frame.morph.is_some())
978 .then(|| layer_resolved.content_mask_effect(density, frame.clone()));
979 GraphicsLayer {
980 backdrop_effect: Some(layer_resolved.backdrop_effect(density, frame)),
981 render_effect,
982 shape: shape.layer_shape(),
983 clip,
984 ..Default::default()
985 }
986 })
987 }
988}
989
990#[cfg(test)]
991mod tests {
992 use super::*;
993
994 fn light_colors() -> LiquidColors {
995 LiquidColors::light(Color::from_rgb_u8(0, 122, 255))
996 }
997
998 fn terminal_shader(effect: RenderEffect) -> RuntimeShader {
999 match effect {
1000 RenderEffect::Shader { shader } => shader,
1001 RenderEffect::Chain { second, .. } => terminal_shader(*second),
1002 effect => panic!("expected runtime shader, got {effect:?}"),
1003 }
1004 }
1005
1006 #[test]
1007 fn glass_light_direction_defaults_overhead_and_reaches_the_shader() {
1008 assert_eq!(glass_light_direction(), (0.0, 1.0));
1009 let resolved = Glass::regular().resolve(&light_colors());
1010 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1011 let shader = terminal_shader(effect);
1012 let u = shader.uniforms();
1013 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM], 0.0);
1014 assert_eq!(u[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 1.0);
1015
1016 set_glass_light_direction((1.0, 0.0));
1017 let effect = resolved.backdrop_effect(2.0, GlassDynamics::default());
1018 let u_rotated = terminal_shader(effect);
1019 let u_rotated = u_rotated.uniforms();
1020 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM], 1.0);
1021 assert_eq!(u_rotated[GLASS_LIGHT_DIRECTION_UNIFORM + 1], 0.0);
1022 set_glass_light_direction((0.0, 1.0));
1023 }
1024
1025 #[test]
1026 fn liquid_shape_builds_matching_clip_and_layer_shapes() {
1027 for shape in [
1028 LiquidShape::Capsule,
1029 LiquidShape::Circle,
1030 LiquidShape::RoundedRect(12.0),
1031 ] {
1032 assert_eq!(shape.layer_shape(), LayerShape::Rounded(shape.clip_shape()));
1033 }
1034 }
1035
1036 #[test]
1037 fn glass_shadow_clamps_negative_radius() {
1038 let shadow = GlassShadow::new(Color::BLACK, -2.0, 3.0, -1.0);
1039 assert_eq!(shadow.radius, 0.0);
1040 assert_eq!(shadow.offset_y, 3.0);
1041 assert_eq!(shadow.spread, -1.0);
1042 }
1043
1044 #[test]
1045 fn incompressible_deformation_normalizes_axis_and_conserves_area() {
1046 let deformation = GlassDeformation::incompressible((3.0, 4.0), 1.25);
1047 assert_eq!(deformation.axis(), (0.6, 0.8));
1048 assert_eq!(deformation.along(), 1.25);
1049 assert!((deformation.along() * deformation.across() - 1.0).abs() < 1.0e-6);
1050 assert_eq!(
1051 GlassDeformation::incompressible((0.0, 0.0), 0.0).axis(),
1052 (1.0, 0.0)
1053 );
1054 }
1055
1056 #[test]
1057 fn glass_builders_clamp_physical_inputs() {
1058 let glass = Glass::lens()
1059 .shape(LiquidShape::Circle)
1060 .tint(Color::BLACK)
1061 .blur_radius(-2.0)
1062 .saturation(1.2)
1063 .refraction_depth(3.0)
1064 .refraction_depth_dp(-3.0)
1065 .refraction_curve(2.0)
1066 .dispersion(2.0)
1067 .transmission_refraction(2.0)
1068 .meniscus_absorption(2.0)
1069 .highlight(0.4)
1070 .lift(-0.2)
1071 .adaptive_frost(Color::WHITE, 2.0)
1072 .shadow(false)
1073 .no_clip();
1074 assert_eq!(glass.shape, LiquidShape::Circle);
1075 assert_eq!(glass.tint, Some(Color::BLACK));
1076 assert_eq!(glass.blur_radius, Some(-2.0));
1077 assert_eq!(glass.saturation, Some(1.2));
1078 assert_eq!(glass.refraction_depth, 2.0);
1079 assert_eq!(glass.refraction_depth_dp, Some(0.0));
1080 assert_eq!(glass.refraction_curve, 1.0);
1081 assert_eq!(glass.dispersion, 1.0);
1082 assert_eq!(glass.transmission_refraction, 1.0);
1083 assert_eq!(glass.meniscus_absorption, 1.0);
1084 assert_eq!(glass.highlight, 0.4);
1085 assert_eq!(glass.lift, Some(-0.2));
1086 assert_eq!(glass.adaptive_frost, 1.0);
1087 assert!(!glass.shadow);
1088 assert!(!glass.clip);
1089 }
1090
1091 #[test]
1092 fn material_variants_resolve_distinct_frost_levels() {
1093 let regular = Glass::regular().resolve(&light_colors());
1094 let clear = Glass::clear().resolve(&light_colors());
1095 let lens = Glass::lens().resolve(&light_colors());
1096 assert!(regular.blur_radius_dp > clear.blur_radius_dp);
1097 assert!(clear.blur_radius_dp > lens.blur_radius_dp);
1098 assert!(regular.saturation > clear.saturation);
1099 assert_eq!(lens.rim_style, 1.0);
1100 assert_eq!(regular.refraction_depth, 0.0);
1101 assert_eq!(regular.transmission_refraction, 0.0);
1102 assert_eq!(regular.refraction_curve, 0.25);
1103 assert_eq!(lens.refraction_curve, 1.0);
1104 assert_eq!(regular.dispersion, 0.0);
1105 assert_eq!(lens.dispersion, 0.30);
1106 }
1107
1108 #[test]
1109 fn neutral_surface_helpers_follow_foreground_polarity() {
1110 assert_eq!(
1111 neutral_surface_tint(Color::BLACK, 0.08, 0.10),
1112 Color::BLACK.with_alpha(0.08)
1113 );
1114 assert_eq!(
1115 neutral_surface_tint(Color::WHITE, 0.08, 0.10),
1116 Color::WHITE.with_alpha(0.10)
1117 );
1118 assert_eq!(neutral_surface_lift(Color::BLACK, 0.7, -0.3), 0.7);
1119 assert_eq!(neutral_surface_lift(Color::WHITE, 0.7, -0.3), -0.3);
1120 }
1121
1122 #[test]
1123 fn dynamic_saturation_reaches_the_material_tint() {
1124 let resting = Color::from_rgb_u8(0, 199, 208);
1125 let raised = boost_tint_saturation(resting, 0.55);
1126 assert_eq!(raised.r(), 0.0);
1127 assert!(raised.g() > resting.g());
1128 assert!(raised.b() > resting.b());
1129 assert_eq!(raised.a(), resting.a());
1130 }
1131
1132 #[test]
1133 fn resolved_material_packs_wcksrd_and_dynamic_tint() {
1134 let resolved = Glass::lens()
1135 .refraction_depth(0.72)
1136 .refraction_depth_dp(21.0)
1137 .refraction_curve(0.8)
1138 .dispersion(0.42)
1139 .transmission_refraction(0.35)
1140 .meniscus_absorption(0.3)
1141 .blur_radius(3.0)
1142 .tint(Color::BLACK.with_alpha(0.8))
1143 .resolve(&light_colors());
1144 let effect = resolved.backdrop_effect(
1145 2.0,
1146 GlassDynamics {
1147 highlight_boost: 0.2,
1148 saturation_boost: 0.35,
1149 tint_alpha_multiplier: Some(0.25),
1150 ..Default::default()
1151 },
1152 );
1153 let RenderEffect::Chain { first, .. } = &effect else {
1154 panic!("macroscopic frost must precede the wcKSRD optical pass");
1155 };
1156 assert!(matches!(
1157 first.as_ref(),
1158 RenderEffect::Blur {
1159 radius_x: 6.0,
1160 radius_y: 6.0,
1161 ..
1162 }
1163 ));
1164 let shader = terminal_shader(effect);
1165 assert_eq!(shader.uniforms()[9], 0.72);
1166 assert_eq!(
1167 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_UNIFORM],
1168 21.0
1169 );
1170 assert_eq!(
1171 shader.uniforms()[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
1172 1.0
1173 );
1174 assert_eq!(shader.uniforms()[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1175 assert_eq!(shader.uniforms()[GLASS_DISPERSION_UNIFORM], 0.42);
1176 assert_eq!(
1177 shader.uniforms()[GLASS_TRANSMISSION_REFRACTION_UNIFORM],
1178 0.35
1179 );
1180 assert_eq!(shader.uniforms()[GLASS_MENISCUS_ABSORPTION_UNIFORM], 0.3);
1181 assert_eq!(shader.uniforms()[11], resolved.highlight + 0.2);
1182 assert_eq!(shader.uniforms()[18], resolved.saturation + 0.35);
1183 assert!((shader.uniforms()[17] - 0.2).abs() < 1.0e-6);
1184 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1185 assert_eq!(shader.uniforms()[GLASS_EFFECT_DENSITY_UNIFORM], 2.0);
1186 }
1187
1188 #[test]
1189 fn heavy_backdrop_blur_rides_the_gaussian_pass_alone() {
1190 let effect = Glass::regular()
1191 .resolve(&light_colors())
1192 .backdrop_effect(3.0, GlassDynamics::default());
1193 let RenderEffect::Chain { first, .. } = &effect else {
1194 panic!("heavy blur must ride the separable Gaussian pre-pass");
1195 };
1196 assert!(matches!(
1197 first.as_ref(),
1198 RenderEffect::Blur {
1199 radius_x: 24.0,
1200 radius_y: 24.0,
1201 ..
1202 }
1203 ));
1204 assert_eq!(
1205 terminal_shader(effect).uniforms()[GLASS_BLUR_RADIUS_UNIFORM],
1206 0.0
1207 );
1208 }
1209
1210 #[test]
1211 fn featherweight_backdrop_blur_stays_in_the_optical_tap() {
1212 let RenderEffect::Shader { shader } = Glass::regular()
1213 .blur_radius(0.5)
1214 .resolve(&light_colors())
1215 .backdrop_effect(3.0, GlassDynamics::default())
1216 else {
1217 panic!("a sub-cap blur must not build a Gaussian chain");
1218 };
1219 assert_eq!(shader.uniforms()[GLASS_BLUR_RADIUS_UNIFORM], 1.5);
1220 }
1221
1222 #[test]
1223 fn raised_tint_density_stays_premultiplied_alpha_safe() {
1224 let effect = Glass::lens()
1225 .tint(Color::WHITE.with_alpha(0.8))
1226 .resolve(&light_colors())
1227 .backdrop_effect(
1228 1.0,
1229 GlassDynamics {
1230 tint_alpha_multiplier: Some(2.0),
1231 ..Default::default()
1232 },
1233 );
1234 assert_eq!(terminal_shader(effect).uniforms()[17], 1.0);
1235 }
1236
1237 #[test]
1238 fn optical_activity_reaches_identity_without_removing_the_glass_geometry() {
1239 let resolved = Glass::lens()
1240 .refraction_depth(0.72)
1241 .refraction_curve(0.8)
1242 .dispersion(0.42)
1243 .blur_radius(3.0)
1244 .saturation(1.4)
1245 .highlight(0.6)
1246 .lift(0.2)
1247 .tint(Color::BLACK.with_alpha(0.8))
1248 .no_clip()
1249 .resolve(&light_colors());
1250 let morph = GlassMorph {
1251 node_size: (120.0, 72.0),
1252 primary: (60.0, 36.0, 96.0, 52.0, -1.0),
1253 wobble_amplitude: 3.0,
1254 bulge_amplitude: 4.0,
1255 deformation: Some(GlassDeformation::incompressible((1.0, 0.0), 1.25)),
1256 ..Default::default()
1257 };
1258
1259 let RenderEffect::Shader { shader } = resolved.backdrop_effect(
1260 2.0,
1261 GlassDynamics {
1262 activity: Some(0.0),
1263 morph: Some(morph),
1264 ..Default::default()
1265 },
1266 ) else {
1267 panic!("material must resolve to the shared wcKSRD shader");
1268 };
1269 let uniforms = shader.uniforms();
1270 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 0.0);
1271 assert_eq!(uniforms[9], 0.0);
1272 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.0);
1273 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.0);
1274 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 0.0);
1275 assert_eq!(uniforms[11], 0.0);
1276 assert_eq!(uniforms[17], 0.0);
1277 assert_eq!(uniforms[18], 1.0);
1278 assert_eq!(uniforms[20], 0.0);
1279 assert_eq!(uniforms[21], 0.0);
1280 assert_eq!(uniforms[24], 1.0);
1281 assert_eq!(uniforms[28], 0.0);
1282 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1283 assert_eq!(uniforms[91], 0.0);
1284 assert_eq!(uniforms[102], 0.0);
1285 assert_eq!(
1286 &uniforms[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1287 &[0.0, 0.0, 0.0, 0.0]
1288 );
1289 assert_eq!(uniforms[32], 0.0);
1290 assert_eq!(uniforms[26], 0.0);
1291 assert_eq!(&uniforms[108..110], &[1.0, 1.0]);
1292 assert_eq!(&uniforms[2..6], &[60.0, 36.0, 96.0, 52.0]);
1293 }
1294
1295 #[test]
1296 fn resting_surface_tint_survives_zero_optical_activity() {
1297 let tint = Color::BLACK.with_alpha(0.11);
1298 let RenderEffect::Shader { shader } = Glass::lens()
1299 .no_clip()
1300 .resolve(&light_colors())
1301 .backdrop_effect(
1302 1.0,
1303 GlassDynamics {
1304 activity: Some(0.0),
1305 resting_tint: Some(tint),
1306 ..Default::default()
1307 },
1308 )
1309 else {
1310 panic!("resting surface must use the shared wcKSRD shader");
1311 };
1312 assert_eq!(
1313 &shader.uniforms()[GLASS_RESTING_TINT_UNIFORM..GLASS_RESTING_TINT_UNIFORM + 4],
1314 &[tint.r(), tint.g(), tint.b(), tint.a()]
1315 );
1316 assert_eq!(shader.uniforms()[GLASS_ACTIVITY_UNIFORM], 0.0);
1317 }
1318
1319 #[test]
1320 fn full_optical_activity_preserves_the_resolved_material() {
1321 let resolved = Glass::lens()
1322 .refraction_depth(0.72)
1323 .refraction_curve(0.8)
1324 .dispersion(0.42)
1325 .blur_radius(3.0)
1326 .resolve(&light_colors());
1327 let shader = terminal_shader(resolved.backdrop_effect(
1328 2.0,
1329 GlassDynamics {
1330 activity: Some(1.0),
1331 ..Default::default()
1332 },
1333 ));
1334 let uniforms = shader.uniforms();
1335 assert_eq!(uniforms[GLASS_ACTIVITY_UNIFORM], 1.0);
1336 assert_eq!(uniforms[9], 0.72);
1337 assert_eq!(
1338 uniforms[GLASS_PHYSICAL_REFRACTION_DEPTH_ENABLED_UNIFORM],
1339 0.0
1340 );
1341 assert_eq!(uniforms[GLASS_REFRACTION_CURVE_UNIFORM], 0.8);
1342 assert_eq!(uniforms[GLASS_DISPERSION_UNIFORM], 0.42);
1343 assert_eq!(uniforms[GLASS_TRANSMISSION_REFRACTION_UNIFORM], 1.0);
1344 assert_eq!(uniforms[GLASS_BLUR_RADIUS_UNIFORM], 0.0);
1345 }
1346
1347 #[test]
1348 fn morph_geometry_and_incompressible_strain_are_packed() {
1349 let deformation = GlassDeformation::incompressible((0.0, 2.0), 1.25);
1350 let morph = GlassMorph {
1351 node_size: (78.0, 59.0),
1352 primary: (39.0, 29.5, 58.0, 39.0, -1.0),
1353 shapes: vec![(70.0, 29.5, 40.0, 40.0, -1.0)],
1354 glue: 8.0,
1355 wobble_amplitude: 1.0,
1356 wobble_phase: 0.5,
1357 bulge_amplitude: 2.0,
1358 bulge_direction: 0.25,
1359 ellipse_blend: 0.3,
1360 deformation: Some(deformation),
1361 zoom_anchor: (0.0, 0.0),
1362 };
1363 let RenderEffect::Shader { shader } = Glass::lens()
1364 .no_clip()
1365 .resolve(&light_colors())
1366 .backdrop_effect(
1367 1.0,
1368 GlassDynamics {
1369 morph: Some(morph),
1370 ..Default::default()
1371 },
1372 )
1373 else {
1374 panic!("morph must use the shared wcKSRD shader");
1375 };
1376 let uniforms = shader.uniforms();
1377 assert_eq!(&uniforms[0..6], &[78.0, 59.0, 39.0, 29.5, 58.0, 39.0]);
1378 assert_eq!(uniforms[30], 1.0);
1379 assert_eq!(&uniforms[106..110], &[0.0, 1.0, 1.25, 0.8]);
1380 assert_eq!(uniforms[110], 0.3);
1381 assert!(shader.output_padding() > 0.0);
1382 }
1383
1384 #[test]
1385 fn optical_builders_hold_their_own_legal_range() {
1386 let glass = Glass::regular()
1387 .fold_depth(-4.0)
1388 .optical_zoom(0.5)
1389 .rim_reflection(9.0);
1390 assert_eq!(glass.fold_depth, 0.0, "a fold cannot be negative deep");
1391 assert_eq!(glass.optical_zoom, 1.0, "a lens never shrinks its face");
1392 assert_eq!(
1393 glass.rim_reflection, 2.0,
1394 "the rim tops out at twice the line"
1395 );
1396
1397 let glass = Glass::regular()
1398 .fold_depth(6.0)
1399 .optical_zoom(1.4)
1400 .rim_reflection(0.25);
1401 assert_eq!(glass.fold_depth, 6.0);
1402 assert_eq!(glass.optical_zoom, 1.4);
1403 assert_eq!(glass.rim_reflection, 0.25);
1404 }
1405
1406 #[test]
1407 fn ink_recolor_records_its_color_at_a_clamped_strength() {
1408 let red = Color::from_rgb_u8(255, 0, 0);
1409 let glass = Glass::regular().ink_recolor(red, 3.0);
1410 assert_eq!(glass.ink_recolor, Some((red, 1.0)));
1411
1412 let glass = Glass::regular().ink_recolor(red, -1.0);
1413 assert_eq!(glass.ink_recolor, Some((red, 0.0)));
1414 assert_eq!(Glass::regular().ink_recolor, None, "no recolor by default");
1415 }
1416
1417 #[test]
1418 fn a_touched_up_surface_amplifies_what_it_already_is() {
1419 let dynamics = GlassDynamics::default().touched_up(1.0, None, (10.0, 4.0));
1420 assert!(
1421 dynamics.highlight_boost > 0.0,
1422 "a pressed surface goes brighter"
1423 );
1424 assert!(dynamics.saturation_boost > 0.0, "and more saturated");
1425 assert_eq!(
1426 dynamics.touch,
1427 Some((10.0, 4.0, 1.0)),
1428 "with no finger the glow sits at the surface's own heart"
1429 );
1430 assert_eq!(
1431 dynamics.resting_tint, None,
1432 "a touch never repaints the surface with another color"
1433 );
1434 }
1435
1436 #[test]
1437 fn a_touch_glow_follows_the_finger_and_accumulates_to_full() {
1438 let dynamics = GlassDynamics::default()
1439 .touched_up(0.5, Some((3.0, 7.0)), (10.0, 4.0))
1440 .touched_up(0.8, Some((3.0, 7.0)), (10.0, 4.0));
1441 let (x, y, intensity) = dynamics.touch.expect("a pressed surface glows");
1442 assert_eq!(
1443 (x, y),
1444 (3.0, 7.0),
1445 "the glow tracks the finger, not the center"
1446 );
1447 assert_eq!(intensity, 1.0, "carried press saturates at full");
1448 }
1449
1450 #[test]
1451 fn an_unpressed_surface_is_left_exactly_as_it_was() {
1452 let resting = GlassDynamics::default();
1453 let untouched = resting
1454 .clone()
1455 .touched_up(0.0, Some((3.0, 7.0)), (10.0, 4.0));
1456 assert_eq!(untouched.highlight_boost, resting.highlight_boost);
1457 assert_eq!(untouched.saturation_boost, resting.saturation_boost);
1458 assert_eq!(untouched.touch, None, "no press, no glow");
1459 }
1460}