Skip to main content

ftui_layout/
visibility.rs

1#![forbid(unsafe_code)]
2
3//! Breakpoint-based visibility helpers.
4//!
5//! [`Visibility`] determines whether a widget should be rendered at a given
6//! breakpoint. Unlike CSS `display: none` vs `visibility: hidden`, these
7//! helpers always reclaim space — a hidden widget produces zero layout area.
8//!
9//! # Usage
10//!
11//! ```ignore
12//! use ftui_layout::{Breakpoint, Visibility};
13//!
14//! // Only visible at Md and above.
15//! let vis = Visibility::visible_above(Breakpoint::Md);
16//! assert!(!vis.is_visible(Breakpoint::Sm));
17//! assert!(vis.is_visible(Breakpoint::Md));
18//! assert!(vis.is_visible(Breakpoint::Lg));
19//!
20//! // Hidden at Xs and Sm only.
21//! let vis = Visibility::hidden_below(Breakpoint::Md);
22//! assert!(!vis.is_visible(Breakpoint::Xs));
23//! assert!(vis.is_visible(Breakpoint::Md));
24//! ```
25//!
26//! # Invariants
27//!
28//! 1. `Always` is visible at every breakpoint.
29//! 2. `Never` is hidden at every breakpoint.
30//! 3. `visible_above(bp)` shows at `bp` and all larger breakpoints.
31//! 4. `visible_below(bp)` shows at `bp` and all smaller breakpoints.
32//! 5. `only(bp)` shows at exactly one breakpoint.
33//! 6. `custom()` allows arbitrary per-breakpoint bitmask.
34//! 7. `filter_rects()` removes rects for hidden widgets (space reclamation).
35//!
36//! # Failure Modes
37//!
38//! None — all operations are infallible.
39
40use super::Breakpoint;
41
42// ---------------------------------------------------------------------------
43// Types
44// ---------------------------------------------------------------------------
45
46/// Breakpoint-aware visibility rule.
47///
48/// Encodes which breakpoints a widget should be visible at.
49/// Use with [`filter_rects`](Self::filter_rects) to reclaim space from
50/// hidden widgets during layout.
51#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub struct Visibility {
53    /// Bitmask: bit i set = visible at Breakpoint with ordinal i.
54    mask: u8,
55}
56
57// ---------------------------------------------------------------------------
58// Constants
59// ---------------------------------------------------------------------------
60
61impl Visibility {
62    /// Visible at all breakpoints.
63    pub const ALWAYS: Self = Self { mask: 0b11111 };
64
65    /// Hidden at all breakpoints.
66    pub const NEVER: Self = Self { mask: 0 };
67}
68
69// ---------------------------------------------------------------------------
70// Construction
71// ---------------------------------------------------------------------------
72
73impl Visibility {
74    /// Visible at the given breakpoint and all larger ones.
75    ///
76    /// Example: `visible_above(Md)` → visible at Md, Lg, Xl.
77    #[must_use]
78    pub const fn visible_above(bp: Breakpoint) -> Self {
79        let idx = bp as u8;
80        // Set bits from idx..=4
81        let mask = 0b11111u8 << idx;
82        Self {
83            mask: mask & 0b11111,
84        }
85    }
86
87    /// Visible at the given breakpoint and all smaller ones.
88    ///
89    /// Example: `visible_below(Md)` → visible at Xs, Sm, Md.
90    #[must_use]
91    pub const fn visible_below(bp: Breakpoint) -> Self {
92        let idx = bp as u8;
93        // Set bits from 0..=idx
94        let mask = (1u8 << (idx + 1)) - 1;
95        Self { mask }
96    }
97
98    /// Visible at exactly one breakpoint.
99    #[must_use]
100    pub const fn only(bp: Breakpoint) -> Self {
101        Self {
102            mask: 1u8 << (bp as u8),
103        }
104    }
105
106    /// Visible at the specified breakpoints.
107    #[must_use]
108    pub fn at(breakpoints: &[Breakpoint]) -> Self {
109        let mut mask = 0u8;
110        for &bp in breakpoints {
111            mask |= 1u8 << (bp as u8);
112        }
113        Self { mask }
114    }
115
116    /// Hidden at the given breakpoint and all smaller ones (visible above).
117    ///
118    /// Example: `hidden_below(Md)` → hidden at Xs, Sm; visible at Md, Lg, Xl.
119    #[must_use]
120    pub const fn hidden_below(bp: Breakpoint) -> Self {
121        Self::visible_above(bp)
122    }
123
124    /// Hidden at the given breakpoint and all larger ones (visible below).
125    ///
126    /// Example: `hidden_above(Md)` → visible at Xs, Sm; hidden at Md, Lg, Xl.
127    #[must_use]
128    pub const fn hidden_above(bp: Breakpoint) -> Self {
129        let idx = bp as u8;
130        // Visible below bp (not including bp)
131        if idx == 0 {
132            return Self::NEVER;
133        }
134        let mask = (1u8 << idx) - 1;
135        Self { mask }
136    }
137
138    /// Create from a raw bitmask (bits 0–4 correspond to Xs–Xl).
139    #[must_use]
140    pub const fn from_mask(mask: u8) -> Self {
141        Self {
142            mask: mask & 0b11111,
143        }
144    }
145}
146
147// ---------------------------------------------------------------------------
148// Queries
149// ---------------------------------------------------------------------------
150
151impl Visibility {
152    /// Whether the widget is visible at the given breakpoint.
153    #[must_use]
154    pub const fn is_visible(self, bp: Breakpoint) -> bool {
155        self.mask & (1u8 << (bp as u8)) != 0
156    }
157
158    /// Whether the widget is hidden at the given breakpoint.
159    #[must_use]
160    pub const fn is_hidden(self, bp: Breakpoint) -> bool {
161        !self.is_visible(bp)
162    }
163
164    /// Whether the widget is always visible (at every breakpoint).
165    #[must_use]
166    pub const fn is_always(self) -> bool {
167        self.mask == 0b11111
168    }
169
170    /// Whether the widget is never visible (hidden at every breakpoint).
171    #[must_use]
172    pub const fn is_never(self) -> bool {
173        self.mask == 0
174    }
175
176    /// The raw bitmask.
177    #[must_use]
178    pub const fn mask(self) -> u8 {
179        self.mask
180    }
181
182    /// Count of breakpoints where this is visible.
183    #[must_use]
184    pub const fn visible_count(self) -> u32 {
185        self.mask.count_ones()
186    }
187
188    /// Iterator over breakpoints where visible.
189    pub fn visible_breakpoints(self) -> impl Iterator<Item = Breakpoint> {
190        Breakpoint::ALL
191            .into_iter()
192            .filter(move |&bp| self.is_visible(bp))
193    }
194}
195
196// ---------------------------------------------------------------------------
197// Layout integration
198// ---------------------------------------------------------------------------
199
200impl Visibility {
201    /// Filter a list of rects, keeping only those whose visibility allows
202    /// the given breakpoint.
203    ///
204    /// Returns `(index, rect)` pairs for visible items. The index is the
205    /// original position in the input, useful for mapping back to widget state.
206    ///
207    /// This achieves space reclamation: hidden widgets don't get any layout area.
208    pub fn filter_rects<'a>(
209        visibilities: &'a [Visibility],
210        rects: &'a [super::Rect],
211        bp: Breakpoint,
212    ) -> Vec<(usize, super::Rect)> {
213        visibilities
214            .iter()
215            .zip(rects.iter())
216            .enumerate()
217            .filter(|(_, (vis, _))| vis.is_visible(bp))
218            .map(|(i, (_, rect))| (i, *rect))
219            .collect()
220    }
221
222    /// Count how many items are visible at a given breakpoint.
223    pub fn count_visible(visibilities: &[Visibility], bp: Breakpoint) -> usize {
224        visibilities.iter().filter(|v| v.is_visible(bp)).count()
225    }
226}
227
228// ---------------------------------------------------------------------------
229// Trait impls
230// ---------------------------------------------------------------------------
231
232impl Default for Visibility {
233    fn default() -> Self {
234        Self::ALWAYS
235    }
236}
237
238impl std::fmt::Display for Visibility {
239    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
240        if self.is_always() {
241            return f.write_str("always");
242        }
243        if self.is_never() {
244            return f.write_str("never");
245        }
246        let mut first = true;
247        for bp in Breakpoint::ALL {
248            if self.is_visible(bp) {
249                if !first {
250                    f.write_str("+")?;
251                }
252                f.write_str(bp.label())?;
253                first = false;
254            }
255        }
256        Ok(())
257    }
258}
259
260// ---------------------------------------------------------------------------
261// Tests
262// ---------------------------------------------------------------------------
263
264#[cfg(test)]
265mod tests {
266    use super::*;
267    use crate::Rect;
268
269    #[test]
270    fn always_visible_at_all() {
271        for bp in Breakpoint::ALL {
272            assert!(Visibility::ALWAYS.is_visible(bp));
273        }
274        assert!(Visibility::ALWAYS.is_always());
275        assert!(!Visibility::ALWAYS.is_never());
276    }
277
278    #[test]
279    fn never_visible_at_none() {
280        for bp in Breakpoint::ALL {
281            assert!(Visibility::NEVER.is_hidden(bp));
282        }
283        assert!(Visibility::NEVER.is_never());
284        assert!(!Visibility::NEVER.is_always());
285    }
286
287    #[test]
288    fn visible_above() {
289        let vis = Visibility::visible_above(Breakpoint::Md);
290        assert!(!vis.is_visible(Breakpoint::Xs));
291        assert!(!vis.is_visible(Breakpoint::Sm));
292        assert!(vis.is_visible(Breakpoint::Md));
293        assert!(vis.is_visible(Breakpoint::Lg));
294        assert!(vis.is_visible(Breakpoint::Xl));
295    }
296
297    #[test]
298    fn visible_above_xs() {
299        let vis = Visibility::visible_above(Breakpoint::Xs);
300        assert!(vis.is_always());
301    }
302
303    #[test]
304    fn visible_above_xl() {
305        let vis = Visibility::visible_above(Breakpoint::Xl);
306        assert!(vis.is_visible(Breakpoint::Xl));
307        assert!(!vis.is_visible(Breakpoint::Lg));
308        assert_eq!(vis.visible_count(), 1);
309    }
310
311    #[test]
312    fn visible_below() {
313        let vis = Visibility::visible_below(Breakpoint::Md);
314        assert!(vis.is_visible(Breakpoint::Xs));
315        assert!(vis.is_visible(Breakpoint::Sm));
316        assert!(vis.is_visible(Breakpoint::Md));
317        assert!(!vis.is_visible(Breakpoint::Lg));
318        assert!(!vis.is_visible(Breakpoint::Xl));
319    }
320
321    #[test]
322    fn visible_below_xl() {
323        let vis = Visibility::visible_below(Breakpoint::Xl);
324        assert!(vis.is_always());
325    }
326
327    #[test]
328    fn visible_below_xs() {
329        let vis = Visibility::visible_below(Breakpoint::Xs);
330        assert!(vis.is_visible(Breakpoint::Xs));
331        assert!(!vis.is_visible(Breakpoint::Sm));
332        assert_eq!(vis.visible_count(), 1);
333    }
334
335    #[test]
336    fn only_single_breakpoint() {
337        let vis = Visibility::only(Breakpoint::Lg);
338        assert!(!vis.is_visible(Breakpoint::Xs));
339        assert!(!vis.is_visible(Breakpoint::Sm));
340        assert!(!vis.is_visible(Breakpoint::Md));
341        assert!(vis.is_visible(Breakpoint::Lg));
342        assert!(!vis.is_visible(Breakpoint::Xl));
343        assert_eq!(vis.visible_count(), 1);
344    }
345
346    #[test]
347    fn at_multiple() {
348        let vis = Visibility::at(&[Breakpoint::Xs, Breakpoint::Lg, Breakpoint::Xl]);
349        assert!(vis.is_visible(Breakpoint::Xs));
350        assert!(!vis.is_visible(Breakpoint::Sm));
351        assert!(!vis.is_visible(Breakpoint::Md));
352        assert!(vis.is_visible(Breakpoint::Lg));
353        assert!(vis.is_visible(Breakpoint::Xl));
354        assert_eq!(vis.visible_count(), 3);
355    }
356
357    #[test]
358    fn hidden_below() {
359        let vis = Visibility::hidden_below(Breakpoint::Md);
360        // Same as visible_above(Md)
361        assert!(!vis.is_visible(Breakpoint::Xs));
362        assert!(!vis.is_visible(Breakpoint::Sm));
363        assert!(vis.is_visible(Breakpoint::Md));
364    }
365
366    #[test]
367    fn hidden_above() {
368        let vis = Visibility::hidden_above(Breakpoint::Md);
369        assert!(vis.is_visible(Breakpoint::Xs));
370        assert!(vis.is_visible(Breakpoint::Sm));
371        assert!(!vis.is_visible(Breakpoint::Md));
372        assert!(!vis.is_visible(Breakpoint::Lg));
373    }
374
375    #[test]
376    fn hidden_above_xs() {
377        let vis = Visibility::hidden_above(Breakpoint::Xs);
378        assert!(vis.is_never());
379    }
380
381    #[test]
382    fn from_mask() {
383        let vis = Visibility::from_mask(0b10101); // Xs, Md, Xl
384        assert!(vis.is_visible(Breakpoint::Xs));
385        assert!(!vis.is_visible(Breakpoint::Sm));
386        assert!(vis.is_visible(Breakpoint::Md));
387        assert!(!vis.is_visible(Breakpoint::Lg));
388        assert!(vis.is_visible(Breakpoint::Xl));
389    }
390
391    #[test]
392    fn from_mask_truncates() {
393        let vis = Visibility::from_mask(0xFF);
394        assert_eq!(vis.mask(), 0b11111);
395    }
396
397    #[test]
398    fn visible_breakpoints_iterator() {
399        let vis = Visibility::at(&[Breakpoint::Sm, Breakpoint::Lg]);
400        let bps: Vec<_> = vis.visible_breakpoints().collect();
401        assert_eq!(bps, vec![Breakpoint::Sm, Breakpoint::Lg]);
402    }
403
404    #[test]
405    fn filter_rects_basic() {
406        let rects = vec![
407            Rect::new(0, 0, 20, 10),
408            Rect::new(20, 0, 30, 10),
409            Rect::new(50, 0, 40, 10),
410        ];
411        let visibilities = vec![
412            Visibility::ALWAYS,
413            Visibility::hidden_below(Breakpoint::Md), // Hidden at Xs, Sm
414            Visibility::ALWAYS,
415        ];
416
417        // At Sm: middle rect hidden
418        let visible = Visibility::filter_rects(&visibilities, &rects, Breakpoint::Sm);
419        assert_eq!(visible.len(), 2);
420        assert_eq!(visible[0].0, 0); // index 0
421        assert_eq!(visible[1].0, 2); // index 2
422
423        // At Md: all visible
424        let visible = Visibility::filter_rects(&visibilities, &rects, Breakpoint::Md);
425        assert_eq!(visible.len(), 3);
426    }
427
428    #[test]
429    fn count_visible_helper() {
430        let visibilities = vec![
431            Visibility::ALWAYS,
432            Visibility::only(Breakpoint::Xl),
433            Visibility::visible_above(Breakpoint::Lg),
434        ];
435
436        assert_eq!(Visibility::count_visible(&visibilities, Breakpoint::Xs), 1);
437        assert_eq!(Visibility::count_visible(&visibilities, Breakpoint::Lg), 2);
438        assert_eq!(Visibility::count_visible(&visibilities, Breakpoint::Xl), 3);
439    }
440
441    #[test]
442    fn default_is_always() {
443        assert_eq!(Visibility::default(), Visibility::ALWAYS);
444    }
445
446    #[test]
447    fn display_always() {
448        assert_eq!(format!("{}", Visibility::ALWAYS), "always");
449    }
450
451    #[test]
452    fn display_never() {
453        assert_eq!(format!("{}", Visibility::NEVER), "never");
454    }
455
456    #[test]
457    fn display_partial() {
458        let vis = Visibility::at(&[Breakpoint::Sm, Breakpoint::Lg]);
459        assert_eq!(format!("{}", vis), "sm+lg");
460    }
461
462    #[test]
463    fn equality() {
464        assert_eq!(
465            Visibility::visible_above(Breakpoint::Md),
466            Visibility::hidden_below(Breakpoint::Md)
467        );
468    }
469
470    #[test]
471    fn clone_independence() {
472        let a = Visibility::only(Breakpoint::Md);
473        let b = a;
474        assert_eq!(a, b);
475    }
476
477    #[test]
478    fn debug_format() {
479        let dbg = format!("{:?}", Visibility::ALWAYS);
480        assert!(dbg.contains("Visibility"));
481    }
482}