1#![allow(non_snake_case)]
2
3use std::cell::Cell;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6use web_time::Duration;
7
8use repose_core::animation::{AnimationSpec, CubicBezier, Easing, KeyframesSpec, RepeatableSpec};
9use repose_core::*;
10use repose_ui::anim::{animate_color, animate_f32};
11use repose_ui::{Box, Column, Row, Text, TextStyle, ViewExt};
12
13use super::*;
14
15use crate::ripple::{ripple, RippleConfig};
16use crate::{Icon, Symbol};
17
18#[derive(Clone, Copy, Debug)]
20pub struct TopAppBarColors {
21 pub container_color: Color,
22 pub navigation_icon_content_color: Color,
23 pub title_content_color: Color,
24 pub action_icon_content_color: Color,
25}
26
27impl Default for TopAppBarColors {
28 fn default() -> Self {
29 Self {
30 container_color: TopAppBarDefaults::container_color(),
31 navigation_icon_content_color: TopAppBarDefaults::navigation_icon_content_color(),
32 title_content_color: TopAppBarDefaults::title_content_color(),
33 action_icon_content_color: TopAppBarDefaults::action_icon_content_color(),
34 }
35 }
36}
37
38#[derive(Clone, Debug)]
40pub struct TopAppBarConfig {
41 pub modifier: Modifier,
42 pub colors: TopAppBarColors,
43 pub height: f32,
44 pub window_insets: WindowInsets,
45 pub content_padding: PaddingValues,
46}
47
48#[derive(Clone, Copy, Debug)]
50pub struct WindowInsets {
51 pub top: f32,
52 pub bottom: f32,
53 pub left: f32,
54 pub right: f32,
55}
56
57impl Default for WindowInsets {
58 fn default() -> Self {
59 Self {
60 top: 0.0,
61 bottom: 0.0,
62 left: 0.0,
63 right: 0.0,
64 }
65 }
66}
67
68impl Default for TopAppBarConfig {
69 fn default() -> Self {
70 Self {
71 modifier: Modifier::new(),
72 colors: TopAppBarColors::default(),
73 height: TopAppBarDefaults::HEIGHT,
74 window_insets: WindowInsets::default(),
75 content_padding: PaddingValues {
76 left: 4.0,
77 right: 4.0,
78 top: 0.0,
79 bottom: 0.0,
80 },
81 }
82 }
83}
84
85fn top_app_bar_layout(
86 title: View,
87 navigation_icon: Option<View>,
88 actions: Vec<View>,
89 config: TopAppBarConfig,
90 centered: bool,
91) -> View {
92 let insets = config.window_insets;
93 let mut m = Modifier::new()
94 .min_width(200.0)
95 .height(config.height + insets.top)
96 .background(config.colors.container_color)
97 .padding_values(PaddingValues {
98 left: config.content_padding.left + insets.left,
99 right: config.content_padding.right + insets.right,
100 top: config.content_padding.top + insets.top,
101 bottom: config.content_padding.bottom + insets.bottom,
102 })
103 .align_items(AlignItems::Center)
104 .then(config.modifier);
105 if centered {
106 m = m.justify_content(JustifyContent::Center);
107 }
108 Row(m).child((
109 navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
110 Box(Modifier::new()
111 .padding_values(PaddingValues {
112 left: 16.0,
113 right: if centered { 0.0 } else { 0.0 },
114 top: 0.0,
115 bottom: 0.0,
116 })
117 .flex_grow(1.0))
118 .child(with_content_color(
119 config.colors.title_content_color,
120 || title,
121 )),
122 Row(Modifier::new()
123 .align_items(AlignItems::Center)
124 .clip_rounded(20.0))
125 .child(
126 actions
127 .into_iter()
128 .map(|a| {
129 with_content_color(config.colors.action_icon_content_color, move || a.clone())
130 })
131 .collect::<Vec<_>>(),
132 ),
133 ))
134}
135
136pub fn TopAppBar(
139 title: View,
140 navigation_icon: Option<View>,
141 actions: Vec<View>,
142 config: TopAppBarConfig,
143) -> View {
144 top_app_bar_layout(title, navigation_icon, actions, config, false)
145}
146
147pub fn CenterAlignedTopAppBar(
149 title: View,
150 navigation_icon: Option<View>,
151 actions: Vec<View>,
152 config: TopAppBarConfig,
153) -> View {
154 top_app_bar_layout(title, navigation_icon, actions, config, true)
155}
156
157#[derive(Clone, Debug)]
159pub struct SurfaceConfig {
160 pub modifier: Modifier,
161 pub color: Color,
162 pub content_color: Color,
163 pub shape_radius: f32,
164 pub tonal_elevation: f32,
165 pub shadow_elevation: f32,
166 pub border: Option<(f32, Color)>,
167}
168
169impl Default for SurfaceConfig {
170 fn default() -> Self {
171 Self {
172 modifier: Modifier::new(),
173 color: SurfaceDefaults::color(),
174 content_color: SurfaceDefaults::content_color(),
175 shape_radius: SurfaceDefaults::SHAPE_RADIUS,
176 tonal_elevation: SurfaceDefaults::TONAL_ELEVATION,
177 shadow_elevation: SurfaceDefaults::SHADOW_ELEVATION,
178 border: None,
179 }
180 }
181}
182
183pub fn Surface(config: SurfaceConfig, content: impl FnOnce() -> View) -> View {
186 let mut m = Modifier::new()
187 .background(config.color)
188 .clip_rounded(config.shape_radius)
189 .then(config.modifier);
190 if config.tonal_elevation > 0.0 {
191 m = m.state_elevation(StateElevation {
192 default: config.tonal_elevation,
193 hovered: config.tonal_elevation,
194 pressed: config.tonal_elevation,
195 disabled: 0.0,
196 });
197 }
198 if config.shadow_elevation > 0.0 {
199 m = m.shadow(config.shadow_elevation, 0.0);
200 }
201 if let Some((w, c)) = config.border {
202 m = m.border(w, c, config.shape_radius);
203 }
204 Box(m).color(config.content_color).child(content())
205}
206
207#[derive(Clone, Copy, Debug)]
209pub struct IconButtonColors {
210 pub container_color: Color,
211 pub content_color: Color,
212 pub disabled_container_color: Color,
213 pub disabled_content_color: Color,
214}
215
216impl IconButtonColors {
217 pub fn container(&self, enabled: bool) -> Color {
218 if enabled {
219 self.container_color
220 } else {
221 self.disabled_container_color
222 }
223 }
224 pub fn content(&self, enabled: bool) -> Color {
225 if enabled {
226 self.content_color
227 } else {
228 self.disabled_content_color
229 }
230 }
231}
232
233#[derive(Clone, Debug)]
235pub struct IconButtonConfig {
236 pub modifier: Modifier,
237 pub enabled: bool,
238 pub colors: IconButtonColors,
239 pub container_size: Option<f32>,
240}
241
242impl Default for IconButtonConfig {
243 fn default() -> Self {
244 Self {
245 modifier: Modifier::new(),
246 enabled: true,
247 colors: IconButtonColors {
248 container_color: Color::TRANSPARENT,
249 content_color: IconButtonDefaults::content_color(),
250 disabled_container_color: Color::TRANSPARENT,
251 disabled_content_color: IconButtonDefaults::disabled_content_color(),
252 },
253 container_size: None,
254 }
255 }
256}
257
258fn icon_button_render(
259 icon: View,
260 on_click: impl Fn() + 'static,
261 config: &IconButtonConfig,
262 sz: f32,
263 bg: Option<Color>,
264 bdr: Option<(f32, Color)>,
265 state_colors: StateColors,
266) -> View {
267 let is_enabled = config.enabled;
268 let content_color = config.colors.content(is_enabled);
269 let mut m = Modifier::new()
270 .size(sz, sz)
271 .clip_rounded(sz * 0.5)
272 .state_colors(state_colors)
273 .align_items(AlignItems::Center)
274 .justify_content(JustifyContent::Center)
275 .then(config.modifier.clone());
276
277 if let Some(bg_color) = bg {
278 m = m.background(bg_color);
279 }
280 if let Some((w, c)) = bdr {
281 m = m.border(w, c, sz * 0.5);
282 }
283 if is_enabled {
284 m = m.clickable().on_pointer_down(move |_| on_click());
285 }
286
287 Box(m).child(icon)
288}
289
290pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
292 let th = theme();
293 let sz = config
294 .container_size
295 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
296 icon_button_render(
297 icon,
298 on_click,
299 &config,
300 sz,
301 None,
302 None,
303 StateColors {
304 default: Color::TRANSPARENT,
305 hovered: th.on_surface.with_alpha_f32(0.08),
306 pressed: th.on_surface.with_alpha_f32(0.12),
307 disabled: Color::TRANSPARENT,
308 },
309 )
310}
311
312pub fn FilledIconButton(
314 icon: View,
315 on_click: impl Fn() + 'static,
316 config: IconButtonConfig,
317) -> View {
318 let th = theme();
319 let is_enabled = config.enabled;
320 let sz = config
321 .container_size
322 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
323 let bg = config.colors.container(is_enabled);
324 let content_color = config.colors.content(is_enabled);
325 icon_button_render(
326 icon,
327 on_click,
328 &config,
329 sz,
330 Some(bg),
331 None,
332 StateColors {
333 default: Color::TRANSPARENT,
334 hovered: content_color.with_alpha_f32(0.08),
335 pressed: content_color.with_alpha_f32(0.12),
336 disabled: th.on_surface.with_alpha_f32(0.12),
337 },
338 )
339}
340
341pub fn FilledTonalIconButton(
343 icon: View,
344 on_click: impl Fn() + 'static,
345 config: IconButtonConfig,
346) -> View {
347 let th = theme();
348 let is_enabled = config.enabled;
349 let sz = config
350 .container_size
351 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
352 let bg = config.colors.container(is_enabled);
353 let content_color = config.colors.content(is_enabled);
354 icon_button_render(
355 icon,
356 on_click,
357 &config,
358 sz,
359 Some(bg),
360 None,
361 StateColors {
362 default: Color::TRANSPARENT,
363 hovered: content_color.with_alpha_f32(0.08),
364 pressed: content_color.with_alpha_f32(0.12),
365 disabled: th.on_surface.with_alpha_f32(0.12),
366 },
367 )
368}
369
370pub fn OutlinedIconButton(
372 icon: View,
373 on_click: impl Fn() + 'static,
374 config: IconButtonConfig,
375) -> View {
376 let th = theme();
377 let sz = config
378 .container_size
379 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
380 let border_color = if config.enabled {
381 th.outline
382 } else {
383 th.on_surface.with_alpha_f32(0.12)
384 };
385 icon_button_render(
386 icon,
387 on_click,
388 &config,
389 sz,
390 None,
391 Some((1.0, border_color)),
392 StateColors {
393 default: Color::TRANSPARENT,
394 hovered: th.on_surface.with_alpha_f32(0.08),
395 pressed: th.on_surface.with_alpha_f32(0.12),
396 disabled: Color::TRANSPARENT,
397 },
398 )
399}
400
401#[derive(Clone, Copy, Debug)]
403pub struct ButtonColors {
404 pub container_color: Color,
405 pub content_color: Color,
406 pub disabled_container_color: Color,
407 pub disabled_content_color: Color,
408}
409
410impl ButtonColors {
411 pub fn container(&self, enabled: bool) -> Color {
412 if enabled {
413 self.container_color
414 } else {
415 self.disabled_container_color
416 }
417 }
418 pub fn content(&self, enabled: bool) -> Color {
419 if enabled {
420 self.content_color
421 } else {
422 self.disabled_content_color
423 }
424 }
425}
426
427#[derive(Clone, Copy, Debug)]
429pub struct ButtonElevation {
430 pub default: f32,
431 pub pressed: f32,
432 pub focused: f32,
433 pub hovered: f32,
434 pub disabled: f32,
435}
436
437#[derive(Clone, Debug)]
439pub struct ButtonConfig {
440 pub modifier: Modifier,
441 pub enabled: bool,
442 pub content_color: Option<Color>,
443 pub container_color: Option<Color>,
444 pub state_colors: StateColors,
445 pub state_elevation: Option<StateElevation>,
446 pub border: Option<(f32, Color, f32)>,
447 pub shape_radius: f32,
448 pub content_padding: Option<PaddingValues>,
449 pub height: f32,
450 pub colors: Option<ButtonColors>,
451 pub elevation: Option<ButtonElevation>,
452}
453
454impl Default for ButtonConfig {
455 fn default() -> Self {
456 Self {
457 modifier: Modifier::new(),
458 enabled: true,
459 content_color: None,
460 container_color: None,
461 state_colors: ButtonDefaults::state_colors_default(),
462 state_elevation: None,
463 border: None,
464 shape_radius: ButtonDefaults::SHAPE_RADIUS,
465 content_padding: None,
466 height: ButtonDefaults::HEIGHT,
467 colors: None,
468 elevation: None,
469 }
470 }
471}
472
473fn resolve_button_colors(
476 config: &ButtonConfig,
477 def: ButtonColors,
478) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
479 if let Some(colors) = &config.colors {
480 let bg = if config.enabled {
481 colors.container_color
482 } else {
483 colors.disabled_container_color
484 };
485 let cc = if config.enabled {
486 colors.content_color
487 } else {
488 colors.disabled_content_color
489 };
490 let sc = StateColors {
491 default: Color::TRANSPARENT,
492 hovered: colors.content_color.with_alpha_f32(0.08),
493 pressed: colors.content_color.with_alpha_f32(0.12),
494 disabled: Color::TRANSPARENT,
495 };
496 let se = config.elevation.map(|e| StateElevation {
497 default: e.default,
498 hovered: e.hovered,
499 pressed: e.pressed,
500 disabled: e.disabled,
501 });
502 (cc, Some(bg), sc, se)
503 } else {
504 let cc = config.content_color.unwrap_or(def.content_color);
505 let bg = Some(config.container_color.unwrap_or(def.container_color));
506 let sc = if config.enabled {
507 config.state_colors
508 } else {
509 StateColors {
510 default: Color::TRANSPARENT,
511 hovered: Color::TRANSPARENT,
512 pressed: Color::TRANSPARENT,
513 disabled: config.state_colors.disabled,
514 }
515 };
516 let se = config.state_elevation;
517 (cc, bg, sc, se)
518 }
519}
520
521fn button_impl(
522 outer_modifier: Modifier,
523 on_click: impl Fn() + 'static,
524 content: impl FnOnce() -> View,
525 content_color: Color,
526 container_color: Option<Color>,
527 state_colors: StateColors,
528 state_elevation: Option<StateElevation>,
529 border: Option<(f32, Color, f32)>,
530 padding_left: f32,
531 padding_right: f32,
532 height: f32,
533 shape_radius: f32,
534 enabled: bool,
535) -> View {
536 let mut m = Modifier::new().height(height).min_width(48.0);
537 if let Some(bg) = container_color {
538 m = m.background(bg);
539 }
540 m = m.state_colors(if enabled {
541 state_colors
542 } else {
543 StateColors {
544 default: Color::TRANSPARENT,
545 hovered: Color::TRANSPARENT,
546 pressed: Color::TRANSPARENT,
547 disabled: state_colors.disabled,
548 }
549 });
550 if let Some(se) = state_elevation {
551 m = m.state_elevation(se);
552 }
553 if let Some((w, c, r)) = border {
554 m = m.border(w, c, r);
555 }
556 m = m
557 .clip_rounded(shape_radius)
558 .padding_values(PaddingValues {
559 left: padding_left,
560 right: padding_right,
561 top: 0.0,
562 bottom: 0.0,
563 })
564 .align_items(AlignItems::Center)
565 .justify_content(JustifyContent::Center);
566
567 let source = remember(MutableInteractionSource::new);
569 m = m.interaction_source(&*source);
570 m = m.indication(ripple(RippleConfig {
571 color: Some(content_color),
572 bounded: true,
573 ..Default::default()
574 }));
575
576 if enabled {
577 m = m.clickable().on_pointer_down(move |_| on_click());
578 }
579 m = m.then(outer_modifier);
580 let effective = if enabled {
581 content_color
582 } else {
583 content_color.with_alpha_f32(0.38)
584 };
585 let content = with_content_color(effective, content);
586 Box(m).child(content)
587}
588
589pub fn Button(
592 modifier: Modifier,
593 on_click: impl Fn() + 'static,
594 config: ButtonConfig,
595 content: impl FnOnce() -> View,
596) -> View {
597 let def = ButtonColors {
598 container_color: ButtonDefaults::container_color(),
599 content_color: ButtonDefaults::content_color(),
600 disabled_container_color: ButtonDefaults::container_color()
601 .with_alpha_f32(0.12)
602 .composite_over(theme().surface_container_low),
603 disabled_content_color: ButtonDefaults::content_color().with_alpha_f32(0.38),
604 };
605 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
606 let pad = config.content_padding.unwrap_or(PaddingValues {
607 left: 24.0,
608 right: 24.0,
609 top: 0.0,
610 bottom: 0.0,
611 });
612 button_impl(
613 modifier.then(config.modifier),
614 on_click,
615 content,
616 cc,
617 bg,
618 sc,
619 se.or(Some(ButtonDefaults::state_elevation_default())),
620 config.border,
621 pad.left,
622 pad.right,
623 config.height,
624 config.shape_radius,
625 config.enabled,
626 )
627}
628
629pub fn FilledTonalButton(
631 modifier: Modifier,
632 on_click: impl Fn() + 'static,
633 config: ButtonConfig,
634 content: impl FnOnce() -> View,
635) -> View {
636 let th = theme();
637 let def = ButtonColors {
638 container_color: ButtonDefaults::tonal_container_color(),
639 content_color: ButtonDefaults::tonal_content_color(),
640 disabled_container_color: th
641 .on_surface
642 .with_alpha_f32(0.12)
643 .composite_over(th.surface_container_low),
644 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
645 };
646 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
647 let pad = config.content_padding.unwrap_or(PaddingValues {
648 left: 24.0,
649 right: 24.0,
650 top: 0.0,
651 bottom: 0.0,
652 });
653 button_impl(
654 modifier.then(config.modifier),
655 on_click,
656 content,
657 cc,
658 bg,
659 sc,
660 se.or(Some(ButtonDefaults::state_elevation_default())),
661 config.border,
662 pad.left,
663 pad.right,
664 config.height,
665 config.shape_radius,
666 config.enabled,
667 )
668}
669
670pub fn OutlinedButton(
672 modifier: Modifier,
673 on_click: impl Fn() + 'static,
674 config: ButtonConfig,
675 content: impl FnOnce() -> View,
676) -> View {
677 let th = theme();
678 let def = ButtonColors {
679 container_color: Color::TRANSPARENT,
680 content_color: ButtonDefaults::outlined_content_color(),
681 disabled_container_color: Color::TRANSPARENT,
682 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
683 };
684 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
685 let border = config
686 .border
687 .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
688 let pad = config.content_padding.unwrap_or(PaddingValues {
689 left: 24.0,
690 right: 24.0,
691 top: 0.0,
692 bottom: 0.0,
693 });
694 button_impl(
695 modifier.then(config.modifier),
696 on_click,
697 content,
698 cc,
699 bg,
700 sc,
701 se,
702 Some(border),
703 pad.left,
704 pad.right,
705 config.height,
706 config.shape_radius,
707 config.enabled,
708 )
709}
710
711pub fn TextButton(
713 modifier: Modifier,
714 on_click: impl Fn() + 'static,
715 config: ButtonConfig,
716 content: impl FnOnce() -> View,
717) -> View {
718 let th = theme();
719 let def = ButtonColors {
720 container_color: Color::TRANSPARENT,
721 content_color: ButtonDefaults::text_content_color(),
722 disabled_container_color: Color::TRANSPARENT,
723 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
724 };
725 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
726 let pad = config.content_padding.unwrap_or(PaddingValues {
727 left: 12.0,
728 right: 12.0,
729 top: 0.0,
730 bottom: 0.0,
731 });
732 button_impl(
733 modifier.then(config.modifier),
734 on_click,
735 content,
736 cc,
737 bg,
738 sc,
739 se,
740 None,
741 pad.left,
742 pad.right,
743 config.height,
744 config.shape_radius,
745 config.enabled,
746 )
747}
748
749pub fn ElevatedButton(
751 modifier: Modifier,
752 on_click: impl Fn() + 'static,
753 config: ButtonConfig,
754 content: impl FnOnce() -> View,
755) -> View {
756 let th = theme();
757 let def = ButtonColors {
758 container_color: ButtonDefaults::elevated_container_color(),
759 content_color: ButtonDefaults::elevated_content_color(),
760 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
761 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
762 };
763 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
764 let pad = config.content_padding.unwrap_or(PaddingValues {
765 left: 24.0,
766 right: 24.0,
767 top: 0.0,
768 bottom: 0.0,
769 });
770 button_impl(
771 modifier.then(config.modifier),
772 on_click,
773 content,
774 cc,
775 bg,
776 sc,
777 se.or(Some(ButtonDefaults::elevated_state_elevation())),
778 config.border,
779 pad.left,
780 pad.right,
781 config.height,
782 config.shape_radius,
783 config.enabled,
784 )
785}
786
787#[derive(Clone, Debug)]
789pub struct ToggleButtonConfig {
790 pub modifier: Modifier,
791 pub enabled: bool,
792 pub container_color: Option<Color>,
793 pub content_color: Option<Color>,
794 pub checked_container_color: Option<Color>,
795 pub checked_content_color: Option<Color>,
796 pub state_colors: StateColors,
797 pub state_elevation: Option<StateElevation>,
798 pub border: Option<(f32, Color, f32)>,
799 pub shape_radius: f32,
800 pub height: f32,
801}
802
803impl Default for ToggleButtonConfig {
804 fn default() -> Self {
805 Self {
806 modifier: Modifier::new(),
807 enabled: true,
808 container_color: None,
809 content_color: None,
810 checked_container_color: None,
811 checked_content_color: None,
812 state_colors: ToggleButtonDefaults::state_colors_default(),
813 state_elevation: None,
814 border: None,
815 shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
816 height: ToggleButtonDefaults::HEIGHT,
817 }
818 }
819}
820
821fn toggle_button_impl(
822 checked: bool,
823 on_checked_change: impl Fn(bool) + 'static,
824 content: impl FnOnce(bool) -> View,
825 content_color: Color,
826 container_color: Option<Color>,
827 checked_container_color: Option<Color>,
828 checked_content_color: Option<Color>,
829 state_colors: StateColors,
830 state_elevation: StateElevation,
831 border: Option<(f32, Color, f32)>,
832 pad_left: f32,
833 pad_right: f32,
834 height: f32,
835 shape_radius: f32,
836 enabled: bool,
837) -> View {
838 let th = theme();
839 let bg = if checked {
840 checked_container_color.unwrap_or(th.primary)
841 } else {
842 container_color.unwrap_or(Color::TRANSPARENT)
843 };
844 let fg = if checked {
845 checked_content_color.unwrap_or(th.on_primary)
846 } else {
847 content_color
848 };
849 let mut m = Modifier::new()
850 .height(height)
851 .padding_values(PaddingValues {
852 left: pad_left,
853 right: pad_right,
854 top: 0.0,
855 bottom: 0.0,
856 })
857 .background(bg)
858 .clip_rounded(shape_radius)
859 .state_colors(state_colors)
860 .state_elevation(state_elevation);
861 if let Some((w, c, r)) = border {
862 m = m.border(w, c, r);
863 }
864 if enabled {
865 m = m.clickable().on_pointer_down({
866 let cb = on_checked_change;
867 move |_| cb(!checked)
868 });
869 } else {
870 m = m.alpha(0.38);
871 }
872 with_content_color(fg, || Box(m).child(content(checked)))
873}
874
875pub fn ToggleButton(
877 checked: bool,
878 on_checked_change: impl Fn(bool) + 'static,
879 config: ToggleButtonConfig,
880 content: impl FnOnce(bool) -> View,
881) -> View {
882 let cc = config
883 .content_color
884 .unwrap_or_else(ToggleButtonDefaults::content_color);
885 let checked_cc = config
886 .checked_content_color
887 .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
888 let checked_bg = config
889 .checked_container_color
890 .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
891 let se = config
892 .state_elevation
893 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
894 toggle_button_impl(
895 checked,
896 on_checked_change,
897 content,
898 cc,
899 None,
900 Some(checked_bg),
901 Some(checked_cc),
902 config.state_colors,
903 se,
904 config.border,
905 ToggleButtonDefaults::HORIZONTAL_PADDING,
906 ToggleButtonDefaults::HORIZONTAL_PADDING,
907 config.height,
908 config.shape_radius,
909 config.enabled,
910 )
911}
912
913pub fn TonalToggleButton(
915 checked: bool,
916 on_checked_change: impl Fn(bool) + 'static,
917 config: ToggleButtonConfig,
918 content: impl FnOnce(bool) -> View,
919) -> View {
920 let cc = config
921 .content_color
922 .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
923 let checked_cc = config
924 .checked_content_color
925 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
926 let checked_bg = config
927 .checked_container_color
928 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
929 let se = config
930 .state_elevation
931 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
932 toggle_button_impl(
933 checked,
934 on_checked_change,
935 content,
936 cc,
937 None,
938 Some(checked_bg),
939 Some(checked_cc),
940 config.state_colors,
941 se,
942 config.border,
943 ToggleButtonDefaults::HORIZONTAL_PADDING,
944 ToggleButtonDefaults::HORIZONTAL_PADDING,
945 config.height,
946 config.shape_radius,
947 config.enabled,
948 )
949}
950
951pub fn OutlinedToggleButton(
953 checked: bool,
954 on_checked_change: impl Fn(bool) + 'static,
955 config: ToggleButtonConfig,
956 content: impl FnOnce(bool) -> View,
957) -> View {
958 let cc = config
959 .content_color
960 .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
961 let checked_cc = config
962 .checked_content_color
963 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
964 let checked_bg = config
965 .checked_container_color
966 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
967 let se = config
968 .state_elevation
969 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
970 let border = if !checked {
971 Some(config.border.unwrap_or((
972 1.0,
973 ToggleButtonDefaults::outlined_border_color(),
974 config.shape_radius,
975 )))
976 } else {
977 config.border
978 };
979 toggle_button_impl(
980 checked,
981 on_checked_change,
982 content,
983 cc,
984 None,
985 Some(checked_bg),
986 Some(checked_cc),
987 config.state_colors,
988 se,
989 border,
990 ToggleButtonDefaults::HORIZONTAL_PADDING,
991 ToggleButtonDefaults::HORIZONTAL_PADDING,
992 config.height,
993 config.shape_radius,
994 config.enabled,
995 )
996}
997
998pub fn ElevatedToggleButton(
1000 checked: bool,
1001 on_checked_change: impl Fn(bool) + 'static,
1002 config: ToggleButtonConfig,
1003 content: impl FnOnce(bool) -> View,
1004) -> View {
1005 let cc = config
1006 .content_color
1007 .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
1008 let checked_cc = config
1009 .checked_content_color
1010 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
1011 let checked_bg = config
1012 .checked_container_color
1013 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
1014 let se = config
1015 .state_elevation
1016 .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
1017 toggle_button_impl(
1018 checked,
1019 on_checked_change,
1020 content,
1021 cc,
1022 None,
1023 Some(checked_bg),
1024 Some(checked_cc),
1025 config.state_colors,
1026 se,
1027 config.border,
1028 ToggleButtonDefaults::HORIZONTAL_PADDING,
1029 ToggleButtonDefaults::HORIZONTAL_PADDING,
1030 config.height,
1031 config.shape_radius,
1032 config.enabled,
1033 )
1034}
1035
1036#[derive(Clone, Debug)]
1038pub struct FABConfig {
1039 pub modifier: Modifier,
1040 pub enabled: bool,
1041 pub container_color: Color,
1042 pub content_color: Color,
1043 pub state_elevation: StateElevation,
1044 pub shape_radius: f32,
1045 pub size: f32,
1046}
1047
1048impl Default for FABConfig {
1049 fn default() -> Self {
1050 Self {
1051 modifier: Modifier::new(),
1052 enabled: true,
1053 container_color: FABDefaults::container_color(),
1054 content_color: FABDefaults::content_color(),
1055 state_elevation: FABDefaults::state_elevation(),
1056 shape_radius: FABDefaults::SHAPE_RADIUS,
1057 size: FABDefaults::SIZE,
1058 }
1059 }
1060}
1061
1062fn fab_impl(
1063 icon: View,
1064 on_click: impl Fn() + 'static,
1065 size: f32,
1066 shape_r: f32,
1067 config: FABConfig,
1068) -> View {
1069 let th = theme();
1070 let is_enabled = config.enabled;
1071 let bg = if is_enabled {
1072 config.container_color
1073 } else {
1074 th.on_surface
1075 .with_alpha_f32(0.12)
1076 .composite_over(th.surface_container_low)
1077 };
1078 let content_color = if is_enabled {
1079 config.content_color
1080 } else {
1081 th.on_surface.with_alpha_f32(0.38)
1082 };
1083
1084 let mut m = Modifier::new()
1085 .size(size, size)
1086 .background(bg)
1087 .state_colors(StateColors {
1088 default: Color::TRANSPARENT,
1089 hovered: config.content_color.with_alpha_f32(0.08),
1090 pressed: config.content_color.with_alpha_f32(0.12),
1091 disabled: th.on_surface.with_alpha_f32(0.12),
1092 })
1093 .state_elevation(config.state_elevation)
1094 .clip_rounded(shape_r)
1095 .align_items(AlignItems::Center)
1096 .justify_content(JustifyContent::Center)
1097 .then(config.modifier);
1098
1099 if is_enabled {
1100 m = m.clickable().on_pointer_down(move |_| on_click());
1101 }
1102
1103 Box(m).child(icon)
1104}
1105
1106pub fn FAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1108 fab_impl(
1109 icon,
1110 on_click,
1111 FABDefaults::SIZE,
1112 FABDefaults::SHAPE_RADIUS,
1113 config,
1114 )
1115}
1116
1117pub fn SmallFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1119 fab_impl(
1120 icon,
1121 on_click,
1122 FABDefaults::SMALL_SIZE,
1123 FABDefaults::SMALL_SHAPE_RADIUS,
1124 config,
1125 )
1126}
1127
1128pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
1130 fab_impl(
1131 icon,
1132 on_click,
1133 FABDefaults::LARGE_SIZE,
1134 FABDefaults::LARGE_SHAPE_RADIUS,
1135 config,
1136 )
1137}
1138
1139pub fn ExtendedFAB(
1141 icon: Option<View>,
1142 label: impl Into<String>,
1143 on_click: impl Fn() + 'static,
1144 config: FABConfig,
1145) -> View {
1146 let th = theme();
1147 let has_icon = icon.is_some();
1148 let is_enabled = config.enabled;
1149 let bg = if is_enabled {
1150 config.container_color
1151 } else {
1152 th.on_surface
1153 .with_alpha_f32(0.12)
1154 .composite_over(th.surface_container_low)
1155 };
1156 let content_color = if is_enabled {
1157 config.content_color
1158 } else {
1159 th.on_surface.with_alpha_f32(0.38)
1160 };
1161
1162 let mut m = Modifier::new()
1163 .height(56.0)
1164 .min_width(80.0)
1165 .background(bg)
1166 .state_colors(StateColors {
1167 default: Color::TRANSPARENT,
1168 hovered: config.content_color.with_alpha_f32(0.08),
1169 pressed: config.content_color.with_alpha_f32(0.12),
1170 disabled: theme().on_surface.with_alpha_f32(0.12),
1171 })
1172 .state_elevation(config.state_elevation)
1173 .clip_rounded(FABDefaults::SHAPE_RADIUS)
1174 .padding_values(PaddingValues {
1175 left: 16.0,
1176 right: 20.0,
1177 top: 0.0,
1178 bottom: 0.0,
1179 })
1180 .align_items(AlignItems::Center);
1181
1182 if is_enabled {
1183 m = m.clickable().on_pointer_down(move |_| on_click());
1184 }
1185 m = m.then(config.modifier);
1186 Row(m).child((
1187 icon.unwrap_or(Box(Modifier::new())),
1188 Box(Modifier::new()
1189 .width(if has_icon { 12.0 } else { 0.0 })
1190 .fill_max_height()),
1191 Text(label)
1192 .color(content_color)
1193 .size(th.typography.label_large)
1194 .single_line(),
1195 ))
1196}
1197
1198#[derive(Clone, Debug)]
1200pub struct DividerConfig {
1201 pub modifier: Modifier,
1202 pub thickness: f32,
1203 pub color: Color,
1204}
1205
1206impl Default for DividerConfig {
1207 fn default() -> Self {
1208 Self {
1209 modifier: Modifier::new(),
1210 thickness: DividerDefaults::THICKNESS,
1211 color: DividerDefaults::color(),
1212 }
1213 }
1214}
1215
1216pub fn HorizontalDivider(config: DividerConfig) -> View {
1219 Box(Modifier::new()
1220 .min_width(200.0)
1221 .height(config.thickness)
1222 .background(config.color)
1223 .then(config.modifier))
1224}
1225
1226#[deprecated(since = "0.19.5", note = "renamed to HorizontalDivider")]
1227pub fn Divider(config: DividerConfig) -> View {
1228 HorizontalDivider(config)
1229}
1230
1231pub fn VerticalDivider(config: DividerConfig) -> View {
1233 Box(Modifier::new()
1234 .width(config.thickness)
1235 .fill_max_height()
1236 .background(config.color)
1237 .then(config.modifier))
1238}
1239
1240#[derive(Clone, Debug)]
1242pub struct BadgeConfig {
1243 pub modifier: Modifier,
1244 pub container_color: Color,
1245 pub content_color: Color,
1246}
1247
1248impl Default for BadgeConfig {
1249 fn default() -> Self {
1250 Self {
1251 modifier: Modifier::new(),
1252 container_color: BadgeDefaults::container_color(),
1253 content_color: BadgeDefaults::content_color(),
1254 }
1255 }
1256}
1257
1258pub fn Badge(content: Option<View>, config: BadgeConfig) -> View {
1261 match content {
1262 None => Box(Modifier::new()
1263 .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1264 .background(config.container_color)
1265 .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1266 .then(config.modifier)),
1267 Some(view) => Box(Modifier::new()
1268 .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1269 .height(BadgeDefaults::LABEL_HEIGHT)
1270 .background(config.container_color)
1271 .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1272 .padding_values(PaddingValues {
1273 left: 4.0,
1274 right: 4.0,
1275 top: 0.0,
1276 bottom: 0.0,
1277 })
1278 .align_items(AlignItems::Center)
1279 .justify_content(JustifyContent::Center)
1280 .then(config.modifier))
1281 .child(view),
1282 }
1283}
1284
1285#[derive(Clone, Debug)]
1287pub struct BadgedBoxConfig {
1288 pub modifier: Modifier,
1289 pub dot_offset_x: f32,
1291 pub dot_offset_y: f32,
1293 pub content_offset_x: f32,
1295 pub content_offset_y: f32,
1297}
1298
1299impl Default for BadgedBoxConfig {
1300 fn default() -> Self {
1301 Self {
1302 modifier: Modifier::new(),
1303 dot_offset_x: BadgeDefaults::DOT_OFFSET_X,
1304 dot_offset_y: BadgeDefaults::DOT_OFFSET_Y,
1305 content_offset_x: BadgeDefaults::CONTENT_OFFSET_X,
1306 content_offset_y: BadgeDefaults::CONTENT_OFFSET_Y,
1307 }
1308 }
1309}
1310
1311pub fn BadgedBox(badge: View, content: View, config: BadgedBoxConfig) -> View {
1314 Stack(Modifier::new()).child((
1315 content,
1316 Box(Modifier::new().absolute().offset(
1317 None,
1318 Some(config.dot_offset_y),
1319 Some(config.dot_offset_x),
1320 None,
1321 ))
1322 .child(badge),
1323 ))
1324}
1325
1326#[derive(Clone, Debug)]
1329pub struct ListItemColors {
1330 pub container_color: Color,
1331 pub headline_color: Color,
1332 pub supporting_color: Color,
1333 pub overline_color: Color,
1334 pub leading_icon_color: Color,
1335 pub trailing_icon_color: Color,
1336
1337 pub disabled_container_color: Color,
1338 pub disabled_headline_color: Color,
1339 pub disabled_supporting_color: Color,
1340 pub disabled_overline_color: Color,
1341 pub disabled_leading_icon_color: Color,
1342 pub disabled_trailing_icon_color: Color,
1343
1344 pub selected_container_color: Color,
1345 pub selected_headline_color: Color,
1346 pub selected_supporting_color: Color,
1347 pub selected_overline_color: Color,
1348 pub selected_leading_icon_color: Color,
1349 pub selected_trailing_icon_color: Color,
1350
1351 pub dragged_container_color: Color,
1352 pub dragged_headline_color: Color,
1353 pub dragged_supporting_color: Color,
1354 pub dragged_overline_color: Color,
1355 pub dragged_leading_icon_color: Color,
1356 pub dragged_trailing_icon_color: Color,
1357}
1358
1359impl ListItemColors {
1360 pub fn container(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1361 if !enabled {
1362 self.disabled_container_color
1363 } else if dragged {
1364 self.dragged_container_color
1365 } else if selected {
1366 self.selected_container_color
1367 } else {
1368 self.container_color
1369 }
1370 }
1371 pub fn headline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1372 if !enabled {
1373 self.disabled_headline_color
1374 } else if dragged {
1375 self.dragged_headline_color
1376 } else if selected {
1377 self.selected_headline_color
1378 } else {
1379 self.headline_color
1380 }
1381 }
1382 pub fn supporting(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1383 if !enabled {
1384 self.disabled_supporting_color
1385 } else if dragged {
1386 self.dragged_supporting_color
1387 } else if selected {
1388 self.selected_supporting_color
1389 } else {
1390 self.supporting_color
1391 }
1392 }
1393 pub fn overline(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1394 if !enabled {
1395 self.disabled_overline_color
1396 } else if dragged {
1397 self.dragged_overline_color
1398 } else if selected {
1399 self.selected_overline_color
1400 } else {
1401 self.overline_color
1402 }
1403 }
1404 pub fn leading_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1405 if !enabled {
1406 self.disabled_leading_icon_color
1407 } else if dragged {
1408 self.dragged_leading_icon_color
1409 } else if selected {
1410 self.selected_leading_icon_color
1411 } else {
1412 self.leading_icon_color
1413 }
1414 }
1415 pub fn trailing_icon(&self, enabled: bool, selected: bool, dragged: bool) -> Color {
1416 if !enabled {
1417 self.disabled_trailing_icon_color
1418 } else if dragged {
1419 self.dragged_trailing_icon_color
1420 } else if selected {
1421 self.selected_trailing_icon_color
1422 } else {
1423 self.trailing_icon_color
1424 }
1425 }
1426}
1427
1428impl Default for ListItemColors {
1429 fn default() -> Self {
1430 Self {
1431 container_color: Color::TRANSPARENT,
1432 headline_color: ListItemDefaults::headline_color(),
1433 supporting_color: ListItemDefaults::supporting_color(),
1434 overline_color: ListItemDefaults::overline_color(),
1435 leading_icon_color: ListItemDefaults::leading_icon_color(),
1436 trailing_icon_color: ListItemDefaults::trailing_icon_color(),
1437 disabled_container_color: ListItemDefaults::disabled_container_color(),
1438 disabled_headline_color: ListItemDefaults::disabled_headline_color(),
1439 disabled_supporting_color: ListItemDefaults::disabled_supporting_color(),
1440 disabled_overline_color: ListItemDefaults::disabled_overline_color(),
1441 disabled_leading_icon_color: ListItemDefaults::disabled_leading_icon_color(),
1442 disabled_trailing_icon_color: ListItemDefaults::disabled_trailing_icon_color(),
1443 selected_container_color: ListItemDefaults::selected_container_color(),
1444 selected_headline_color: ListItemDefaults::selected_headline_color(),
1445 selected_supporting_color: ListItemDefaults::selected_supporting_color(),
1446 selected_overline_color: ListItemDefaults::selected_overline_color(),
1447 selected_leading_icon_color: ListItemDefaults::selected_leading_icon_color(),
1448 selected_trailing_icon_color: ListItemDefaults::selected_trailing_icon_color(),
1449 dragged_container_color: ListItemDefaults::dragged_container_color(),
1450 dragged_headline_color: ListItemDefaults::dragged_headline_color(),
1451 dragged_supporting_color: ListItemDefaults::dragged_supporting_color(),
1452 dragged_overline_color: ListItemDefaults::dragged_overline_color(),
1453 dragged_leading_icon_color: ListItemDefaults::dragged_leading_icon_color(),
1454 dragged_trailing_icon_color: ListItemDefaults::dragged_trailing_icon_color(),
1455 }
1456 }
1457}
1458
1459#[derive(Clone, Debug)]
1461pub struct ListItemConfig {
1462 pub modifier: Modifier,
1463 pub enabled: bool,
1465 pub colors: ListItemColors,
1466 pub tonal_elevation: f32,
1467 pub shadow_elevation: f32,
1468 pub shape_radius: f32,
1469 pub horizontal_padding: f32,
1470 pub trailing_padding: f32,
1471 pub one_line_height: f32,
1472 pub two_line_height: f32,
1473 pub three_line_height: f32,
1474}
1475
1476impl Default for ListItemConfig {
1477 fn default() -> Self {
1478 Self {
1479 modifier: Modifier::new(),
1480 enabled: true,
1481 colors: ListItemColors::default(),
1482 tonal_elevation: 0.0,
1483 shadow_elevation: 0.0,
1484 shape_radius: 0.0,
1485 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1486 trailing_padding: ListItemDefaults::TRAILING_PADDING,
1487 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1488 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1489 three_line_height: ListItemDefaults::THREE_LINE_HEIGHT,
1490 }
1491 }
1492}
1493
1494pub fn ListItem(
1496 headline: impl Into<String>,
1497 supporting_text: Option<String>,
1498 leading: Option<View>,
1499 trailing: Option<View>,
1500 on_click: Option<Rc<dyn Fn()>>,
1501 config: ListItemConfig,
1502) -> View {
1503 let th = theme();
1504 let is_enabled = config.enabled;
1505 let c = &config.colors;
1506
1507 let hd_col = c.headline(is_enabled, false, false);
1508 let sp_col = c.supporting(is_enabled, false, false);
1509 let ld_col = c.leading_icon(is_enabled, false, false);
1510 let tr_col = c.trailing_icon(is_enabled, false, false);
1511 let bg = c.container(is_enabled, false, false);
1512
1513 let line_count = if supporting_text.is_some() { 2 } else { 1 };
1514 let min_h = match line_count {
1515 2 => config.two_line_height,
1516 3 => config.three_line_height,
1517 _ => config.one_line_height,
1518 };
1519
1520 let mut modifier = Modifier::new()
1521 .min_width(200.0)
1522 .min_height(min_h)
1523 .background(bg)
1524 .clip_rounded(config.shape_radius)
1525 .state_colors(StateColors {
1526 default: Color::TRANSPARENT,
1527 hovered: th.on_surface.with_alpha_f32(0.08),
1528 pressed: th.on_surface.with_alpha_f32(0.12),
1529 disabled: Color::TRANSPARENT,
1530 })
1531 .padding_values(PaddingValues {
1532 left: config.horizontal_padding,
1533 right: config.trailing_padding,
1534 top: 8.0,
1535 bottom: 8.0,
1536 })
1537 .align_items(AlignItems::Center)
1538 .then(config.modifier);
1539
1540 if config.tonal_elevation > 0.0 {
1541 modifier = modifier.state_elevation(StateElevation {
1542 default: config.tonal_elevation,
1543 hovered: config.tonal_elevation,
1544 pressed: config.tonal_elevation,
1545 disabled: 0.0,
1546 });
1547 }
1548 if config.shadow_elevation > 0.0 {
1549 modifier = modifier.shadow(config.shadow_elevation, 0.0);
1550 }
1551
1552 if let Some(cb) = on_click {
1553 let cb = cb.clone();
1554 modifier = modifier.clickable().on_pointer_down(move |_| {
1555 if is_enabled {
1556 cb();
1557 }
1558 });
1559 }
1560
1561 let wrap_icon = |color: Color, v: View| -> View { with_content_color(color, move || v) };
1562
1563 Row(modifier).child((
1564 leading
1565 .map(|v| {
1566 Box(Modifier::new().padding_values(PaddingValues {
1567 left: 0.0,
1568 right: 16.0,
1569 top: 0.0,
1570 bottom: 0.0,
1571 }))
1572 .child(wrap_icon(ld_col, v))
1573 })
1574 .unwrap_or(Box(Modifier::new())),
1575 Column(
1576 Modifier::new()
1577 .flex_grow(1.0)
1578 .justify_content(JustifyContent::Center),
1579 )
1580 .child((
1581 Text(headline)
1582 .color(hd_col)
1583 .size(th.typography.body_large)
1584 .single_line(),
1585 supporting_text
1586 .map(|st| {
1587 Text(st)
1588 .color(sp_col)
1589 .size(th.typography.body_medium)
1590 .max_lines(2)
1591 .overflow_ellipsize()
1592 })
1593 .unwrap_or(Box(Modifier::new())),
1594 )),
1595 trailing
1596 .map(|v| {
1597 Box(Modifier::new().padding_values(PaddingValues {
1598 left: 16.0,
1599 right: 0.0,
1600 top: 0.0,
1601 bottom: 0.0,
1602 }))
1603 .child(wrap_icon(tr_col, v))
1604 })
1605 .unwrap_or(Box(Modifier::new())),
1606 ))
1607}
1608
1609pub struct Tab {
1611 pub label: String,
1612 pub icon: Option<View>,
1613 pub on_click: Rc<dyn Fn()>,
1614 pub enabled: bool,
1615}
1616
1617#[derive(Clone, Debug)]
1619pub struct TabRowConfig {
1620 pub modifier: Modifier,
1621 pub container_color: Color,
1622 pub selected_content_color: Color,
1623 pub unselected_content_color: Color,
1624 pub indicator_color: Color,
1625 pub height: f32,
1626 pub indicator_height: f32,
1627}
1628
1629impl Default for TabRowConfig {
1630 fn default() -> Self {
1631 Self {
1632 modifier: Modifier::new(),
1633 container_color: TabDefaults::container_color(),
1634 selected_content_color: TabDefaults::selected_content_color(),
1635 unselected_content_color: TabDefaults::unselected_content_color(),
1636 indicator_color: TabDefaults::indicator_color(),
1637 height: TabDefaults::HEIGHT,
1638 indicator_height: TabDefaults::INDICATOR_HEIGHT,
1639 }
1640 }
1641}
1642
1643static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
1644
1645pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
1649 let th = theme();
1650 let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
1651 let default_effects = AnimationSpec::spring_crit(40.0);
1652 Column(Modifier::new().fill_max_width().then(config.modifier)).child((
1653 Row(Modifier::new()
1654 .fill_max_width()
1655 .height(config.height)
1656 .background(config.container_color)
1657 .semantics(Semantics::new(Role::Container).with_selectable_group()))
1658 .child(
1659 tabs.into_iter()
1660 .enumerate()
1661 .map(|(i, tab)| {
1662 let selected = i == selected_index;
1663 let is_enabled = tab.enabled;
1664 let color = animate_color(
1665 format!("tab_clr_{}_{}", id, i),
1666 if selected {
1667 config.selected_content_color
1668 } else {
1669 config.unselected_content_color
1670 },
1671 default_effects,
1672 );
1673 let indicator_h = animate_f32(
1674 format!("tab_ind_h_{}_{}", id, i),
1675 if selected {
1676 config.indicator_height
1677 } else {
1678 0.0
1679 },
1680 default_effects,
1681 );
1682 let cb = tab.on_click.clone();
1683
1684 let mut tab_m = Modifier::new()
1685 .flex_grow(1.0)
1686 .fill_max_height()
1687 .align_items(AlignItems::Center)
1688 .justify_content(JustifyContent::Center)
1689 .state_colors(StateColors {
1690 default: Color::TRANSPARENT,
1691 hovered: th.on_surface.with_alpha_f32(0.08),
1692 pressed: th.on_surface.with_alpha_f32(0.12),
1693 disabled: Color::TRANSPARENT,
1694 })
1695 .semantics(Semantics::new(Role::Tab).with_label(&tab.label));
1696
1697 if is_enabled {
1698 tab_m = tab_m.clickable().on_pointer_down(move |_| cb());
1699 }
1700
1701 Column(tab_m).child((
1702 tab.icon.unwrap_or(Box(Modifier::new())),
1703 Text(tab.label)
1704 .color(color)
1705 .size(th.typography.title_small)
1706 .single_line(),
1707 Box(Modifier::new()
1708 .fill_max_width()
1709 .height(indicator_h)
1710 .background(config.indicator_color)
1711 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
1712 ))
1713 })
1714 .collect::<Vec<_>>(),
1715 ),
1716 Box(Modifier::new()
1718 .fill_max_width()
1719 .height(1.0)
1720 .background(th.outline_variant)),
1721 ))
1722}
1723
1724pub struct Segment {
1726 pub label: String,
1727 pub icon: Option<View>,
1728 pub on_click: Rc<dyn Fn()>,
1729 pub enabled: bool,
1730}
1731
1732#[derive(Clone, Debug)]
1734pub struct SegmentedButtonConfig {
1735 pub modifier: Modifier,
1736 pub border_color: Color,
1737 pub selected_container_color: Color,
1738 pub selected_content_color: Color,
1739 pub unselected_content_color: Color,
1740 pub state_colors: StateColors,
1741 pub height: f32,
1742 pub shape_radius: f32,
1743}
1744
1745impl Default for SegmentedButtonConfig {
1746 fn default() -> Self {
1747 Self {
1748 modifier: Modifier::new(),
1749 border_color: SegmentedButtonDefaults::border_color(),
1750 selected_container_color: SegmentedButtonDefaults::selected_container_color(),
1751 selected_content_color: SegmentedButtonDefaults::selected_content_color(),
1752 unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
1753 state_colors: SegmentedButtonDefaults::state_colors_default(),
1754 height: SegmentedButtonDefaults::HEIGHT,
1755 shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
1756 }
1757 }
1758}
1759
1760static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
1761
1762pub fn SegmentedButton(
1767 selected: &[usize],
1768 segments: Vec<Segment>,
1769 config: SegmentedButtonConfig,
1770) -> View {
1771 let th = theme();
1772 let count = segments.len();
1773 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
1774 let spec = th.motion.color;
1775 let shape_r = config.shape_radius;
1776
1777 let segment_radii = |i: usize| -> [f32; 4] {
1779 if count == 1 {
1780 [shape_r, shape_r, shape_r, shape_r]
1781 } else if i == 0 {
1782 [shape_r, 0.0, 0.0, shape_r]
1783 } else if i == count - 1 {
1784 [0.0, shape_r, shape_r, 0.0]
1785 } else {
1786 [0.0, 0.0, 0.0, 0.0]
1787 }
1788 };
1789
1790 Row(Modifier::new()
1792 .height(config.height)
1793 .border(1.0, config.border_color, shape_r)
1794 .then(config.modifier))
1795 .child(
1796 segments
1797 .into_iter()
1798 .enumerate()
1799 .map(|(i, seg)| {
1800 let is_selected = selected.contains(&i);
1801
1802 let bg = animate_color(
1803 format!("sb_bg_{}_{}", id, i),
1804 if is_selected {
1805 config.selected_container_color
1806 } else {
1807 Color::TRANSPARENT
1808 },
1809 spec,
1810 );
1811 let fg = animate_color(
1812 format!("sb_fg_{}_{}", id, i),
1813 if is_selected {
1814 config.selected_content_color
1815 } else {
1816 config.unselected_content_color
1817 },
1818 spec,
1819 );
1820
1821 let cb = seg.on_click.clone();
1822 let radii = segment_radii(i);
1823 let is_enabled = seg.enabled;
1824
1825 let state_colors = config.state_colors;
1826 let content_modifier = Modifier::new()
1827 .flex_grow(1.0)
1828 .fill_max_height()
1829 .clip_rounded_radii(radii)
1830 .background(bg)
1831 .state_colors(state_colors)
1832 .align_items(AlignItems::Center)
1833 .justify_content(JustifyContent::Center)
1834 .padding_values(PaddingValues {
1835 left: 12.0,
1836 right: 12.0,
1837 top: 0.0,
1838 bottom: 0.0,
1839 });
1840
1841 let content_modifier = if is_enabled {
1842 content_modifier.clickable().on_pointer_down(move |_| cb())
1843 } else {
1844 content_modifier
1845 };
1846
1847 Row(Modifier::new().flex_grow(1.0).fill_max_height()).child((
1848 Row(content_modifier).child((
1849 seg.icon.unwrap_or(Box(Modifier::new())),
1850 Text(seg.label)
1851 .color(fg)
1852 .size(th.typography.label_large)
1853 .single_line(),
1854 )),
1855 if i < count - 1 {
1856 Box(Modifier::new()
1857 .width(1.0)
1858 .fill_max_height()
1859 .background(th.outline))
1860 } else {
1861 Box(Modifier::new())
1862 },
1863 ))
1864 })
1865 .collect::<Vec<_>>(),
1866 )
1867}
1868
1869#[derive(Clone, Debug)]
1871pub struct CircularProgressIndicatorConfig {
1872 pub color: Color,
1873 pub track_color: Color,
1874 pub stroke_width: f32,
1875 pub stroke_cap: StrokeCap,
1876 pub gap_size: f32,
1877}
1878
1879impl Default for CircularProgressIndicatorConfig {
1880 fn default() -> Self {
1881 Self {
1882 color: ProgressIndicatorDefaults::circular_color(),
1883 track_color: ProgressIndicatorDefaults::circular_track_color(),
1884 stroke_width: ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH,
1885 stroke_cap: StrokeCap::Round,
1886 gap_size: 0.0,
1887 }
1888 }
1889}
1890
1891pub fn CircularProgressIndicator(
1896 value: Option<f32>,
1897 config: CircularProgressIndicatorConfig,
1898) -> View {
1899 let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
1900 let stroke_px = dp_to_px(config.stroke_width);
1901 let val = value.map(|v| v.clamp(0.0, 1.0));
1902
1903 let (global_rotation, additional_rotation, sweep_val) = if value.is_none() {
1908 let shared = remember_state_with_key("circ_ind_shared", || {
1909 let mut a = AnimatedValue::new(
1910 0.0f32,
1911 AnimationSpec::tween(Duration::from_millis(6000), Easing::Linear)
1912 .repeated(RepeatableSpec::infinite()),
1913 );
1914 a.set_target(1.0);
1915 a
1916 });
1917 let mut s = shared.borrow_mut();
1918 s.update();
1919 let t = *s.get();
1920 drop(s);
1921
1922 let gv = t * 1080.0;
1923
1924 let emph = Easing::Custom(CubicBezier::new(0.05, 0.7, 0.1, 1.0));
1925 let add_kf = remember_state_with_key("circ_ind_add_kf", || KeyframesSpec {
1926 keyframes: vec![
1927 (0.0, 0.0, None),
1928 (0.05, 90.0, Some(emph)),
1929 (0.25, 90.0, None),
1930 (0.30, 180.0, None),
1931 (0.50, 180.0, None),
1932 (0.55, 270.0, None),
1933 (0.75, 270.0, None),
1934 (0.80, 360.0, None),
1935 (1.0, 360.0, None),
1936 ],
1937 });
1938 let av = add_kf.borrow().evaluate(t);
1939
1940 let std_dec = Easing::Custom(CubicBezier::new(0.2, 0.0, 0.0, 1.0));
1941 let sweep_kf = remember_state_with_key("circ_ind_sweep_kf", || KeyframesSpec {
1942 keyframes: vec![
1943 (0.0, 0.1, None),
1944 (0.5, 0.87, Some(std_dec)),
1945 (1.0, 0.1, None),
1946 ],
1947 });
1948 let sv = sweep_kf.borrow().evaluate(t);
1949
1950 (gv, av, sv)
1951 } else {
1952 (0.0, 0.0, 0.0)
1953 };
1954
1955 let indicator_size_dp = ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE;
1957 let adjusted_gap_dp = if config.stroke_cap == StrokeCap::Butt {
1958 config.gap_size
1959 } else {
1960 config.gap_size + config.stroke_width
1961 };
1962 let circle_dia_dp = indicator_size_dp - config.stroke_width;
1963 let gap_sweep_rad = 2.0 * adjusted_gap_dp / circle_dia_dp;
1964
1965 Box(Modifier::new()
1966 .size(sz, sz)
1967 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1968 let mul_c = |c: Color| {
1969 Color(
1970 c.0,
1971 c.1,
1972 c.2,
1973 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1974 )
1975 };
1976 let cx = rect.x + rect.w * 0.5;
1977 let cy = rect.y + rect.h * 0.5;
1978 let r = (rect.w.min(rect.h)) * 0.5 - stroke_px * 0.5;
1979 let circle = Rect {
1980 x: cx - r,
1981 y: cy - r,
1982 w: r * 2.0,
1983 h: r * 2.0,
1984 };
1985
1986 match val {
1987 Some(p) => {
1988 let sweep_rad = p * std::f32::consts::TAU;
1989 let start_angle = -std::f32::consts::FRAC_PI_2;
1990 let effective_gap = gap_sweep_rad.min(sweep_rad);
1991
1992 if p > 0.0 {
1994 scene.nodes.push(SceneNode::Arc {
1995 rect: circle,
1996 start_angle,
1997 sweep_angle: sweep_rad,
1998 stroke_width: stroke_px,
1999 color: mul_c(config.color),
2000 cap: config.stroke_cap,
2001 });
2002 }
2003
2004 let track_start = start_angle + sweep_rad + effective_gap;
2006 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2007 if track_sweep > 0.0 {
2008 scene.nodes.push(SceneNode::Arc {
2009 rect: circle,
2010 start_angle: track_start,
2011 sweep_angle: track_sweep,
2012 stroke_width: stroke_px,
2013 color: mul_c(config.track_color),
2014 cap: config.stroke_cap,
2015 });
2016 }
2017 }
2018 None => {
2019 let radians =
2020 (global_rotation + additional_rotation) * std::f32::consts::PI / 180.0;
2021 let start_angle = -std::f32::consts::FRAC_PI_2 + radians;
2022 let sweep_rad = sweep_val * std::f32::consts::TAU;
2023 let effective_gap = gap_sweep_rad.min(sweep_rad);
2024
2025 scene.nodes.push(SceneNode::Arc {
2027 rect: circle,
2028 start_angle,
2029 sweep_angle: sweep_rad,
2030 stroke_width: stroke_px,
2031 color: mul_c(config.color),
2032 cap: config.stroke_cap,
2033 });
2034
2035 let track_start = start_angle + sweep_rad + effective_gap;
2037 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
2038 if track_sweep > 0.0 {
2039 scene.nodes.push(SceneNode::Arc {
2040 rect: circle,
2041 start_angle: track_start,
2042 sweep_angle: track_sweep,
2043 stroke_width: stroke_px,
2044 color: mul_c(config.track_color),
2045 cap: config.stroke_cap,
2046 });
2047 }
2048 }
2049 }
2050 }))
2051 .semantics(Semantics {
2052 role: Role::ProgressBar,
2053 label: None,
2054 focused: false,
2055 enabled: true,
2056 selectable_group: false,
2057 })
2058}
2059
2060#[derive(Clone, Debug)]
2062pub struct LinearProgressIndicatorConfig {
2063 pub color: Color,
2064 pub track_color: Color,
2065 pub stroke_cap: StrokeCap,
2067 pub gap_size: f32,
2069 pub stop_size: f32,
2071}
2072
2073impl Default for LinearProgressIndicatorConfig {
2074 fn default() -> Self {
2075 Self {
2076 color: ProgressIndicatorDefaults::linear_color(),
2077 track_color: ProgressIndicatorDefaults::linear_track_color(),
2078 stroke_cap: StrokeCap::Round,
2079 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
2080 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
2081 }
2082 }
2083}
2084
2085pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
2090 Box(Modifier::new()
2091 .fill_max_width()
2092 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
2093 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2094 let mul_c = |c: Color| {
2095 Color(
2096 c.0,
2097 c.1,
2098 c.2,
2099 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2100 )
2101 };
2102 let track_h = rect.h;
2103 let corner = track_h * 0.5;
2104 let dot_r = dp_to_px(config.stop_size) * 0.5;
2105 let cy = rect.y + rect.h * 0.5;
2106 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
2107
2108 let cap_radius = if config.stroke_cap == StrokeCap::Butt {
2109 0.0
2110 } else {
2111 corner
2112 };
2113
2114 let gap = dp_to_px(config.gap_size)
2115 - if config.stroke_cap == StrokeCap::Butt {
2116 0.0
2117 } else {
2118 cap_radius
2119 };
2120
2121 let cap_ofs = cap_radius;
2122 let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
2123 let ind_w = (ind_end - cap_ofs).max(0.0);
2124
2125 if t > 0.0 && ind_w > 0.0 {
2127 scene.nodes.push(SceneNode::Rect {
2128 rect: Rect {
2129 x: rect.x + cap_ofs,
2130 y: cy - corner,
2131 w: ind_w,
2132 h: track_h,
2133 },
2134 brush: Brush::Solid(mul_c(config.color)),
2135 radius: [cap_radius; 4],
2136 });
2137 }
2138
2139 let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
2141 let track_w = (rect.x + rect.w - track_start).max(0.0);
2142 if t < 1.0 && track_w > 0.0 {
2143 let track_left = track_start + cap_ofs;
2144 let track_right = rect.x + rect.w;
2145 if track_right > track_left {
2146 scene.nodes.push(SceneNode::Rect {
2147 rect: Rect {
2148 x: track_left,
2149 y: cy - corner,
2150 w: track_right - track_left,
2151 h: track_h,
2152 },
2153 brush: Brush::Solid(mul_c(config.track_color)),
2154 radius: [cap_radius; 4],
2155 });
2156 }
2157 }
2158
2159 {
2161 let sx = rect.x + rect.w - dot_r;
2162 scene.nodes.push(SceneNode::Ellipse {
2163 rect: Rect {
2164 x: sx - dot_r,
2165 y: cy - dot_r,
2166 w: dot_r * 2.0,
2167 h: dot_r * 2.0,
2168 },
2169 brush: Brush::Solid(mul_c(config.color)),
2170 });
2171 }
2172 }))
2173 .semantics(Semantics {
2174 role: Role::ProgressBar,
2175 label: None,
2176 focused: false,
2177 enabled: true,
2178 selectable_group: false,
2179 })
2180}
2181
2182#[allow(dead_code)]
2185#[derive(Clone, Debug)]
2186pub struct TextFieldColors {
2187 pub focused_text_color: Color,
2188 pub unfocused_text_color: Color,
2189 pub disabled_text_color: Color,
2190 pub error_text_color: Color,
2191 pub focused_container_color: Color,
2192 pub unfocused_container_color: Color,
2193 pub disabled_container_color: Color,
2194 pub error_container_color: Color,
2195 pub cursor_color: Color,
2196 pub error_cursor_color: Color,
2197 pub focused_indicator_color: Color,
2198 pub unfocused_indicator_color: Color,
2199 pub disabled_indicator_color: Color,
2200 pub error_indicator_color: Color,
2201 pub focused_leading_icon_color: Color,
2202 pub unfocused_leading_icon_color: Color,
2203 pub disabled_leading_icon_color: Color,
2204 pub error_leading_icon_color: Color,
2205 pub focused_trailing_icon_color: Color,
2206 pub unfocused_trailing_icon_color: Color,
2207 pub disabled_trailing_icon_color: Color,
2208 pub error_trailing_icon_color: Color,
2209 pub focused_label_color: Color,
2210 pub unfocused_label_color: Color,
2211 pub disabled_label_color: Color,
2212 pub error_label_color: Color,
2213 pub focused_placeholder_color: Color,
2214 pub unfocused_placeholder_color: Color,
2215 pub disabled_placeholder_color: Color,
2216 pub error_placeholder_color: Color,
2217 pub focused_supporting_text_color: Color,
2218 pub unfocused_supporting_text_color: Color,
2219 pub disabled_supporting_text_color: Color,
2220 pub error_supporting_text_color: Color,
2221 pub focused_prefix_color: Color,
2222 pub unfocused_prefix_color: Color,
2223 pub disabled_prefix_color: Color,
2224 pub error_prefix_color: Color,
2225 pub focused_suffix_color: Color,
2226 pub unfocused_suffix_color: Color,
2227 pub disabled_suffix_color: Color,
2228 pub error_suffix_color: Color,
2229}
2230
2231#[allow(dead_code)]
2232impl TextFieldColors {
2233 pub fn text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2234 if !enabled {
2235 self.disabled_text_color
2236 } else if is_error {
2237 self.error_text_color
2238 } else if focused {
2239 self.focused_text_color
2240 } else {
2241 self.unfocused_text_color
2242 }
2243 }
2244 pub fn container_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2245 if !enabled {
2246 self.disabled_container_color
2247 } else if is_error {
2248 self.error_container_color
2249 } else if focused {
2250 self.focused_container_color
2251 } else {
2252 self.unfocused_container_color
2253 }
2254 }
2255 pub fn cursor_color(&self, is_error: bool) -> Color {
2256 if is_error {
2257 self.error_cursor_color
2258 } else {
2259 self.cursor_color
2260 }
2261 }
2262 pub fn indicator_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2263 if !enabled {
2264 self.disabled_indicator_color
2265 } else if is_error {
2266 self.error_indicator_color
2267 } else if focused {
2268 self.focused_indicator_color
2269 } else {
2270 self.unfocused_indicator_color
2271 }
2272 }
2273 pub fn leading_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2274 if !enabled {
2275 self.disabled_leading_icon_color
2276 } else if is_error {
2277 self.error_leading_icon_color
2278 } else if focused {
2279 self.focused_leading_icon_color
2280 } else {
2281 self.unfocused_leading_icon_color
2282 }
2283 }
2284 pub fn trailing_icon_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2285 if !enabled {
2286 self.disabled_trailing_icon_color
2287 } else if is_error {
2288 self.error_trailing_icon_color
2289 } else if focused {
2290 self.focused_trailing_icon_color
2291 } else {
2292 self.unfocused_trailing_icon_color
2293 }
2294 }
2295 pub fn label_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2296 if !enabled {
2297 self.disabled_label_color
2298 } else if is_error {
2299 self.error_label_color
2300 } else if focused {
2301 self.focused_label_color
2302 } else {
2303 self.unfocused_label_color
2304 }
2305 }
2306 pub fn placeholder_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2307 if !enabled {
2308 self.disabled_placeholder_color
2309 } else if is_error {
2310 self.error_placeholder_color
2311 } else if focused {
2312 self.focused_placeholder_color
2313 } else {
2314 self.unfocused_placeholder_color
2315 }
2316 }
2317 pub fn supporting_text_color(&self, enabled: bool, is_error: bool, focused: bool) -> Color {
2318 if !enabled {
2319 self.disabled_supporting_text_color
2320 } else if is_error {
2321 self.error_supporting_text_color
2322 } else if focused {
2323 self.focused_supporting_text_color
2324 } else {
2325 self.unfocused_supporting_text_color
2326 }
2327 }
2328}
2329
2330pub struct TextFieldDefaults;
2332
2333impl TextFieldDefaults {
2334 pub fn colors() -> TextFieldColors {
2335 let th = theme();
2336 TextFieldColors {
2337 focused_text_color: th.on_surface,
2338 unfocused_text_color: th.on_surface,
2339 disabled_text_color: th.on_surface.with_alpha_f32(0.38),
2340 error_text_color: th.on_surface,
2341 focused_container_color: th.surface_container_highest,
2342 unfocused_container_color: th.surface_container_highest,
2343 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
2344 error_container_color: th.surface_container_highest,
2345 cursor_color: th.primary,
2346 error_cursor_color: th.error,
2347 focused_indicator_color: th.primary,
2348 unfocused_indicator_color: th.on_surface_variant,
2349 disabled_indicator_color: th.on_surface.with_alpha_f32(0.12),
2350 error_indicator_color: th.error,
2351 focused_leading_icon_color: th.on_surface_variant,
2352 unfocused_leading_icon_color: th.on_surface_variant,
2353 disabled_leading_icon_color: th.on_surface.with_alpha_f32(0.38),
2354 error_leading_icon_color: th.error,
2355 focused_trailing_icon_color: th.on_surface_variant,
2356 unfocused_trailing_icon_color: th.on_surface_variant,
2357 disabled_trailing_icon_color: th.on_surface.with_alpha_f32(0.38),
2358 error_trailing_icon_color: th.error,
2359 focused_label_color: th.primary,
2360 unfocused_label_color: th.on_surface_variant,
2361 disabled_label_color: th.on_surface.with_alpha_f32(0.38),
2362 error_label_color: th.error,
2363 focused_placeholder_color: th.on_surface_variant,
2364 unfocused_placeholder_color: th.on_surface_variant,
2365 disabled_placeholder_color: th.on_surface.with_alpha_f32(0.38),
2366 error_placeholder_color: th.error,
2367 focused_supporting_text_color: th.on_surface_variant,
2368 unfocused_supporting_text_color: th.on_surface_variant,
2369 disabled_supporting_text_color: th.on_surface.with_alpha_f32(0.38),
2370 error_supporting_text_color: th.error,
2371 focused_prefix_color: th.on_surface,
2372 unfocused_prefix_color: th.on_surface,
2373 disabled_prefix_color: th.on_surface.with_alpha_f32(0.38),
2374 error_prefix_color: th.on_surface,
2375 focused_suffix_color: th.on_surface,
2376 unfocused_suffix_color: th.on_surface,
2377 disabled_suffix_color: th.on_surface.with_alpha_f32(0.38),
2378 error_suffix_color: th.on_surface,
2379 }
2380 }
2381}
2382
2383#[derive(Clone)]
2385pub struct OutlinedTextFieldConfig {
2386 pub label: Option<String>,
2390 pub placeholder: Option<String>,
2394 pub leading_icon: Option<View>,
2396 pub trailing_icon: Option<View>,
2398 pub single_line: bool,
2400 pub is_error: bool,
2402 pub enabled: bool,
2404 pub on_submit: Option<Rc<dyn Fn(String)>>,
2406 pub colors: Option<TextFieldColors>,
2408}
2409
2410impl Default for OutlinedTextFieldConfig {
2411 fn default() -> Self {
2412 Self {
2413 label: None,
2414 placeholder: None,
2415 leading_icon: None,
2416 trailing_icon: None,
2417 single_line: true,
2418 is_error: false,
2419 enabled: true,
2420 on_submit: None,
2421 colors: None,
2422 }
2423 }
2424}
2425
2426pub fn OutlinedTextField(
2448 modifier: Modifier,
2449 value: String,
2450 on_value_change: impl Fn(String) + 'static,
2451 config: OutlinedTextFieldConfig,
2452) -> View {
2453 let th = theme();
2454 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
2455 let has_label = label_str.is_some();
2456
2457 let anim_key = match &label_str {
2459 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
2460 None => "otf_nolabel".into(),
2461 };
2462
2463 let focus_tracker: Rc<Cell<bool>> =
2467 remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
2468 let is_focused = focus_tracker.get();
2469 let should_float = !value.is_empty() || is_focused;
2470
2471 let float_t = animate_f32(
2472 anim_key.clone(),
2473 if should_float { 1.0 } else { 0.0 },
2474 th.motion.color,
2475 );
2476
2477 let (border_color, label_color) = if let Some(ref tc) = config.colors {
2479 let enf = config.enabled && is_focused;
2480 (
2481 tc.indicator_color(config.enabled, config.is_error, enf),
2482 tc.label_color(config.enabled, config.is_error, enf),
2483 )
2484 } else {
2485 (
2486 if config.is_error {
2487 th.error
2488 } else if float_t > 0.5 {
2489 th.primary
2490 } else {
2491 th.outline
2492 },
2493 if config.is_error {
2494 th.error
2495 } else if float_t > 0.5 {
2496 th.primary
2497 } else {
2498 th.on_surface_variant
2499 },
2500 )
2501 };
2502
2503 let label_size = 16.0 - 4.0 * float_t;
2505
2506 let label_y = 16.0 - 20.0 * float_t;
2508
2509 let tf_placeholder = if has_label {
2512 String::new()
2513 } else {
2514 config.placeholder.unwrap_or_default()
2515 };
2516
2517 Box(modifier
2518 .clip_rounded(th.shapes.small)
2519 .border(1.0, border_color, th.shapes.small)
2520 .background(th.surface))
2521 .child(
2522 Stack(Modifier::new().fill_max_size()).child((
2523 Row(Modifier::new()
2526 .fill_max_size()
2527 .padding_values(PaddingValues {
2528 left: 16.0,
2529 right: 16.0,
2530 top: 16.0,
2531 bottom: 8.0,
2532 })
2533 .align_items(AlignItems::Center))
2534 .child((
2535 config.leading_icon.unwrap_or(Box(Modifier::new())),
2536 View::new(0, ViewKind::Box)
2537 .modifier(
2538 Modifier::new()
2539 .flex_grow(1.0)
2540 .padding_values(PaddingValues {
2541 left: 8.0,
2542 right: 8.0,
2543 top: 0.0,
2544 bottom: 0.0,
2545 })
2546 .text_input(TextInputConfig {
2547 hint: tf_placeholder,
2548 multiline: false,
2549 on_change: Some(Rc::new(on_value_change) as _),
2550 on_submit: config.on_submit.clone().map(|f| {
2551 let f = f.clone();
2552 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
2553 }),
2554 focus_tracker: Some(focus_tracker.clone()),
2555 value: value.clone(),
2556 visual_transformation: None,
2557 keyboard_type: None,
2558 ime_action: None,
2559 enabled: config.enabled,
2560 read_only: false,
2561 max_lines: None,
2562 min_lines: None,
2563 cursor_color: config
2564 .colors
2565 .as_ref()
2566 .map(|c| c.cursor_color(config.is_error)),
2567 on_text_layout: None,
2568 }),
2569 )
2570 .semantics(Semantics {
2571 role: Role::TextField,
2572 label: None,
2573 focused: false,
2574 enabled: true,
2575 selectable_group: false,
2576 }),
2577 config.trailing_icon.unwrap_or(Box(Modifier::new())),
2578 )),
2579 if let Some(lbl) = label_str {
2583 Box(Modifier::new()
2584 .min_width(200.0)
2585 .padding_values(PaddingValues {
2586 left: 20.0,
2587 right: 20.0,
2588 top: 0.0,
2589 bottom: 0.0,
2590 })
2591 .absolute()
2592 .offset(Some(0.0), Some(label_y), None, None))
2593 .child(
2594 Box(Modifier::new()
2595 .background(th.surface)
2596 .padding_values(PaddingValues {
2597 left: 4.0,
2598 right: 4.0,
2599 top: 2.0,
2600 bottom: 2.0,
2601 }))
2602 .child(
2603 Text(lbl.as_ref().to_string())
2604 .color(label_color)
2605 .size(label_size),
2606 ),
2607 )
2608 } else {
2609 Box(Modifier::new())
2610 },
2611 )),
2612 )
2613}
2614
2615#[derive(Clone)]
2617pub struct TextFieldConfig {
2618 pub label: Option<String>,
2619 pub placeholder: Option<String>,
2620 pub leading_icon: Option<View>,
2621 pub trailing_icon: Option<View>,
2622 pub single_line: bool,
2623 pub is_error: bool,
2624 pub enabled: bool,
2625 pub on_submit: Option<Rc<dyn Fn(String)>>,
2626 pub colors: Option<TextFieldColors>,
2627}
2628
2629impl Default for TextFieldConfig {
2630 fn default() -> Self {
2631 Self {
2632 label: None,
2633 placeholder: None,
2634 leading_icon: None,
2635 trailing_icon: None,
2636 single_line: true,
2637 is_error: false,
2638 enabled: true,
2639 on_submit: None,
2640 colors: None,
2641 }
2642 }
2643}
2644
2645pub fn TextField(
2652 modifier: Modifier,
2653 value: String,
2654 on_value_change: impl Fn(String) + 'static,
2655 config: TextFieldConfig,
2656) -> View {
2657 let th = theme();
2658 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
2659 let has_label = label_str.is_some();
2660
2661 let anim_key = match &label_str {
2662 Some(l) => format!("tf_{}", &l[..l.len().min(32)]),
2663 None => "tf_nolabel".into(),
2664 };
2665
2666 let focus_tracker: Rc<Cell<bool>> =
2667 remember_with_key(format!("tf_focus_{}", anim_key), || Cell::new(false));
2668 let is_focused = focus_tracker.get();
2669 let should_float = !value.is_empty() || is_focused;
2670
2671 let float_t = animate_f32(
2672 anim_key.clone(),
2673 if should_float { 1.0 } else { 0.0 },
2674 th.motion.color,
2675 );
2676
2677 let (indicator_color, label_color, container_bg) = if let Some(ref tc) = config.colors {
2679 let enf = config.enabled && is_focused;
2680 let ind = tc.indicator_color(config.enabled, config.is_error, enf);
2681 let lb = tc.label_color(config.enabled, config.is_error, enf);
2682 let bg = tc.container_color(config.enabled, config.is_error, enf);
2683 (ind, lb, bg)
2684 } else {
2685 let ind = if config.is_error {
2686 th.error
2687 } else if float_t > 0.5 {
2688 th.primary
2689 } else {
2690 th.on_surface_variant
2691 };
2692 let lb = if config.is_error {
2693 th.error
2694 } else if float_t > 0.5 {
2695 th.primary
2696 } else {
2697 th.on_surface_variant
2698 };
2699 let bg = if config.enabled {
2700 th.surface_container_highest
2701 } else {
2702 th.on_surface
2703 .with_alpha_f32(0.04)
2704 .composite_over(th.surface)
2705 };
2706 (ind, lb, bg)
2707 };
2708
2709 let label_size = 16.0 - 4.0 * float_t;
2710 let label_y = 16.0 - 20.0 * float_t;
2711
2712 let tf_placeholder = if has_label {
2713 String::new()
2714 } else {
2715 config.placeholder.unwrap_or_default()
2716 };
2717
2718 let indicator_active = config.is_error || (config.enabled && is_focused);
2719 let indicator_target_w = if indicator_active { 2.0 } else { 1.0 };
2720 let indicator_w = animate_f32(
2721 format!("tf_ind_w_{}", anim_key),
2722 indicator_target_w,
2723 th.motion.color,
2724 );
2725
2726 Box(modifier
2727 .clip_rounded(th.shapes.extra_small)
2728 .background(container_bg))
2729 .child(
2730 Stack(Modifier::new().fill_max_size()).child((
2731 Box(Modifier::new()
2733 .fill_max_size()
2734 .align_items(AlignItems::FlexEnd))
2735 .child(Box(Modifier::new()
2736 .fill_max_width()
2737 .height(indicator_w)
2738 .background(indicator_color))),
2739 Row(Modifier::new()
2741 .fill_max_size()
2742 .padding_values(PaddingValues {
2743 left: 16.0,
2744 right: 16.0,
2745 top: 16.0,
2746 bottom: 10.0,
2747 })
2748 .align_items(AlignItems::Center))
2749 .child((
2750 config.leading_icon.unwrap_or(Box(Modifier::new())),
2751 View::new(0, ViewKind::Box)
2752 .modifier(
2753 Modifier::new()
2754 .flex_grow(1.0)
2755 .padding_values(PaddingValues {
2756 left: 8.0,
2757 right: 8.0,
2758 top: 0.0,
2759 bottom: 0.0,
2760 })
2761 .text_input(TextInputConfig {
2762 hint: tf_placeholder,
2763 multiline: !config.single_line,
2764 on_change: Some(Rc::new(on_value_change) as _),
2765 on_submit: config.on_submit.clone().map(|f| {
2766 let f = f.clone();
2767 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
2768 }),
2769 focus_tracker: Some(focus_tracker.clone()),
2770 value: value.clone(),
2771 visual_transformation: None,
2772 keyboard_type: None,
2773 ime_action: None,
2774 enabled: config.enabled,
2775 read_only: false,
2776 max_lines: None,
2777 min_lines: None,
2778 cursor_color: config
2779 .colors
2780 .as_ref()
2781 .map(|c| c.cursor_color(config.is_error)),
2782 on_text_layout: None,
2783 }),
2784 )
2785 .semantics(Semantics {
2786 role: Role::TextField,
2787 label: None,
2788 focused: false,
2789 enabled: true,
2790 selectable_group: false,
2791 }),
2792 config.trailing_icon.unwrap_or(Box(Modifier::new())),
2793 )),
2794 if let Some(lbl) = label_str {
2796 Box(Modifier::new()
2797 .min_width(200.0)
2798 .padding_values(PaddingValues {
2799 left: 20.0,
2800 right: 20.0,
2801 top: 0.0,
2802 bottom: 0.0,
2803 })
2804 .absolute()
2805 .offset(Some(0.0), Some(label_y), None, None))
2806 .child(
2807 Box(Modifier::new()
2808 .background(th.surface_container_highest)
2809 .padding_values(PaddingValues {
2810 left: 4.0,
2811 right: 4.0,
2812 top: 2.0,
2813 bottom: 2.0,
2814 }))
2815 .child(
2816 Text(lbl.as_ref().to_string())
2817 .color(label_color)
2818 .size(label_size),
2819 ),
2820 )
2821 } else {
2822 Box(Modifier::new())
2823 },
2824 )),
2825 )
2826}
2827
2828#[derive(Clone, Debug)]
2830pub struct CheckboxConfig {
2831 pub modifier: Modifier,
2832 pub enabled: bool,
2834 pub checked_color: Color,
2835 pub unchecked_color: Color,
2836 pub checkmark_color: Color,
2837 pub checked_border_color: Color,
2839 pub unchecked_border_color: Color,
2841 pub disabled_checked_box_color: Color,
2842 pub disabled_unchecked_box_color: Color,
2843 pub disabled_indeterminate_box_color: Color,
2844 pub disabled_checkmark_color: Color,
2845 pub disabled_checked_border_color: Color,
2846 pub disabled_unchecked_border_color: Color,
2847 pub disabled_indeterminate_border_color: Color,
2848 pub state_colors: StateColors,
2849}
2850
2851impl Default for CheckboxConfig {
2852 fn default() -> Self {
2853 Self {
2854 modifier: Modifier::new(),
2855 enabled: true,
2856 checked_color: CheckboxDefaults::checked_color(),
2857 unchecked_color: CheckboxDefaults::unchecked_color(),
2858 checkmark_color: CheckboxDefaults::checkmark_color(),
2859 checked_border_color: CheckboxDefaults::checked_color(),
2860 unchecked_border_color: CheckboxDefaults::unchecked_color(),
2861 disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
2862 disabled_unchecked_box_color: Color::TRANSPARENT,
2863 disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
2864 disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
2865 disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
2866 disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
2867 disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
2868 state_colors: CheckboxDefaults::state_colors_default(),
2869 }
2870 }
2871}
2872
2873static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
2877pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
2878 let th = theme();
2879 let sz = CheckboxDefaults::BOX_SIZE;
2880
2881 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
2882 let spec = th.motion.color_fast;
2883
2884 let is_enabled = config.enabled;
2885
2886 let fill = animate_color(
2887 format!("cb_fill_{}", id),
2888 if !is_enabled {
2889 if checked {
2890 config.disabled_checked_box_color
2891 } else {
2892 config.disabled_unchecked_box_color
2893 }
2894 } else if checked {
2895 config.checked_color
2896 } else {
2897 Color::TRANSPARENT
2898 },
2899 spec,
2900 );
2901 let bd_w = animate_f32(
2902 format!("cb_bw_{}", id),
2903 if !is_enabled && checked {
2904 0.0
2905 } else if !is_enabled {
2906 CheckboxDefaults::STROKE_WIDTH
2907 } else if checked {
2908 0.0
2909 } else {
2910 CheckboxDefaults::STROKE_WIDTH
2911 },
2912 spec,
2913 );
2914 let bd = animate_color(
2915 format!("cb_bd_{}", id),
2916 if !is_enabled {
2917 if checked {
2918 config.disabled_checked_border_color
2919 } else {
2920 config.disabled_unchecked_border_color
2921 }
2922 } else if checked {
2923 Color::TRANSPARENT
2924 } else {
2925 config.unchecked_border_color
2926 },
2927 spec,
2928 );
2929 let check_alpha = animate_f32(
2930 format!("cb_ca_{}", id),
2931 if checked { 1.0 } else { 0.0 },
2932 spec,
2933 );
2934 let check_col = if !is_enabled {
2935 config.disabled_checkmark_color
2936 } else {
2937 config.checkmark_color
2938 };
2939
2940 let cb = move |_| {
2941 if config.enabled {
2942 on_change(!checked)
2943 }
2944 };
2945
2946 Box(Modifier::new()
2947 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
2948 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
2949 .padding(0.0)
2950 .clip_rounded(20.0)
2951 .background(Color::TRANSPARENT)
2952 .state_colors(config.state_colors)
2953 .clickable()
2954 .align_items(AlignItems::Center)
2955 .justify_content(JustifyContent::Center)
2956 .on_pointer_down(cb)
2957 .then(config.modifier))
2958 .child(
2959 Box(Modifier::new()
2960 .size(sz, sz)
2961 .background(fill)
2962 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
2963 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
2964 .align_items(AlignItems::Center)
2965 .justify_content(JustifyContent::Center))
2966 .child(if check_alpha > 0.01 {
2967 Box(Modifier::new().alpha(check_alpha)).child(
2968 Icon(Symbol::new("done", '\u{E876}'))
2969 .color(check_col)
2970 .size(CheckboxDefaults::CHECK_ICON_SIZE),
2971 )
2972 } else {
2973 Box(Modifier::new())
2974 }),
2975 )
2976}
2977
2978#[derive(Clone, Copy, Debug, PartialEq)]
2980pub enum TriState {
2981 Checked,
2982 Unchecked,
2983 Indeterminate,
2984}
2985
2986pub fn TriStateCheckbox(
2989 state: TriState,
2990 on_change: impl Fn(TriState) + 'static,
2991 config: CheckboxConfig,
2992) -> View {
2993 let th = theme();
2994 let sz = CheckboxDefaults::BOX_SIZE;
2995
2996 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
2997 let spec = th.motion.color_fast;
2998
2999 let is_checked = state == TriState::Checked;
3000 let is_indeterminate = state == TriState::Indeterminate;
3001 let has_fill = is_checked || is_indeterminate;
3002 let is_enabled = config.enabled;
3003
3004 let fill = animate_color(
3005 format!("tc_fill_{}", id),
3006 if !is_enabled {
3007 if has_fill {
3008 config.disabled_indeterminate_box_color
3009 } else {
3010 config.disabled_unchecked_box_color
3011 }
3012 } else if has_fill {
3013 config.checked_color
3014 } else {
3015 Color::TRANSPARENT
3016 },
3017 spec,
3018 );
3019 let bd_w = animate_f32(
3020 format!("tc_bw_{}", id),
3021 if !is_enabled {
3022 if has_fill {
3023 0.0
3024 } else {
3025 CheckboxDefaults::STROKE_WIDTH
3026 }
3027 } else if has_fill {
3028 0.0
3029 } else {
3030 CheckboxDefaults::STROKE_WIDTH
3031 },
3032 spec,
3033 );
3034 let bd = animate_color(
3035 format!("tc_bd_{}", id),
3036 if !is_enabled {
3037 if has_fill {
3038 config.disabled_indeterminate_border_color
3039 } else {
3040 config.disabled_unchecked_border_color
3041 }
3042 } else if has_fill {
3043 Color::TRANSPARENT
3044 } else {
3045 config.unchecked_border_color
3046 },
3047 spec,
3048 );
3049 let symbol_alpha = animate_f32(
3050 format!("tc_sa_{}", id),
3051 if has_fill { 1.0 } else { 0.0 },
3052 spec,
3053 );
3054 let symbol_col = if !is_enabled {
3055 config.disabled_checkmark_color
3056 } else {
3057 config.checkmark_color
3058 };
3059
3060 Box(Modifier::new()
3061 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
3062 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
3063 .padding(0.0)
3064 .clip_rounded(20.0)
3065 .background(Color::TRANSPARENT)
3066 .clickable()
3067 .align_items(AlignItems::Center)
3068 .justify_content(JustifyContent::Center)
3069 .on_pointer_down(move |_| {
3070 if is_enabled {
3071 on_change(match state {
3072 TriState::Checked => TriState::Unchecked,
3073 TriState::Indeterminate => TriState::Checked,
3074 TriState::Unchecked => TriState::Checked,
3075 })
3076 }
3077 })
3078 .then(config.modifier))
3079 .child(
3080 Box(Modifier::new()
3081 .size(sz, sz)
3082 .background(fill)
3083 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
3084 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
3085 .align_items(AlignItems::Center)
3086 .justify_content(JustifyContent::Center))
3087 .child(if symbol_alpha > 0.01 {
3088 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
3089 Box(Modifier::new()
3091 .width(10.0)
3092 .height(2.0)
3093 .background(symbol_col)
3094 .clip_rounded(1.0))
3095 } else {
3096 Icon(Symbol::new("done", '\u{E876}'))
3097 .color(symbol_col)
3098 .size(CheckboxDefaults::CHECK_ICON_SIZE)
3099 })
3100 } else {
3101 Box(Modifier::new())
3102 }),
3103 )
3104}
3105
3106#[derive(Clone, Debug)]
3108pub struct RadioButtonConfig {
3109 pub modifier: Modifier,
3110 pub enabled: bool,
3112 pub selected_color: Color,
3113 pub unselected_color: Color,
3114 pub disabled_selected_color: Color,
3115 pub disabled_unselected_color: Color,
3116 pub state_colors: StateColors,
3117}
3118
3119impl Default for RadioButtonConfig {
3120 fn default() -> Self {
3121 Self {
3122 modifier: Modifier::new(),
3123 enabled: true,
3124 selected_color: RadioButtonDefaults::selected_color(),
3125 unselected_color: RadioButtonDefaults::unselected_color(),
3126 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
3127 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
3128 state_colors: RadioButtonDefaults::state_colors_default(),
3129 }
3130 }
3131}
3132
3133static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
3137pub fn RadioButton(
3138 selected: bool,
3139 on_select: impl Fn() + 'static,
3140 config: RadioButtonConfig,
3141) -> View {
3142 let th = theme();
3143 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
3144
3145 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
3146 let color_spec = th.motion.color_fast;
3147 let spring = th.motion.spring;
3148
3149 let ring_col = animate_color(
3150 format!("rb_ring_{}", id),
3151 if !config.enabled {
3152 if selected {
3153 config.disabled_selected_color
3154 } else {
3155 config.disabled_unselected_color
3156 }
3157 } else if selected {
3158 config.selected_color
3159 } else {
3160 config.unselected_color
3161 },
3162 color_spec,
3163 );
3164 let dot_size = animate_f32(
3165 format!("rb_dot_{}", id),
3166 if selected {
3167 RadioButtonDefaults::DOT_RADIUS * 2.0
3168 } else {
3169 0.0
3170 },
3171 spring,
3172 );
3173 let dot_col = if !config.enabled {
3174 config.disabled_selected_color
3175 } else {
3176 config.selected_color
3177 };
3178
3179 let cb = move |_| {
3180 if config.enabled {
3181 on_select()
3182 }
3183 };
3184
3185 Box(Modifier::new()
3186 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3187 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
3188 .padding(0.0)
3189 .clip_rounded(20.0)
3190 .background(Color::TRANSPARENT)
3191 .state_colors(config.state_colors)
3192 .clickable()
3193 .align_items(AlignItems::Center)
3194 .justify_content(JustifyContent::Center)
3195 .on_pointer_down(cb)
3196 .then(config.modifier))
3197 .child(
3198 Box(Modifier::new()
3199 .size(d, d)
3200 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
3201 .clip_rounded(d * 0.5)
3202 .align_items(AlignItems::Center)
3203 .justify_content(JustifyContent::Center))
3204 .child(if dot_size > 0.5 {
3205 Box(Modifier::new()
3206 .size(dot_size, dot_size)
3207 .background(dot_col)
3208 .clip_rounded(dot_size * 0.5))
3209 } else {
3210 Box(Modifier::new())
3211 }),
3212 )
3213}
3214
3215#[derive(Clone, Debug)]
3217pub struct SwitchConfig {
3218 pub modifier: Modifier,
3219 pub enabled: bool,
3221 pub checked_track_color: Color,
3222 pub unchecked_track_color: Color,
3223 pub checked_thumb_color: Color,
3224 pub unchecked_thumb_color: Color,
3225 pub checked_icon_color: Color,
3227 pub unchecked_icon_color: Color,
3229 pub checked_border_color: Color,
3231 pub unchecked_border_color: Color,
3233 pub disabled_checked_thumb_color: Color,
3234 pub disabled_checked_track_color: Color,
3235 pub disabled_checked_border_color: Color,
3236 pub disabled_checked_icon_color: Color,
3237 pub disabled_unchecked_thumb_color: Color,
3238 pub disabled_unchecked_track_color: Color,
3239 pub disabled_unchecked_border_color: Color,
3240 pub disabled_unchecked_icon_color: Color,
3241 pub state_colors: StateColors,
3242}
3243
3244impl Default for SwitchConfig {
3245 fn default() -> Self {
3246 Self {
3247 modifier: Modifier::new(),
3248 enabled: true,
3249 checked_track_color: SwitchDefaults::checked_track_color(),
3250 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
3251 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
3252 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
3253 checked_icon_color: SwitchDefaults::checked_icon_color(),
3254 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
3255 checked_border_color: Color::TRANSPARENT,
3256 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
3257 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
3258 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
3259 disabled_checked_border_color: Color::TRANSPARENT,
3260 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
3261 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
3262 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
3263 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
3264 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
3265 state_colors: SwitchDefaults::state_colors_default(),
3266 }
3267 }
3268}
3269
3270static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
3274pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
3275 let th = theme();
3276 let track_w = SwitchDefaults::TRACK_WIDTH;
3277 let track_h = SwitchDefaults::TRACK_HEIGHT;
3278
3279 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
3280
3281 let hovered = remember(|| Signal::new(false));
3282 let pressed = remember(|| Signal::new(false));
3283
3284 let thumb_target_pos = if checked {
3286 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
3287 } else {
3288 8.0
3289 };
3290 let thumb_target_d = if checked {
3291 SwitchDefaults::THUMB_CHECKED_SIZE
3292 } else {
3293 SwitchDefaults::THUMB_UNCHECKED_SIZE
3294 };
3295 let spring = th.motion.spring;
3296
3297 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
3298 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
3299 let thumb_top = (track_h - thumb_d) * 0.5;
3300
3301 let color_spec = th.motion.color_fast;
3302 let is_enabled = config.enabled;
3303
3304 let track_bg = animate_color(
3305 format!("sw_tbg_{}", id),
3306 if !is_enabled {
3307 if checked {
3308 config.disabled_checked_track_color
3309 } else {
3310 config.disabled_unchecked_track_color
3311 }
3312 } else if checked {
3313 config.checked_track_color
3314 } else {
3315 config.unchecked_track_color
3316 },
3317 color_spec,
3318 );
3319 let thumb_bg = animate_color(
3320 format!("sw_tmbg_{}", id),
3321 if !is_enabled {
3322 if checked {
3323 config.disabled_checked_thumb_color
3324 } else {
3325 config.disabled_unchecked_thumb_color
3326 }
3327 } else if checked {
3328 config.checked_thumb_color
3329 } else {
3330 config.unchecked_thumb_color
3331 },
3332 color_spec,
3333 );
3334 let track_border = animate_f32(
3335 format!("sw_tb_{}", id),
3336 if !is_enabled {
3337 if checked { 0.0 } else { 2.0 }
3338 } else if checked {
3339 0.0
3340 } else {
3341 2.0
3342 },
3343 color_spec,
3344 );
3345 let border_color = animate_color(
3346 format!("sw_bc_{}", id),
3347 if !is_enabled {
3348 if checked {
3349 config.disabled_checked_border_color
3350 } else {
3351 config.disabled_unchecked_border_color
3352 }
3353 } else if checked {
3354 config.checked_border_color
3355 } else {
3356 config.unchecked_border_color
3357 },
3358 color_spec,
3359 );
3360
3361 let state_overlay = animate_color(
3362 format!("sw_ol_{}", id),
3363 if !is_enabled {
3364 Color::TRANSPARENT
3365 } else if pressed.get() {
3366 config.state_colors.pressed
3367 } else if hovered.get() {
3368 config.state_colors.hovered
3369 } else {
3370 config.state_colors.default
3371 },
3372 color_spec,
3373 );
3374
3375 Box(Modifier::new()
3376 .size(track_w, track_h)
3377 .padding(0.0)
3378 .clip_rounded(track_h * 0.5)
3379 .background(track_bg)
3380 .border(track_border, border_color, track_h * 0.5)
3381 .clickable()
3382 .on_pointer_enter({
3383 let h = hovered.clone();
3384 move |_| h.set(true)
3385 })
3386 .on_pointer_leave({
3387 let h = hovered.clone();
3388 let p = pressed.clone();
3389 move |_| {
3390 h.set(false);
3391 p.set(false);
3392 }
3393 })
3394 .on_pointer_down({
3395 let p = pressed.clone();
3396 let cb = on_change;
3397 let en = is_enabled;
3398 move |_| {
3399 p.set(true);
3400 if en {
3401 cb(!checked);
3402 }
3403 }
3404 })
3405 .on_pointer_up({
3406 let p = pressed.clone();
3407 move |_| p.set(false)
3408 })
3409 .then(config.modifier))
3410 .child((
3411 Box(Modifier::new()
3412 .size(thumb_d, thumb_d)
3413 .background(thumb_bg)
3414 .clip_rounded(thumb_d * 0.5)
3415 .absolute()
3416 .offset(Some(thumb_left), Some(thumb_top), None, None)),
3417 Box(Modifier::new()
3418 .size(40.0, 40.0)
3419 .clip_rounded(20.0)
3420 .background(state_overlay)
3421 .absolute()
3422 .offset(
3423 Some(thumb_left + thumb_d * 0.5 - 20.0),
3424 Some(track_h * 0.5 - 20.0),
3425 None,
3426 None,
3427 )),
3428 ))
3429}
3430
3431#[derive(Clone, Debug)]
3433pub struct SliderConfig {
3434 pub modifier: Modifier,
3435 pub enabled: bool,
3437 pub active_track_color: Color,
3438 pub inactive_track_color: Color,
3439 pub thumb_color: Color,
3440 pub active_tick_color: Color,
3441 pub inactive_tick_color: Color,
3442 pub disabled_thumb_color: Color,
3443 pub disabled_active_track_color: Color,
3444 pub disabled_inactive_track_color: Color,
3445 pub disabled_active_tick_color: Color,
3446 pub disabled_inactive_tick_color: Color,
3447 pub state_colors: StateColors,
3448}
3449
3450impl Default for SliderConfig {
3451 fn default() -> Self {
3452 Self {
3453 modifier: Modifier::new(),
3454 enabled: true,
3455 active_track_color: SliderDefaults::active_track_color(),
3456 inactive_track_color: SliderDefaults::inactive_track_color(),
3457 thumb_color: SliderDefaults::thumb_color(),
3458 active_tick_color: SliderDefaults::active_tick_color(),
3459 inactive_tick_color: SliderDefaults::inactive_tick_color(),
3460 disabled_thumb_color: SliderDefaults::disabled_thumb_color(),
3461 disabled_active_track_color: SliderDefaults::disabled_active_track_color(),
3462 disabled_inactive_track_color: SliderDefaults::disabled_inactive_track_color(),
3463 disabled_active_tick_color: SliderDefaults::disabled_active_tick_color(),
3464 disabled_inactive_tick_color: SliderDefaults::disabled_inactive_tick_color(),
3465 state_colors: SliderDefaults::state_colors_default(),
3466 }
3467 }
3468}
3469
3470static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
3471
3472fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
3473 let v = v.clamp(min, max);
3474 if let Some(s) = step.filter(|s| *s > 0.0) {
3475 let t = ((v - min) / s).round();
3476 (min + t * s).clamp(min, max)
3477 } else {
3478 v
3479 }
3480}
3481
3482fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
3483 let w = rect.w.max(1.0);
3484 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
3485 let v = min + t * (max - min);
3486 snap_step(v, min, max, step)
3487}
3488
3489pub fn Slider(
3490 value: f32,
3491 range: (f32, f32),
3492 step: Option<f32>,
3493 on_change: impl Fn(f32) + 'static,
3494 config: SliderConfig,
3495) -> View {
3496 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
3497 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
3498 let drag_active = remember_state_with_key(format!("ms_da_{}", id), || false);
3499 let hovered = remember(|| Signal::new(false));
3500
3501 let track_rect_p = track_rect.clone();
3502 let drag_active_p = drag_active.clone();
3503 let hovered_sig = hovered.clone();
3504 let sc = config.state_colors;
3505
3506 let min = range.0;
3507 let max = range.1;
3508 let oc = Rc::new(on_change);
3509 let range_size = (max - min).max(1e-6);
3510 let t = ((value - min) / range_size).clamp(0.0, 1.0);
3511
3512 let tick_frac: Vec<f32> = if let Some(s) = step {
3513 let n = ((max - min) / s.max(1e-6)).round() as usize;
3514 (0..=n).map(|i| i as f32 / n as f32).collect()
3515 } else {
3516 Vec::new()
3517 };
3518
3519 Box(Modifier::new()
3520 .min_width(200.0)
3521 .height(44.0)
3522 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
3523 let mul_c = |c: Color| {
3524 Color(
3525 c.0,
3526 c.1,
3527 c.2,
3528 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
3529 )
3530 };
3531 let track_h = dp_to_px(16.0);
3532 let thumb_w = dp_to_px(4.0);
3533 let thumb_h = dp_to_px(44.0);
3534 let dot_r = dp_to_px(2.0);
3535 let corner = track_h * 0.5;
3536 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
3537 let pad = thumb_w * 0.5;
3538 let track_x = rect.x + pad;
3539 let track_w = (rect.w - thumb_w).max(0.0);
3540 let cy = rect.y + rect.h * 0.5;
3541
3542 let kx = if step.is_some() && !tick_frac.is_empty() {
3543 let is_first = (t - tick_frac[0]).abs() < 1e-6;
3544 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
3545 if is_first || is_last {
3546 track_x + t * track_w
3547 } else {
3548 track_x + (track_w - track_h) * t + corner
3549 }
3550 } else {
3551 track_x + t * track_w
3552 };
3553
3554 *track_rect_p.borrow_mut() = Rect {
3555 x: track_x,
3556 y: rect.y,
3557 w: track_w,
3558 h: rect.h,
3559 };
3560
3561 let inactive_x = track_x.max(kx + gap);
3562 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
3563 if inactive_w > 0.0 {
3564 scene.nodes.push(SceneNode::Rect {
3565 rect: Rect {
3566 x: inactive_x,
3567 y: cy - track_h * 0.5,
3568 w: inactive_w,
3569 h: track_h,
3570 },
3571 brush: Brush::Solid(mul_c(config.inactive_track_color)),
3572 radius: [corner; 4],
3573 });
3574 }
3575 let fill_w = (kx - gap - track_x).max(0.0);
3576 if fill_w > 0.0 {
3577 scene.nodes.push(SceneNode::Rect {
3578 rect: Rect {
3579 x: track_x,
3580 y: cy - track_h * 0.5,
3581 w: fill_w,
3582 h: track_h,
3583 },
3584 brush: Brush::Solid(mul_c(config.active_track_color)),
3585 radius: [corner; 4],
3586 });
3587 }
3588 let tick_start = track_x + corner;
3589 let tick_end = track_x + track_w - corner;
3590 for (i, &tf) in tick_frac.iter().enumerate() {
3591 let tx = tick_start + tf * (tick_end - tick_start);
3592 if i == tick_frac.len() - 1 {
3594 continue;
3595 }
3596 if tx >= kx - gap && tx <= kx + gap {
3597 continue;
3598 }
3599 let on_active = tx <= kx - gap;
3600 scene.nodes.push(SceneNode::Ellipse {
3601 rect: Rect {
3602 x: tx - dot_r,
3603 y: cy - dot_r,
3604 w: dot_r * 2.0,
3605 h: dot_r * 2.0,
3606 },
3607 brush: Brush::Solid(mul_c(if on_active {
3608 config.active_tick_color
3609 } else {
3610 config.inactive_tick_color
3611 })),
3612 });
3613 }
3614 let sx = track_x + track_w - corner;
3615 scene.nodes.push(SceneNode::Ellipse {
3616 rect: Rect {
3617 x: sx - dot_r,
3618 y: cy - dot_r,
3619 w: dot_r * 2.0,
3620 h: dot_r * 2.0,
3621 },
3622 brush: Brush::Solid(mul_c(config.inactive_tick_color)),
3623 });
3624 let da = *drag_active_p.borrow();
3625 let hv = hovered_sig.get();
3626 let tw = if da { thumb_w * 0.5 } else { thumb_w };
3627 scene.nodes.push(SceneNode::Rect {
3628 rect: Rect {
3629 x: kx - tw * 0.5,
3630 y: cy - thumb_h * 0.5,
3631 w: tw,
3632 h: thumb_h,
3633 },
3634 brush: Brush::Solid(mul_c(config.thumb_color)),
3635 radius: [tw * 0.5; 4],
3636 });
3637 let sc_target = if da {
3638 sc.pressed
3639 } else if hv {
3640 sc.hovered
3641 } else {
3642 sc.default
3643 };
3644 if sc_target.3 > 0 {
3645 scene.nodes.push(SceneNode::Rect {
3646 rect: Rect {
3647 x: kx - tw * 0.5,
3648 y: cy - thumb_h * 0.5,
3649 w: tw,
3650 h: thumb_h,
3651 },
3652 brush: Brush::Solid(mul_c(sc_target)),
3653 radius: [tw * 0.5; 4],
3654 });
3655 }
3656 })
3657 .on_pointer_enter({
3658 let h = hovered.clone();
3659 move |_pe: PointerEvent| h.set(true)
3660 })
3661 .on_pointer_leave({
3662 let h = hovered.clone();
3663 move |_pe: PointerEvent| h.set(false)
3664 })
3665 .on_pointer_down({
3666 let oc = oc.clone();
3667 let track_rect = track_rect.clone();
3668 let drag_active = drag_active.clone();
3669 move |pe: PointerEvent| {
3670 *drag_active.borrow_mut() = true;
3671 let r = *track_rect.borrow();
3672 (oc)(value_from_x(pe.position.x, r, min, max, step));
3673 }
3674 })
3675 .on_pointer_move({
3676 let oc = oc.clone();
3677 let track_rect = track_rect.clone();
3678 let drag_active = drag_active.clone();
3679 move |pe: PointerEvent| {
3680 if !*drag_active.borrow() {
3681 return;
3682 }
3683 let r = *track_rect.borrow();
3684 (oc)(value_from_x(pe.position.x, r, min, max, step));
3685 }
3686 })
3687 .on_pointer_up(move |_pe: PointerEvent| {
3688 *drag_active.borrow_mut() = false;
3689 })
3690 .on_scroll({
3691 let oc = oc.clone();
3692 move |d: Vec2| -> Vec2 {
3693 let dir = if d.y < -0.5 {
3694 1
3695 } else if d.y > 0.5 {
3696 -1
3697 } else {
3698 0
3699 };
3700 if dir == 0 {
3701 return d;
3702 }
3703 let step_val = step.unwrap_or(1.0).max(1e-6);
3704 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
3705 if (new_val - value).abs() > 1e-6 {
3706 (oc)(new_val);
3707 Vec2 { x: d.x, y: 0.0 }
3708 } else {
3709 d
3710 }
3711 }
3712 })
3713 .then(config.modifier))
3714 .semantics(Semantics {
3715 role: Role::Slider,
3716 label: None,
3717 focused: false,
3718 enabled: true,
3719 selectable_group: false,
3720 })
3721}
3722
3723pub fn RangeSlider(
3724 start: f32,
3725 end: f32,
3726 range: (f32, f32),
3727 step: Option<f32>,
3728 on_change: impl Fn(f32, f32) + 'static,
3729 config: SliderConfig,
3730) -> View {
3731 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
3732 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
3733 let drag_active = remember_state_with_key(format!("mrs_da_{}", id), || false);
3734 let active_thumb = remember_state_with_key(format!("mrs_at_{}", id), || false);
3735 let hovered = remember(|| Signal::new(false));
3736
3737 let min = range.0;
3738 let max = range.1;
3739 let oc = Rc::new(on_change);
3740 let range_size = (max - min).max(1e-6);
3741 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
3742 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
3743 let sc = config.state_colors;
3744 let is_enabled = config.enabled;
3745
3746 let act_trk = if !is_enabled {
3747 config.disabled_active_track_color
3748 } else {
3749 config.active_track_color
3750 };
3751 let inact_trk = if !is_enabled {
3752 config.disabled_inactive_track_color
3753 } else {
3754 config.inactive_track_color
3755 };
3756 let act_tick = if !is_enabled {
3757 config.disabled_active_tick_color
3758 } else {
3759 config.active_tick_color
3760 };
3761 let inact_tick = if !is_enabled {
3762 config.disabled_inactive_tick_color
3763 } else {
3764 config.inactive_tick_color
3765 };
3766 let thumb_col = if !is_enabled {
3767 config.disabled_thumb_color
3768 } else {
3769 config.thumb_color
3770 };
3771
3772 let tick_frac: Vec<f32> = if let Some(s) = step {
3773 let n = ((max - min) / s.max(1e-6)).round() as usize;
3774 (0..=n).map(|i| i as f32 / n as f32).collect()
3775 } else {
3776 Vec::new()
3777 };
3778
3779 let track_rect_p = track_rect.clone();
3780 let drag_active_p = drag_active.clone();
3781 let active_thumb_p = active_thumb.clone();
3782 let hovered_sig = hovered.clone();
3783
3784 Box(Modifier::new()
3785 .min_width(200.0)
3786 .height(44.0)
3787 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
3788 let mul_c = |c: Color| {
3789 Color(
3790 c.0,
3791 c.1,
3792 c.2,
3793 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
3794 )
3795 };
3796 let track_h = dp_to_px(16.0);
3797 let thumb_w = dp_to_px(4.0);
3798 let thumb_h = dp_to_px(44.0);
3799 let dot_r = dp_to_px(2.0);
3800 let corner = track_h * 0.5;
3801 let gap = thumb_w * 0.5 + dp_to_px(ProgressIndicatorDefaults::SLIDER_THUMB_TRACK_GAP);
3802 let pad = thumb_w * 0.5;
3803 let track_x = rect.x + pad;
3804 let track_w = (rect.w - thumb_w).max(0.0);
3805 let cy = rect.y + rect.h * 0.5;
3806
3807 let thumb_pos = |tf: f32, fracs: &[f32]| {
3808 if step.is_some() && !fracs.is_empty() {
3809 let is_first = (tf - fracs[0]).abs() < 1e-6;
3810 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
3811 if is_first || is_last {
3812 track_x + tf * track_w
3813 } else {
3814 track_x + (track_w - track_h) * tf + corner
3815 }
3816 } else {
3817 track_x + tf * track_w
3818 }
3819 };
3820 let k0 = thumb_pos(t0, &tick_frac);
3821 let k1 = thumb_pos(t1, &tick_frac);
3822 let active_l = k0.min(k1);
3823 let active_r = k0.max(k1);
3824
3825 *track_rect_p.borrow_mut() = Rect {
3826 x: track_x,
3827 y: rect.y,
3828 w: track_w,
3829 h: rect.h,
3830 };
3831
3832 let linactive_w = (active_l - gap - track_x).max(0.0);
3833 if linactive_w > 0.0 {
3834 scene.nodes.push(SceneNode::Rect {
3835 rect: Rect {
3836 x: track_x,
3837 y: cy - track_h * 0.5,
3838 w: linactive_w,
3839 h: track_h,
3840 },
3841 brush: Brush::Solid(mul_c(inact_trk)),
3842 radius: [corner; 4],
3843 });
3844 }
3845 let rinactive_x = (active_r + gap).min(track_x + track_w);
3846 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
3847 if rinactive_w > 0.0 {
3848 scene.nodes.push(SceneNode::Rect {
3849 rect: Rect {
3850 x: rinactive_x,
3851 y: cy - track_h * 0.5,
3852 w: rinactive_w,
3853 h: track_h,
3854 },
3855 brush: Brush::Solid(mul_c(inact_trk)),
3856 radius: [corner; 4],
3857 });
3858 }
3859 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
3860 if active_w > 0.0 {
3861 scene.nodes.push(SceneNode::Rect {
3862 rect: Rect {
3863 x: active_l + gap,
3864 y: cy - track_h * 0.5,
3865 w: active_w,
3866 h: track_h,
3867 },
3868 brush: Brush::Solid(mul_c(act_trk)),
3869 radius: [corner; 4],
3870 });
3871 }
3872 let tick_start = track_x + corner;
3873 let tick_end = track_x + track_w - corner;
3874 for (i, &tf) in tick_frac.iter().enumerate() {
3875 let tx = tick_start + tf * (tick_end - tick_start);
3876 if i == 0 || i == tick_frac.len() - 1 {
3878 continue;
3879 }
3880 let in_lgap = tx >= active_l - gap && tx <= active_l + gap;
3881 let in_rgap = tx >= active_r - gap && tx <= active_r + gap;
3882 if in_lgap || in_rgap {
3883 continue;
3884 }
3885 let on_active = tx >= active_l + gap && tx <= active_r - gap;
3886 scene.nodes.push(SceneNode::Ellipse {
3887 rect: Rect {
3888 x: tx - dot_r,
3889 y: cy - dot_r,
3890 w: dot_r * 2.0,
3891 h: dot_r * 2.0,
3892 },
3893 brush: Brush::Solid(mul_c(if on_active { act_tick } else { inact_tick })),
3894 });
3895 }
3896 let sx0 = track_x + corner;
3898 scene.nodes.push(SceneNode::Ellipse {
3899 rect: Rect {
3900 x: sx0 - dot_r,
3901 y: cy - dot_r,
3902 w: dot_r * 2.0,
3903 h: dot_r * 2.0,
3904 },
3905 brush: Brush::Solid(mul_c(inact_tick)),
3906 });
3907 let sx = track_x + track_w - corner;
3908 scene.nodes.push(SceneNode::Ellipse {
3909 rect: Rect {
3910 x: sx - dot_r,
3911 y: cy - dot_r,
3912 w: dot_r * 2.0,
3913 h: dot_r * 2.0,
3914 },
3915 brush: Brush::Solid(mul_c(inact_tick)),
3916 });
3917 let da = *drag_active_p.borrow();
3918 let at = *active_thumb_p.borrow();
3919 let hv = hovered_sig.get();
3920 let thumbs = [k0, k1];
3921 for (idx, &kx) in thumbs.iter().enumerate() {
3922 let is_active = da && (if idx == 0 { !at } else { at });
3923 let tw = if is_active { thumb_w * 0.5 } else { thumb_w };
3924 scene.nodes.push(SceneNode::Rect {
3925 rect: Rect {
3926 x: kx - tw * 0.5,
3927 y: cy - thumb_h * 0.5,
3928 w: tw,
3929 h: thumb_h,
3930 },
3931 brush: Brush::Solid(mul_c(thumb_col)),
3932 radius: [tw * 0.5; 4],
3933 });
3934 let sc_target = if !is_enabled {
3935 Color::TRANSPARENT
3936 } else if is_active {
3937 sc.pressed
3938 } else if hv {
3939 sc.hovered
3940 } else {
3941 sc.default
3942 };
3943 if sc_target.3 > 0 {
3944 scene.nodes.push(SceneNode::Rect {
3945 rect: Rect {
3946 x: kx - tw * 0.5,
3947 y: cy - thumb_h * 0.5,
3948 w: tw,
3949 h: thumb_h,
3950 },
3951 brush: Brush::Solid(mul_c(sc_target)),
3952 radius: [tw * 0.5; 4],
3953 });
3954 }
3955 }
3956 })
3957 .on_pointer_enter({
3958 let h = hovered.clone();
3959 let en = is_enabled;
3960 move |_pe: PointerEvent| {
3961 if en {
3962 h.set(true);
3963 }
3964 }
3965 })
3966 .on_pointer_leave({
3967 let h = hovered.clone();
3968 move |_pe: PointerEvent| h.set(false)
3969 })
3970 .on_pointer_down({
3971 let oc = oc.clone();
3972 let track_rect = track_rect.clone();
3973 let drag_active = drag_active.clone();
3974 let active_thumb = active_thumb.clone();
3975 let en = is_enabled;
3976 move |pe: PointerEvent| {
3977 if !en {
3978 return;
3979 }
3980 *drag_active.borrow_mut() = true;
3981 let r = *track_rect.borrow();
3982 let v = value_from_x(pe.position.x, r, min, max, step);
3983 let use_end = (v - end).abs() < (v - start).abs();
3984 *active_thumb.borrow_mut() = use_end;
3985 let (a, b) = if use_end {
3986 (start, v.max(start))
3987 } else {
3988 (v.min(end), end)
3989 };
3990 (oc)(a, b);
3991 }
3992 })
3993 .on_pointer_move({
3994 let oc = oc.clone();
3995 let track_rect = track_rect.clone();
3996 let drag_active = drag_active.clone();
3997 let active_thumb = active_thumb.clone();
3998 move |pe: PointerEvent| {
3999 if !*drag_active.borrow() {
4000 return;
4001 }
4002 let r = *track_rect.borrow();
4003 let v = value_from_x(pe.position.x, r, min, max, step);
4004 let use_end = *active_thumb.borrow();
4005 let (a, b) = if use_end {
4006 (start, v.max(start))
4007 } else {
4008 (v.min(end), end)
4009 };
4010 (oc)(a, b);
4011 }
4012 })
4013 .on_pointer_up({
4014 let drag_active = drag_active.clone();
4015 let active_thumb = active_thumb.clone();
4016 move |_pe: PointerEvent| {
4017 *drag_active.borrow_mut() = false;
4018 *active_thumb.borrow_mut() = false;
4019 }
4020 })
4021 .on_scroll({
4022 let oc = oc.clone();
4023 let active_thumb = active_thumb.clone();
4024 let en = is_enabled;
4025 move |d: Vec2| -> Vec2 {
4026 if !en {
4027 return d;
4028 }
4029 let dir = if d.y < -0.5 {
4030 1
4031 } else if d.y > 0.5 {
4032 -1
4033 } else {
4034 0
4035 };
4036 if dir == 0 {
4037 return d;
4038 }
4039 let step_val = step.unwrap_or(1.0).max(1e-6);
4040 let use_end = *active_thumb.borrow();
4041 let (mut a, mut b) = (start, end);
4042 if use_end {
4043 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
4044 } else {
4045 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
4046 }
4047 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
4048 (oc)(a, b);
4049 Vec2 { x: d.x, y: 0.0 }
4050 } else {
4051 d
4052 }
4053 }
4054 })
4055 .then(config.modifier))
4056 .semantics(Semantics {
4057 role: Role::Slider,
4058 label: None,
4059 focused: false,
4060 enabled: is_enabled,
4061 selectable_group: false,
4062 })
4063}
4064
4065#[derive(Clone, Debug)]
4067pub struct CardConfig {
4068 pub modifier: Modifier,
4069 pub enabled: bool,
4071 pub container_color: Color,
4072 pub content_color: Color,
4073 pub disabled_container_color: Color,
4074 pub disabled_content_color: Color,
4075 pub shape_radius: f32,
4076 pub tonal_elevation: f32,
4077 pub state_elevation: Option<StateElevation>,
4078 pub border: Option<(f32, Color)>,
4079}
4080
4081impl Default for CardConfig {
4082 fn default() -> Self {
4083 Self {
4084 modifier: Modifier::new(),
4085 enabled: true,
4086 container_color: CardDefaults::filled_container_color(),
4087 content_color: CardDefaults::filled_content_color(),
4088 disabled_container_color: CardDefaults::disabled_container_color(),
4089 disabled_content_color: CardDefaults::disabled_content_color(),
4090 shape_radius: CardDefaults::SHAPE_RADIUS,
4091 tonal_elevation: CardDefaults::ELEVATION,
4092 state_elevation: None,
4093 border: None,
4094 }
4095 }
4096}
4097
4098pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
4100 let bg = if !config.enabled {
4101 config.disabled_container_color
4102 } else {
4103 config.container_color
4104 };
4105 let fg = if !config.enabled {
4106 config.disabled_content_color
4107 } else {
4108 config.content_color
4109 };
4110 let mut m = Modifier::new()
4111 .background(bg)
4112 .clip_rounded(config.shape_radius)
4113 .then(config.modifier);
4114 if let Some((w, c)) = config.border {
4115 m = m.border(w, c, config.shape_radius);
4116 }
4117 if let Some(se) = config.state_elevation {
4118 m = m.state_elevation(se);
4119 } else if config.tonal_elevation > 0.0 {
4120 m = m.state_elevation(StateElevation {
4121 default: config.tonal_elevation,
4122 hovered: config.tonal_elevation,
4123 pressed: config.tonal_elevation,
4124 disabled: 0.0,
4125 });
4126 }
4127 Box(m).color(fg).child(content())
4128}
4129
4130pub fn ElevatedCard(modifier: Modifier, content: View) -> View {
4132 let th = theme();
4133 Card(
4134 CardConfig {
4135 modifier,
4136 container_color: CardDefaults::elevated_container_color(),
4137 state_elevation: Some(StateElevation {
4138 default: th.elevation.level1,
4139 hovered: th.elevation.level2,
4140 pressed: th.elevation.level3,
4141 disabled: 0.0,
4142 }),
4143 ..Default::default()
4144 },
4145 || Column(Modifier::new().fill_max_size()).child(content),
4146 )
4147}
4148
4149pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
4151 Card(
4152 CardConfig {
4153 modifier,
4154 container_color: CardDefaults::outlined_container_color(),
4155 border: Some((1.0, CardDefaults::outlined_border_color())),
4156 ..Default::default()
4157 },
4158 || Column(Modifier::new().fill_max_size()).child(content),
4159 )
4160}
4161
4162fn card_state_colors(bg: Color) -> StateColors {
4163 let th = theme();
4164 StateColors {
4165 default: Color::TRANSPARENT,
4166 hovered: th.on_surface.with_alpha_f32(0.08).composite_over(bg),
4167 pressed: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4168 disabled: th.on_surface.with_alpha_f32(0.12).composite_over(bg),
4169 }
4170}
4171
4172fn clickable_card_impl(
4173 on_click: impl Fn() + 'static,
4174 modifier: Modifier,
4175 bg: Color,
4176 shape_radius: f32,
4177 config: CardConfig,
4178 content: impl FnOnce() -> View,
4179) -> View {
4180 let m = modifier
4181 .state_colors(card_state_colors(bg))
4182 .clickable()
4183 .on_pointer_down({
4184 let cb = on_click;
4185 let en = config.enabled;
4186 move |_| {
4187 if en {
4188 cb();
4189 }
4190 }
4191 });
4192 Card(
4193 CardConfig {
4194 modifier: m,
4195 enabled: config.enabled,
4196 container_color: bg,
4197 content_color: config.content_color,
4198 disabled_container_color: config.disabled_container_color,
4199 disabled_content_color: config.disabled_content_color,
4200 shape_radius,
4201 border: config.border,
4202 state_elevation: config.state_elevation,
4203 tonal_elevation: config.tonal_elevation,
4204 },
4205 || Column(Modifier::new().fill_max_size()).child(content()),
4206 )
4207}
4208
4209pub fn ClickableCard(
4211 on_click: impl Fn() + 'static,
4212 modifier: Modifier,
4213 config: CardConfig,
4214 content: impl FnOnce() -> View,
4215) -> View {
4216 let th = theme();
4217 clickable_card_impl(
4218 on_click,
4219 modifier,
4220 th.surface_container_highest,
4221 th.shapes.medium,
4222 config,
4223 content,
4224 )
4225}
4226
4227pub fn ClickableElevatedCard(
4229 on_click: impl Fn() + 'static,
4230 modifier: Modifier,
4231 config: CardConfig,
4232 content: impl FnOnce() -> View,
4233) -> View {
4234 let th = theme();
4235 let cfg = CardConfig {
4236 state_elevation: Some(StateElevation {
4237 default: th.elevation.level1,
4238 hovered: th.elevation.level2,
4239 pressed: th.elevation.level3,
4240 disabled: 0.0,
4241 }),
4242 ..config
4243 };
4244 clickable_card_impl(
4245 on_click,
4246 modifier,
4247 th.surface,
4248 th.shapes.medium,
4249 cfg,
4250 content,
4251 )
4252}
4253
4254pub fn ClickableOutlinedCard(
4256 on_click: impl Fn() + 'static,
4257 modifier: Modifier,
4258 config: CardConfig,
4259 content: impl FnOnce() -> View,
4260) -> View {
4261 let th = theme();
4262 let cfg = CardConfig {
4263 border: Some((1.0, th.outline_variant)),
4264 ..config
4265 };
4266 clickable_card_impl(
4267 on_click,
4268 modifier,
4269 th.surface,
4270 th.shapes.medium,
4271 cfg,
4272 content,
4273 )
4274}
4275
4276#[derive(Clone, Debug)]
4278pub struct SnackbarConfig {
4279 pub modifier: Modifier,
4280 pub container_color: Color,
4281 pub content_color: Color,
4282 pub action_color: Color,
4283 pub dismiss_action_content_color: Color,
4284 pub action_on_new_line: bool,
4285 pub shape_radius: f32,
4286 pub min_height: f32,
4287 pub min_width: f32,
4288 pub max_width: f32,
4289}
4290
4291impl Default for SnackbarConfig {
4292 fn default() -> Self {
4293 Self {
4294 modifier: Modifier::new(),
4295 container_color: SnackbarDefaults::container_color(),
4296 content_color: SnackbarDefaults::content_color(),
4297 action_color: SnackbarDefaults::action_color(),
4298 dismiss_action_content_color: SnackbarDefaults::dismiss_action_content_color(),
4299 action_on_new_line: false,
4300 shape_radius: SnackbarDefaults::SHAPE_RADIUS,
4301 min_height: SnackbarDefaults::MIN_HEIGHT,
4302 min_width: SnackbarDefaults::MIN_WIDTH,
4303 max_width: SnackbarDefaults::MAX_WIDTH,
4304 }
4305 }
4306}
4307
4308#[derive(Clone, Copy, Debug)]
4310pub struct ChipColors {
4311 pub container_color: Color,
4312 pub label_color: Color,
4313 pub leading_icon_color: Color,
4314 pub trailing_icon_color: Color,
4315 pub disabled_container_color: Color,
4316 pub disabled_label_color: Color,
4317 pub disabled_leading_icon_color: Color,
4318 pub disabled_trailing_icon_color: Color,
4319 pub selected_container_color: Color,
4320 pub selected_label_color: Color,
4321 pub selected_leading_icon_color: Color,
4322 pub selected_trailing_icon_color: Color,
4323 pub disabled_selected_container_color: Color,
4324}
4325
4326impl ChipColors {
4327 pub fn container(&self, enabled: bool, selected: bool) -> Color {
4328 match (enabled, selected) {
4329 (true, true) => self.selected_container_color,
4330 (true, false) => self.container_color,
4331 (false, true) => self.disabled_selected_container_color,
4332 (false, false) => self.disabled_container_color,
4333 }
4334 }
4335 pub fn label(&self, enabled: bool, selected: bool) -> Color {
4336 if !enabled {
4337 self.disabled_label_color
4338 } else if selected {
4339 self.selected_label_color
4340 } else {
4341 self.label_color
4342 }
4343 }
4344 pub fn leading_icon(&self, enabled: bool, selected: bool) -> Color {
4345 if !enabled {
4346 self.disabled_leading_icon_color
4347 } else if selected {
4348 self.selected_leading_icon_color
4349 } else {
4350 self.leading_icon_color
4351 }
4352 }
4353 pub fn trailing_icon(&self, enabled: bool, selected: bool) -> Color {
4354 if !enabled {
4355 self.disabled_trailing_icon_color
4356 } else if selected {
4357 self.selected_trailing_icon_color
4358 } else {
4359 self.trailing_icon_color
4360 }
4361 }
4362}
4363
4364#[derive(Clone, Copy, Debug)]
4366pub struct ChipElevation {
4367 pub default: f32,
4368 pub hovered: f32,
4369 pub focused: f32,
4370 pub pressed: f32,
4371 pub dragged: f32,
4372 pub disabled: f32,
4373}
4374
4375impl ChipElevation {
4376 pub fn to_state_elevation(&self) -> StateElevation {
4377 StateElevation {
4378 default: self.default,
4379 hovered: self.hovered,
4380 pressed: self.pressed,
4381 disabled: self.disabled,
4382 }
4383 }
4384}
4385
4386impl Default for ChipElevation {
4387 fn default() -> Self {
4388 Self {
4389 default: ChipDefaults::elevation_default(),
4390 hovered: ChipDefaults::elevation_hovered(),
4391 focused: ChipDefaults::elevation_focused(),
4392 pressed: ChipDefaults::elevation_pressed(),
4393 dragged: ChipDefaults::elevation_dragged(),
4394 disabled: ChipDefaults::elevation_disabled(),
4395 }
4396 }
4397}
4398
4399#[derive(Clone, Debug)]
4401pub struct ChipConfig {
4402 pub modifier: Modifier,
4403 pub enabled: bool,
4404 pub colors: ChipColors,
4405 pub elevation: ChipElevation,
4406 pub border_width: f32,
4407 pub border_color: Color,
4408 pub selected_border_color: Color,
4409 pub disabled_border_color: Color,
4410 pub disabled_selected_border_color: Color,
4411 pub shape_radius: f32,
4412 pub horizontal_padding: f32,
4413}
4414
4415impl Default for ChipConfig {
4416 fn default() -> Self {
4417 Self {
4418 modifier: Modifier::new(),
4419 enabled: true,
4420 colors: ChipColors {
4421 container_color: ChipDefaults::container_color(),
4422 label_color: ChipDefaults::label_color(),
4423 leading_icon_color: ChipDefaults::leading_icon_color(),
4424 trailing_icon_color: ChipDefaults::trailing_icon_color(),
4425 disabled_container_color: ChipDefaults::disabled_container_color(),
4426 disabled_label_color: ChipDefaults::disabled_label_color(),
4427 disabled_leading_icon_color: ChipDefaults::disabled_leading_icon_color(),
4428 disabled_trailing_icon_color: ChipDefaults::disabled_trailing_icon_color(),
4429 selected_container_color: ChipDefaults::selected_container_color(),
4430 selected_label_color: ChipDefaults::selected_label_color(),
4431 selected_leading_icon_color: ChipDefaults::selected_leading_icon_color(),
4432 selected_trailing_icon_color: ChipDefaults::selected_trailing_icon_color(),
4433 disabled_selected_container_color: ChipDefaults::disabled_selected_container_color(
4434 ),
4435 },
4436 elevation: ChipElevation::default(),
4437 border_width: ChipDefaults::BORDER_WIDTH,
4438 border_color: ChipDefaults::border_color(),
4439 selected_border_color: ChipDefaults::selected_border_color(),
4440 disabled_border_color: ChipDefaults::disabled_border_color(),
4441 disabled_selected_border_color: ChipDefaults::disabled_selected_border_color(),
4442 shape_radius: ChipDefaults::SHAPE_RADIUS,
4443 horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
4444 }
4445 }
4446}
4447
4448pub fn AssistChip(
4450 on_click: impl Fn() + 'static,
4451 label: View,
4452 leading_icon: Option<View>,
4453 trailing_icon: Option<View>,
4454 config: ChipConfig,
4455) -> View {
4456 let th = theme();
4457 let is_enabled = config.enabled;
4458 let colors = &config.colors;
4459 let bg = colors.container(is_enabled, false);
4460 let label_color = colors.label(is_enabled, false);
4461 let leading_color = colors.leading_icon(is_enabled, false);
4462 let trailing_color = colors.trailing_icon(is_enabled, false);
4463 let border = if is_enabled {
4464 config.border_color
4465 } else {
4466 config.disabled_border_color
4467 };
4468 let shape = config.shape_radius;
4469
4470 let mut m = Modifier::new()
4471 .state_colors(StateColors {
4472 default: Color::TRANSPARENT,
4473 hovered: th.on_surface.with_alpha_f32(0.08),
4474 pressed: th.on_surface.with_alpha_f32(0.12),
4475 disabled: Color::TRANSPARENT,
4476 })
4477 .padding_values(PaddingValues {
4478 left: config.horizontal_padding,
4479 right: config.horizontal_padding,
4480 top: 8.0,
4481 bottom: 8.0,
4482 })
4483 .background(bg)
4484 .clip_rounded(shape)
4485 .then(config.modifier);
4486
4487 if config.border_width > 0.0 && border != Color::TRANSPARENT {
4488 m = m.border(config.border_width, border, shape);
4489 }
4490
4491 if is_enabled {
4492 m = m.clickable().on_pointer_down(move |_| on_click());
4493 }
4494
4495 Box(m).child(
4496 Row(Modifier::new().align_items(AlignItems::Center)).child((
4497 leading_icon
4498 .map(|v| {
4499 Box(Modifier::new().padding_values(PaddingValues {
4500 left: 0.0,
4501 right: 8.0,
4502 top: 0.0,
4503 bottom: 0.0,
4504 }))
4505 .child(with_content_color(leading_color, move || v))
4506 })
4507 .unwrap_or(Box(Modifier::new())),
4508 with_content_color(label_color, move || label),
4509 trailing_icon
4510 .map(|v| {
4511 Box(Modifier::new().padding_values(PaddingValues {
4512 left: 8.0,
4513 right: 0.0,
4514 top: 0.0,
4515 bottom: 0.0,
4516 }))
4517 .child(with_content_color(trailing_color, move || v))
4518 })
4519 .unwrap_or(Box(Modifier::new())),
4520 )),
4521 )
4522}
4523
4524pub fn ElevatedAssistChip(
4526 on_click: impl Fn() + 'static,
4527 label: View,
4528 leading_icon: Option<View>,
4529 trailing_icon: Option<View>,
4530 config: ChipConfig,
4531) -> View {
4532 let th = theme();
4533 let is_enabled = config.enabled;
4534 let colors = &config.colors;
4535 let bg = colors.container(is_enabled, false);
4536 let label_color = colors.label(is_enabled, false);
4537 let leading_color = colors.leading_icon(is_enabled, false);
4538 let trailing_color = colors.trailing_icon(is_enabled, false);
4539 let shape = config.shape_radius;
4540
4541 let mut m = Modifier::new()
4542 .state_colors(StateColors {
4543 default: Color::TRANSPARENT,
4544 hovered: th.on_surface.with_alpha_f32(0.08),
4545 pressed: th.on_surface.with_alpha_f32(0.12),
4546 disabled: Color::TRANSPARENT,
4547 })
4548 .state_elevation(config.elevation.to_state_elevation())
4549 .padding_values(PaddingValues {
4550 left: config.horizontal_padding,
4551 right: config.horizontal_padding,
4552 top: 8.0,
4553 bottom: 8.0,
4554 })
4555 .background(bg)
4556 .clip_rounded(shape)
4557 .then(config.modifier);
4558
4559 if is_enabled {
4560 m = m.clickable().on_pointer_down(move |_| on_click());
4561 }
4562
4563 Box(m).child(
4564 Row(Modifier::new().align_items(AlignItems::Center)).child((
4565 leading_icon
4566 .map(|v| {
4567 Box(Modifier::new().padding_values(PaddingValues {
4568 left: 0.0,
4569 right: 8.0,
4570 top: 0.0,
4571 bottom: 0.0,
4572 }))
4573 .child(with_content_color(leading_color, move || v))
4574 })
4575 .unwrap_or(Box(Modifier::new())),
4576 with_content_color(label_color, move || label),
4577 trailing_icon
4578 .map(|v| {
4579 Box(Modifier::new().padding_values(PaddingValues {
4580 left: 8.0,
4581 right: 0.0,
4582 top: 0.0,
4583 bottom: 0.0,
4584 }))
4585 .child(with_content_color(trailing_color, move || v))
4586 })
4587 .unwrap_or(Box(Modifier::new())),
4588 )),
4589 )
4590}
4591
4592#[derive(Clone, Debug)]
4594pub struct NavigationBarConfig {
4595 pub modifier: Modifier,
4596 pub container_color: Color,
4597 pub content_color: Color,
4598 pub selected_icon_color: Color,
4599 pub selected_text_color: Color,
4600 pub unselected_icon_color: Color,
4601 pub unselected_text_color: Color,
4602 pub indicator_color: Color,
4603 pub height: f32,
4604 pub tonal_elevation: f32,
4605 pub indicator_opacity: f32,
4606 pub indicator_radius: f32,
4607 pub item_spacing: f32,
4608 pub indicator_width: f32,
4609 pub indicator_height: f32,
4610}
4611
4612impl Default for NavigationBarConfig {
4613 fn default() -> Self {
4614 Self {
4615 modifier: Modifier::new(),
4616 container_color: NavigationBarDefaults::container_color(),
4617 content_color: NavigationBarDefaults::content_color(),
4618 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
4619 selected_text_color: NavigationBarDefaults::selected_text_color(),
4620 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
4621 unselected_text_color: NavigationBarDefaults::unselected_text_color(),
4622 indicator_color: NavigationBarDefaults::indicator_color(),
4623 height: NavigationBarDefaults::HEIGHT,
4624 tonal_elevation: NavigationBarDefaults::TONAL_ELEVATION,
4625 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
4626 indicator_radius: NavigationBarDefaults::INDICATOR_RADIUS,
4627 item_spacing: NavigationBarDefaults::ITEM_SPACING,
4628 indicator_width: NavigationBarDefaults::ACTIVE_INDICATOR_WIDTH,
4629 indicator_height: NavigationBarDefaults::ACTIVE_INDICATOR_HEIGHT,
4630 }
4631 }
4632}
4633
4634#[derive(Clone, Debug)]
4636pub struct NavigationRailConfig {
4637 pub modifier: Modifier,
4638 pub container_color: Color,
4639 pub selected_icon_color: Color,
4640 pub selected_text_color: Color,
4641 pub unselected_icon_color: Color,
4642 pub unselected_text_color: Color,
4643 pub indicator_color: Color,
4644 pub width: f32,
4645 pub item_radius: f32,
4646 pub indicator_opacity: f32,
4647 pub item_spacing: f32,
4648 pub indicator_width: f32,
4649 pub indicator_height: f32,
4650}
4651
4652impl Default for NavigationRailConfig {
4653 fn default() -> Self {
4654 Self {
4655 modifier: Modifier::new(),
4656 container_color: NavigationRailDefaults::container_color(),
4657 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
4658 selected_text_color: NavigationRailDefaults::selected_text_color(),
4659 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
4660 unselected_text_color: NavigationRailDefaults::unselected_text_color(),
4661 indicator_color: NavigationRailDefaults::indicator_color(),
4662 width: NavigationRailDefaults::WIDTH,
4663 item_radius: NavigationRailDefaults::ITEM_RADIUS,
4664 indicator_opacity: NavigationRailDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
4665 item_spacing: NavigationRailDefaults::ITEM_SPACING,
4666 indicator_width: NavigationRailDefaults::ACTIVE_INDICATOR_WIDTH,
4667 indicator_height: NavigationRailDefaults::ACTIVE_INDICATOR_HEIGHT,
4668 }
4669 }
4670}
4671
4672#[derive(Clone, Debug)]
4674pub struct ScaffoldConfig {
4675 pub modifier: Modifier,
4676 pub container_color: Color,
4677 pub top_bar_height: f32,
4678 pub bottom_bar_height: f32,
4679 pub fab_margin: f32,
4680}
4681
4682impl Default for ScaffoldConfig {
4683 fn default() -> Self {
4684 Self {
4685 modifier: Modifier::new(),
4686 container_color: ScaffoldDefaults::container_color(),
4687 top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
4688 bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
4689 fab_margin: ScaffoldDefaults::FAB_MARGIN,
4690 }
4691 }
4692}
4693
4694#[derive(Clone, Debug)]
4696pub struct NavigationDrawerConfig {
4697 pub modifier: Modifier,
4698 pub container_color: Color,
4699 pub content_color: Color,
4700 pub scrim_color: Color,
4701 pub tonal_elevation: f32,
4702 pub width: f32,
4703 pub shape_radius: f32,
4704}
4705
4706impl Default for NavigationDrawerConfig {
4707 fn default() -> Self {
4708 Self {
4709 modifier: Modifier::new(),
4710 container_color: NavigationDrawerDefaults::container_color(),
4711 content_color: NavigationDrawerDefaults::content_color(),
4712 scrim_color: NavigationDrawerDefaults::scrim_color(),
4713 tonal_elevation: NavigationDrawerDefaults::TONAL_ELEVATION,
4714 width: NavigationDrawerDefaults::WIDTH,
4715 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
4716 }
4717 }
4718}
4719
4720#[derive(Clone, Debug)]
4722pub struct BottomSheetConfig {
4723 pub modifier: Modifier,
4724 pub container_color: Color,
4725 pub content_color: Color,
4726 pub scrim_color: Color,
4727 pub tonal_elevation: f32,
4728 pub drag_handle_color: Color,
4729 pub shape_radius: f32,
4730 pub max_width: f32,
4731 pub drag_handle_width: f32,
4732 pub drag_handle_height: f32,
4733 pub peek_height: f32,
4734}
4735
4736impl Default for BottomSheetConfig {
4737 fn default() -> Self {
4738 Self {
4739 modifier: Modifier::new(),
4740 container_color: BottomSheetDefaults::container_color(),
4741 content_color: BottomSheetDefaults::content_color(),
4742 scrim_color: BottomSheetDefaults::scrim_color(),
4743 tonal_elevation: BottomSheetDefaults::TONAL_ELEVATION,
4744 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
4745 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
4746 max_width: BottomSheetDefaults::MAX_WIDTH,
4747 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
4748 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
4749 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
4750 }
4751 }
4752}
4753
4754#[derive(Clone, Debug)]
4756pub struct SearchBarConfig {
4757 pub modifier: Modifier,
4758 pub container_color: Color,
4759 pub active_container_color: Color,
4760 pub content_color: Color,
4761 pub placeholder_color: Color,
4762 pub height: f32,
4763 pub expanded_width: f32,
4764 pub collapsed_width: f32,
4765}
4766
4767impl Default for SearchBarConfig {
4768 fn default() -> Self {
4769 Self {
4770 modifier: Modifier::new(),
4771 container_color: SearchBarDefaults::container_color(),
4772 active_container_color: SearchBarDefaults::active_container_color(),
4773 content_color: SearchBarDefaults::content_color(),
4774 placeholder_color: SearchBarDefaults::placeholder_color(),
4775 height: SearchBarDefaults::HEIGHT,
4776 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
4777 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
4778 }
4779 }
4780}
4781
4782#[derive(Clone, Debug)]
4784pub struct DropdownMenuConfig {
4785 pub modifier: Modifier,
4786 pub container_color: Color,
4787 pub item_text_color: Color,
4788 pub disabled_item_text_color: Color,
4789 pub divider_color: Color,
4790 pub min_width: f32,
4791 pub item_height: f32,
4792}
4793
4794impl Default for DropdownMenuConfig {
4795 fn default() -> Self {
4796 Self {
4797 modifier: Modifier::new(),
4798 container_color: DropdownMenuDefaults::container_color(),
4799 item_text_color: DropdownMenuDefaults::item_text_color(),
4800 disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
4801 divider_color: DropdownMenuDefaults::divider_color(),
4802 min_width: DropdownMenuDefaults::MIN_WIDTH,
4803 item_height: DropdownMenuDefaults::ITEM_HEIGHT,
4804 }
4805 }
4806}
4807
4808#[derive(Clone, Debug)]
4810pub struct TooltipConfig {
4811 pub modifier: Modifier,
4812 pub container_color: Color,
4813 pub content_color: Color,
4814 pub offset_y: f32,
4815 pub horizontal_padding: f32,
4816 pub vertical_padding: f32,
4817}
4818
4819impl Default for TooltipConfig {
4820 fn default() -> Self {
4821 Self {
4822 modifier: Modifier::new(),
4823 container_color: TooltipDefaults::container_color(),
4824 content_color: TooltipDefaults::content_color(),
4825 offset_y: TooltipDefaults::OFFSET_Y,
4826 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
4827 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
4828 }
4829 }
4830}
4831
4832#[derive(Clone, Debug)]
4834pub struct SwipeToDismissConfig {
4835 pub modifier: Modifier,
4836 pub dismiss_threshold: f32,
4837 pub dismissed_offset: f32,
4838 pub animation_spec: AnimationSpec,
4839}
4840
4841impl Default for SwipeToDismissConfig {
4842 fn default() -> Self {
4843 Self {
4844 modifier: Modifier::new(),
4845 dismiss_threshold: SwipeToDismissDefaults::DISMISS_THRESHOLD,
4846 dismissed_offset: SwipeToDismissDefaults::DISMISSED_OFFSET,
4847 animation_spec: AnimationSpec::spring_gentle(),
4848 }
4849 }
4850}
4851
4852#[derive(Clone, Debug)]
4854pub struct PullToRefreshConfig {
4855 pub modifier: Modifier,
4856 pub indicator_color: Color,
4857 pub threshold: f32,
4858}
4859
4860impl Default for PullToRefreshConfig {
4861 fn default() -> Self {
4862 Self {
4863 modifier: Modifier::new(),
4864 indicator_color: PullToRefreshDefaults::indicator_color(),
4865 threshold: PullToRefreshDefaults::THRESHOLD,
4866 }
4867 }
4868}