Skip to main content

egui_cha/
sub.rs

1//! Subscription type for continuous effects (intervals, timers, etc.)
2//!
3//! Unlike `Cmd` which represents one-shot effects, `Sub` represents
4//! continuous subscriptions that persist across frames.
5//!
6//! # Example
7//! ```ignore
8//! use egui_cha::prelude::*;
9//! use std::time::Duration;
10//!
11//! fn subscriptions(model: &Model) -> Sub<Msg> {
12//!     if model.auto_refresh {
13//!         Sub::interval("refresh", Duration::from_secs(30), Msg::Refresh)
14//!     } else {
15//!         Sub::none()
16//!     }
17//! }
18//! ```
19
20use std::collections::HashSet;
21use std::time::Duration;
22
23/// A subscription representing a continuous effect
24///
25/// Subscriptions are declared each frame based on model state.
26/// The runtime manages starting/stopping subscriptions as they
27/// appear or disappear from the returned `Sub`.
28#[derive(Clone)]
29pub enum Sub<Msg> {
30    /// No subscription
31    None,
32
33    /// Multiple subscriptions
34    Batch(Vec<Sub<Msg>>),
35
36    /// Periodic interval timer
37    ///
38    /// Emits the message at regular intervals.
39    /// The `id` must be unique - subscriptions with the same ID
40    /// are considered identical and won't be restarted.
41    Interval {
42        /// Unique identifier for this interval
43        id: &'static str,
44        /// Time between emissions
45        duration: Duration,
46        /// Message to emit
47        msg: Msg,
48    },
49}
50
51impl<Msg> Default for Sub<Msg> {
52    fn default() -> Self {
53        Sub::None
54    }
55}
56
57impl<Msg: Clone> Sub<Msg> {
58    /// Create an empty subscription
59    #[inline]
60    pub fn none() -> Self {
61        Sub::None
62    }
63
64    /// Create a batch of subscriptions
65    pub fn batch(subs: impl IntoIterator<Item = Sub<Msg>>) -> Self {
66        let subs: Vec<_> = subs.into_iter().collect();
67        if subs.is_empty() {
68            Sub::None
69        } else if subs.len() == 1 {
70            subs.into_iter().next().unwrap()
71        } else {
72            Sub::Batch(subs)
73        }
74    }
75
76    /// Create a periodic interval subscription
77    ///
78    /// The message will be emitted every `duration` after the subscription starts.
79    ///
80    /// # Arguments
81    /// - `id`: Unique identifier. Intervals with the same ID won't restart.
82    /// - `duration`: Time between message emissions
83    /// - `msg`: Message to emit each interval
84    ///
85    /// # Example
86    /// ```ignore
87    /// // Auto-save every 30 seconds when enabled
88    /// fn subscriptions(model: &Model) -> Sub<Msg> {
89    ///     if model.auto_save_enabled {
90    ///         Sub::interval("auto_save", Duration::from_secs(30), Msg::AutoSave)
91    ///     } else {
92    ///         Sub::none()
93    ///     }
94    /// }
95    /// ```
96    pub fn interval(id: &'static str, duration: Duration, msg: Msg) -> Self {
97        Sub::Interval { id, duration, msg }
98    }
99
100    /// Collect all interval IDs in this subscription tree
101    pub(crate) fn collect_interval_ids(&self, ids: &mut HashSet<&'static str>) {
102        match self {
103            Sub::None => {}
104            Sub::Batch(subs) => {
105                for sub in subs {
106                    sub.collect_interval_ids(ids);
107                }
108            }
109            Sub::Interval { id, .. } => {
110                ids.insert(id);
111            }
112        }
113    }
114
115    /// Iterate over all intervals in this subscription
116    pub(crate) fn intervals(&self) -> Vec<(&'static str, Duration, Msg)> {
117        let mut result = Vec::new();
118        self.collect_intervals(&mut result);
119        result
120    }
121
122    fn collect_intervals(&self, result: &mut Vec<(&'static str, Duration, Msg)>) {
123        match self {
124            Sub::None => {}
125            Sub::Batch(subs) => {
126                for sub in subs {
127                    sub.collect_intervals(result);
128                }
129            }
130            Sub::Interval { id, duration, msg } => {
131                result.push((id, *duration, msg.clone()));
132            }
133        }
134    }
135}
136
137// ============================================================
138// Test helpers
139// ============================================================
140
141impl<Msg> Sub<Msg> {
142    /// Check if this is Sub::None
143    #[inline]
144    pub fn is_none(&self) -> bool {
145        matches!(self, Sub::None)
146    }
147
148    /// Check if this is Sub::Interval
149    #[inline]
150    pub fn is_interval(&self) -> bool {
151        matches!(self, Sub::Interval { .. })
152    }
153
154    /// Check if this is Sub::Batch
155    #[inline]
156    pub fn is_batch(&self) -> bool {
157        matches!(self, Sub::Batch(_))
158    }
159
160    /// Get the number of subscriptions (1 for single, n for batch, 0 for none)
161    pub fn len(&self) -> usize {
162        match self {
163            Sub::None => 0,
164            Sub::Batch(subs) => subs.iter().map(|s| s.len()).sum(),
165            Sub::Interval { .. } => 1,
166        }
167    }
168
169    /// Check if empty
170    pub fn is_empty(&self) -> bool {
171        self.len() == 0
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_sub_none() {
181        let sub: Sub<i32> = Sub::none();
182        assert!(sub.is_none());
183        assert_eq!(sub.len(), 0);
184    }
185
186    #[test]
187    fn test_sub_interval() {
188        let sub = Sub::interval("test", Duration::from_secs(1), 42);
189        assert!(sub.is_interval());
190        assert_eq!(sub.len(), 1);
191    }
192
193    #[test]
194    fn test_sub_batch() {
195        let sub = Sub::batch([
196            Sub::interval("a", Duration::from_secs(1), 1),
197            Sub::interval("b", Duration::from_secs(2), 2),
198        ]);
199        assert!(sub.is_batch());
200        assert_eq!(sub.len(), 2);
201    }
202
203    #[test]
204    fn test_collect_interval_ids() {
205        let sub = Sub::batch([
206            Sub::interval("a", Duration::from_secs(1), 1),
207            Sub::interval("b", Duration::from_secs(2), 2),
208            Sub::none(),
209        ]);
210
211        let mut ids = HashSet::new();
212        sub.collect_interval_ids(&mut ids);
213
214        assert!(ids.contains("a"));
215        assert!(ids.contains("b"));
216        assert_eq!(ids.len(), 2);
217    }
218
219    #[test]
220    fn test_batch_flattening() {
221        // Empty batch becomes None
222        let empty: Sub<i32> = Sub::batch([]);
223        assert!(empty.is_none());
224
225        // Single-item batch unwraps
226        let single = Sub::batch([Sub::interval("x", Duration::from_secs(1), 1)]);
227        assert!(single.is_interval());
228    }
229
230    #[test]
231    fn test_intervals_iterator() {
232        let sub = Sub::batch([
233            Sub::interval("a", Duration::from_secs(1), 10),
234            Sub::interval("b", Duration::from_secs(2), 20),
235        ]);
236
237        let intervals = sub.intervals();
238        assert_eq!(intervals.len(), 2);
239        assert_eq!(intervals[0], ("a", Duration::from_secs(1), 10));
240        assert_eq!(intervals[1], ("b", Duration::from_secs(2), 20));
241    }
242}