Skip to main content

gpui_motion_kit/
lib.rs

1//! GPUI Kit bindings for the portable gpui-ce-motion animation engine.
2//!
3//! This adapter uses Kit's GPUI types. It does not enable the core crate's CE
4//! feature, and it does not change either renderer. Retain one state per logical
5//! object, or use [`Motions`] to collect ordinary transitions by key. Declare
6//! destinations during render and notify after input changes.
7//!
8//! ```
9//! use gpui_ce_motion::{Motion, Target};
10//! use gpui_motion_kit::MotionState;
11//! use gpui_kit::{AnyElement, App, Window, div, prelude::*};
12//!
13//! fn panel(state: &mut MotionState, open: bool, window: &mut Window, cx: &App) -> AnyElement {
14//!     let frame = state.frame(Motion::new(Target::default()).present(open), window, cx);
15//!
16//!     frame.render(|frame| frame.apply(div().child("Details")))
17//! }
18//! ```
19
20use gpui_ce_motion::{Motion, MotionValue, Transition, ValueFrame};
21use gpui_kit::{AnyElement, App, Empty, IntoElement, Styled, Window, px};
22
23use std::{
24    ops::Deref,
25    time::{Duration, Instant},
26};
27
28/// The portable motion engine, available without enabling GPUI CE.
29pub use gpui_ce_motion as core;
30
31mod gesture_native;
32mod layout;
33mod measured_size;
34mod motions;
35mod playback;
36mod pop_layout;
37mod reorder_native;
38mod scroll_native;
39mod shared_layout;
40mod style;
41mod svg;
42mod vector;
43
44pub use gesture_native::{GestureElement, GestureState};
45pub use measured_size::{AutoSize, SizeAxis};
46pub use motions::Motions;
47pub use playback::{AnimationState, Timeline};
48pub use pop_layout::{PopElement, PopLayout, PopRequest};
49pub use reorder_native::{ReorderElement, ReorderGroup};
50pub use scroll_native::{Axis, ScrollMotion, native_scroll_progress};
51pub use shared_layout::{SharedElement, SharedLayout};
52pub use style::{StyleFrame, StyleMotionState, UnsupportedStyle, color_to_rgba};
53pub use svg::{SvgFrame, SvgMotionState};
54
55#[derive(Debug, Default)]
56struct Clock(Option<Instant>);
57
58impl Clock {
59    fn tick(&mut self, cx: &App) -> Duration {
60        let now = cx.background_executor().now();
61        let epoch = *self.0.get_or_insert(now);
62
63        now.saturating_duration_since(epoch)
64    }
65
66    fn now(&self, cx: &App) -> Duration {
67        self.0.map_or(Duration::ZERO, |epoch| {
68            cx.background_executor()
69                .now()
70                .saturating_duration_since(epoch)
71        })
72    }
73}
74
75pub use vector::{VectorElement, VectorState};
76
77pub use layout::{LayoutGroup, LayoutPositionElement};
78
79/// View-owned motion state using GPUI Kit's monotonic executor clock.
80#[derive(Debug, Default)]
81pub struct MotionState {
82    inner: gpui_ce_motion::MotionState,
83    epoch: Option<Instant>,
84}
85
86impl MotionState {
87    /// Samples during render and requests another frame only while moving.
88    /// The Kit application supplies the operating-system reduced-motion policy.
89    pub fn frame(&mut self, motion: Motion, window: &mut Window, cx: &App) -> Frame {
90        let now = cx.background_executor().now();
91        let epoch = *self.epoch.get_or_insert(now);
92
93        let frame = self.inner.sample(
94            motion,
95            now.saturating_duration_since(epoch),
96            cx.reduce_motion(),
97        );
98
99        if frame.is_animating() {
100            window.request_animation_frame();
101        }
102
103        Frame(frame)
104    }
105
106    /// Samples an explicit monotonic epoch for simulations and deterministic tests.
107    /// Do not mix explicit sampling and `frame` clocks on the same state.
108    pub fn sample(&mut self, motion: Motion, now: Duration, reduced_motion: bool) -> Frame {
109        Frame(self.inner.sample(motion, now, reduced_motion))
110    }
111}
112
113/// A sampled frame with Kit-native element builders and portable status getters.
114#[derive(Clone, Copy, Debug)]
115pub struct Frame(gpui_ce_motion::Frame);
116
117impl From<gpui_ce_motion::Frame> for Frame {
118    fn from(frame: gpui_ce_motion::Frame) -> Self {
119        Self(frame)
120    }
121}
122
123impl Deref for Frame {
124    type Target = gpui_ce_motion::Frame;
125
126    fn deref(&self) -> &Self::Target {
127        &self.0
128    }
129}
130
131impl Frame {
132    /// Creates a static Kit-ready frame, for custom mappings and portable sampling.
133    pub fn at(target: gpui_ce_motion::Target) -> Self {
134        gpui_ce_motion::Frame::at(target).into()
135    }
136
137    /// Applies opacity, relative position, and declared dimensions/radius.
138    /// Dimensions perform real layout. This sets relative position, left, and top;
139    /// use an outer reservation when absolute placement is required.
140    pub fn apply<E: Styled>(self, mut element: E) -> E {
141        let target = self.target();
142
143        if let Some(width) = target.width_px() {
144            element = element.w(px(width));
145        }
146
147        if let Some(height) = target.height_px() {
148            element = element.h(px(height));
149        }
150
151        if let Some(radius) = target.border_radius_px() {
152            element = element.rounded(px(radius));
153        }
154
155        element
156            .relative()
157            .left(px(target.x_px()))
158            .top(px(target.y_px()))
159            .opacity(target.alpha())
160    }
161
162    /// Keeps exiting elements rendered until absent. The owner must guard input
163    /// and restore focus before removing a focused outgoing element.
164    pub fn render<E: IntoElement>(self, build: impl FnOnce(Self) -> E) -> AnyElement {
165        if self.is_mounted() {
166            build(self).into_any_element()
167        } else {
168            Empty.into_any_element()
169        }
170    }
171}
172
173/// A retained scalar presentation in caller-chosen units.
174#[derive(Debug)]
175pub struct ValueState {
176    inner: MotionValue,
177    epoch: Option<Instant>,
178}
179
180impl ValueState {
181    /// Creates a settled scalar. Panics for non-finite input.
182    pub fn new(value: f64) -> Self {
183        Self {
184            inner: MotionValue::new(value),
185            epoch: None,
186        }
187    }
188
189    /// Retargets and samples during render, preserving momentum on interruption.
190    pub fn frame(
191        &mut self,
192        target: f64,
193        transition: Transition,
194        window: &mut Window,
195        cx: &App,
196    ) -> ValueFrame {
197        let clock = cx.background_executor().now();
198        let epoch = *self.epoch.get_or_insert(clock);
199        let now = clock.saturating_duration_since(epoch);
200
201        self.inner.animate(target, transition, now);
202        let frame = self.inner.sample(now, cx.reduce_motion());
203
204        if frame.is_animating() {
205            window.request_animation_frame();
206        }
207
208        frame
209    }
210}
211
212mod text {
213    fn text_color_to_native(color: gpui_ce_motion::Color) -> gpui_kit::Hsla {
214        crate::color_to_rgba(color).into()
215    }
216
217    fn text_color_from_native(color: gpui_kit::Hsla) -> gpui_ce_motion::Color {
218        let color: gpui_kit::Rgba = color.into();
219
220        gpui_ce_motion::Color::rgba(
221            color.r.into(),
222            color.g.into(),
223            color.b.into(),
224            color.a.into(),
225        )
226    }
227
228    gpui_ce_motion::__motion_text_adapter!(gpui_kit, text_color_to_native, text_color_from_native);
229}
230
231pub use gpui_ce_motion::{
232    NumberEffect, Scramble, TextClip, TextError, TextMatch, TextPreset, TextTarget, TextUnit,
233    TextUnitInfo,
234};
235
236pub use text::{NumberElement, NumberMotionState, TextElement, TextMotionState};
237
238mod typed;
239
240pub use typed::{TypedState, TypedTimeline};
241
242mod spatial {
243    gpui_ce_motion::__motion_spatial_adapter!(gpui_kit, crate);
244}
245
246pub use core::ScrollFrame;
247
248pub use spatial::{
249    AnchorElement, GraphState, MotionAnchor, MotionTreeState, ScrollTracker, SharedOverlay,
250    SharedOverlayElement,
251};