Skip to main content

ftui_runtime/tick_strategy/
uniform.rs

1//! [`Uniform`] strategy: tick all inactive screens at a fixed divisor.
2
3use super::{TickDecision, TickStrategy};
4
5/// Tick every inactive screen at a fixed reduced rate: every Nth frame.
6///
7/// This is the direct framework-level equivalent of a global
8/// `INACTIVE_SCREEN_TICK_DIVISOR`.
9///
10/// - `divisor=1`: every frame (no throttling, matches current ftui default)
11/// - `divisor=5`: every 5th frame (recommended default)
12/// - `divisor=10`: every 10th frame
13#[derive(Debug, Clone, Copy)]
14pub struct Uniform {
15    divisor: u64,
16}
17
18impl Uniform {
19    /// Create a uniform strategy with the given divisor.
20    ///
21    /// A divisor of 0 is clamped to 1 (tick every frame).
22    #[must_use]
23    pub const fn new(divisor: u64) -> Self {
24        Self {
25            divisor: if divisor == 0 { 1 } else { divisor },
26        }
27    }
28
29    /// Return the effective divisor.
30    #[must_use]
31    pub const fn divisor(&self) -> u64 {
32        self.divisor
33    }
34}
35
36impl Default for Uniform {
37    fn default() -> Self {
38        Self::new(5)
39    }
40}
41
42impl TickStrategy for Uniform {
43    fn should_tick(
44        &mut self,
45        _screen_id: &str,
46        tick_count: u64,
47        _active_screen: &str,
48    ) -> TickDecision {
49        if tick_count.is_multiple_of(self.divisor) {
50            TickDecision::Tick
51        } else {
52            TickDecision::Skip
53        }
54    }
55
56    fn name(&self) -> &str {
57        "Uniform"
58    }
59
60    fn debug_stats(&self) -> Vec<(String, String)> {
61        vec![
62            ("strategy".into(), "Uniform".into()),
63            ("divisor".into(), self.divisor.to_string()),
64        ]
65    }
66}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71
72    #[test]
73    fn divisor_1_always_ticks() {
74        let mut s = Uniform::new(1);
75        for tick in 0..20 {
76            assert_eq!(
77                s.should_tick("x", tick, "active"),
78                TickDecision::Tick,
79                "divisor=1 should always Tick, failed at tick={tick}"
80            );
81        }
82    }
83
84    #[test]
85    fn divisor_5_pattern() {
86        let mut s = Uniform::new(5);
87        for tick in 0..20 {
88            let expected = if tick % 5 == 0 {
89                TickDecision::Tick
90            } else {
91                TickDecision::Skip
92            };
93            assert_eq!(
94                s.should_tick("x", tick, "active"),
95                expected,
96                "divisor=5, tick={tick}"
97            );
98        }
99    }
100
101    #[test]
102    fn divisor_0_clamped_to_1() {
103        let s = Uniform::new(0);
104        assert_eq!(s.divisor(), 1);
105    }
106
107    #[test]
108    fn consistent_across_screen_ids() {
109        let mut s = Uniform::new(3);
110        let d1 = s.should_tick("alpha", 6, "active");
111        let d2 = s.should_tick("beta", 6, "active");
112        assert_eq!(d1, d2);
113    }
114
115    #[test]
116    fn name_is_stable() {
117        assert_eq!(Uniform::new(5).name(), "Uniform");
118    }
119
120    #[test]
121    fn debug_stats_reports_divisor() {
122        let stats = Uniform::new(7).debug_stats();
123        assert!(stats.iter().any(|(k, v)| k == "divisor" && v == "7"));
124    }
125
126    #[test]
127    fn default_is_divisor_5() {
128        assert_eq!(Uniform::default().divisor(), 5);
129    }
130}