Skip to main content

gpui_ce_motion/
native.rs

1use gpui::{AnyElement, App, Empty, IntoElement, Styled, Window, px};
2
3use crate::{Frame, Motion, MotionState, Motions};
4
5impl MotionState {
6    /// Samples native monotonic time and GPUI's reduced-motion flag. Call only
7    /// from `Render::render` or an element drawing phase: frame requests require
8    /// an active view. Mutate model state and call `cx.notify()` from handlers.
9    /// No frames are scheduled after settling or after a reduced-motion snap.
10    ///
11    /// ```
12    /// use gpui::{AnyElement, App, Window, div, prelude::*};
13    /// use gpui_ce_motion::{Motion, MotionState, Target};
14    ///
15    /// fn panel(state: &mut MotionState, open: bool, window: &mut Window, cx: &App) -> AnyElement {
16    ///     let frame = state.frame(
17    ///         Motion::new(Target::default()).present(open), window, cx,
18    ///     );
19    ///     frame.render(|frame| frame.apply(div().id("panel").child("Panel")))
20    /// }
21    /// ```
22    pub fn frame(&mut self, motion: Motion, window: &mut Window, cx: &App) -> Frame {
23        let frame = self.sample(motion, self.epoch.elapsed(), cx.reduce_motion());
24
25        if frame.is_animating() {
26            window.request_animation_frame();
27        }
28
29        frame
30    }
31}
32
33impl Frame {
34    /// Applies opacity, relative left/top insets, and declared width, height,
35    /// and radius to a styled element. Sizes cause real layout reflow; these
36    /// are not GPU transforms or layout projection. Undeclared layout properties
37    /// preserve the element's style. Radius does not enable overflow clipping.
38    /// This replaces the div's position mode and left/top values. Use a wrapper
39    /// if the div itself needs absolute positioning. Normal layout space remains
40    /// reserved until exit completes; siblings do not move with these offsets.
41    pub fn apply<E: Styled>(self, element: E) -> E {
42        let target = self.target();
43        let mut element = element;
44
45        if let Some(width) = target.width_px() {
46            element = element.w(px(width));
47        }
48
49        if let Some(height) = target.height_px() {
50            element = element.h(px(height));
51        }
52
53        if let Some(radius) = target.border_radius_px() {
54            element = element.rounded(px(radius));
55        }
56
57        element
58            .relative()
59            .left(px(self.target().x_px()))
60            .top(px(self.target().y_px()))
61            .opacity(self.target().alpha())
62    }
63
64    /// Builds an element only while mounted, retaining it throughout exit.
65    /// Continue declaring this every render even when `present(false)`.
66    /// This does not disable handlers or transfer focus during exit: the owner
67    /// must guard input using `is_present()` and restore focus before removal.
68    pub fn render<E: IntoElement>(self, build: impl FnOnce(Self) -> E) -> AnyElement {
69        if self.is_mounted() {
70            build(self).into_any_element()
71        } else {
72            Empty.into_any_element()
73        }
74    }
75}
76
77impl crate::AnimationState {
78    /// Current timestamp for native playback controls. Do not mix native `frame`
79    /// with another epoch; use `state.pause(state.now())`, seek, or speed controls.
80    pub fn now(&self) -> std::time::Duration {
81        self.epoch.elapsed()
82    }
83
84    /// Samples native time and requests frames only while this prepared clip is playing.
85    pub fn frame(&mut self, animation: &crate::Animation, window: &mut Window, cx: &App) -> Frame {
86        let frame = self.sample(animation, self.epoch.elapsed(), cx.reduce_motion());
87
88        if frame.is_animating() {
89            window.request_animation_frame();
90        }
91
92        frame
93    }
94}
95
96impl crate::Color {
97    /// Converts the sampled color to GPUI's sRGB representation.
98    pub fn to_rgba(self) -> gpui::Rgba {
99        let [r, g, b, a] = self.channels();
100
101        gpui::Rgba::new(r, g, b, a)
102    }
103}
104
105impl crate::SvgMotionState {
106    /// Samples native time and the app's reduced-motion flag. Call only during
107    /// render/element drawing; notify the owner after target changes in input handlers.
108    pub fn frame(
109        &mut self,
110        motion: crate::SvgMotion,
111        window: &mut Window,
112        cx: &App,
113    ) -> crate::SvgFrame {
114        let frame = self.sample(motion, self.epoch.elapsed(), cx.reduce_motion());
115
116        if frame.is_animating() {
117            window.request_animation_frame();
118        }
119
120        frame
121    }
122}
123
124impl crate::SvgFrame {
125    /// Converts to CE's center-origin SVG transform: signed scale, then rotation.
126    /// Only the drawing angle is reduced modulo one turn, preserving the engine's
127    /// unwrapped presentation and velocity. This replaces any existing SVG transform.
128    pub fn transformation(self) -> gpui::Transformation {
129        let [x, y] = self.target().scale_factors();
130
131        let angle = self
132            .target()
133            .rotation_degrees()
134            .rem_euclid(360.0)
135            .to_radians() as f32;
136
137        gpui::Transformation::scale(gpui::size(x, y))
138            .with_rotation(gpui::radians(angle))
139            .with_translation(gpui::point(
140                px(self.target().translation()[0]),
141                px(self.target().translation()[1]),
142            ))
143    }
144
145    /// Converts with a custom pivot expressed as logical pixels from the SVG's
146    /// top-left. Supply its untransformed bounds size. Translation compensates
147    /// for CE's fixed center origin. Native layout/hitbox remain unchanged.
148    pub fn transformation_about(
149        self,
150        bounds: gpui::Size<gpui::Pixels>,
151        pivot: gpui::Point<gpui::Pixels>,
152    ) -> gpui::Transformation {
153        assert!(
154            [
155                f32::from(bounds.width),
156                f32::from(bounds.height),
157                f32::from(pivot.x),
158                f32::from(pivot.y)
159            ]
160            .iter()
161            .all(|v| v.is_finite())
162                && bounds.width >= px(0.)
163                && bounds.height >= px(0.),
164            "SVG bounds and pivot must be finite with non-negative size"
165        );
166        let [sx, sy] = self.target().scale_factors().map(f64::from);
167
168        let angle = self
169            .target()
170            .rotation_degrees()
171            .rem_euclid(360.)
172            .to_radians();
173
174        let (sin, cos) = angle.sin_cos();
175        let dx = f64::from(pivot.x) - f64::from(bounds.width) * 0.5;
176        let dy = f64::from(pivot.y) - f64::from(bounds.height) * 0.5;
177        let [tx, ty] = self.target().translation().map(f64::from);
178        let finite = |v: f64| px(v.clamp(f64::from(f32::MIN), f64::from(f32::MAX)) as f32);
179
180        self.transformation().with_translation(gpui::point(
181            finite(tx + dx - (cos * sx * dx - sin * sy * dy)),
182            finite(ty + dy - (sin * sx * dx + cos * sy * dy)),
183        ))
184    }
185
186    /// Applies GPU scale/rotation to one CE SVG drawing around its bounds center.
187    /// Native layout and hitbox stay unchanged, and clipping follows CE's existing
188    /// axis-aligned masks. This does not transform surrounding divs, text, or children.
189    /// Use a fixed, sufficiently padded parent as the interactive hit target.
190    ///
191    /// ```
192    /// use gpui::{svg, prelude::*, px};
193    /// use gpui_ce_motion::{SvgMotion, SvgMotionState, SvgTarget};
194    /// use std::time::Duration;
195    /// let mut state = SvgMotionState::default();
196    /// let frame = state.sample(SvgMotion::new(SvgTarget::default().scale(1.2).rotate_degrees(30.0)), Duration::ZERO, false);
197    /// let element = frame.apply(svg().path("icons/arrow.svg").size(px(24.0)));
198    /// ```
199    pub fn apply(self, element: gpui::Svg) -> gpui::Svg {
200        element.with_transformation(self.transformation())
201    }
202}
203
204impl<K: Eq + std::hash::Hash> Motions<K> {
205    /// Samples a batch during render using one executor timestamp and the native
206    /// reduced-motion preference. Requests one frame if any returned frame is active.
207    /// Omitted entries do not request frames. Notify the owner after input changes.
208    /// Do not mix this clock with explicit `sample` timestamps.
209    ///
210    /// # Panics
211    /// Panics on duplicate keys, as described by [`Self::sample`].
212    pub fn frame<const N: usize>(
213        &mut self,
214        declarations: [(K, Motion); N],
215        window: &mut Window,
216        cx: &App,
217    ) -> [Frame; N] {
218        let clock = cx.background_executor().now();
219        let epoch = *self.epoch.get_or_insert(clock);
220
221        let frames = self.sample(
222            declarations,
223            clock.saturating_duration_since(epoch),
224            cx.reduce_motion(),
225        );
226
227        if frames.iter().any(|frame| frame.is_animating()) {
228            window.request_animation_frame();
229        }
230
231        frames
232    }
233
234    /// Samples one native entry. Use [`Self::frame`] to synchronize several entries.
235    pub fn frame_one(&mut self, key: K, motion: Motion, window: &mut Window, cx: &App) -> Frame {
236        let [frame] = self.frame([(key, motion)], window, cx);
237
238        frame
239    }
240}