1use super::*;
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
13pub enum IconButtonWidth {
14 Narrow,
15 #[default]
16 Default,
17 Wide,
18}
19
20impl IconButtonWidth {
21 pub const ALL: [Self; 3] = [Self::Narrow, Self::Default, Self::Wide];
22 pub const fn name(self) -> &'static str {
23 match self {
24 Self::Narrow => "narrow",
25 Self::Default => "default",
26 Self::Wide => "wide",
27 }
28 }
29}
30
31#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
34pub enum FabSize {
35 Small,
37 #[default]
39 Regular,
40 Medium,
42 Large,
44}
45
46impl FabSize {
47 pub const ALL: [Self; 4] = [Self::Small, Self::Regular, Self::Medium, Self::Large];
48 pub const fn name(self) -> &'static str {
49 match self {
50 Self::Small => "small",
51 Self::Regular => "regular",
52 Self::Medium => "medium",
53 Self::Large => "large",
54 }
55 }
56 pub(crate) const fn container(self) -> f32 {
58 match self {
59 Self::Small => 40.,
60 Self::Regular => 56.,
61 Self::Medium => 80.,
62 Self::Large => 96.,
63 }
64 }
65 pub(crate) const fn radius(self) -> f32 {
66 match self {
67 Self::Small => 12.,
68 Self::Regular => 16.,
69 Self::Medium => 20.,
70 Self::Large => 28.,
71 }
72 }
73 pub(crate) const fn icon(self) -> f32 {
74 match self {
75 Self::Small | Self::Regular => 24.,
76 Self::Medium => 28.,
77 Self::Large => 36.,
78 }
79 }
80 pub(crate) const fn extended(self) -> (f32, f32, f32, TypeRole) {
82 match self {
83 Self::Small | Self::Regular => (56., 16., 8., TypeRole::TitleMedium),
84 Self::Medium => (80., 26., 12., TypeRole::TitleLarge),
85 Self::Large => (96., 28., 16., TypeRole::HeadlineSmall),
86 }
87 }
88 pub(crate) const fn extended_size(self) -> Self {
90 match self {
91 Self::Small => Self::Regular,
92 other => other,
93 }
94 }
95}
96
97#[derive(Clone, Copy, Debug, PartialEq, Eq)]
99pub(crate) enum Segment {
100 Leading,
101 Trailing,
102}
103
104#[derive(Clone, Copy, Debug, Default, PartialEq)]
105pub(crate) enum Form {
106 #[default]
107 Label,
108 Icon(IconButtonWidth),
109 Fab(FabSize),
110 ExtendedFab {
112 size: FabSize,
113 expansion: f32,
114 },
115 Segment(Segment),
116 FabMenuToggle {
119 open: bool,
120 size: FabSize,
121 },
122 FabMenuItem {
124 expansion: f32,
125 },
126}
127
128pub(crate) const FAB_MENU_ITEM: (f32, f32, f32) = (56., 24., 8.);
130
131#[derive(Clone, Copy, Debug, PartialEq)]
133pub(crate) struct Roles {
134 pub container: Option<ColorRole>,
135 pub content: ColorRole,
136}
137
138#[derive(Clone, Copy, Debug, PartialEq)]
141pub(crate) struct Palette {
142 pub rest: Roles,
143 pub selected: Option<Roles>,
145 pub disabled_container: bool,
147 pub elevation: (u8, u8),
149}
150
151impl Palette {
152 pub(crate) fn targets(
153 &self,
154 disabled: bool,
155 toggle: bool,
156 selected: bool,
157 ) -> (CssColor, CssColor, CssColor) {
158 use ColorRole as C;
159 if disabled {
160 return (
161 if self.disabled_container {
162 CssColor::alpha_role(C::ON_SURFACE, 0.10)
163 } else {
164 CssColor::transparent()
165 },
166 CssColor::alpha_role(C::ON_SURFACE, state::DISABLED_CONTENT),
167 CssColor::transparent(),
168 );
169 }
170 let roles = match self.selected {
171 Some(roles) if toggle && selected => roles,
172 _ => self.rest,
173 };
174 (
175 roles
176 .container
177 .map_or_else(CssColor::transparent, CssColor::role),
178 CssColor::role(roles.content),
179 CssColor::transparent(),
180 )
181 }
182
183 pub(crate) fn shadow_level(&self, disabled: bool, hovered: bool, active: bool) -> u8 {
184 if disabled {
185 0
186 } else if hovered && !active {
187 self.elevation.1
188 } else {
189 self.elevation.0
190 }
191 }
192}
193
194pub(crate) const fn icon_button_metrics(size: ButtonSize) -> (f32, [f32; 3]) {
196 match size {
197 ButtonSize::ExtraSmall => (20., [28., 32., 40.]),
198 ButtonSize::Small => (24., [32., 40., 52.]),
199 ButtonSize::Medium => (24., [48., 56., 72.]),
200 ButtonSize::Large => (32., [64., 96., 128.]),
201 ButtonSize::ExtraLarge => (40., [104., 136., 184.]),
202 }
203}
204
205pub(crate) const fn icon_button_width(size: ButtonSize, width: IconButtonWidth) -> f32 {
206 let (_, widths) = icon_button_metrics(size);
207 widths[match width {
208 IconButtonWidth::Narrow => 0,
209 IconButtonWidth::Default => 1,
210 IconButtonWidth::Wide => 2,
211 }]
212}
213
214#[derive(Clone, Copy, Debug, PartialEq)]
216pub(crate) struct SegmentMetrics {
217 pub inner: f32,
219 pub inner_active: f32,
220 pub leading_padding: (f32, f32),
222 pub trailing_width: f32,
225 pub trailing_icon: f32,
226 pub trailing_padding: (f32, f32),
227}
228
229pub(crate) const fn segment_metrics(size: ButtonSize) -> SegmentMetrics {
231 let (inner, inner_active, leading_padding, trailing_width, trailing_icon, trailing_padding) =
232 match size {
233 ButtonSize::ExtraSmall => (4., 8., (12., 10.), 48., 22., (14., 12.)),
234 ButtonSize::Small => (4., 12., (16., 12.), 48., 22., (14., 12.)),
235 ButtonSize::Medium => (4., 12., (24., 24.), 56., 26., (17., 13.)),
236 ButtonSize::Large => (8., 20., (48., 48.), 96., 38., (32., 26.)),
237 ButtonSize::ExtraLarge => (12., 20., (64., 64.), 136., 50., (49., 37.)),
238 };
239 SegmentMetrics {
240 inner,
241 inner_active,
242 leading_padding,
243 trailing_width,
244 trailing_icon,
245 trailing_padding,
246 }
247}
248
249#[derive(Clone, Copy, Debug, PartialEq)]
251pub(crate) struct FormGeometry {
252 pub height: f32,
253 pub width: Option<f32>,
254 pub min_width: f32,
255 pub padding: (f32, f32),
256 pub gap: f32,
257 pub icon: f32,
258 pub typography: TypeRole,
259 pub outline: f32,
260}
261
262impl Form {
263 pub(crate) fn sized_shape(self) -> bool {
266 matches!(self, Form::Label | Form::Icon(_))
267 }
268
269 pub(crate) fn geometry(self, size: ButtonSize, direction: ButtonDirection) -> FormGeometry {
270 let g = size.geometry();
271 let label = FormGeometry {
272 height: g.height,
273 width: None,
274 min_width: 58.,
275 padding: horizontal_padding(size),
276 gap: g.gap,
277 icon: size.icon_size(),
278 typography: g.typography,
279 outline: g.outline,
280 };
281 match self {
282 Form::Label => label,
283 Form::Icon(width) => FormGeometry {
284 width: Some(icon_button_width(size, width)),
285 min_width: 0.,
286 padding: (0., 0.),
287 gap: 0.,
288 icon: icon_button_metrics(size).0,
289 ..label
290 },
291 Form::Fab(fab) => FormGeometry {
292 height: fab.container(),
293 width: Some(fab.container()),
294 min_width: 0.,
295 padding: (0., 0.),
296 gap: 0.,
297 icon: fab.icon(),
298 typography: TypeRole::LabelLarge,
299 outline: 0.,
300 },
301 Form::ExtendedFab {
302 size: fab,
303 expansion,
304 } => {
305 let fab = fab.extended_size();
306 let (height, padding, gap, typography) = fab.extended();
307 let t = expansion.clamp(0., 1.);
308 let collapsed = (height - fab.icon()) / 2.;
310 let pad = collapsed + (padding - collapsed) * t;
311 FormGeometry {
312 height,
313 width: None,
314 min_width: height,
315 padding: (pad, pad),
316 gap: gap * t,
317 icon: fab.icon(),
318 typography,
319 outline: 0.,
320 }
321 }
322 Form::Segment(Segment::Leading) => {
323 let (start, inner) = segment_metrics(size).leading_padding;
324 FormGeometry {
325 min_width: 0.,
326 padding: if direction == ButtonDirection::Rtl {
327 (inner, start)
328 } else {
329 (start, inner)
330 },
331 outline: 1.,
332 ..label
333 }
334 }
335 Form::FabMenuToggle { size: fab, .. } => Form::Fab(fab).geometry(size, direction),
336 Form::FabMenuItem { expansion } => {
337 let (height, padding, gap) = FAB_MENU_ITEM;
338 let t = expansion.clamp(0., 1.);
339 let collapsed = (height - 24.) / 2.;
340 let pad = collapsed + (padding - collapsed) * t;
341 FormGeometry {
342 height,
343 width: None,
344 min_width: height,
345 padding: (pad, pad),
346 gap: gap * t,
347 icon: 24.,
348 typography: TypeRole::TitleMedium,
349 outline: 0.,
350 }
351 }
352 Form::Segment(Segment::Trailing) => {
353 let metrics = segment_metrics(size);
354 let (outer, inner) = metrics.trailing_padding;
355 FormGeometry {
356 width: Some(metrics.trailing_width),
357 min_width: 0.,
358 padding: if direction == ButtonDirection::Rtl {
361 (outer, inner)
362 } else {
363 (inner, outer)
364 },
365 outline: 1.,
366 gap: 0.,
367 icon: metrics.trailing_icon,
368 ..label
369 }
370 }
371 }
372 }
373
374 pub(crate) fn fixed_radius(self) -> Option<f32> {
376 match self {
377 Form::Fab(fab) => Some(fab.radius()),
378 Form::FabMenuToggle { open: false, size } => Some(size.radius()),
379 Form::FabMenuToggle { open: true, size } => Some(size.container() / 2.),
380 Form::FabMenuItem { .. } => Some(FAB_MENU_ITEM.0 / 2.),
381 Form::ExtendedFab { size, .. } => Some(size.extended_size().radius()),
382 _ => None,
383 }
384 }
385}
386
387#[cfg(test)]
388mod tests {
389 use super::*;
390
391 #[core::prelude::v1::test]
392 fn icon_button_widths_follow_the_expressive_tokens() {
393 assert_eq!(
394 ButtonSize::ALL.map(|size| icon_button_width(size, IconButtonWidth::Default)),
395 [32., 40., 56., 96., 136.],
396 "default icon buttons are square"
397 );
398 assert_eq!(
399 icon_button_width(ButtonSize::Small, IconButtonWidth::Narrow),
400 32.
401 );
402 assert_eq!(
403 icon_button_width(ButtonSize::ExtraLarge, IconButtonWidth::Wide),
404 184.
405 );
406 let g =
407 Form::Icon(IconButtonWidth::Wide).geometry(ButtonSize::Medium, ButtonDirection::Ltr);
408 assert_eq!(
409 (g.height, g.width, g.padding, g.icon),
410 (56., Some(72.), (0., 0.), 24.)
411 );
412 }
413
414 #[core::prelude::v1::test]
415 fn fab_sizes_and_extended_expansion() {
416 assert_eq!(FabSize::ALL.map(FabSize::container), [40., 56., 80., 96.]);
417 assert_eq!(FabSize::ALL.map(FabSize::radius), [12., 16., 20., 28.]);
418 let geometry = |expansion| {
419 Form::ExtendedFab {
420 size: FabSize::Medium,
421 expansion,
422 }
423 .geometry(ButtonSize::Small, ButtonDirection::Ltr)
424 };
425 let collapsed = geometry(0.);
426 assert_eq!(
427 collapsed.padding,
428 (26., 26.),
429 "a centered 28dp icon in 80dp"
430 );
431 assert_eq!(collapsed.gap, 0.);
432 assert_eq!(collapsed.padding.0 * 2. + collapsed.icon, 80.);
433 let extended = geometry(1.);
434 assert_eq!((extended.padding, extended.gap), ((26., 26.), 12.));
435 assert_eq!(geometry(0.5).gap, 6.);
436 }
437
438 #[core::prelude::v1::test]
439 fn standard_palette_selects_primary_and_drops_container_when_disabled() {
440 let palette = Palette {
441 rest: Roles {
442 container: None,
443 content: ColorRole::ON_SURFACE_VARIANT,
444 },
445 selected: Some(Roles {
446 container: None,
447 content: ColorRole::PRIMARY,
448 }),
449 disabled_container: false,
450 elevation: (0, 0),
451 };
452 let resolve = |(bg, fg, _): (CssColor, CssColor, CssColor)| (bg.resolve(), fg.resolve());
453 let (bg, fg) = resolve(palette.targets(false, true, true));
454 assert_eq!(bg.a, 0.);
455 assert_eq!(fg, crate::theme::color(ColorRole::PRIMARY));
456 let (_, fg) = resolve(palette.targets(false, false, true));
457 assert_eq!(
458 fg,
459 crate::theme::color(ColorRole::ON_SURFACE_VARIANT),
460 "selection needs toggle semantics"
461 );
462 let (bg, fg) = resolve(palette.targets(true, true, true));
463 assert_eq!(bg.a, 0.);
464 let ink = crate::theme::color(ColorRole::ON_SURFACE);
465 assert!((fg.a - ink.a * state::DISABLED_CONTENT).abs() < 1e-4);
466 }
467
468 #[core::prelude::v1::test]
469 fn fab_elevation_rises_on_hover_only() {
470 let palette = Palette {
471 rest: Roles {
472 container: Some(ColorRole::PRIMARY_CONTAINER),
473 content: ColorRole::ON_PRIMARY_CONTAINER,
474 },
475 selected: None,
476 disabled_container: true,
477 elevation: (3, 4),
478 };
479 assert_eq!(palette.shadow_level(false, false, false), 3);
480 assert_eq!(palette.shadow_level(false, true, false), 4);
481 assert_eq!(palette.shadow_level(false, true, true), 3);
482 assert_eq!(palette.shadow_level(true, true, false), 0);
483 }
484
485 #[core::prelude::v1::test]
486 fn split_segments_share_the_button_height() {
487 for size in ButtonSize::ALL {
488 let leading = Form::Segment(Segment::Leading).geometry(size, ButtonDirection::Ltr);
489 let trailing = Form::Segment(Segment::Trailing).geometry(size, ButtonDirection::Ltr);
490 assert_eq!(leading.height, trailing.height);
491 assert_eq!(leading.padding, segment_metrics(size).leading_padding);
492 assert_eq!((leading.outline, trailing.outline), (1., 1.));
493 let m = segment_metrics(size);
494 assert!(m.inner < m.inner_active);
495 assert_eq!(
496 trailing.padding.0 + trailing.padding.1 + trailing.icon,
497 m.trailing_width,
498 "the chevron fills its token box"
499 );
500 }
501 }
502}