1use crate::geometry::{Frame, Point, Size};
4
5coded_enum! {
6 #[derive(Clone, Copy, Debug, PartialEq, Eq)]
7 pub enum Axis {
8 Horizontal,
9 Vertical,
10 }
11}
12
13coded_enum! {
14 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
16 pub enum Align {
17 #[default]
18 Stretch,
19 Start,
20 Center,
21 End,
22 }
23}
24
25coded_enum! {
26 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
28 pub enum Justify {
29 #[default]
30 Start,
31 Center,
32 End,
33 SpaceBetween,
34 }
35}
36
37coded_enum! {
38 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
41 pub enum Surface {
42 #[default]
44 None,
45 Sidebar,
47 Content,
49 Card,
51 }
52}
53
54coded_enum! {
55 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
57 pub enum Backdrop {
58 #[default]
61 Standard,
62 Translucent,
65 Solid,
67 }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq)]
72#[repr(C)]
73pub struct StackStyle {
74 pub axis: Axis,
75 pub spacing: f32,
76 pub padding: f32,
77 pub align: Align,
78 pub justify: Justify,
79 pub surface: Surface,
80 pub grow: bool,
82 pub width: f32,
84 pub max_width: f32,
87}
88
89impl StackStyle {
90 pub const FIT: f32 = 0.0;
93
94 #[must_use]
95 pub const fn new(axis: Axis) -> Self {
96 Self {
97 axis,
98 spacing: 0.0,
99 padding: 0.0,
100 align: Align::Stretch,
101 justify: Justify::Start,
102 surface: Surface::None,
103 grow: false,
104 width: Self::FIT,
105 max_width: Self::FIT,
106 }
107 }
108
109 #[must_use]
111 pub fn fixed_width(&self) -> Option<f32> {
112 sets_size(self.width)
113 }
114
115 #[must_use]
117 pub fn width_limit(&self) -> Option<f32> {
118 sets_size(self.max_width)
119 }
120}
121
122fn sets_size(points: f32) -> Option<f32> {
123 (points > StackStyle::FIT).then_some(points)
124}
125
126#[derive(Clone, Copy, Debug, PartialEq)]
130#[repr(C)]
131pub struct ImageSize {
132 pub width: f32,
133 pub height: f32,
134}
135
136impl ImageSize {
137 pub const FIT: f32 = 0.0;
139
140 #[must_use]
144 pub fn frame(self, natural: Size) -> Size {
145 let ratio = |along: f32, across: f32| if along > 0.0 { across / along } else { 0.0 };
146 match (sets_size(self.width), sets_size(self.height)) {
147 (Some(width), Some(height)) => Size::new(width, height),
148 (Some(width), None) => Size::new(width, width * ratio(natural.width, natural.height)),
149 (None, Some(height)) => {
150 Size::new(height * ratio(natural.height, natural.width), height)
151 }
152 (None, None) => natural,
153 }
154 }
155
156 #[must_use]
159 pub fn fit(natural: Size, frame: Size) -> Frame {
160 if natural.width <= 0.0 || natural.height <= 0.0 {
161 return Frame::default();
162 }
163 let scale = (frame.width / natural.width).min(frame.height / natural.height);
164 let size = Size::new(natural.width * scale, natural.height * scale);
165 Frame {
166 origin: Point {
167 x: (frame.width - size.width) / 2.0,
168 y: (frame.height - size.height) / 2.0,
169 },
170 size,
171 }
172 }
173}
174
175coded_enum! {
176 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
178 pub enum TextStyle {
179 #[default]
180 Body,
181 Title,
182 Headline,
183 Caption,
184 LargeTitle,
186 }
187}
188
189impl TextStyle {
190 #[must_use]
192 pub const fn default_color(self) -> TextColor {
193 match self {
194 Self::Caption => TextColor::Secondary,
195 Self::Body | Self::Title | Self::Headline | Self::LargeTitle => TextColor::Primary,
196 }
197 }
198}
199
200coded_enum! {
201 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
203 pub enum TextColor {
204 #[default]
205 Primary,
206 Secondary,
207 Tertiary,
208 Accent,
209 Success,
210 Warning,
211 Danger,
212 }
213}
214
215coded_enum! {
216 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
218 pub enum TextAlign {
219 #[default]
220 Start,
221 Center,
222 End,
223 }
224}
225
226#[derive(Clone, Copy, Debug, PartialEq, Eq)]
228#[repr(C)]
229pub struct LabelStyle {
230 pub font: TextStyle,
231 pub color: TextColor,
232 pub align: TextAlign,
233 pub selectable: bool,
235}
236
237impl LabelStyle {
238 #[must_use]
239 pub const fn new(font: TextStyle) -> Self {
240 Self {
241 font,
242 color: font.default_color(),
243 align: TextAlign::Start,
244 selectable: false,
245 }
246 }
247}
248
249impl Default for LabelStyle {
250 fn default() -> Self {
251 Self::new(TextStyle::default())
252 }
253}
254
255coded_enum! {
256 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
257 pub enum ButtonStyle {
258 #[default]
259 Normal,
260 Primary,
262 Plain,
264 Icon,
267 }
268}
269
270coded_enum! {
271 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
273 pub enum ToggleStyle {
274 #[default]
276 Checkbox,
277 Switch,
279 }
280}
281
282coded_enum! {
283 #[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
286 pub enum Icon {
287 #[default]
289 None,
290 Plus,
291 Trash,
292 Star,
293 StarFill,
294 Check,
295 CheckCircle,
296 Circle,
297 Home,
298 Inbox,
299 Archive,
300 List,
301 Calendar,
302 Sun,
303 Flag,
304 Close,
305 Gear,
306 Search,
307 Mail,
308 Send,
309 Folder,
310 Clock,
311 Tag,
312 Person,
313 Document,
314 Download,
315 Heart,
316 Bell,
317 Pencil,
318 Share,
319 Info,
320 }
321}
322
323#[cfg(test)]
324mod tests {
325 use super::*;
326
327 fn round_trips<T: Copy + PartialEq + std::fmt::Debug>(
328 all: &[T],
329 code: fn(T) -> u8,
330 from_code: fn(u8) -> Option<T>,
331 ) {
332 for (index, variant) in all.iter().enumerate() {
333 assert_eq!(usize::from(code(*variant)), index);
334 assert_eq!(from_code(code(*variant)), Some(*variant));
335 }
336 let past_the_end = u8::try_from(all.len()).expect("few variants");
337 assert_eq!(from_code(past_the_end), None);
338 }
339
340 #[test]
341 fn images_keep_their_shape_unless_both_sides_are_fixed() {
342 let natural = Size::new(200.0, 100.0);
343 let size = |width, height| ImageSize { width, height };
344 assert_eq!(size(0.0, 0.0).frame(natural), natural);
345 assert_eq!(size(50.0, 0.0).frame(natural), Size::new(50.0, 25.0));
346 assert_eq!(size(0.0, 50.0).frame(natural), Size::new(100.0, 50.0));
347 assert_eq!(size(30.0, 30.0).frame(natural), Size::new(30.0, 30.0));
348 assert_eq!(size(30.0, 0.0).frame(Size::ZERO), Size::new(30.0, 0.0));
349 assert_eq!(
350 ImageSize::fit(natural, Size::new(50.0, 50.0)),
351 Frame {
352 origin: Point { x: 0.0, y: 12.5 },
353 size: Size::new(50.0, 25.0),
354 }
355 );
356 }
357
358 #[test]
359 fn every_style_round_trips_through_its_code() {
360 round_trips(Axis::ALL, Axis::code, Axis::from_code);
361 round_trips(Align::ALL, Align::code, Align::from_code);
362 round_trips(Justify::ALL, Justify::code, Justify::from_code);
363 round_trips(Surface::ALL, Surface::code, Surface::from_code);
364 round_trips(Backdrop::ALL, Backdrop::code, Backdrop::from_code);
365 round_trips(TextStyle::ALL, TextStyle::code, TextStyle::from_code);
366 round_trips(TextColor::ALL, TextColor::code, TextColor::from_code);
367 round_trips(TextAlign::ALL, TextAlign::code, TextAlign::from_code);
368 round_trips(ButtonStyle::ALL, ButtonStyle::code, ButtonStyle::from_code);
369 round_trips(ToggleStyle::ALL, ToggleStyle::code, ToggleStyle::from_code);
370 round_trips(Icon::ALL, Icon::code, Icon::from_code);
371 }
372}