Skip to main content

cranpose_liquid/
motion.rs

1//! Liquid motion: the spring presets every component shares, and the press
2//! interaction (scale + specular boost) that makes glass feel physical.
3
4use cranpose_animation::{spring, AnimationType};
5use cranpose_core::State;
6use cranpose_macros::composable;
7use cranpose_ui::Modifier;
8use cranpose_ui::MutableInteractionSource;
9use cranpose_ui_graphics::GraphicsLayer;
10
11/// Named springs used across the Liquid components (value-space, velocity
12/// preserving).
13pub struct LiquidMotion;
14
15impl LiquidMotion {
16    /// Snappy interactions: presses, toggles, selection moves.
17    pub fn snappy() -> AnimationType {
18        spring(0.85, 900.0)
19    }
20
21    /// The droplet feel: visible overshoot for morphing shapes.
22    pub fn bouncy() -> AnimationType {
23        spring(0.55, 500.0)
24    }
25
26    /// Gentle settle for large surfaces (sheets, menus).
27    pub fn smooth() -> AnimationType {
28        spring(1.0, 400.0)
29    }
30
31    /// The leading edge of a stretching selection blob (runs ahead).
32    pub fn blob_leading() -> AnimationType {
33        spring(0.8, 900.0)
34    }
35
36    /// The trailing edge of a stretching selection blob (drags behind, giving
37    /// the droplet elongation while in motion).
38    pub fn blob_trailing() -> AnimationType {
39        spring(0.9, 380.0)
40    }
41}
42
43/// Press feedback for glass controls, per the Liquid Glass law: touched glass
44/// GROWS (spring scale toward `pressed_scale` — never smaller) and turns MORE
45/// TRANSPARENT (the returned content alpha dips while pressed, the reference
46/// "…" dots fading as the button lifts). Returns the pressed state so callers
47/// can also boost the specular highlight.
48///
49/// Apply the returned modifier *outside* the glass effect so the whole lens
50/// scales together; apply the content alpha to the label/icon layer.
51#[composable]
52pub fn liquid_press_scale(
53    modifier: Modifier,
54    interaction_source: MutableInteractionSource,
55    pressed_scale: f32,
56) -> (Modifier, State<bool>, State<f32>) {
57    let pressed = interaction_source.collectIsPressedAsState();
58    let scale = cranpose_animation::animateFloatAsState(
59        if pressed.get() {
60            pressed_scale.max(1.0)
61        } else {
62            1.0
63        },
64        LiquidMotion::snappy(),
65        "liquid-press-scale",
66    );
67    let content_alpha = cranpose_animation::animateFloatAsState(
68        // The reference down-state ghosts glyphs hard (the menu button's
69        // dots drop to ~30% while held).
70        if pressed.get() { 0.35 } else { 1.0 },
71        LiquidMotion::smooth(),
72        "liquid-press-content",
73    );
74    let modifier = modifier.graphics_layer(move || {
75        let scale = scale.get();
76        GraphicsLayer {
77            scale_x: scale,
78            scale_y: scale,
79            ..Default::default()
80        }
81    });
82    (modifier, pressed, content_alpha)
83}