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