Skip to main content

gpui_motion_kit/
vector.rs

1//! Native vector fill/stroke with reusable path morph ownership.
2use gpui_ce_motion::{
3    Color, MotionValue, PathCommand, PathError, PathMorphState, PathShape, StrokeReveal, Transition,
4};
5
6use gpui_kit::{
7    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
8    Path, PathBuilder, Pixels, Style, Window, point, px, size,
9};
10
11use std::{cell::RefCell, rc::Rc, time::Instant};
12
13struct Data {
14    morph: PathMorphState,
15    epoch: Option<Instant>,
16    reveal: [MotionValue; 3],
17    error: Option<PathError>,
18}
19
20/// Retain one vector state per drawing. Clones share morph/reveal storage.
21/// Path sampling allocates no memory; native tessellation/paint preparation does.
22#[derive(Clone)]
23pub struct VectorState(Rc<RefCell<Data>>);
24
25impl VectorState {
26    /// Creates prepared coordinate/reveal state.
27    ///
28    /// ```
29    /// use gpui_motion_kit::VectorState;
30    /// use gpui_ce_motion::{PathShape, PathCommand, Color};
31    /// let path = PathShape::new([PathCommand::Move([0.,0.]), PathCommand::Line([100.,0.])])?;
32    /// let state = VectorState::new(path.clone()); // persistent view field
33    /// let drawing = state.element("path", path, [120., 20.], Color::rgb(0xffffff))?.stroke(2.);
34    /// # Ok::<(), gpui_ce_motion::PathError>(())
35    /// ```
36    pub fn new(shape: PathShape) -> Self {
37        Self(Rc::new(RefCell::new(Data {
38            morph: PathMorphState::new(shape),
39            epoch: None,
40            reveal: [
41                MotionValue::new(1.),
42                MotionValue::new(0.),
43                MotionValue::new(1.),
44            ],
45            error: None,
46        })))
47    }
48
49    /// Most recent native tessellation error; invalid topology is returned by `element`.
50    pub fn error(&self) -> Option<PathError> {
51        self.0.borrow().error
52    }
53
54    /// Creates a native drawing, validating topology before any mutation.
55    /// Geometry uses logical coordinates relative to the fixed reservation.
56    pub fn element(
57        &self,
58        id: impl Into<ElementId>,
59        target: PathShape,
60        bounds: [f32; 2],
61        color: Color,
62    ) -> Result<VectorElement, PathError> {
63        self.0.borrow().morph.validate(&target)?;
64
65        if !bounds.iter().all(|v| v.is_finite() && *v >= 0.) {
66            return Err(PathError::Geometry);
67        }
68
69        Ok(VectorElement {
70            id: id.into(),
71            state: self.clone(),
72            target,
73            bounds,
74            color,
75            stroke: Some(2.),
76            transition: Transition::default(),
77            reveal: StrokeReveal::default(),
78        })
79    }
80}
81
82/// Fixed-size native vector drawing. This paints vector geometry, not an SVG
83/// sprite or a GPUI subtree; it does not install pointer semantics.
84pub struct VectorElement {
85    id: ElementId,
86    state: VectorState,
87    target: PathShape,
88    bounds: [f32; 2],
89    color: Color,
90    stroke: Option<f32>,
91    transition: Transition,
92    reveal: StrokeReveal,
93}
94
95impl VectorElement {
96    /// Selects coordinate and stroke reveal timing.
97    pub fn transition(mut self, transition: Transition) -> Self {
98        self.transition = transition;
99
100        self
101    }
102
103    /// Selects full-path fill. Reveal applies only to strokes.
104    pub fn fill(mut self) -> Self {
105        self.stroke = None;
106
107        self
108    }
109
110    /// Selects a finite positive stroke width.
111    pub fn stroke(mut self, width: f32) -> Self {
112        assert!(width.is_finite() && width > 0.);
113        self.stroke = Some(width);
114
115        self
116    }
117
118    /// Animates normalized path length/offset/spacing. Springs are clamped at the
119    /// drawing boundary; the coordinate/reveal momentum remains retained.
120    pub fn reveal(mut self, reveal: StrokeReveal) -> Self {
121        self.reveal = reveal;
122
123        self
124    }
125}
126
127impl IntoElement for VectorElement {
128    type Element = Self;
129
130    fn into_element(self) -> Self {
131        self
132    }
133}
134
135impl Element for VectorElement {
136    type RequestLayoutState = ();
137    type PrepaintState = Option<Path<Pixels>>;
138
139    fn id(&self) -> Option<ElementId> {
140        Some(self.id.clone())
141    }
142
143    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
144        None
145    }
146
147    fn request_layout(
148        &mut self,
149        _: Option<&GlobalElementId>,
150        _: Option<&InspectorElementId>,
151        w: &mut Window,
152        cx: &mut App,
153    ) -> (LayoutId, ()) {
154        let style = Style {
155            size: size(px(self.bounds[0]).into(), px(self.bounds[1]).into()),
156            flex_shrink: 0.,
157            ..Default::default()
158        };
159
160        (w.request_layout(style, [], cx), ())
161    }
162
163    fn prepaint(
164        &mut self,
165        _: Option<&GlobalElementId>,
166        _: Option<&InspectorElementId>,
167        bounds: Bounds<Pixels>,
168        _: &mut (),
169        w: &mut Window,
170        cx: &mut App,
171    ) -> Option<Path<Pixels>> {
172        let mut data = self.state.0.borrow_mut();
173
174        let first = data.epoch.is_none();
175
176        let epoch = *data
177            .epoch
178            .get_or_insert_with(|| cx.background_executor().now());
179
180        let now = cx
181            .background_executor()
182            .now()
183            .saturating_duration_since(epoch);
184
185        let mut reveal = [0.; 3];
186        let mut active = false;
187
188        for (i, value) in data.reveal.iter_mut().enumerate() {
189            if first {
190                value.jump(self.reveal.values()[i], now);
191            }
192
193            value.animate(self.reveal.values()[i], self.transition, now);
194            let frame = value.sample(now, cx.reduce_motion());
195
196            reveal[i] = frame.value();
197            active |= frame.is_animating();
198        }
199
200        // Clamping only affects drawing. Avoid tiny spring-overshoot dash periods.
201        let reveal = StrokeReveal::new(reveal[0].clamp(0., 1.), reveal[1], reveal[2].clamp(0., 1.))
202            .unwrap_or(self.reveal);
203
204        let frame = data
205            .morph
206            .sample(&self.target, self.transition, now, cx.reduce_motion())
207            .expect("validated topology");
208
209        active |= frame.is_animating();
210
211        if active {
212            w.request_animation_frame();
213        }
214
215        let point_at = |p: [f64; 2]| bounds.origin + point(px(p[0] as f32), px(p[1] as f32));
216
217        let mut builder = self
218            .stroke
219            .map_or_else(PathBuilder::fill, |width| PathBuilder::stroke(px(width)));
220
221        if self.stroke.is_some() {
222            let mut previous = None;
223
224            frame.stroke_segments(reveal, |a, b| {
225                if previous != Some(a) {
226                    builder.move_to(point_at(a));
227                }
228
229                builder.line_to(point_at(b));
230                previous = Some(b);
231            });
232        } else {
233            for &command in frame.commands() {
234                match command {
235                    PathCommand::Move(p) => builder.move_to(point_at(p)),
236                    PathCommand::Line(p) => builder.line_to(point_at(p)),
237                    PathCommand::Quadratic(control, to) => {
238                        builder.curve_to(point_at(to), point_at(control))
239                    }
240                    PathCommand::Cubic(a, b, to) => {
241                        builder.cubic_bezier_to(point_at(to), point_at(a), point_at(b))
242                    }
243                    PathCommand::Close => builder.close(),
244                }
245            }
246        }
247
248        match builder.build() {
249            Ok(path) => {
250                data.error = None;
251
252                Some(path)
253            }
254            Err(_) => {
255                data.error = Some(PathError::Tessellation);
256
257                None
258            }
259        }
260    }
261
262    fn paint(
263        &mut self,
264        _: Option<&GlobalElementId>,
265        _: Option<&InspectorElementId>,
266        _: Bounds<Pixels>,
267        _: &mut (),
268        path: &mut Option<Path<Pixels>>,
269        w: &mut Window,
270        _: &mut App,
271    ) {
272        if let Some(path) = path.take() {
273            let [r, g, b, a] = self.color.channels();
274
275            w.paint_path(path, gpui_kit::Rgba { r, g, b, a });
276        }
277    }
278}