ftui_runtime/tick_strategy/
active_plus_adjacent.rs1use std::collections::HashMap;
4
5use super::{TickDecision, TickStrategy};
6
7#[derive(Debug, Clone)]
13pub struct ActivePlusAdjacent {
14 adjacency: HashMap<String, Vec<String>>,
15 background_divisor: u64,
16}
17
18impl ActivePlusAdjacent {
19 #[must_use]
23 pub fn new(background_divisor: u64) -> Self {
24 Self {
25 adjacency: HashMap::new(),
26 background_divisor: background_divisor.max(1),
27 }
28 }
29
30 pub fn add_adjacency(&mut self, screen: &str, neighbors: &[&str]) {
32 self.adjacency
33 .entry(screen.to_owned())
34 .or_default()
35 .extend(neighbors.iter().map(|s| (*s).to_owned()));
36 }
37
38 #[must_use]
48 pub fn from_tab_order(screens: &[&str], background_divisor: u64) -> Self {
49 let mut s = Self::new(background_divisor);
50 for window in screens.windows(2) {
51 let (a, b) = (window[0], window[1]);
52 s.adjacency
53 .entry(a.to_owned())
54 .or_default()
55 .push(b.to_owned());
56 s.adjacency
57 .entry(b.to_owned())
58 .or_default()
59 .push(a.to_owned());
60 }
61 s
62 }
63
64 #[must_use]
66 pub const fn background_divisor(&self) -> u64 {
67 self.background_divisor
68 }
69
70 #[must_use]
72 pub fn adjacency(&self) -> &HashMap<String, Vec<String>> {
73 &self.adjacency
74 }
75}
76
77impl TickStrategy for ActivePlusAdjacent {
78 fn should_tick(
79 &mut self,
80 screen_id: &str,
81 tick_count: u64,
82 active_screen: &str,
83 ) -> TickDecision {
84 if self
86 .adjacency
87 .get(active_screen)
88 .is_some_and(|adj| adj.iter().any(|a| a == screen_id))
89 {
90 return TickDecision::Tick;
91 }
92
93 if tick_count.is_multiple_of(self.background_divisor) {
95 TickDecision::Tick
96 } else {
97 TickDecision::Skip
98 }
99 }
100
101 fn name(&self) -> &str {
102 "ActivePlusAdjacent"
103 }
104
105 fn debug_stats(&self) -> Vec<(String, String)> {
106 vec![
107 ("strategy".into(), "ActivePlusAdjacent".into()),
108 (
109 "background_divisor".into(),
110 self.background_divisor.to_string(),
111 ),
112 ("adjacency_entries".into(), self.adjacency.len().to_string()),
113 ]
114 }
115}
116
117#[cfg(test)]
118mod tests {
119 use super::*;
120
121 #[test]
122 fn adjacent_screens_always_tick() {
123 let mut s = ActivePlusAdjacent::from_tab_order(&["A", "B", "C"], 100);
124 assert_eq!(s.should_tick("B", 1, "A"), TickDecision::Tick);
126 assert_eq!(s.should_tick("B", 7, "A"), TickDecision::Tick);
127 }
128
129 #[test]
130 fn non_adjacent_respects_divisor() {
131 let mut s = ActivePlusAdjacent::from_tab_order(&["A", "B", "C"], 5);
132 assert_eq!(s.should_tick("C", 1, "A"), TickDecision::Skip);
134 assert_eq!(s.should_tick("C", 5, "A"), TickDecision::Tick);
135 }
136
137 #[test]
138 fn from_tab_order_builds_bidirectional() {
139 let s = ActivePlusAdjacent::from_tab_order(&["X", "Y", "Z"], 10);
140 let adj = s.adjacency();
141 assert!(adj["X"].contains(&"Y".to_owned()));
142 assert!(adj["Y"].contains(&"X".to_owned()));
143 assert!(adj["Y"].contains(&"Z".to_owned()));
144 assert!(adj["Z"].contains(&"Y".to_owned()));
145 assert!(!adj["X"].contains(&"Z".to_owned()));
147 assert!(!adj["Z"].contains(&"X".to_owned()));
148 }
149
150 #[test]
151 fn edge_screens_have_one_neighbor() {
152 let s = ActivePlusAdjacent::from_tab_order(&["A", "B", "C", "D"], 10);
153 assert_eq!(s.adjacency()["A"].len(), 1); assert_eq!(s.adjacency()["D"].len(), 1); assert_eq!(s.adjacency()["B"].len(), 2); }
157
158 #[test]
159 fn unknown_screen_uses_divisor() {
160 let mut s = ActivePlusAdjacent::from_tab_order(&["A", "B"], 4);
161 assert_eq!(s.should_tick("unknown", 3, "A"), TickDecision::Skip);
163 assert_eq!(s.should_tick("unknown", 4, "A"), TickDecision::Tick);
164 }
165
166 #[test]
167 fn add_adjacency_appends() {
168 let mut s = ActivePlusAdjacent::new(10);
169 s.add_adjacency("home", &["settings", "profile"]);
170 assert_eq!(s.adjacency()["home"].len(), 2);
171 s.add_adjacency("home", &["help"]);
172 assert_eq!(s.adjacency()["home"].len(), 3);
173 }
174
175 #[test]
176 fn name_is_stable() {
177 let s = ActivePlusAdjacent::new(5);
178 assert_eq!(s.name(), "ActivePlusAdjacent");
179 }
180
181 #[test]
182 fn debug_stats_reports_fields() {
183 let s = ActivePlusAdjacent::from_tab_order(&["A", "B", "C"], 8);
184 let stats = s.debug_stats();
185 assert!(
186 stats
187 .iter()
188 .any(|(k, v)| k == "background_divisor" && v == "8")
189 );
190 assert!(
191 stats
192 .iter()
193 .any(|(k, v)| k == "adjacency_entries" && v == "3")
194 );
195 }
196
197 #[test]
198 fn divisor_0_clamped() {
199 let s = ActivePlusAdjacent::new(0);
200 assert_eq!(s.background_divisor(), 1);
201 }
202
203 #[test]
204 fn empty_tab_order() {
205 let s = ActivePlusAdjacent::from_tab_order(&[], 5);
206 assert!(s.adjacency().is_empty());
207 }
208
209 #[test]
210 fn single_screen_tab_order() {
211 let s = ActivePlusAdjacent::from_tab_order(&["only"], 5);
212 assert!(s.adjacency().is_empty());
213 }
214}