Skip to main content

gpui_ce_motion/
graph.rs

1//! Explicitly owned, batched motion-value dependencies and subscriptions.
2use crate::{MotionValue, Property, Target, Transition};
3
4use std::{
5    cell::Cell,
6    fmt,
7    rc::{Rc, Weak},
8    sync::atomic::{AtomicU64, Ordering},
9    time::Duration,
10};
11
12static NEXT_GRAPH: AtomicU64 = AtomicU64::new(1);
13
14const START: u8 = 1;
15const CANCEL: u8 = 2;
16const CHANGE: u8 = 4;
17const COMPLETE: u8 = 8;
18const RESET_VELOCITY: u8 = 16;
19
20/// Identity of a value inside one graph. Values from different graphs cannot mix.
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
22pub struct ValueId {
23    owner: u64,
24    index: usize,
25}
26
27/// A graph operation referred to another owner or attempted to edit a derived value.
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29pub enum GraphError {
30    /// Value belongs to another graph or does not exist.
31    Identity,
32    /// Derived values are read-only; edit their sources instead.
33    Derived,
34}
35
36impl fmt::Display for GraphError {
37    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
38        write!(f, "invalid motion graph operation: {self:?}")
39    }
40}
41
42impl std::error::Error for GraphError {}
43
44/// Coalesced lifecycle event delivered after a graph's entire dependency pass.
45#[derive(Clone, Copy, Debug, PartialEq, Eq)]
46pub enum MotionEventKind {
47    /// A new trajectory started.
48    Start,
49    /// A previous trajectory was interrupted or explicitly jumped.
50    Cancel,
51    /// The sampled presentation changed.
52    Change,
53    /// A running trajectory settled.
54    Complete,
55}
56
57/// One sampled graph event. Velocity uses value units per second.
58#[derive(Clone, Copy, Debug)]
59pub struct MotionEvent {
60    /// Changed source or derived value.
61    pub id: ValueId,
62    /// Lifecycle phase.
63    pub kind: MotionEventKind,
64    /// Current presentation.
65    pub value: f64,
66    /// Current velocity.
67    pub velocity: f64,
68}
69
70/// Retain this handle while listening. Dropping it disables the callback immediately.
71#[derive(Debug)]
72pub struct GraphSubscription(Rc<Cell<bool>>);
73
74impl Drop for GraphSubscription {
75    fn drop(&mut self) {
76        self.0.set(false);
77    }
78}
79
80impl GraphSubscription {
81    /// Cancels without borrowing the graph, including from inside a callback.
82    pub fn cancel(&self) {
83        self.0.set(false);
84    }
85}
86
87struct Listener {
88    id: ValueId,
89    alive: Weak<Cell<bool>>,
90    callback: Box<dyn FnMut(MotionEvent)>,
91}
92
93type Mapper = Box<dyn Fn(&[f64]) -> f64>;
94
95enum Node {
96    Source(MotionValue),
97    Derived {
98        inputs: Vec<ValueId>,
99        values: Vec<f64>,
100        map: Mapper,
101    },
102}
103
104struct Value {
105    node: Node,
106    value: f64,
107    velocity: f64,
108    active: bool,
109    events: u8,
110}
111
112/// Acyclic, insertion-ordered motion graph. Sources must precede their dependants.
113/// Setup allocates; sampling reuses nodes, scratch storage and listeners. Callbacks
114/// run after all derived values update. Defer edits to this graph until callbacks
115/// return; they may update other application state or cancel their subscriptions.
116///
117/// ```
118/// use gpui_ce_motion::{MotionGraph, Transition};
119/// use std::time::Duration;
120/// let mut graph = MotionGraph::default();
121/// let x = graph.source(0.0);
122/// let opacity = graph.derive(&[x], |v| (1.0 - v[0] / 200.0).clamp(0.0, 1.0)).unwrap();
123/// graph.animate(x, 100.0, Transition::Instant, Duration::ZERO).unwrap();
124/// graph.sample(Duration::ZERO, false);
125/// assert_eq!(graph.get(opacity).unwrap(), 0.5);
126/// ```
127pub struct MotionGraph {
128    owner: u64,
129    values: Vec<Value>,
130    listeners: Vec<Listener>,
131    last: Duration,
132    active: bool,
133}
134
135impl fmt::Debug for MotionGraph {
136    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
137        f.debug_struct("MotionGraph")
138            .field("values", &self.values.len())
139            .field("active", &self.active)
140            .finish()
141    }
142}
143
144impl Default for MotionGraph {
145    fn default() -> Self {
146        Self {
147            owner: NEXT_GRAPH.fetch_add(1, Ordering::Relaxed),
148            values: Vec::new(),
149            listeners: Vec::new(),
150            last: Duration::ZERO,
151            active: false,
152        }
153    }
154}
155
156impl MotionGraph {
157    /// Adds a finite scalar source. Panics for non-finite values.
158    pub fn source(&mut self, value: f64) -> ValueId {
159        let node = MotionValue::new(value);
160
161        let id = ValueId {
162            owner: self.owner,
163            index: self.values.len(),
164        };
165
166        self.values.push(Value {
167            node: Node::Source(node),
168            value,
169            velocity: 0.0,
170            active: false,
171            events: 0,
172        });
173
174        id
175    }
176
177    /// Adds an automatically updated, multi-input mapping. An empty input list is
178    /// a constant. The mapper must return finite values and must not edit this graph.
179    ///
180    /// # Errors
181    /// Returns `Identity` for inputs from another graph.
182    /// # Panics
183    /// Panics if the mapping returns a non-finite value, during setup or sampling.
184    pub fn derive(
185        &mut self,
186        inputs: &[ValueId],
187        map: impl Fn(&[f64]) -> f64 + 'static,
188    ) -> Result<ValueId, GraphError> {
189        let values = inputs
190            .iter()
191            .map(|&id| self.get(id))
192            .collect::<Result<Vec<_>, _>>()?;
193
194        let value = map(&values);
195
196        assert!(
197            value.is_finite(),
198            "graph mapping must return a finite value"
199        );
200        let id = ValueId {
201            owner: self.owner,
202            index: self.values.len(),
203        };
204
205        self.values.push(Value {
206            node: Node::Derived {
207                inputs: inputs.to_vec(),
208                values,
209                map: Box::new(map),
210            },
211            value,
212            velocity: 0.0,
213            active: false,
214            events: 0,
215        });
216
217        Ok(id)
218    }
219
220    fn index(&self, id: ValueId) -> Result<usize, GraphError> {
221        if id.owner != self.owner || id.index >= self.values.len() {
222            return Err(GraphError::Identity);
223        }
224
225        Ok(id.index)
226    }
227
228    /// Last sampled presentation. Does not advance the graph.
229    pub fn get(&self, id: ValueId) -> Result<f64, GraphError> {
230        Ok(self.values[self.index(id)?].value)
231    }
232
233    /// Last sampled velocity, including finite-difference derived velocities.
234    pub fn velocity(&self, id: ValueId) -> Result<f64, GraphError> {
235        Ok(self.values[self.index(id)?].velocity)
236    }
237
238    /// Retargets a source with the shared spring/tween solver. Identical declarations
239    /// do not produce new start/cancel events. Panics for non-finite destinations.
240    pub fn animate(
241        &mut self,
242        id: ValueId,
243        target: f64,
244        transition: Transition,
245        now: Duration,
246    ) -> Result<(), GraphError> {
247        let i = self.index(id)?;
248        let value = &mut self.values[i];
249
250        let Node::Source(source) = &mut value.node else {
251            return Err(GraphError::Derived);
252        };
253
254        let changed = !source.matches(target, transition);
255        let was_active = source.active();
256
257        source.animate(target, transition, now.max(self.last));
258
259        if changed && was_active {
260            value.events |= CANCEL;
261        }
262
263        if changed && source.active() {
264            value.events |= START;
265        }
266
267        Ok(())
268    }
269
270    /// Directly edits a source, with optional retained pointer velocity. This is
271    /// batched: dependants and listeners update on the next `sample`.
272    pub fn set(&mut self, id: ValueId, value: f64, now: Duration) -> Result<(), GraphError> {
273        self.edit(id, value, now, false)
274    }
275
276    /// Cancels a source trajectory and clears momentum on the next sample.
277    pub fn jump(&mut self, id: ValueId, value: f64, now: Duration) -> Result<(), GraphError> {
278        self.edit(id, value, now, true)
279    }
280
281    fn edit(
282        &mut self,
283        id: ValueId,
284        target: f64,
285        now: Duration,
286        jump: bool,
287    ) -> Result<(), GraphError> {
288        let i = self.index(id)?;
289        let value = &mut self.values[i];
290
291        let Node::Source(source) = &mut value.node else {
292            return Err(GraphError::Derived);
293        };
294
295        if source.active() {
296            value.events |= CANCEL;
297        }
298
299        if jump {
300            value.events = (value.events & !START) | RESET_VELOCITY;
301            source.jump(target, now.max(self.last));
302        } else {
303            source.set(target, now.max(self.last));
304        }
305
306        Ok(())
307    }
308
309    /// Listens to sampled changes and lifecycle events. Store the returned handle.
310    pub fn subscribe(
311        &mut self,
312        id: ValueId,
313        callback: impl FnMut(MotionEvent) + 'static,
314    ) -> Result<GraphSubscription, GraphError> {
315        self.index(id)?;
316        let alive = Rc::new(Cell::new(true));
317
318        self.listeners.push(Listener {
319            id,
320            alive: Rc::downgrade(&alive),
321            callback: Box::new(callback),
322        });
323
324        Ok(GraphSubscription(alive))
325    }
326
327    /// Advances sources, then derived dependencies, then dispatches listeners.
328    /// Multiple writes coalesce to one change event per value and sample. Backward
329    /// timestamps hold. Reduced motion settles sources using the shared policy.
330    pub fn sample(&mut self, now: Duration, reduced_motion: bool) {
331        let now = now.max(self.last);
332        let dt = now.saturating_sub(self.last).as_secs_f64();
333
334        self.active = false;
335
336        for index in 0..self.values.len() {
337            let (before, rest) = self.values.split_at_mut(index);
338            let value = &mut rest[0];
339
340            let (next, velocity, active, completed) = match &mut value.node {
341                Node::Source(source) => {
342                    let frame = source.sample(now, reduced_motion);
343
344                    (
345                        frame.value(),
346                        frame.velocity(),
347                        frame.is_animating(),
348                        frame.just_completed(),
349                    )
350                }
351                Node::Derived {
352                    inputs,
353                    values,
354                    map,
355                } => {
356                    let active = inputs.iter().any(|id| before[id.index].active);
357
358                    let propagated = inputs.iter().fold(0, |events, id| {
359                        events | (before[id.index].events & (START | CANCEL | RESET_VELOCITY))
360                    });
361
362                    value.events |= propagated;
363
364                    for (id, slot) in inputs.iter().zip(values.iter_mut()) {
365                        *slot = before[id.index].value;
366                    }
367
368                    let next = map(values);
369
370                    assert!(next.is_finite(), "graph mapping must return a finite value");
371                    let velocity = if propagated & RESET_VELOCITY != 0 {
372                        0.0
373                    } else if dt > 0.0 {
374                        crate::value::finite((next - value.value) / dt)
375                    } else {
376                        value.velocity
377                    };
378
379                    (
380                        next,
381                        velocity,
382                        active,
383                        !active
384                            && inputs
385                                .iter()
386                                .any(|id| before[id.index].events & COMPLETE != 0),
387                    )
388                }
389            };
390
391            if !value.active && active {
392                value.events |= START;
393            }
394
395            if value.value != next {
396                value.events |= CHANGE;
397            }
398
399            if completed {
400                value.events |= COMPLETE;
401            }
402
403            value.value = next;
404            value.velocity = velocity;
405            value.active = active;
406            self.active |= active;
407        }
408
409        self.last = now;
410        self.listeners.retain_mut(|listener| {
411            let Some(alive) = listener.alive.upgrade().filter(|alive| alive.get()) else {
412                return false;
413            };
414
415            let value = &self.values[listener.id.index];
416
417            for (flag, kind) in [
418                (CANCEL, MotionEventKind::Cancel),
419                (START, MotionEventKind::Start),
420                (CHANGE, MotionEventKind::Change),
421                (COMPLETE, MotionEventKind::Complete),
422            ] {
423                if value.events & flag != 0 && alive.get() {
424                    (listener.callback)(MotionEvent {
425                        id: listener.id,
426                        kind,
427                        value: value.value,
428                        velocity: value.velocity,
429                    });
430                }
431            }
432
433            alive.get()
434        });
435
436        for value in &mut self.values {
437            value.events = 0;
438        }
439    }
440
441    /// Whether any source or dependant requires another frame.
442    pub fn is_animating(&self) -> bool {
443        self.active
444    }
445
446    /// Binds sampled values to declared target properties. Values are saturated
447    /// to drawable bounds; opacity is clamped and dimensions are non-negative.
448    pub fn target(
449        &self,
450        base: Target,
451        bindings: &[(Property, ValueId)],
452    ) -> Result<Target, GraphError> {
453        let mut values = base.values();
454        let mut mask = base.mask();
455
456        for &(property, id) in bindings {
457            let i = property as usize;
458            let value = self.get(id)?;
459
460            values[i] = if i == 2 {
461                value.clamp(0.0, 1.0)
462            } else if i >= 3 {
463                value.clamp(0.0, f32::MAX.into())
464            } else {
465                value.clamp(f32::MIN.into(), f32::MAX.into())
466            };
467            mask |= 1 << i;
468        }
469
470        Ok(Target::from_values(values, mask))
471    }
472}
473
474impl MotionGraph {
475    /// Adds the numeric channels of a typed value, allocating only during setup.
476    /// Panics for invalid channels as `source` does.
477    pub fn typed_source<T: crate::MotionData<N>, const N: usize>(
478        &mut self,
479        value: T,
480    ) -> [ValueId; N] {
481        let values = value.channels();
482
483        assert!(
484            N > 0 && values.iter().all(|v| v.is_finite()),
485            "invalid typed source"
486        );
487
488        std::array::from_fn(|i| self.source(values[i]))
489    }
490
491    /// Reconstructs a typed value from automatically sampled/derived channels.
492    pub fn typed_value<T: crate::MotionData<N>, const N: usize>(
493        &self,
494        template: T,
495        ids: [ValueId; N],
496    ) -> Result<T, GraphError> {
497        for id in ids {
498            self.index(id)?;
499        }
500
501        Ok(template.with_channels(std::array::from_fn(|i| self.values[ids[i].index].value)))
502    }
503
504    /// Retargets typed source channels transactionally. Invalid or derived IDs
505    /// leave all sources unchanged; values/velocities use the shared solver.
506    pub fn animate_typed<T: crate::MotionData<N>, const N: usize>(
507        &mut self,
508        ids: [ValueId; N],
509        target: T,
510        transition: Transition,
511        now: Duration,
512    ) -> Result<(), GraphError> {
513        let values = target.channels();
514
515        assert!(
516            values.iter().all(|v| v.is_finite()),
517            "invalid typed destination"
518        );
519
520        for id in ids {
521            if !matches!(self.values[self.index(id)?].node, Node::Source(_)) {
522                return Err(GraphError::Derived);
523            }
524        }
525
526        for i in 0..N {
527            self.animate(ids[i], values[i], transition, now)?;
528        }
529
530        Ok(())
531    }
532}