Skip to main content

ftui_layout/
responsive.rs

1#![forbid(unsafe_code)]
2
3//! Responsive value mapping: apply different values based on breakpoint.
4//!
5//! [`Responsive<T>`] maps [`Breakpoint`] tiers to values of any type,
6//! with inheritance from smaller breakpoints. If no value is set for a
7//! given breakpoint, the value from the next smaller breakpoint is used.
8//!
9//! # Usage
10//!
11//! ```ignore
12//! use ftui_layout::{Responsive, Breakpoint};
13//!
14//! let padding = Responsive::new(1)     // xs: 1
15//!     .at(Breakpoint::Md, 2)           // md: 2
16//!     .at(Breakpoint::Xl, 4);          // xl: 4
17//!
18//! // sm inherits from xs → 1
19//! // lg inherits from md → 2
20//! assert_eq!(padding.resolve(Breakpoint::Sm), &1);
21//! assert_eq!(padding.resolve(Breakpoint::Lg), &2);
22//! ```
23//!
24//! # Invariants
25//!
26//! 1. `Xs` always has a value (set via `new()`).
27//! 2. Inheritance follows breakpoint order: a missing tier inherits from
28//!    the nearest smaller tier that has a value.
29//! 3. `resolve()` never fails — it always returns a reference.
30//! 4. Setting a value at a tier only affects that tier and tiers that
31//!    inherit from it (does not affect tiers with explicit values).
32//!
33//! # Failure Modes
34//!
35//! None — the type system guarantees a base value at `Xs`.
36
37use super::Breakpoint;
38
39/// A breakpoint-aware value with inheritance from smaller tiers.
40///
41/// Each slot is `Option<T>`. Slot 0 (Xs) is always `Some` (enforced
42/// by the constructor). Resolution walks downward from the requested
43/// breakpoint until a `Some` slot is found.
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct Responsive<T> {
46    /// Values indexed by `Breakpoint` ordinal (0=Xs .. 4=Xl).
47    /// Slot 0 is always `Some`.
48    values: [Option<T>; 5],
49}
50
51impl<T: Clone> Responsive<T> {
52    /// Create a responsive value with a base value for `Xs`.
53    ///
54    /// All larger breakpoints inherit this value until explicitly overridden.
55    #[must_use]
56    pub fn new(base: T) -> Self {
57        Self {
58            values: [Some(base), None, None, None, None],
59        }
60    }
61
62    /// Set the value for a specific breakpoint (builder pattern).
63    #[must_use]
64    pub fn at(mut self, bp: Breakpoint, value: T) -> Self {
65        self.values[bp as usize] = Some(value);
66        self
67    }
68
69    /// Set the value for a specific breakpoint (mutating).
70    pub fn set(&mut self, bp: Breakpoint, value: T) {
71        self.values[bp as usize] = Some(value);
72    }
73
74    /// Clear the override for a specific breakpoint, reverting to inheritance.
75    ///
76    /// Clearing `Xs` is a no-op (it always has a value).
77    pub fn clear(&mut self, bp: Breakpoint) {
78        if bp != Breakpoint::Xs {
79            self.values[bp as usize] = None;
80        }
81    }
82
83    /// Resolve the value for a given breakpoint.
84    ///
85    /// Walks downward from `bp` to `Xs` until an explicit value is found.
86    /// Always succeeds because `Xs` is always set.
87    #[must_use]
88    pub fn resolve(&self, bp: Breakpoint) -> &T {
89        let idx = bp as usize;
90        for i in (0..=idx).rev() {
91            if let Some(ref v) = self.values[i] {
92                return v;
93            }
94        }
95        // SAFETY: values[0] (Xs) is always Some.
96        self.values[0].as_ref().expect("Xs always has a value")
97    }
98
99    /// Resolve and clone the value for a given breakpoint.
100    #[must_use]
101    pub fn resolve_cloned(&self, bp: Breakpoint) -> T {
102        self.resolve(bp).clone()
103    }
104
105    /// Whether a specific breakpoint has an explicit (non-inherited) value.
106    #[must_use]
107    pub fn has_explicit(&self, bp: Breakpoint) -> bool {
108        self.values[bp as usize].is_some()
109    }
110
111    /// Get all explicitly set breakpoints and their values.
112    pub fn explicit_values(&self) -> impl Iterator<Item = (Breakpoint, &T)> {
113        Breakpoint::ALL
114            .iter()
115            .zip(self.values.iter())
116            .filter_map(|(&bp, v)| v.as_ref().map(|val| (bp, val)))
117    }
118
119    /// Map the values to a new type.
120    #[must_use]
121    pub fn map<U: Clone>(&self, f: impl Fn(&T) -> U) -> Responsive<U> {
122        Responsive {
123            values: [
124                self.values[0].as_ref().map(&f),
125                self.values[1].as_ref().map(&f),
126                self.values[2].as_ref().map(&f),
127                self.values[3].as_ref().map(&f),
128                self.values[4].as_ref().map(&f),
129            ],
130        }
131    }
132}
133
134impl<T: Clone + Default> Default for Responsive<T> {
135    fn default() -> Self {
136        Self::new(T::default())
137    }
138}
139
140impl<T: Clone + std::fmt::Display> std::fmt::Display for Responsive<T> {
141    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
142        write!(f, "Responsive(")?;
143        let mut first = true;
144        for (bp, val) in self.explicit_values() {
145            if !first {
146                write!(f, ", ")?;
147            }
148            write!(f, "{}={}", bp, val)?;
149            first = false;
150        }
151        write!(f, ")")
152    }
153}
154
155// ---------------------------------------------------------------------------
156// Tests
157// ---------------------------------------------------------------------------
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn base_value_at_all_breakpoints() {
165        let r = Responsive::new(42);
166        for bp in Breakpoint::ALL {
167            assert_eq!(r.resolve(bp), &42);
168        }
169    }
170
171    #[test]
172    fn override_single_breakpoint() {
173        let r = Responsive::new(1).at(Breakpoint::Md, 2);
174
175        assert_eq!(r.resolve(Breakpoint::Xs), &1);
176        assert_eq!(r.resolve(Breakpoint::Sm), &1); // Inherits from Xs.
177        assert_eq!(r.resolve(Breakpoint::Md), &2); // Explicit.
178        assert_eq!(r.resolve(Breakpoint::Lg), &2); // Inherits from Md.
179        assert_eq!(r.resolve(Breakpoint::Xl), &2); // Inherits from Md.
180    }
181
182    #[test]
183    fn override_multiple_breakpoints() {
184        let r = Responsive::new(0)
185            .at(Breakpoint::Sm, 1)
186            .at(Breakpoint::Lg, 3);
187
188        assert_eq!(r.resolve(Breakpoint::Xs), &0);
189        assert_eq!(r.resolve(Breakpoint::Sm), &1);
190        assert_eq!(r.resolve(Breakpoint::Md), &1); // Inherits from Sm.
191        assert_eq!(r.resolve(Breakpoint::Lg), &3);
192        assert_eq!(r.resolve(Breakpoint::Xl), &3); // Inherits from Lg.
193    }
194
195    #[test]
196    fn set_mutating() {
197        let mut r = Responsive::new(0);
198        r.set(Breakpoint::Xl, 5);
199        assert_eq!(r.resolve(Breakpoint::Xl), &5);
200    }
201
202    #[test]
203    fn clear_reverts_to_inheritance() {
204        let mut r = Responsive::new(1).at(Breakpoint::Md, 2);
205        assert_eq!(r.resolve(Breakpoint::Md), &2);
206
207        r.clear(Breakpoint::Md);
208        assert_eq!(r.resolve(Breakpoint::Md), &1); // Back to inheriting from Xs.
209    }
210
211    #[test]
212    fn clear_xs_is_noop() {
213        let mut r = Responsive::new(42);
214        r.clear(Breakpoint::Xs);
215        assert_eq!(r.resolve(Breakpoint::Xs), &42);
216    }
217
218    #[test]
219    fn has_explicit() {
220        let r = Responsive::new(0).at(Breakpoint::Lg, 3);
221
222        assert!(r.has_explicit(Breakpoint::Xs));
223        assert!(!r.has_explicit(Breakpoint::Sm));
224        assert!(!r.has_explicit(Breakpoint::Md));
225        assert!(r.has_explicit(Breakpoint::Lg));
226        assert!(!r.has_explicit(Breakpoint::Xl));
227    }
228
229    #[test]
230    fn explicit_values_iterator() {
231        let r = Responsive::new(0)
232            .at(Breakpoint::Md, 2)
233            .at(Breakpoint::Xl, 4);
234
235        let explicit: Vec<_> = r.explicit_values().collect();
236        assert_eq!(explicit.len(), 3);
237        assert_eq!(explicit[0], (Breakpoint::Xs, &0));
238        assert_eq!(explicit[1], (Breakpoint::Md, &2));
239        assert_eq!(explicit[2], (Breakpoint::Xl, &4));
240    }
241
242    #[test]
243    fn map_values() {
244        let r = Responsive::new(10).at(Breakpoint::Lg, 20);
245        let doubled = r.map(|v| v * 2);
246
247        assert_eq!(doubled.resolve(Breakpoint::Xs), &20);
248        assert_eq!(doubled.resolve(Breakpoint::Lg), &40);
249    }
250
251    #[test]
252    fn resolve_cloned() {
253        let r = Responsive::new("hello".to_string());
254        let val: String = r.resolve_cloned(Breakpoint::Md);
255        assert_eq!(val, "hello");
256    }
257
258    #[test]
259    fn default() {
260        let r: Responsive<i32> = Responsive::default();
261        assert_eq!(r.resolve(Breakpoint::Xs), &0);
262    }
263
264    #[test]
265    fn clone_independence() {
266        let r1 = Responsive::new(1);
267        let mut r2 = r1.clone();
268        r2.set(Breakpoint::Md, 99);
269
270        assert_eq!(r1.resolve(Breakpoint::Md), &1);
271        assert_eq!(r2.resolve(Breakpoint::Md), &99);
272    }
273
274    #[test]
275    fn display_format() {
276        let r = Responsive::new(0).at(Breakpoint::Md, 2);
277        let s = format!("{}", r);
278        assert!(s.contains("xs=0"));
279        assert!(s.contains("md=2"));
280    }
281
282    #[test]
283    fn string_responsive() {
284        let r = Responsive::new("compact".to_string())
285            .at(Breakpoint::Md, "standard".to_string())
286            .at(Breakpoint::Xl, "expanded".to_string());
287
288        assert_eq!(r.resolve(Breakpoint::Xs), "compact");
289        assert_eq!(r.resolve(Breakpoint::Sm), "compact");
290        assert_eq!(r.resolve(Breakpoint::Md), "standard");
291        assert_eq!(r.resolve(Breakpoint::Lg), "standard");
292        assert_eq!(r.resolve(Breakpoint::Xl), "expanded");
293    }
294
295    #[test]
296    fn all_breakpoints_overridden() {
297        let r = Responsive::new(0)
298            .at(Breakpoint::Sm, 1)
299            .at(Breakpoint::Md, 2)
300            .at(Breakpoint::Lg, 3)
301            .at(Breakpoint::Xl, 4);
302
303        assert_eq!(r.resolve(Breakpoint::Xs), &0);
304        assert_eq!(r.resolve(Breakpoint::Sm), &1);
305        assert_eq!(r.resolve(Breakpoint::Md), &2);
306        assert_eq!(r.resolve(Breakpoint::Lg), &3);
307        assert_eq!(r.resolve(Breakpoint::Xl), &4);
308    }
309
310    #[test]
311    fn equality() {
312        let r1 = Responsive::new(1).at(Breakpoint::Md, 2);
313        let r2 = Responsive::new(1).at(Breakpoint::Md, 2);
314        assert_eq!(r1, r2);
315    }
316}