Skip to main content

frus_widgets/
visualdensity.rs

1//! [`VisualDensity`]: how compact the controls are (`theme_data.dart:3183`, milestone 631).
2
3use frus_core::TargetPlatform;
4
5/// **How compact the controls are** — the reference's `VisualDensity`.
6///
7/// Two numbers from −4 to 4, one per axis. Each step is four logical pixels on a control's
8/// base size: a button at the compact density (−2, −2) is 8 px shorter than at the
9/// standard one, and its padding loses 8 px across. Zero is the standard density.
10///
11/// A theme carries one ([`Theme::visual_density`](crate::Theme::visual_density)). Unset,
12/// it follows the theme's platform as the reference's does
13/// ([`default_for_platform`](Self::default_for_platform)): standard on phones, compact
14/// on the desktops, where a pointer is precise and a screen holds more.
15#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct VisualDensity {
17    /// Across: negative is narrower.
18    pub horizontal: f32,
19    /// Down: negative is shorter.
20    pub vertical: f32,
21}
22
23impl VisualDensity {
24    /// The smallest a density goes.
25    pub const MINIMUM: f32 = -4.0;
26    /// The largest a density goes.
27    pub const MAXIMUM: f32 = 4.0;
28    /// The design's own size.
29    pub const STANDARD: Self = Self {
30        horizontal: 0.0,
31        vertical: 0.0,
32    };
33    /// A step tighter than standard on both axes.
34    pub const COMFORTABLE: Self = Self {
35        horizontal: -1.0,
36        vertical: -1.0,
37    };
38    /// Two steps tighter on both axes: the desktops' default.
39    pub const COMPACT: Self = Self {
40        horizontal: -2.0,
41        vertical: -2.0,
42    };
43
44    /// A density of `horizontal` and `vertical`, each kept within
45    /// [`MINIMUM`](Self::MINIMUM)..=[`MAXIMUM`](Self::MAXIMUM).
46    pub fn new(horizontal: f32, vertical: f32) -> Self {
47        Self {
48            horizontal: horizontal.clamp(Self::MINIMUM, Self::MAXIMUM),
49            vertical: vertical.clamp(Self::MINIMUM, Self::MAXIMUM),
50        }
51    }
52
53    /// **The density a platform's controls have**: standard on Android, iOS and Fuchsia,
54    /// compact on Linux, macOS and Windows (`theme_data.dart:3252`).
55    pub const fn default_for_platform(platform: TargetPlatform) -> Self {
56        match platform {
57            TargetPlatform::Android | TargetPlatform::Ios | TargetPlatform::Fuchsia => {
58                Self::STANDARD
59            }
60            TargetPlatform::Linux | TargetPlatform::MacOs | TargetPlatform::Windows => {
61                Self::COMPACT
62            }
63        }
64    }
65
66    /// **What it adds to a control's base size**, in logical pixels: four per step, on
67    /// each axis (`theme_data.dart:3307`).
68    pub fn base_size_adjustment(self) -> (f32, f32) {
69        (self.horizontal * 4.0, self.vertical * 4.0)
70    }
71
72    /// **A minimum size, adjusted**: `min_width` and `min_height` moved by the adjustment,
73    /// never below zero nor above `max_width` and `max_height` (`theme_data.dart:3332`).
74    pub fn effective_min_size(
75        self,
76        (min_width, min_height): (f32, f32),
77        (max_width, max_height): (f32, f32),
78    ) -> (f32, f32) {
79        let (dx, dy) = self.base_size_adjustment();
80        (
81            (min_width + dx).clamp(0.0, max_width.max(0.0)),
82            (min_height + dy).clamp(0.0, max_height.max(0.0)),
83        )
84    }
85
86    /// Between `a` and `b` at `t`.
87    pub fn lerp(a: Self, b: Self, t: f32) -> Self {
88        Self::new(
89            a.horizontal + (b.horizontal - a.horizontal) * t,
90            a.vertical + (b.vertical - a.vertical) * t,
91        )
92    }
93}
94
95#[cfg(test)]
96mod tests {
97    use super::VisualDensity;
98    use frus_core::TargetPlatform as P;
99
100    /// **The reference's densities and the platforms that take them.**
101    #[test]
102    fn the_platforms_take_the_reference_s_densities() {
103        for p in [P::Android, P::Ios, P::Fuchsia] {
104            assert_eq!(
105                VisualDensity::default_for_platform(p),
106                VisualDensity::STANDARD
107            );
108        }
109        for p in [P::Linux, P::MacOs, P::Windows] {
110            assert_eq!(
111                VisualDensity::default_for_platform(p),
112                VisualDensity::COMPACT
113            );
114        }
115        assert_eq!(VisualDensity::COMPACT.base_size_adjustment(), (-8.0, -8.0));
116        assert_eq!(
117            VisualDensity::COMFORTABLE.base_size_adjustment(),
118            (-4.0, -4.0)
119        );
120        assert_eq!(VisualDensity::new(9.0, -9.0), VisualDensity::new(4.0, -4.0));
121    }
122
123    /// **A theme's density follows its platform until it is given one**, and a fade between
124    /// two densities passes through the ones between (`theme_data.dart:412`).
125    #[test]
126    fn a_theme_s_density_follows_its_platform() {
127        use crate::Theme;
128        let phone = Theme::light().with_platform(P::Android);
129        let desk = Theme::light().with_platform(P::Windows);
130        assert_eq!(phone.visual_density(), VisualDensity::STANDARD);
131        assert_eq!(desk.visual_density(), VisualDensity::COMPACT);
132        let told = desk.clone().with_visual_density(VisualDensity::COMFORTABLE);
133        assert_eq!(told.visual_density(), VisualDensity::COMFORTABLE);
134        assert_eq!(
135            told.clone().with_platform(P::Ios).visual_density(),
136            VisualDensity::COMFORTABLE,
137            "a density given stays given"
138        );
139        assert_eq!(
140            phone.lerp(&desk, 0.4).visual_density,
141            None,
142            "both follow their platform"
143        );
144        let half = phone.lerp(&told, 0.5).visual_density();
145        assert_eq!(half, VisualDensity::new(-0.5, -0.5));
146    }
147
148    /// **A minimum size moves by four pixels a step and stays in its bounds**: a 64 × 40
149    /// button at the compact density is at least 56 × 32.
150    #[test]
151    fn a_minimum_size_moves_by_four_a_step() {
152        let compact = VisualDensity::COMPACT;
153        assert_eq!(
154            compact.effective_min_size((64.0, 40.0), (f32::INFINITY, f32::INFINITY)),
155            (56.0, 32.0)
156        );
157        assert_eq!(
158            compact.effective_min_size((4.0, 4.0), (f32::INFINITY, f32::INFINITY)),
159            (0.0, 0.0),
160            "never below zero"
161        );
162        assert_eq!(
163            VisualDensity::new(4.0, 4.0).effective_min_size((40.0, 40.0), (50.0, 50.0)),
164            (50.0, 50.0),
165            "never above the maximum"
166        );
167        let half = VisualDensity::lerp(VisualDensity::STANDARD, compact, 0.5);
168        assert_eq!(half, VisualDensity::COMFORTABLE);
169    }
170}