Skip to main content

gpui_ce_motion/
motions.rs

1//! Keyed ownership of independent transitions, using the ordinary motion sampler.
2use crate::{Frame, Motion, MotionState};
3use std::{collections::HashMap, hash::Hash, time::Duration};
4
5#[derive(Debug, Default)]
6struct Entry {
7    state: MotionState,
8    batch: u64,
9}
10
11/// Component-owned transitions keyed by stable part or item IDs.
12///
13/// Each key retains an independent [`MotionState`]. Batch results follow input
14/// order; reordering declarations does not change identity. Existing entries
15/// sample without allocating. Each new key allocates its state and may grow the
16/// hash table. Key construction/destruction may also allocate depending on `K`.
17///
18/// Omitted keys are neither sampled nor removed. Their elapsed time still passes:
19/// submitting them again samples their old trajectory at the new time. Continue
20/// submitting `present(false)` through an exit, then call [`Self::remove`] after
21/// its frame is no longer mounted. Dropping the collection cancels all its motion.
22///
23/// ```
24/// use gpui_ce_motion::{Motion, Motions, Target};
25/// use std::time::Duration;
26///
27/// let mut motions = Motions::with_capacity(2);
28/// let panel = Motion::new(Target::default().height(240.0))
29///     .initial(Target::default().height(80.0));
30/// let title = Motion::new(Target::default()).initial(Target::default().opacity(0.0));
31///
32/// let [panel, title] = motions.sample(
33///     [("panel", panel), ("title", title)],
34///     Duration::ZERO,
35///     false,
36/// );
37///
38/// assert_eq!(panel.target().height_px(), Some(80.0));
39/// assert_eq!(title.target().alpha(), 0.0);
40/// assert_eq!(motions.len(), 2);
41/// ```
42#[derive(Debug)]
43pub struct Motions<K> {
44    // Large trajectories do not occupy unused hash buckets.
45    entries: HashMap<K, Box<Entry>>,
46    batch: u64,
47    last: Duration,
48    #[cfg(feature = "gpui")]
49    pub(crate) epoch: Option<std::time::Instant>,
50}
51
52impl<K> Default for Motions<K> {
53    fn default() -> Self {
54        Self::new()
55    }
56}
57
58impl<K> Motions<K> {
59    /// Creates an empty collection without allocating.
60    pub fn new() -> Self {
61        Self::with_capacity(0)
62    }
63
64    /// Reserves space for at least `capacity` keys, without creating trajectories.
65    pub fn with_capacity(capacity: usize) -> Self {
66        Self {
67            entries: HashMap::with_capacity(capacity),
68            batch: 0,
69            last: Duration::ZERO,
70            #[cfg(feature = "gpui")]
71            epoch: None,
72        }
73    }
74
75    /// Number of retained keys, including settled or absent objects.
76    pub fn len(&self) -> usize {
77        self.entries.len()
78    }
79
80    /// Whether no keys are retained.
81    pub fn is_empty(&self) -> bool {
82        self.entries.is_empty()
83    }
84
85    /// Number of keys that fit before the hash table must grow.
86    pub fn capacity(&self) -> usize {
87        self.entries.capacity()
88    }
89
90    /// Cancels all trajectories and drops their keys, keeping table capacity.
91    /// The collection's clock continues. Call [`Self::shrink_to_fit`] to release
92    /// unused table storage, or drop the collection to release everything.
93    pub fn clear(&mut self) {
94        self.entries.clear();
95    }
96}
97
98impl<K: Eq + Hash> Motions<K> {
99    /// Samples a batch at one absolute monotonic timestamp, returning owned frames.
100    ///
101    /// Springs preserve velocity when retargeted. Reduced motion snaps each
102    /// submitted entry; backward timestamps clamp to the collection's last sample.
103    /// An empty batch is valid and leaves entries retained. Expected cost is O(N)
104    /// hash lookups plus sampling, independent of the number of omitted entries.
105    /// Do not mix explicit timestamps with native `frame` calls on this collection.
106    ///
107    /// # Panics
108    /// Panics if a key occurs twice in this batch. This is a declaration error,
109    /// not a transaction: entries preceding the duplicate may already be sampled.
110    pub fn sample<const N: usize>(
111        &mut self,
112        declarations: [(K, Motion); N],
113        now: Duration,
114        reduced_motion: bool,
115    ) -> [Frame; N] {
116        self.last = self.last.max(now);
117        self.batch = self.batch.wrapping_add(1);
118
119        if self.batch == 0 {
120            for entry in self.entries.values_mut() {
121                entry.batch = 0;
122            }
123
124            self.batch = 1;
125        }
126
127        declarations.map(|(key, motion)| {
128            let entry = self.entries.entry(key).or_default();
129
130            assert_ne!(entry.batch, self.batch, "duplicate key in Motions batch");
131            entry.batch = self.batch;
132
133            entry.state.sample(motion, self.last, reduced_motion)
134        })
135    }
136
137    /// Samples one key, creating its state on first use. Repeated calls for the
138    /// same key are allowed; each declaration retargets the same trajectory.
139    /// Uses the same clock and interruption contract as [`Self::sample`].
140    pub fn sample_one(
141        &mut self,
142        key: K,
143        motion: Motion,
144        now: Duration,
145        reduced_motion: bool,
146    ) -> Frame {
147        self.last = self.last.max(now);
148
149        self.entries
150            .entry(key)
151            .or_default()
152            .state
153            .sample(motion, self.last, reduced_motion)
154    }
155
156    /// Whether this key has retained state, including a completed exit.
157    pub fn contains_key(&self, key: &K) -> bool {
158        self.entries.contains_key(key)
159    }
160
161    /// Immediately cancels and removes a key; returns whether it existed.
162    /// Reusing a removed key starts fresh, including its `initial` declaration.
163    /// For animated removal, wait until its sampled frame is no longer mounted.
164    /// Hash table capacity is preserved.
165    pub fn remove(&mut self, key: &K) -> bool {
166        self.entries.remove(key).is_some()
167    }
168
169    /// Immediately removes keys for which `keep` returns false, preserving capacity.
170    /// This does not wait for exits; keep exiting keys until their final sample.
171    /// Iteration order is unspecified; visits the table in O(capacity) time.
172    pub fn retain(&mut self, mut keep: impl FnMut(&K) -> bool) {
173        self.entries.retain(|key, _| keep(key));
174    }
175
176    /// Reserves space for at least `additional` new keys beyond the current length.
177    pub fn reserve(&mut self, additional: usize) {
178        self.entries.reserve(additional);
179    }
180
181    /// Releases unused table capacity as far as the standard hash table allows.
182    /// Retained trajectories are unchanged. Do this after cleanup, outside a hot
183    /// sampling loop; it can allocate and move entries.
184    pub fn shrink_to_fit(&mut self) {
185        self.entries.shrink_to_fit();
186    }
187}