gpui_ce_motion/native.rs
1use gpui::{AnyElement, App, Empty, IntoElement, Styled, Window, px};
2
3use crate::{Frame, Motion, MotionState};
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}