Skip to main content

gpui_ce_motion/
vector_native.rs

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