Skip to main content

repame_ui/
queue.rs

1//! One ordered action queue: the view pushes, the sim drains.
2//!
3//! All thirteen hand-rolled copies in the games are the same type — a
4//! `Sink = Arc<Mutex<Vec<UiAction>>>` plus a `drain_actions(&mut World)` free
5//! function plus a boolean `Intents` resource that the drained actions are
6//! folded into. This module is that whole pattern once, split into two pieces
7//! with fixed semantics:
8//!
9//! * [`ActionQueue<A>`] and [`Sink<A>`] are two handles on one buffer. The sink
10//!   goes into click closures; the queue stays where the frame can drain it.
11//!   Order is preserved across any interleaving of pushes and drains, and a
12//!   drained batch never contains an action that was pushed after the drain
13//!   started.
14//! * [`Intents<A>`] is the ordered edge view of the drained batch: ask
15//!   [`Intents::take`] for one occurrence of an action and it is consumed, so a
16//!   boolean flag per action is no longer needed. Payload-carrying actions go
17//!   through [`Intents::take_matching`].
18//!
19//! Neither type has to be an ECS resource, so this crate carries no `bevy_ecs`
20//! dependency: a game puts the handles inside the resource wrapper it already
21//! declares (all thirteen copies already have one). A sim system reads them
22//! through that resource exactly as it reads any other:
23//!
24//! ```ignore
25//! fn absorb_ui(ui: Res<UiActions>, mut intents: ResMut<UiIntents>) {
26//!     repame_ui::absorb(&ui.0, &mut intents.0);
27//! }
28//! ```
29//!
30//! or, without a resource at all, a closure system holding the queue:
31//!
32//! ```ignore
33//! let queue = ActionQueue::<UiAction>::new();
34//! let sink = queue.sink();
35//! sim.add_system(move || {
36//!     for action in queue.drain() {
37//!         // answer it
38//!     }
39//! });
40//! ```
41
42use std::fmt;
43use std::sync::{Arc, Mutex, MutexGuard};
44
45/// An action a UI control can raise.
46///
47/// Any `Clone + PartialEq + Debug + Send + Sync + 'static` value qualifies, so
48/// a game's fieldless `UiAction` enum is one without ceremony. `PartialEq` is
49/// what makes [`Intents`] consumption work; `Send`/`Sync` is what lets the sink
50/// live in a click closure while the queue is drained from the sim.
51pub trait UiAction: Clone + PartialEq + fmt::Debug + Send + Sync + 'static {}
52
53impl<T> UiAction for T where T: Clone + PartialEq + fmt::Debug + Send + Sync + 'static {}
54
55type Shared<A> = Arc<Mutex<Vec<A>>>;
56
57/// Lock the buffer, keeping the queued actions if a pusher panicked while
58/// holding the lock.
59///
60/// Recovering the guard is deliberate: the queued actions are plain data and a
61/// lost queue would strand the player in whatever screen pushed last. A
62/// poisoned mutex is reported by the panic itself, not by this function.
63fn lock<A>(shared: &Shared<A>) -> MutexGuard<'_, Vec<A>> {
64    shared.lock().unwrap_or_else(|poisoned| poisoned.into_inner())
65}
66
67/// The push side of an [`ActionQueue`]. Hand it to click closures.
68#[derive(Clone)]
69pub struct Sink<A: UiAction> {
70    shared: Shared<A>,
71}
72
73impl<A: UiAction> Sink<A> {
74    /// Queue one action. Pushing from the sim side is allowed; order is still
75    /// the buffer's.
76    pub fn push(&self, action: A) {
77        lock(&self.shared).push(action);
78    }
79
80    /// Queue several actions in argument order.
81    pub fn extend(&self, actions: impl IntoIterator<Item = A>) {
82        lock(&self.shared).extend(actions);
83    }
84
85    /// Actions waiting to be drained.
86    pub fn pending(&self) -> usize {
87        lock(&self.shared).len()
88    }
89
90    /// Take everything that was queued before this call.
91    pub fn drain(&self) -> Vec<A> {
92        drain_buffer(&self.shared)
93    }
94}
95
96impl<A: UiAction> fmt::Debug for Sink<A> {
97    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
98        f.debug_struct("Sink").field("pending", &self.pending()).finish()
99    }
100}
101
102/// The drain side of an action queue: the same buffer as its [`Sink`].
103#[derive(Clone)]
104pub struct ActionQueue<A: UiAction> {
105    shared: Shared<A>,
106}
107
108impl<A: UiAction> ActionQueue<A> {
109    pub fn new() -> Self {
110        Self {
111            shared: Arc::new(Mutex::new(Vec::new())),
112        }
113    }
114
115    /// A fresh queue and the sink for the view that pushes into it.
116    pub fn pair() -> (Self, Sink<A>) {
117        let queue = Self::new();
118        let sink = queue.sink();
119        (queue, sink)
120    }
121
122    /// The push handle. Cloning it into every control is the intended use.
123    pub fn sink(&self) -> Sink<A> {
124        Sink {
125            shared: Arc::clone(&self.shared),
126        }
127    }
128
129    /// Queue one action from the drain side.
130    pub fn push(&self, action: A) {
131        lock(&self.shared).push(action);
132    }
133
134    /// Actions waiting to be drained.
135    pub fn pending(&self) -> usize {
136        lock(&self.shared).len()
137    }
138
139    pub fn is_empty(&self) -> bool {
140        self.pending() == 0
141    }
142
143    /// Take everything queued before this call, in push order.
144    pub fn drain(&self) -> Vec<A> {
145        drain_buffer(&self.shared)
146    }
147
148    /// Take everything queued before this call, appending to `out`.
149    pub fn drain_into(&self, out: &mut Vec<A>) {
150        let mut buffer = lock(&self.shared);
151        out.append(&mut buffer);
152    }
153
154    /// Drain into an intent buffer. Returns how many actions were absorbed.
155    pub fn absorb(&self, intents: &mut Intents<A>) -> usize {
156        intents.extend(self.drain())
157    }
158}
159
160impl<A: UiAction> Default for ActionQueue<A> {
161    fn default() -> Self {
162        Self::new()
163    }
164}
165
166impl<A: UiAction> fmt::Debug for ActionQueue<A> {
167    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
168        f.debug_struct("ActionQueue")
169            .field("pending", &self.pending())
170            .finish()
171    }
172}
173
174fn drain_buffer<A>(shared: &Shared<A>) -> Vec<A> {
175    std::mem::take(&mut *lock(shared))
176}
177
178/// The actions drained for the current tick, consumed by the systems that
179/// answer them.
180///
181/// Order is the order the controls were clicked. [`take`](Self::take) consumes
182/// one occurrence and reports whether it was there, which is what replaces a
183/// per-action boolean: `if intents.retry` becomes `if intents.take(&UiAction::Retry)`.
184#[derive(Clone, Debug, Default, PartialEq)]
185pub struct Intents<A: UiAction> {
186    pending: Vec<A>,
187}
188
189impl<A: UiAction> Intents<A> {
190    pub fn new() -> Self {
191        Self { pending: Vec::new() }
192    }
193
194    /// Every pending action in order.
195    pub fn requests(&self) -> &[A] {
196        &self.pending
197    }
198
199    pub fn is_empty(&self) -> bool {
200        self.pending.is_empty()
201    }
202
203    pub fn len(&self) -> usize {
204        self.pending.len()
205    }
206
207    /// Forget every pending action without answering any of them.
208    pub fn clear(&mut self) {
209        self.pending.clear();
210    }
211
212    /// Add one request.
213    pub fn request(&mut self, action: A) {
214        self.pending.push(action);
215    }
216
217    /// Add `actions` in argument order. Returns how many were added.
218    pub fn extend(&mut self, actions: impl IntoIterator<Item = A>) -> usize {
219        let before = self.pending.len();
220        self.pending.extend(actions);
221        self.pending.len() - before
222    }
223
224    /// Whether an occurrence is pending, without consuming it.
225    pub fn requested(&self, action: &A) -> bool {
226        self.pending.contains(action)
227    }
228
229    /// How many occurrences are pending.
230    pub fn count(&self, action: &A) -> usize {
231        self.pending.iter().filter(|pending| *pending == action).count()
232    }
233
234    /// Consume one occurrence, reporting whether it was there.
235    pub fn take(&mut self, action: &A) -> bool {
236        match self.pending.iter().position(|pending| pending == action) {
237            Some(index) => {
238                self.pending.remove(index);
239                true
240            }
241            None => false,
242        }
243    }
244
245    /// Consume the first occurrence matching `predicate`, keeping the rest.
246    ///
247    /// The way to handle actions that carry a payload, e.g. a selected index:
248    /// `intents.take_matching(|action| matches!(action, UiAction::Pick(_)))`.
249    pub fn take_matching(&mut self, mut predicate: impl FnMut(&A) -> bool) -> Option<A> {
250        let index = self.pending.iter().position(|action| predicate(action))?;
251        Some(self.pending.remove(index))
252    }
253
254    /// Every pending action in order, leaving the buffer empty.
255    pub fn take_all(&mut self) -> Vec<A> {
256        std::mem::take(&mut self.pending)
257    }
258}
259
260impl<A: UiAction> FromIterator<A> for Intents<A> {
261    fn from_iter<T: IntoIterator<Item = A>>(iter: T) -> Self {
262        Self {
263            pending: iter.into_iter().collect(),
264        }
265    }
266}
267
268/// Fold a drained batch into an intent buffer.
269///
270/// The call site a sim system uses when it drains the queue itself:
271/// `repame_ui::absorb(&ui.0, &mut intents.0)`.
272pub fn absorb<A: UiAction>(queue: &ActionQueue<A>, intents: &mut Intents<A>) -> usize {
273    queue.absorb(intents)
274}
275
276#[cfg(test)]
277mod tests {
278    use super::*;
279
280    #[derive(Clone, Copy, Debug, PartialEq, Eq)]
281    enum Act {
282        Play,
283        Pause,
284        Pick(u8),
285    }
286
287    #[test]
288    fn drains_in_push_order() {
289        let (queue, sink) = ActionQueue::pair();
290        sink.push(Act::Play);
291        sink.push(Act::Pause);
292        sink.push(Act::Play);
293        assert_eq!(queue.drain(), vec![Act::Play, Act::Pause, Act::Play]);
294    }
295
296    #[test]
297    fn drain_empties_the_buffer_once() {
298        let (queue, sink) = ActionQueue::pair();
299        sink.push(Act::Play);
300        assert_eq!(queue.drain(), vec![Act::Play]);
301        assert!(queue.drain().is_empty());
302        assert!(queue.is_empty());
303        assert_eq!(queue.pending(), 0);
304    }
305
306    #[test]
307    fn a_batch_never_contains_what_arrives_after_the_drain_started() {
308        let (queue, sink) = ActionQueue::pair();
309        sink.push(Act::Play);
310        let batch = queue.drain();
311        sink.push(Act::Pause);
312        assert_eq!(batch, vec![Act::Play]);
313        assert_eq!(queue.drain(), vec![Act::Pause]);
314    }
315
316    #[test]
317    fn clones_push_into_the_same_buffer() {
318        let (queue, sink) = ActionQueue::pair();
319        let clone = sink.clone();
320        clone.push(Act::Play);
321        assert_eq!(sink.pending(), 1);
322        assert_eq!(queue.drain(), vec![Act::Play]);
323    }
324
325    #[test]
326    fn drain_into_appends_to_the_caller_buffer() {
327        let (queue, sink) = ActionQueue::pair();
328        let mut out = vec![Act::Play];
329        sink.push(Act::Pause);
330        queue.drain_into(&mut out);
331        assert_eq!(out, vec![Act::Play, Act::Pause]);
332    }
333
334    #[test]
335    fn a_panic_while_pushing_keeps_the_queued_actions() {
336        let (queue, sink) = ActionQueue::pair();
337        sink.push(Act::Play);
338        let shared = Arc::clone(&queue.shared);
339        let _ = std::panic::catch_unwind(std::panic::AssertUnwindSafe(move || {
340            let _guard = shared.lock().unwrap();
341            panic!("pusher panicked while holding the queue");
342        }));
343        sink.push(Act::Pause);
344        assert_eq!(queue.drain(), vec![Act::Play, Act::Pause]);
345    }
346
347    #[test]
348    fn intents_consume_one_occurrence_at_a_time() {
349        let mut intents = Intents::new();
350        intents.extend([Act::Play, Act::Pause, Act::Play]);
351        assert_eq!(intents.count(&Act::Play), 2);
352        assert!(intents.requested(&Act::Play));
353        assert!(intents.take(&Act::Play));
354        assert!(intents.take(&Act::Play));
355        assert!(!intents.take(&Act::Play));
356        assert!(!intents.requested(&Act::Play));
357        assert!(intents.take(&Act::Pause));
358        assert!(intents.is_empty());
359    }
360
361    #[test]
362    fn take_matching_keeps_the_other_requests() {
363        let mut intents = Intents::new();
364        intents.extend([Act::Play, Act::Pick(3), Act::Pick(7)]);
365        assert_eq!(
366            intents.take_matching(|action| matches!(action, Act::Pick(_))),
367            Some(Act::Pick(3))
368        );
369        assert_eq!(intents.requests(), [Act::Play, Act::Pick(7)]);
370        assert_eq!(intents.take_matching(|action| matches!(action, Act::Pick(_))), Some(Act::Pick(7)));
371        assert!(intents.take_matching(|action| matches!(action, Act::Pick(_))).is_none());
372    }
373
374    #[test]
375    fn absorb_reports_how_many_actions_arrived() {
376        let (queue, sink) = ActionQueue::pair();
377        let mut intents = Intents::new();
378        assert_eq!(queue.absorb(&mut intents), 0);
379        sink.extend([Act::Play, Act::Pause]);
380        assert_eq!(queue.absorb(&mut intents), 2);
381        assert_eq!(intents.requests(), [Act::Play, Act::Pause]);
382        assert_eq!(intents.take_all(), vec![Act::Play, Act::Pause]);
383        assert!(intents.is_empty());
384    }
385}