frus_widgets/
visualdensity.rs1use frus_core::TargetPlatform;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
16pub struct VisualDensity {
17 pub horizontal: f32,
19 pub vertical: f32,
21}
22
23impl VisualDensity {
24 pub const MINIMUM: f32 = -4.0;
26 pub const MAXIMUM: f32 = 4.0;
28 pub const STANDARD: Self = Self {
30 horizontal: 0.0,
31 vertical: 0.0,
32 };
33 pub const COMFORTABLE: Self = Self {
35 horizontal: -1.0,
36 vertical: -1.0,
37 };
38 pub const COMPACT: Self = Self {
40 horizontal: -2.0,
41 vertical: -2.0,
42 };
43
44 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 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 pub fn base_size_adjustment(self) -> (f32, f32) {
69 (self.horizontal * 4.0, self.vertical * 4.0)
70 }
71
72 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 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 #[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 #[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 #[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}