ftui_runtime/tick_strategy/
transition_history.rs1use std::collections::VecDeque;
12
13const DEFAULT_CAPACITY: usize = 256;
15
16#[derive(Debug, Clone)]
18pub struct TransitionEntry<S: Clone> {
19 pub from: S,
21 pub to: S,
23 pub timestamp: web_time::Instant,
25 pub tick_count: u64,
27}
28
29#[derive(Debug, Clone)]
43pub struct TransitionHistory<S: Clone> {
44 entries: VecDeque<TransitionEntry<S>>,
45 capacity: usize,
46}
47
48impl<S: Clone> TransitionHistory<S> {
49 #[must_use]
53 pub fn with_capacity(capacity: usize) -> Self {
54 let capacity = capacity.max(1);
55 Self {
56 entries: VecDeque::with_capacity(capacity),
57 capacity,
58 }
59 }
60
61 pub fn record(&mut self, from: S, to: S, tick_count: u64) {
65 if self.entries.len() == self.capacity {
66 self.entries.pop_front();
67 }
68 self.entries.push_back(TransitionEntry {
69 from,
70 to,
71 timestamp: web_time::Instant::now(),
72 tick_count,
73 });
74 }
75
76 #[must_use]
80 pub fn recent(&self, n: usize) -> Vec<&TransitionEntry<S>> {
81 let start = self.entries.len().saturating_sub(n);
82 self.entries.range(start..).collect()
83 }
84
85 #[must_use]
87 pub fn last_n_screens(&self, n: usize) -> Vec<&S>
88 where
89 S: PartialEq,
90 {
91 let mut screens = Vec::new();
92 for entry in self.entries.iter().rev() {
93 if screens.len() >= n {
94 break;
95 }
96 if !screens.contains(&&entry.to) {
97 screens.push(&entry.to);
98 }
99 }
100 screens.reverse();
101 screens
102 }
103
104 #[must_use]
106 pub fn len(&self) -> usize {
107 self.entries.len()
108 }
109
110 #[must_use]
112 pub fn is_empty(&self) -> bool {
113 self.entries.is_empty()
114 }
115
116 #[must_use]
118 pub fn capacity(&self) -> usize {
119 self.capacity
120 }
121
122 pub fn clear(&mut self) {
124 self.entries.clear();
125 }
126}
127
128impl<S: Clone> Default for TransitionHistory<S> {
129 fn default() -> Self {
130 Self::with_capacity(DEFAULT_CAPACITY)
131 }
132}
133
134#[cfg(test)]
135mod tests {
136 use super::*;
137
138 #[test]
139 fn empty_history() {
140 let h = TransitionHistory::<String>::with_capacity(10);
141 assert!(h.is_empty());
142 assert_eq!(h.len(), 0);
143 assert_eq!(h.capacity(), 10);
144 assert!(h.recent(5).is_empty());
145 }
146
147 #[test]
148 fn record_and_retrieve() {
149 let mut h = TransitionHistory::with_capacity(10);
150 h.record("A".to_owned(), "B".to_owned(), 1);
151 h.record("B".to_owned(), "C".to_owned(), 2);
152
153 assert_eq!(h.len(), 2);
154 assert!(!h.is_empty());
155
156 let recent = h.recent(10);
157 assert_eq!(recent.len(), 2);
158 assert_eq!(recent[0].from, "A");
159 assert_eq!(recent[0].to, "B");
160 assert_eq!(recent[0].tick_count, 1);
161 assert_eq!(recent[1].from, "B");
162 assert_eq!(recent[1].to, "C");
163 }
164
165 #[test]
166 fn eviction_at_capacity() {
167 let mut h = TransitionHistory::with_capacity(3);
168 h.record("A".to_owned(), "B".to_owned(), 1);
169 h.record("B".to_owned(), "C".to_owned(), 2);
170 h.record("C".to_owned(), "D".to_owned(), 3);
171 assert_eq!(h.len(), 3);
172
173 h.record("D".to_owned(), "E".to_owned(), 4);
175 assert_eq!(h.len(), 3);
176
177 let recent = h.recent(10);
178 assert_eq!(recent[0].from, "B");
179 assert_eq!(recent[0].to, "C");
180 assert_eq!(recent[2].from, "D");
181 assert_eq!(recent[2].to, "E");
182 }
183
184 #[test]
185 fn recent_returns_tail() {
186 let mut h = TransitionHistory::with_capacity(10);
187 for i in 0..5 {
188 h.record(format!("s{i}"), format!("s{}", i + 1), i as u64);
189 }
190
191 let last2 = h.recent(2);
192 assert_eq!(last2.len(), 2);
193 assert_eq!(last2[0].from, "s3");
194 assert_eq!(last2[1].from, "s4");
195 }
196
197 #[test]
198 fn recent_more_than_stored() {
199 let mut h = TransitionHistory::with_capacity(10);
200 h.record("A".to_owned(), "B".to_owned(), 0);
201
202 let all = h.recent(100);
203 assert_eq!(all.len(), 1);
204 }
205
206 #[test]
207 fn last_n_screens_deduplicates() {
208 let mut h = TransitionHistory::with_capacity(10);
209 h.record("A".to_owned(), "B".to_owned(), 1);
210 h.record("B".to_owned(), "A".to_owned(), 2);
211 h.record("A".to_owned(), "B".to_owned(), 3);
212 h.record("B".to_owned(), "C".to_owned(), 4);
213
214 let screens = h.last_n_screens(3);
216 assert_eq!(screens.len(), 3);
217 assert_eq!(*screens[0], "A");
218 assert_eq!(*screens[1], "B");
219 assert_eq!(*screens[2], "C");
220 }
221
222 #[test]
223 fn last_n_screens_fewer_than_requested() {
224 let mut h = TransitionHistory::with_capacity(10);
225 h.record("A".to_owned(), "B".to_owned(), 1);
226
227 let screens = h.last_n_screens(5);
228 assert_eq!(screens.len(), 1);
229 assert_eq!(*screens[0], "B");
230 }
231
232 #[test]
233 fn capacity_zero_clamped_to_one() {
234 let mut h = TransitionHistory::with_capacity(0);
235 assert_eq!(h.capacity(), 1);
236 h.record("A".to_owned(), "B".to_owned(), 0);
237 assert_eq!(h.len(), 1);
238 h.record("B".to_owned(), "C".to_owned(), 1);
239 assert_eq!(h.len(), 1);
240 assert_eq!(h.recent(1)[0].to, "C");
241 }
242
243 #[test]
244 fn default_capacity() {
245 let h = TransitionHistory::<String>::default();
246 assert_eq!(h.capacity(), 256);
247 }
248
249 #[test]
250 fn clear_empties_buffer() {
251 let mut h = TransitionHistory::with_capacity(10);
252 h.record("A".to_owned(), "B".to_owned(), 0);
253 h.record("B".to_owned(), "C".to_owned(), 1);
254 assert_eq!(h.len(), 2);
255
256 h.clear();
257 assert!(h.is_empty());
258 assert_eq!(h.len(), 0);
259 }
260
261 #[test]
262 fn timestamps_are_monotonic() {
263 let mut h = TransitionHistory::with_capacity(10);
264 h.record("A".to_owned(), "B".to_owned(), 0);
265 h.record("B".to_owned(), "C".to_owned(), 1);
266
267 let entries = h.recent(2);
268 assert!(entries[1].timestamp >= entries[0].timestamp);
269 }
270
271 #[test]
272 fn clone_is_independent() {
273 let mut h = TransitionHistory::with_capacity(5);
274 h.record("A".to_owned(), "B".to_owned(), 0);
275
276 let mut clone = h.clone();
277 clone.record("B".to_owned(), "C".to_owned(), 1);
278
279 assert_eq!(h.len(), 1);
280 assert_eq!(clone.len(), 2);
281 }
282}