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 layout;
31mod vector;
32
33pub use vector::{VectorElement, VectorState};
34
35pub use layout::{LayoutGroup, LayoutPositionElement};
36
37/// View-owned motion state using GPUI Kit's monotonic executor clock.
38#[derive(Debug, Default)]
39pub struct MotionState {
40    inner: gpui_ce_motion::MotionState,
41    epoch: Option<Instant>,
42}
43
44impl MotionState {
45    /// Samples during render and requests another frame only while moving.
46    /// The Kit application supplies the operating-system reduced-motion policy.
47    pub fn frame(&mut self, motion: Motion, window: &mut Window, cx: &App) -> Frame {
48        let now = cx.background_executor().now();
49        let epoch = *self.epoch.get_or_insert(now);
50
51        let frame = self.inner.sample(
52            motion,
53            now.saturating_duration_since(epoch),
54            cx.reduce_motion(),
55        );
56
57        if frame.is_animating() {
58            window.request_animation_frame();
59        }
60
61        Frame(frame)
62    }
63
64    /// Samples an explicit monotonic epoch for simulations and deterministic tests.
65    /// Do not mix explicit sampling and `frame` clocks on the same state.
66    pub fn sample(&mut self, motion: Motion, now: Duration, reduced_motion: bool) -> Frame {
67        Frame(self.inner.sample(motion, now, reduced_motion))
68    }
69}
70
71/// A sampled frame with Kit-native element builders and portable status getters.
72#[derive(Clone, Copy, Debug)]
73pub struct Frame(gpui_ce_motion::Frame);
74
75impl Deref for Frame {
76    type Target = gpui_ce_motion::Frame;
77
78    fn deref(&self) -> &Self::Target {
79        &self.0
80    }
81}
82
83impl Frame {
84    /// Applies opacity, relative position, and declared dimensions/radius.
85    /// Dimensions perform real layout. This sets relative position, left, and top;
86    /// use an outer reservation when absolute placement is required.
87    pub fn apply<E: Styled>(self, mut element: E) -> E {
88        let target = self.target();
89
90        if let Some(width) = target.width_px() {
91            element = element.w(px(width));
92        }
93
94        if let Some(height) = target.height_px() {
95            element = element.h(px(height));
96        }
97
98        if let Some(radius) = target.border_radius_px() {
99            element = element.rounded(px(radius));
100        }
101
102        element
103            .relative()
104            .left(px(target.x_px()))
105            .top(px(target.y_px()))
106            .opacity(target.alpha())
107    }
108
109    /// Keeps exiting elements rendered until absent. The owner must guard input
110    /// and restore focus before removing a focused outgoing element.
111    pub fn render<E: IntoElement>(self, build: impl FnOnce(Self) -> E) -> AnyElement {
112        if self.is_mounted() {
113            build(self).into_any_element()
114        } else {
115            Empty.into_any_element()
116        }
117    }
118}
119
120/// A retained scalar presentation in caller-chosen units.
121#[derive(Debug)]
122pub struct ValueState {
123    inner: MotionValue,
124    epoch: Option<Instant>,
125}
126
127impl ValueState {
128    /// Creates a settled scalar. Panics for non-finite input.
129    pub fn new(value: f64) -> Self {
130        Self {
131            inner: MotionValue::new(value),
132            epoch: None,
133        }
134    }
135
136    /// Retargets and samples during render, preserving momentum on interruption.
137    pub fn frame(
138        &mut self,
139        target: f64,
140        transition: Transition,
141        window: &mut Window,
142        cx: &App,
143    ) -> ValueFrame {
144        let clock = cx.background_executor().now();
145        let epoch = *self.epoch.get_or_insert(clock);
146        let now = clock.saturating_duration_since(epoch);
147
148        self.inner.animate(target, transition, now);
149        let frame = self.inner.sample(now, cx.reduce_motion());
150
151        if frame.is_animating() {
152            window.request_animation_frame();
153        }
154
155        frame
156    }
157}
158
159mod text {
160    gpui_ce_motion::__motion_text_adapter!(gpui_kit);
161}
162
163pub use gpui_ce_motion::{
164    NumberEffect, Scramble, TextClip, TextError, TextMatch, TextPreset, TextTarget, TextUnit,
165    TextUnitInfo,
166};
167
168pub use text::{NumberElement, NumberMotionState, TextElement, TextMotionState};