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}