Skip to main content

glassy_ui/motion/
component.rs

1use std::time::Duration;
2
3use gpui::{
4    div, prelude::*, px, AnimationExt, AnyElement, App, ElementId, IntoElement, ParentElement,
5    RenderOnce, SharedString, StyleRefinement, Styled, Window,
6};
7
8use super::slot::StyledSlot;
9use super::style::{apply_style, MotionStyle, ResolvedStyle};
10use super::transition::{Ease, Transition};
11use super::values::MotionValue;
12use super::variants::Variants;
13
14/// A `div`-like element with motion.dev-style animation props.
15#[derive(IntoElement)]
16pub struct Motion {
17    id: ElementId,
18    style: StyleRefinement,
19    children: Vec<AnyElement>,
20    initial: MotionStyle,
21    animate: MotionStyle,
22    exit: Option<MotionStyle>,
23    transition: Transition,
24    while_hover: Option<MotionStyle>,
25    while_tap: Option<MotionStyle>,
26    variants: Option<Variants>,
27    layout: bool,
28    /// Optional shared value mixed into opacity (0..=1).
29    opacity_value: Option<MotionValue>,
30    /// Previous layout size for layout animations (set via `.layout`).
31    layout_from: Option<(gpui::Pixels, gpui::Pixels)>,
32    layout_to: Option<(gpui::Pixels, gpui::Pixels)>,
33    id_label: SharedString,
34}
35
36impl Default for Motion {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl Motion {
43    pub fn new() -> Self {
44        Self {
45            id: ElementId::Name("motion".into()),
46            id_label: "motion".into(),
47            style: StyleRefinement::default(),
48            children: Vec::new(),
49            initial: MotionStyle::new(),
50            animate: MotionStyle::new(),
51            exit: None,
52            transition: Transition::default(),
53            while_hover: None,
54            while_tap: None,
55            variants: None,
56            layout: false,
57            opacity_value: None,
58            layout_from: None,
59            layout_to: None,
60        }
61    }
62
63    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
64        let label: SharedString = id.into();
65        self.id_label = label.clone();
66        self.id = ElementId::Name(label);
67        self
68    }
69
70    pub fn initial(mut self, style: MotionStyle) -> Self {
71        self.initial = style;
72        self
73    }
74
75    pub fn animate(mut self, style: MotionStyle) -> Self {
76        self.animate = style;
77        self
78    }
79
80    /// Exit style used by [`crate::AnimatePresence`].
81    pub fn exit(mut self, style: MotionStyle) -> Self {
82        self.exit = Some(style);
83        self
84    }
85
86    pub fn exit_style(&self) -> Option<MotionStyle> {
87        self.exit
88    }
89
90    pub fn transition(mut self, transition: Transition) -> Self {
91        self.transition = transition;
92        self
93    }
94
95    /// Hover target (animated enter still runs; hover snaps toward this style).
96    pub fn while_hover(mut self, style: MotionStyle) -> Self {
97        self.while_hover = Some(style);
98        self
99    }
100
101    /// Pressed/active target (motion.dev `whileTap`).
102    pub fn while_tap(mut self, style: MotionStyle) -> Self {
103        self.while_tap = Some(style);
104        self
105    }
106
107    pub fn variants(mut self, variants: Variants) -> Self {
108        self.variants = Some(variants);
109        self
110    }
111
112    pub fn initial_variant(mut self, name: impl AsRef<str>) -> Self {
113        if let Some(style) = self.variants.as_ref().and_then(|v| v.get(name.as_ref())) {
114            self.initial = style;
115        }
116        self
117    }
118
119    pub fn animate_variant(mut self, name: impl AsRef<str>) -> Self {
120        if let Some(style) = self.variants.as_ref().and_then(|v| v.get(name.as_ref())) {
121            self.animate = style;
122        }
123        self
124    }
125
126    pub fn exit_variant(mut self, name: impl AsRef<str>) -> Self {
127        if let Some(style) = self.variants.as_ref().and_then(|v| v.get(name.as_ref())) {
128            self.exit = Some(style);
129        }
130        self
131    }
132
133    /// Enable layout-size animation between `from` and `to` sizes.
134    pub fn layout(
135        mut self,
136        from: (impl Into<gpui::Pixels>, impl Into<gpui::Pixels>),
137        to: (impl Into<gpui::Pixels>, impl Into<gpui::Pixels>),
138    ) -> Self {
139        self.layout = true;
140        self.layout_from = Some((from.0.into(), from.1.into()));
141        self.layout_to = Some((to.0.into(), to.1.into()));
142        self
143    }
144
145    /// Bind opacity to a shared [`MotionValue`] (multiplied with animated opacity).
146    pub fn opacity_from(mut self, value: MotionValue) -> Self {
147        self.opacity_value = Some(value);
148        self
149    }
150
151    pub fn fade_up(self) -> Self {
152        self.initial(MotionStyle::new().opacity(0.).y(px(14.)))
153            .animate(MotionStyle::new().opacity(1.).y(px(0.)))
154            .exit(MotionStyle::new().opacity(0.).y(px(-10.)))
155            .transition(Transition::spring().stiffness(280.).damping(26.))
156    }
157
158    pub fn fade_in(self) -> Self {
159        self.initial(MotionStyle::new().opacity(0.))
160            .animate(MotionStyle::new().opacity(1.))
161            .exit(MotionStyle::new().opacity(0.))
162            .transition(Transition::tween(Duration::from_millis(280)).ease(Ease::EaseOut))
163    }
164
165    /// Quiet feedback for marks that appear after selecting a control.
166    pub fn selection_in(self) -> Self {
167        self.initial(MotionStyle::new().opacity(0.))
168            .animate(MotionStyle::new().opacity(1.))
169            .transition(Transition::tween(Duration::from_millis(140)).ease(Ease::EaseOut))
170    }
171
172    /// Material surface arrival without bounce or geometry movement.
173    pub fn surface_in(self) -> Self {
174        self.initial(MotionStyle::new().opacity(0.))
175            .animate(MotionStyle::new().opacity(1.))
176            .transition(Transition::tween(Duration::from_millis(180)).ease(Ease::EaseOut))
177    }
178
179    pub fn pop_in(self) -> Self {
180        self.initial(MotionStyle::new().opacity(0.).scale(0.92))
181            .animate(MotionStyle::new().opacity(1.).scale(1.))
182            .exit(MotionStyle::new().opacity(0.).scale(0.96))
183            .transition(
184                Transition::spring()
185                    .bounce(0.2)
186                    .duration(Duration::from_millis(420)),
187            )
188    }
189}
190
191impl Styled for Motion {
192    fn style(&mut self) -> &mut StyleRefinement {
193        &mut self.style
194    }
195}
196
197impl ParentElement for Motion {
198    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
199        self.children.extend(elements);
200    }
201}
202
203impl RenderOnce for Motion {
204    fn render(self, _window: &mut Window, _cx: &mut App) -> impl IntoElement {
205        let mut from = self.initial.resolve(ResolvedStyle::default());
206        let mut to = self.animate.resolve(from);
207
208        if self.layout {
209            if let (Some((fw, fh)), Some((tw, th))) = (self.layout_from, self.layout_to) {
210                from.width = Some(fw);
211                from.height = Some(fh);
212                to.width = Some(tw);
213                to.height = Some(th);
214            }
215        }
216
217        let hover = self.while_hover.map(|h| h.resolve(to));
218        let tap = self.while_tap.map(|t| t.resolve(to));
219        let transition = self.transition;
220        let base_style = self.style;
221        let children = self.children;
222        let anim_id = self.id;
223        let interact_id = ElementId::Name(format!("{}__hit", self.id_label).into());
224        let opacity_value = self.opacity_value;
225
226        let mut el = div()
227            .id(interact_id)
228            .refine_style(&base_style)
229            .children(children);
230
231        if let Some(hover) = hover {
232            el = el.hover(move |s| {
233                let mut s = s.opacity(hover.opacity).ml(hover.x).mt(hover.y);
234                if let Some(w) = hover.width {
235                    s = s.w(w);
236                }
237                if let Some(h) = hover.height {
238                    s = s.h(h);
239                }
240                s
241            });
242        }
243
244        if let Some(tap) = tap {
245            el = el.active(move |s| {
246                let mut s = s.opacity(tap.opacity).ml(tap.x).mt(tap.y);
247                if let Some(w) = tap.width {
248                    s = s.w(w);
249                }
250                if let Some(h) = tap.height {
251                    s = s.h(h);
252                }
253                s
254            });
255        }
256
257        el.with_animation(anim_id, transition.into_animation(), move |this, delta| {
258            let mut frame = from.lerp(to, delta);
259            if let Some(value) = opacity_value.as_ref() {
260                frame.opacity = (frame.opacity * value.get().clamp(0.0, 1.0)).clamp(0.0, 1.0);
261            }
262            apply_style(this, frame)
263        })
264    }
265}
266
267/// Convenience for building a labeled variant map quickly.
268#[allow(dead_code)]
269pub fn variant_pair(
270    hidden: MotionStyle,
271    visible: MotionStyle,
272) -> (Variants, SharedString, SharedString) {
273    let variants = Variants::new()
274        .set("hidden", hidden)
275        .set("visible", visible);
276    (variants, "hidden".into(), "visible".into())
277}