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