Skip to main content

tui_lipan/animation/
exit_queue.rs

1//! State-owned storage for keyed entries that are leaving with an [`Animated`](crate::widgets::Animated)
2//! wrapper.
3
4use std::time::Duration;
5
6use web_time::Instant;
7
8struct Entry<K, T> {
9    key: K,
10    value: T,
11    visible: bool,
12    /// When this entry stops being retained even if [`ExitQueue::finish`] never arrives.
13    ///
14    /// Exit animations only complete while their host is actually being rendered. A collection
15    /// living in a hidden tab, an inactive workspace, or a collapsed panel never ticks, so an
16    /// entry waiting purely on completion would be retained forever. Set from
17    /// [`ExitQueue::with_exit_timeout`].
18    deadline: Option<Instant>,
19}
20
21/// An entry removed from one [`ExitQueue`] to be handed to another, preserving its exit progress.
22pub struct ExitTransfer<K, T> {
23    key: K,
24    value: T,
25    visible: bool,
26    deadline: Option<Instant>,
27}
28
29impl<K, T> ExitTransfer<K, T> {
30    /// The key this entry was stored under.
31    pub fn key(&self) -> &K {
32        &self.key
33    }
34
35    /// The transferred value.
36    pub fn value(&self) -> &T {
37        &self.value
38    }
39
40    /// Whether the entry was still live rather than exiting.
41    pub fn is_visible(&self) -> bool {
42        self.visible
43    }
44
45    /// Consume the transfer, yielding its key and value.
46    pub fn into_parts(self) -> (K, T) {
47        (self.key, self.value)
48    }
49}
50
51/// A keyed collection that keeps removed entries available for exit animations.
52///
53/// Call [`ExitQueue::sync`] with the current live `(key, value)` pairs after each state update.
54/// Entries absent from the live set remain in the collection with `visible == false` until
55/// [`ExitQueue::finish`] removes them. If a key is added again before its exit completes, the
56/// existing entry is updated and resurrected instead of being inserted a second time.
57#[derive(Default)]
58pub struct ExitQueue<K: Eq, T> {
59    entries: Vec<Entry<K, T>>,
60    exit_timeout: Option<Duration>,
61}
62
63impl<K: Eq, T> ExitQueue<K, T> {
64    /// Create an empty keyed exit queue with no timeout.
65    ///
66    /// Entries are then retained until [`ExitQueue::finish`] is called for them. Prefer
67    /// [`ExitQueue::with_exit_timeout`] unless the host is guaranteed to stay rendered for the
68    /// whole exit, since an animation that never runs never completes.
69    pub fn new() -> Self {
70        Self {
71            entries: Vec::new(),
72            exit_timeout: None,
73        }
74    }
75
76    /// Create a queue that stops retaining an entry `timeout` after it began exiting.
77    ///
78    /// The timeout is a backstop, not the animation duration: [`ExitQueue::finish`] still removes
79    /// entries as soon as their animation reports completion. Set it slightly longer than the exit
80    /// animation so a slow frame cannot cut one short. Without it, a collection that stops being
81    /// rendered mid-exit (a hidden tab, an inactive workspace) retains those entries indefinitely.
82    pub fn with_exit_timeout(timeout: Duration) -> Self {
83        Self {
84            entries: Vec::new(),
85            exit_timeout: Some(timeout),
86        }
87    }
88
89    /// Drop entries whose exit timeout has elapsed, returning how many were released.
90    ///
91    /// [`ExitQueue::sync`] calls this, so it is only needed when a queue can go many frames
92    /// without syncing and you still want its memory released.
93    pub fn expire(&mut self) -> usize {
94        if self.exit_timeout.is_none() {
95            return 0;
96        }
97        let now = Instant::now();
98        let before = self.entries.len();
99        self.entries
100            .retain(|entry| entry.visible || entry.deadline.is_none_or(|deadline| now < deadline));
101        before - self.entries.len()
102    }
103
104    /// Remove an entry so another queue can adopt it, preserving its exit progress.
105    ///
106    /// Use when an item migrates between collections while leaving, for example a row that moves
107    /// to another group as it is being deleted. Re-inserting it with [`ExitQueue::sync`] on the
108    /// target would restart its exit; [`ExitQueue::adopt`] preserves the original deadline.
109    pub fn transfer_out(&mut self, key: &K) -> Option<ExitTransfer<K, T>> {
110        let index = self.entries.iter().position(|entry| entry.key == *key)?;
111        let entry = self.entries.remove(index);
112        Some(ExitTransfer {
113            key: entry.key,
114            value: entry.value,
115            visible: entry.visible,
116            deadline: entry.deadline,
117        })
118    }
119
120    /// Adopt an entry handed over by [`ExitQueue::transfer_out`].
121    ///
122    /// An existing entry under the same key is replaced. The adopted deadline is kept, so an item
123    /// cannot extend its own retention by hopping between collections.
124    pub fn adopt(&mut self, transfer: ExitTransfer<K, T>) {
125        self.entries.retain(|entry| entry.key != transfer.key);
126        self.entries.push(Entry {
127            key: transfer.key,
128            value: transfer.value,
129            visible: transfer.visible,
130            deadline: transfer.deadline,
131        });
132    }
133
134    /// Synchronize the queue with the current live `(key, value)` pairs.
135    ///
136    /// Existing keys are updated in place. Keys not present in `live` become exiting, while new
137    /// keys are inserted as visible. Duplicate keys in one input are updated rather than doubled.
138    pub fn sync<I>(&mut self, live: I)
139    where
140        K: Clone,
141        I: IntoIterator<Item = (K, T)>,
142    {
143        let now = Instant::now();
144        let deadline = self.exit_timeout.map(|timeout| now + timeout);
145        for entry in &mut self.entries {
146            if entry.visible {
147                // Only entries that are newly leaving get a fresh deadline; one already exiting
148                // keeps its original so repeated syncs cannot extend its retention indefinitely.
149                entry.deadline = deadline;
150            }
151            entry.visible = false;
152        }
153
154        for (key, value) in live {
155            if let Some(entry) = self.entries.iter_mut().find(|entry| entry.key == key) {
156                entry.value = value;
157                entry.visible = true;
158                entry.deadline = None;
159            } else {
160                self.entries.push(Entry {
161                    key,
162                    value,
163                    visible: true,
164                    deadline: None,
165                });
166            }
167        }
168
169        self.expire();
170    }
171
172    /// Iterate over `(key, value, visible)` entries in insertion order.
173    ///
174    /// `visible` is `true` for current live entries and `false` for entries retained only for an
175    /// exit animation.
176    pub fn iter(&self) -> impl Iterator<Item = (&K, &T, bool)> + '_ {
177        self.entries
178            .iter()
179            .map(|entry| (&entry.key, &entry.value, entry.visible))
180    }
181
182    /// Finish an entry's exit animation and remove it from the queue.
183    ///
184    /// Returns `true` when an exiting entry was removed. Calling this for a live entry or an
185    /// unknown key is a no-op and returns `false`.
186    pub fn finish(&mut self, key: &K) -> bool {
187        let Some(index) = self
188            .entries
189            .iter()
190            .position(|entry| !entry.visible && entry.key == *key)
191        else {
192            return false;
193        };
194        self.entries.remove(index);
195        true
196    }
197
198    /// Return whether a key currently exists only for its exit animation.
199    pub fn is_exiting(&self, key: &K) -> bool {
200        self.entries
201            .iter()
202            .any(|entry| !entry.visible && entry.key == *key)
203    }
204
205    /// Return the number of live and exiting entries.
206    pub fn len(&self) -> usize {
207        self.entries.len()
208    }
209
210    /// Return whether the collection has no live or exiting entries.
211    pub fn is_empty(&self) -> bool {
212        self.entries.is_empty()
213    }
214}
215
216#[cfg(test)]
217mod tests {
218    use super::ExitQueue;
219
220    fn entries(queue: &ExitQueue<u32, &'static str>) -> Vec<(u32, &'static str, bool)> {
221        queue
222            .iter()
223            .map(|(key, value, visible)| (*key, *value, visible))
224            .collect()
225    }
226
227    #[test]
228    fn sync_marks_removed_entries_exiting_without_dropping_them() {
229        let mut queue = ExitQueue::new();
230        queue.sync([(1, "one"), (2, "two")]);
231        queue.sync([(2, "updated")]);
232
233        assert_eq!(
234            entries(&queue),
235            vec![(1, "one", false), (2, "updated", true)]
236        );
237        assert!(queue.is_exiting(&1));
238        assert!(!queue.is_exiting(&2));
239        assert_eq!(queue.len(), 2);
240    }
241
242    #[test]
243    fn readding_an_exiting_key_resurrects_the_existing_entry() {
244        let mut queue = ExitQueue::new();
245        queue.sync([(7, "old")]);
246        queue.sync([]);
247        queue.sync([(7, "new")]);
248
249        assert_eq!(entries(&queue), vec![(7, "new", true)]);
250        assert!(!queue.is_exiting(&7));
251        assert_eq!(queue.len(), 1);
252    }
253
254    #[test]
255    fn duplicate_live_keys_do_not_double_insert() {
256        let mut queue = ExitQueue::new();
257        queue.sync([(3, "first"), (3, "last")]);
258
259        assert_eq!(entries(&queue), vec![(3, "last", true)]);
260    }
261
262    #[test]
263    fn an_exit_timeout_releases_entries_whose_animation_never_completes() {
264        // A collection in a hidden tab never ticks, so `finish` never arrives.
265        let mut queue: ExitQueue<u32, &'static str> =
266            ExitQueue::with_exit_timeout(std::time::Duration::from_millis(10));
267        queue.sync([(1, "one"), (2, "two")]);
268        queue.sync([(2, "two")]);
269        assert!(queue.is_exiting(&1));
270
271        std::thread::sleep(std::time::Duration::from_millis(25));
272        queue.sync([(2, "two")]);
273
274        assert!(!queue.is_exiting(&1));
275        assert_eq!(entries(&queue), vec![(2, "two", true)]);
276    }
277
278    #[test]
279    fn repeated_syncs_do_not_extend_an_exiting_entrys_deadline() {
280        let mut queue: ExitQueue<u32, &'static str> =
281            ExitQueue::with_exit_timeout(std::time::Duration::from_millis(20));
282        queue.sync([(1, "one")]);
283        queue.sync([]);
284
285        for _ in 0..4 {
286            std::thread::sleep(std::time::Duration::from_millis(8));
287            queue.sync([]);
288        }
289
290        assert!(queue.is_empty(), "deadline must not be pushed forward");
291    }
292
293    #[test]
294    fn transfer_preserves_exit_progress_across_collections() {
295        let mut source: ExitQueue<u32, &'static str> =
296            ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
297        let mut target: ExitQueue<u32, &'static str> =
298            ExitQueue::with_exit_timeout(std::time::Duration::from_millis(500));
299        source.sync([(1, "one")]);
300        source.sync([]);
301        assert!(source.is_exiting(&1));
302
303        let moved = source.transfer_out(&1).expect("entry exists");
304        assert!(!moved.is_visible());
305        target.adopt(moved);
306
307        assert!(source.is_empty());
308        // Still exiting rather than restarted as a live entry.
309        assert!(target.is_exiting(&1));
310        assert_eq!(entries(&target), vec![(1, "one", false)]);
311    }
312
313    #[test]
314    fn finish_removes_only_the_exiting_entry() {
315        let mut queue = ExitQueue::new();
316        queue.sync([(1, "one"), (2, "two")]);
317        queue.sync([(2, "two")]);
318
319        assert!(!queue.finish(&2));
320        assert!(queue.finish(&1));
321        assert!(!queue.finish(&1));
322        assert_eq!(entries(&queue), vec![(2, "two", true)]);
323    }
324}