ftui_runtime/tick_strategy/
custom.rs1use super::{TickDecision, TickStrategy};
8
9type DeciderFn = dyn Fn(&str, u64, &str) -> TickDecision + Send;
11
12pub struct Custom {
33 decider: Box<DeciderFn>,
34 label: String,
35}
36
37impl Custom {
38 pub fn new<F>(label: impl Into<String>, f: F) -> Self
43 where
44 F: Fn(&str, u64, &str) -> TickDecision + Send + 'static,
45 {
46 Self {
47 decider: Box::new(f),
48 label: label.into(),
49 }
50 }
51}
52
53impl std::fmt::Debug for Custom {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 f.debug_struct("Custom")
56 .field("label", &self.label)
57 .finish_non_exhaustive()
58 }
59}
60
61impl TickStrategy for Custom {
62 fn should_tick(
63 &mut self,
64 screen_id: &str,
65 tick_count: u64,
66 active_screen: &str,
67 ) -> TickDecision {
68 (self.decider)(screen_id, tick_count, active_screen)
69 }
70
71 fn name(&self) -> &str {
72 &self.label
73 }
74
75 fn debug_stats(&self) -> Vec<(String, String)> {
76 vec![("strategy".into(), self.label.clone())]
77 }
78}
79
80#[cfg(test)]
81mod tests {
82 use super::*;
83
84 #[test]
85 fn custom_closure_receives_correct_args() {
86 let mut s = Custom::new("Test", |screen_id, tick_count, active| {
87 assert_eq!(screen_id, "bg");
88 assert_eq!(tick_count, 42);
89 assert_eq!(active, "fg");
90 TickDecision::Tick
91 });
92 assert_eq!(s.should_tick("bg", 42, "fg"), TickDecision::Tick);
93 }
94
95 #[test]
96 fn custom_return_value_is_respected() {
97 let mut always_tick = Custom::new("AlwaysTick", |_, _, _| TickDecision::Tick);
98 assert_eq!(always_tick.should_tick("x", 0, "y"), TickDecision::Tick);
99
100 let mut always_skip = Custom::new("AlwaysSkip", |_, _, _| TickDecision::Skip);
101 assert_eq!(always_skip.should_tick("x", 0, "y"), TickDecision::Skip);
102 }
103
104 #[test]
105 fn name_returns_label() {
106 let s = Custom::new("MyCustom", |_, _, _| TickDecision::Skip);
107 assert_eq!(s.name(), "MyCustom");
108 }
109
110 #[test]
111 fn debug_stats_contains_label() {
112 let s = Custom::new("Labeled", |_, _, _| TickDecision::Skip);
113 let stats = s.debug_stats();
114 assert_eq!(stats.len(), 1);
115 assert_eq!(stats[0], ("strategy".to_owned(), "Labeled".to_owned()));
116 }
117
118 #[test]
119 fn custom_can_be_boxed_as_dyn_tick_strategy() {
120 let s = Custom::new("Boxable", |_, _, _| TickDecision::Tick);
121 let mut boxed: Box<dyn TickStrategy> = Box::new(s);
122 assert_eq!(boxed.should_tick("a", 0, "b"), TickDecision::Tick);
123 assert_eq!(boxed.name(), "Boxable");
124 }
125
126 #[test]
127 fn custom_debug_format() {
128 let s = Custom::new("Dbg", |_, _, _| TickDecision::Skip);
129 let dbg = format!("{s:?}");
130 assert!(dbg.contains("Custom"));
131 assert!(dbg.contains("Dbg"));
132 }
133
134 #[test]
135 fn custom_divisor_logic() {
136 let mut s = Custom::new("DivisorBased", |screen_id, tick_count, _| {
137 let divisor: u64 = match screen_id {
138 "fast" => 2,
139 "slow" => 10,
140 _ => 5,
141 };
142 if tick_count.is_multiple_of(divisor) {
143 TickDecision::Tick
144 } else {
145 TickDecision::Skip
146 }
147 });
148
149 assert_eq!(s.should_tick("fast", 4, "active"), TickDecision::Tick);
151 assert_eq!(s.should_tick("fast", 3, "active"), TickDecision::Skip);
153 assert_eq!(s.should_tick("slow", 10, "active"), TickDecision::Tick);
155 assert_eq!(s.should_tick("slow", 5, "active"), TickDecision::Skip);
157 }
158}