1#![allow(non_snake_case)]
2
3use std::cell::Cell;
4use std::rc::Rc;
5use std::sync::atomic::{AtomicU64, Ordering};
6
7use repose_core::*;
8use repose_ui::anim::{animate_color, animate_f32};
9use repose_ui::{Box, Column, Row, Stack, Text, TextStyle, ViewExt};
10
11use super::*;
12
13use crate::{Icon, Symbol};
14
15#[derive(Clone, Debug)]
17pub struct TopAppBarConfig {
18 pub modifier: Modifier,
19 pub container_color: Color,
20 pub title_color: Color,
21 pub height: f32,
22}
23
24impl Default for TopAppBarConfig {
25 fn default() -> Self {
26 Self {
27 modifier: Modifier::new(),
28 container_color: TopAppBarDefaults::container_color(),
29 title_color: TopAppBarDefaults::title_color(),
30 height: TopAppBarDefaults::HEIGHT,
31 }
32 }
33}
34
35pub fn TopAppBar(
38 title: impl Into<String>,
39 navigation_icon: Option<View>,
40 actions: Vec<View>,
41 config: TopAppBarConfig,
42) -> View {
43 let th = theme();
44 Row(Modifier::new()
45 .min_width(200.0)
46 .height(config.height)
47 .background(config.container_color)
48 .padding_values(PaddingValues {
49 left: 4.0,
50 right: 4.0,
51 top: 0.0,
52 bottom: 0.0,
53 })
54 .align_items(AlignItems::Center)
55 .then(config.modifier))
56 .child((
57 navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
58 Box(Modifier::new()
59 .padding_values(PaddingValues {
60 left: 16.0,
61 right: 0.0,
62 top: 0.0,
63 bottom: 0.0,
64 })
65 .flex_grow(1.0))
66 .child(
67 Text(title)
68 .color(config.title_color)
69 .size(th.typography.title_large),
70 ),
71 Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
72 ))
73}
74
75pub fn CenterAlignedTopAppBar(
77 title: impl Into<String>,
78 navigation_icon: Option<View>,
79 actions: Vec<View>,
80 config: TopAppBarConfig,
81) -> View {
82 let th = theme();
83 Row(Modifier::new()
84 .min_width(200.0)
85 .height(config.height)
86 .background(config.container_color)
87 .padding_values(PaddingValues {
88 left: 4.0,
89 right: 4.0,
90 top: 0.0,
91 bottom: 0.0,
92 })
93 .align_items(AlignItems::Center)
94 .justify_content(JustifyContent::Center)
95 .then(config.modifier))
96 .child((
97 navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
98 Box(Modifier::new().flex_grow(1.0)).child(
99 Text(title)
100 .color(config.title_color)
101 .size(th.typography.title_large),
102 ),
103 Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
104 ))
105}
106
107#[derive(Clone, Debug)]
109pub struct SurfaceConfig {
110 pub modifier: Modifier,
111 pub color: Color,
112 pub shape_radius: f32,
113 pub tonal_elevation: f32,
114 pub border: Option<(f32, Color)>,
115}
116
117impl Default for SurfaceConfig {
118 fn default() -> Self {
119 Self {
120 modifier: Modifier::new(),
121 color: SurfaceDefaults::color(),
122 shape_radius: SurfaceDefaults::SHAPE_RADIUS,
123 tonal_elevation: SurfaceDefaults::TONAL_ELEVATION,
124 border: None,
125 }
126 }
127}
128
129pub fn Surface(config: SurfaceConfig, content: impl FnOnce() -> View) -> View {
132 let mut m = Modifier::new()
133 .background(config.color)
134 .clip_rounded(config.shape_radius)
135 .then(config.modifier);
136 if config.tonal_elevation > 0.0 {
137 m = m.state_elevation(StateElevation {
138 default: config.tonal_elevation,
139 hovered: config.tonal_elevation,
140 pressed: config.tonal_elevation,
141 disabled: 0.0,
142 });
143 }
144 if let Some((w, c)) = config.border {
145 m = m.border(w, c, config.shape_radius);
146 }
147 Box(m).child(content())
148}
149
150#[derive(Clone, Debug)]
152pub struct IconButtonConfig {
153 pub modifier: Modifier,
154 pub content_color: Option<Color>,
155 pub container_size: Option<f32>,
156 pub filler_container_color: Option<Color>,
157 pub state_colors: Option<StateColors>,
158}
159
160impl Default for IconButtonConfig {
161 fn default() -> Self {
162 Self {
163 modifier: Modifier::new(),
164 content_color: None,
165 container_size: None,
166 filler_container_color: None,
167 state_colors: None,
168 }
169 }
170}
171
172pub fn IconButton(icon: View, on_click: impl Fn() + 'static, config: IconButtonConfig) -> View {
174 let sz = config
175 .container_size
176 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
177 let colors = config
178 .state_colors
179 .unwrap_or_else(IconButtonDefaults::state_colors_default);
180 Box(Modifier::new()
181 .size(sz, sz)
182 .clip_rounded(sz * 0.5)
183 .state_colors(colors)
184 .align_items(AlignItems::Center)
185 .justify_content(JustifyContent::Center)
186 .clickable()
187 .on_pointer_down(move |_| on_click())
188 .then(config.modifier))
189 .child(icon)
190}
191
192pub fn FilledIconButton(
194 icon: View,
195 on_click: impl Fn() + 'static,
196 config: IconButtonConfig,
197) -> View {
198 let th = theme();
199 let content_color = config
200 .content_color
201 .unwrap_or_else(IconButtonDefaults::filled_content_color);
202 let bg = config
203 .filler_container_color
204 .unwrap_or_else(IconButtonDefaults::filled_container_color);
205 let sz = config
206 .container_size
207 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
208 Box(Modifier::new()
209 .size(sz, sz)
210 .clip_rounded(sz * 0.5)
211 .background(bg)
212 .state_colors(StateColors {
213 default: Color::TRANSPARENT,
214 hovered: content_color.with_alpha_f32(0.08),
215 pressed: content_color.with_alpha_f32(0.12),
216 disabled: th.on_surface.with_alpha_f32(0.12),
217 })
218 .align_items(AlignItems::Center)
219 .justify_content(JustifyContent::Center)
220 .clickable()
221 .on_pointer_down(move |_| on_click())
222 .then(config.modifier))
223 .child(icon)
224}
225
226pub fn FilledTonalIconButton(
228 icon: View,
229 on_click: impl Fn() + 'static,
230 config: IconButtonConfig,
231) -> View {
232 let th = theme();
233 let content_color = config
234 .content_color
235 .unwrap_or_else(IconButtonDefaults::filled_tonal_content_color);
236 let bg = config
237 .filler_container_color
238 .unwrap_or_else(IconButtonDefaults::filled_tonal_container_color);
239 let sz = config
240 .container_size
241 .unwrap_or(IconButtonDefaults::FILLED_CONTAINER_SIZE);
242 Box(Modifier::new()
243 .size(sz, sz)
244 .clip_rounded(sz * 0.5)
245 .background(bg)
246 .state_colors(StateColors {
247 default: Color::TRANSPARENT,
248 hovered: content_color.with_alpha_f32(0.08),
249 pressed: content_color.with_alpha_f32(0.12),
250 disabled: th.on_surface.with_alpha_f32(0.12),
251 })
252 .align_items(AlignItems::Center)
253 .justify_content(JustifyContent::Center)
254 .clickable()
255 .on_pointer_down(move |_| on_click())
256 .then(config.modifier))
257 .child(icon)
258}
259
260pub fn OutlinedIconButton(
262 icon: View,
263 on_click: impl Fn() + 'static,
264 config: IconButtonConfig,
265) -> View {
266 let th = theme();
267 let sz = config
268 .container_size
269 .unwrap_or(IconButtonDefaults::CONTAINER_SIZE);
270 let colors = config
271 .state_colors
272 .unwrap_or_else(IconButtonDefaults::state_colors_default);
273 Box(Modifier::new()
274 .size(sz, sz)
275 .clip_rounded(sz * 0.5)
276 .state_colors(colors)
277 .border(1.0, th.outline, sz * 0.5)
278 .align_items(AlignItems::Center)
279 .justify_content(JustifyContent::Center)
280 .clickable()
281 .on_pointer_down(move |_| on_click())
282 .then(config.modifier))
283 .child(icon)
284}
285
286#[derive(Clone, Debug)]
288pub struct ButtonConfig {
289 pub modifier: Modifier,
290 pub enabled: bool,
291 pub content_color: Option<Color>,
292 pub container_color: Option<Color>,
293 pub state_colors: Option<StateColors>,
294 pub state_elevation: Option<StateElevation>,
295 pub border: Option<(f32, Color, f32)>,
296 pub shape_radius: f32,
297 pub content_padding: Option<PaddingValues>,
298 pub height: f32,
299}
300
301impl Default for ButtonConfig {
302 fn default() -> Self {
303 Self {
304 modifier: Modifier::new(),
305 enabled: true,
306 content_color: None,
307 container_color: None,
308 state_colors: None,
309 state_elevation: None,
310 border: None,
311 shape_radius: ButtonDefaults::SHAPE_RADIUS,
312 content_padding: None,
313 height: ButtonDefaults::HEIGHT,
314 }
315 }
316}
317
318fn button_impl(
319 outer_modifier: Modifier,
320 on_click: impl Fn() + 'static,
321 content: impl FnOnce() -> View,
322 content_color: Color,
323 container_color: Option<Color>,
324 state_colors: StateColors,
325 state_elevation: Option<StateElevation>,
326 border: Option<(f32, Color, f32)>,
327 padding_left: f32,
328 padding_right: f32,
329 height: f32,
330 shape_radius: f32,
331 enabled: bool,
332) -> View {
333 let mut m = Modifier::new().height(height).min_width(48.0);
334 if let Some(bg) = container_color {
335 m = m.background(bg);
336 }
337 m = m.state_colors(state_colors);
338 if let Some(se) = state_elevation {
339 m = m.state_elevation(se);
340 }
341 if let Some((w, c, r)) = border {
342 m = m.border(w, c, r);
343 }
344 m = m
345 .clip_rounded(shape_radius)
346 .padding_values(PaddingValues {
347 left: padding_left,
348 right: padding_right,
349 top: 0.0,
350 bottom: 0.0,
351 })
352 .align_items(AlignItems::Center)
353 .justify_content(JustifyContent::Center);
354 if enabled {
355 m = m.clickable().on_pointer_down(move |_| on_click());
356 }
357 m = m.then(outer_modifier);
358 let content = with_content_color(
359 if enabled {
360 content_color
361 } else {
362 content_color.with_alpha_f32(0.38)
363 },
364 content,
365 );
366 Box(m).child(content)
367}
368
369pub fn Button(
371 modifier: Modifier,
372 on_click: impl Fn() + 'static,
373 content: impl FnOnce() -> View,
374) -> View {
375 FilledButton(modifier, on_click, ButtonConfig::default(), content)
376}
377
378pub fn FilledButton(
380 modifier: Modifier,
381 on_click: impl Fn() + 'static,
382 config: ButtonConfig,
383 content: impl FnOnce() -> View,
384) -> View {
385 let cc = config
386 .content_color
387 .unwrap_or_else(ButtonDefaults::content_color);
388 let bg = config
389 .container_color
390 .unwrap_or_else(ButtonDefaults::container_color);
391 let sc = config
392 .state_colors
393 .unwrap_or_else(ButtonDefaults::state_colors_default);
394 let se = config
395 .state_elevation
396 .unwrap_or_else(ButtonDefaults::state_elevation_default);
397 let pad = config.content_padding.unwrap_or(PaddingValues {
398 left: 24.0,
399 right: 24.0,
400 top: 0.0,
401 bottom: 0.0,
402 });
403 button_impl(
404 modifier.then(config.modifier),
405 on_click,
406 content,
407 cc,
408 Some(bg),
409 sc,
410 Some(se),
411 config.border,
412 pad.left,
413 pad.right,
414 config.height,
415 config.shape_radius,
416 config.enabled,
417 )
418}
419
420pub fn FilledTonalButton(
422 modifier: Modifier,
423 on_click: impl Fn() + 'static,
424 config: ButtonConfig,
425 content: impl FnOnce() -> View,
426) -> View {
427 let cc = config
428 .content_color
429 .unwrap_or_else(ButtonDefaults::tonal_content_color);
430 let bg = config
431 .container_color
432 .unwrap_or_else(ButtonDefaults::tonal_container_color);
433 let sc = config
434 .state_colors
435 .unwrap_or_else(ButtonDefaults::state_colors_default);
436 let se = config
437 .state_elevation
438 .unwrap_or_else(ButtonDefaults::state_elevation_default);
439 let pad = config.content_padding.unwrap_or(PaddingValues {
440 left: 24.0,
441 right: 24.0,
442 top: 0.0,
443 bottom: 0.0,
444 });
445 button_impl(
446 modifier.then(config.modifier),
447 on_click,
448 content,
449 cc,
450 Some(bg),
451 sc,
452 Some(se),
453 config.border,
454 pad.left,
455 pad.right,
456 config.height,
457 config.shape_radius,
458 config.enabled,
459 )
460}
461
462pub fn OutlinedButton(
464 modifier: Modifier,
465 on_click: impl Fn() + 'static,
466 config: ButtonConfig,
467 content: impl FnOnce() -> View,
468) -> View {
469 let cc = config
470 .content_color
471 .unwrap_or_else(ButtonDefaults::outlined_content_color);
472 let sc = config
473 .state_colors
474 .unwrap_or_else(ButtonDefaults::state_colors_default);
475 let border = config
476 .border
477 .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
478 let pad = config.content_padding.unwrap_or(PaddingValues {
479 left: 24.0,
480 right: 24.0,
481 top: 0.0,
482 bottom: 0.0,
483 });
484 button_impl(
485 modifier.then(config.modifier),
486 on_click,
487 content,
488 cc,
489 None,
490 sc,
491 None,
492 Some(border),
493 pad.left,
494 pad.right,
495 config.height,
496 config.shape_radius,
497 config.enabled,
498 )
499}
500
501pub fn TextButton(
503 modifier: Modifier,
504 on_click: impl Fn() + 'static,
505 config: ButtonConfig,
506 content: impl FnOnce() -> View,
507) -> View {
508 let cc = config
509 .content_color
510 .unwrap_or_else(ButtonDefaults::text_content_color);
511 let sc = config
512 .state_colors
513 .unwrap_or_else(ButtonDefaults::state_colors_default);
514 let pad = config.content_padding.unwrap_or(PaddingValues {
515 left: 12.0,
516 right: 12.0,
517 top: 0.0,
518 bottom: 0.0,
519 });
520 button_impl(
521 modifier.then(config.modifier),
522 on_click,
523 content,
524 cc,
525 None,
526 sc,
527 None,
528 None,
529 pad.left,
530 pad.right,
531 config.height,
532 config.shape_radius,
533 config.enabled,
534 )
535}
536
537pub fn ElevatedButton(
539 modifier: Modifier,
540 on_click: impl Fn() + 'static,
541 config: ButtonConfig,
542 content: impl FnOnce() -> View,
543) -> View {
544 let cc = config
545 .content_color
546 .unwrap_or_else(ButtonDefaults::elevated_content_color);
547 let bg = config
548 .container_color
549 .unwrap_or_else(ButtonDefaults::elevated_container_color);
550 let sc = config
551 .state_colors
552 .unwrap_or_else(ButtonDefaults::state_colors_default);
553 let se = config
554 .state_elevation
555 .unwrap_or_else(ButtonDefaults::elevated_state_elevation);
556 let pad = config.content_padding.unwrap_or(PaddingValues {
557 left: 24.0,
558 right: 24.0,
559 top: 0.0,
560 bottom: 0.0,
561 });
562 button_impl(
563 modifier.then(config.modifier),
564 on_click,
565 content,
566 cc,
567 Some(bg),
568 sc,
569 Some(se),
570 config.border,
571 pad.left,
572 pad.right,
573 config.height,
574 config.shape_radius,
575 config.enabled,
576 )
577}
578
579#[derive(Clone, Debug)]
581pub struct ToggleButtonConfig {
582 pub modifier: Modifier,
583 pub enabled: bool,
584 pub container_color: Option<Color>,
585 pub content_color: Option<Color>,
586 pub checked_container_color: Option<Color>,
587 pub checked_content_color: Option<Color>,
588 pub state_colors: Option<StateColors>,
589 pub state_elevation: Option<StateElevation>,
590 pub border: Option<(f32, Color, f32)>,
591 pub shape_radius: f32,
592 pub height: f32,
593}
594
595impl Default for ToggleButtonConfig {
596 fn default() -> Self {
597 Self {
598 modifier: Modifier::new(),
599 enabled: true,
600 container_color: None,
601 content_color: None,
602 checked_container_color: None,
603 checked_content_color: None,
604 state_colors: None,
605 state_elevation: None,
606 border: None,
607 shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
608 height: ToggleButtonDefaults::HEIGHT,
609 }
610 }
611}
612
613fn toggle_button_impl(
614 checked: bool,
615 on_checked_change: impl Fn(bool) + 'static,
616 content: impl FnOnce(bool) -> View,
617 content_color: Color,
618 container_color: Option<Color>,
619 checked_container_color: Option<Color>,
620 checked_content_color: Option<Color>,
621 state_colors: StateColors,
622 state_elevation: StateElevation,
623 border: Option<(f32, Color, f32)>,
624 pad_left: f32,
625 pad_right: f32,
626 height: f32,
627 shape_radius: f32,
628 enabled: bool,
629) -> View {
630 let th = theme();
631 let bg = if checked {
632 checked_container_color.unwrap_or(th.primary)
633 } else {
634 container_color.unwrap_or(Color::TRANSPARENT)
635 };
636 let fg = if checked {
637 checked_content_color.unwrap_or(th.on_primary)
638 } else {
639 content_color
640 };
641 let mut m = Modifier::new()
642 .height(height)
643 .padding_values(PaddingValues {
644 left: pad_left,
645 right: pad_right,
646 top: 0.0,
647 bottom: 0.0,
648 })
649 .background(bg)
650 .clip_rounded(shape_radius)
651 .state_colors(state_colors)
652 .state_elevation(state_elevation);
653 if let Some((w, c, r)) = border {
654 m = m.border(w, c, r);
655 }
656 if enabled {
657 m = m.clickable().on_pointer_down({
658 let cb = on_checked_change;
659 move |_| cb(!checked)
660 });
661 } else {
662 m = m.alpha(0.38);
663 }
664 with_content_color(fg, || Box(m).child(content(checked)))
665}
666
667pub fn ToggleButton(
669 checked: bool,
670 on_checked_change: impl Fn(bool) + 'static,
671 config: ToggleButtonConfig,
672 content: impl FnOnce(bool) -> View,
673) -> View {
674 let cc = config
675 .content_color
676 .unwrap_or_else(ToggleButtonDefaults::content_color);
677 let checked_cc = config
678 .checked_content_color
679 .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
680 let checked_bg = config
681 .checked_container_color
682 .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
683 let sc = config
684 .state_colors
685 .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
686 let se = config
687 .state_elevation
688 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
689 toggle_button_impl(
690 checked,
691 on_checked_change,
692 content,
693 cc,
694 None,
695 Some(checked_bg),
696 Some(checked_cc),
697 sc,
698 se,
699 config.border,
700 ToggleButtonDefaults::HORIZONTAL_PADDING,
701 ToggleButtonDefaults::HORIZONTAL_PADDING,
702 config.height,
703 config.shape_radius,
704 config.enabled,
705 )
706}
707
708pub fn TonalToggleButton(
710 checked: bool,
711 on_checked_change: impl Fn(bool) + 'static,
712 config: ToggleButtonConfig,
713 content: impl FnOnce(bool) -> View,
714) -> View {
715 let cc = config
716 .content_color
717 .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
718 let checked_cc = config
719 .checked_content_color
720 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
721 let checked_bg = config
722 .checked_container_color
723 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
724 let sc = config
725 .state_colors
726 .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
727 let se = config
728 .state_elevation
729 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
730 toggle_button_impl(
731 checked,
732 on_checked_change,
733 content,
734 cc,
735 None,
736 Some(checked_bg),
737 Some(checked_cc),
738 sc,
739 se,
740 config.border,
741 ToggleButtonDefaults::HORIZONTAL_PADDING,
742 ToggleButtonDefaults::HORIZONTAL_PADDING,
743 config.height,
744 config.shape_radius,
745 config.enabled,
746 )
747}
748
749pub fn OutlinedToggleButton(
751 checked: bool,
752 on_checked_change: impl Fn(bool) + 'static,
753 config: ToggleButtonConfig,
754 content: impl FnOnce(bool) -> View,
755) -> View {
756 let cc = config
757 .content_color
758 .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
759 let checked_cc = config
760 .checked_content_color
761 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
762 let checked_bg = config
763 .checked_container_color
764 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
765 let sc = config
766 .state_colors
767 .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
768 let se = config
769 .state_elevation
770 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
771 let border = if !checked {
772 Some(config.border.unwrap_or((
773 1.0,
774 ToggleButtonDefaults::outlined_border_color(),
775 config.shape_radius,
776 )))
777 } else {
778 config.border
779 };
780 toggle_button_impl(
781 checked,
782 on_checked_change,
783 content,
784 cc,
785 None,
786 Some(checked_bg),
787 Some(checked_cc),
788 sc,
789 se,
790 border,
791 ToggleButtonDefaults::HORIZONTAL_PADDING,
792 ToggleButtonDefaults::HORIZONTAL_PADDING,
793 config.height,
794 config.shape_radius,
795 config.enabled,
796 )
797}
798
799pub fn ElevatedToggleButton(
801 checked: bool,
802 on_checked_change: impl Fn(bool) + 'static,
803 config: ToggleButtonConfig,
804 content: impl FnOnce(bool) -> View,
805) -> View {
806 let cc = config
807 .content_color
808 .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
809 let checked_cc = config
810 .checked_content_color
811 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
812 let checked_bg = config
813 .checked_container_color
814 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
815 let sc = config
816 .state_colors
817 .unwrap_or_else(ToggleButtonDefaults::state_colors_default);
818 let se = config
819 .state_elevation
820 .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
821 toggle_button_impl(
822 checked,
823 on_checked_change,
824 content,
825 cc,
826 None,
827 Some(checked_bg),
828 Some(checked_cc),
829 sc,
830 se,
831 config.border,
832 ToggleButtonDefaults::HORIZONTAL_PADDING,
833 ToggleButtonDefaults::HORIZONTAL_PADDING,
834 config.height,
835 config.shape_radius,
836 config.enabled,
837 )
838}
839
840#[derive(Clone, Debug)]
842pub struct FABConfig {
843 pub modifier: Modifier,
844 pub container_color: Color,
845 pub content_color: Color,
846 pub state_elevation: StateElevation,
847 pub shape_radius: f32,
848 pub size: f32,
849}
850
851impl Default for FABConfig {
852 fn default() -> Self {
853 Self {
854 modifier: Modifier::new(),
855 container_color: FABDefaults::container_color(),
856 content_color: FABDefaults::content_color(),
857 state_elevation: FABDefaults::state_elevation(),
858 shape_radius: FABDefaults::SHAPE_RADIUS,
859 size: FABDefaults::SIZE,
860 }
861 }
862}
863
864fn fab_impl(
865 icon: View,
866 on_click: impl Fn() + 'static,
867 size: f32,
868 shape_r: f32,
869 config: FABConfig,
870) -> View {
871 let th = theme();
872 Box(Modifier::new()
873 .size(size, size)
874 .background(config.container_color)
875 .state_colors(StateColors {
876 default: Color::TRANSPARENT,
877 hovered: config.content_color.with_alpha_f32(0.08),
878 pressed: config.content_color.with_alpha_f32(0.12),
879 disabled: th.on_surface.with_alpha_f32(0.12),
880 })
881 .state_elevation(config.state_elevation)
882 .clip_rounded(shape_r)
883 .align_items(AlignItems::Center)
884 .justify_content(JustifyContent::Center)
885 .clickable()
886 .on_pointer_down(move |_| on_click())
887 .then(config.modifier))
888 .child(icon)
889}
890
891pub fn FAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
893 fab_impl(
894 icon,
895 on_click,
896 FABDefaults::SIZE,
897 FABDefaults::SHAPE_RADIUS,
898 config,
899 )
900}
901
902pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static, config: FABConfig) -> View {
904 fab_impl(
905 icon,
906 on_click,
907 FABDefaults::LARGE_SIZE,
908 FABDefaults::LARGE_SHAPE_RADIUS,
909 config,
910 )
911}
912
913pub fn ExtendedFAB(
915 icon: Option<View>,
916 label: impl Into<String>,
917 on_click: impl Fn() + 'static,
918 config: FABConfig,
919) -> View {
920 let th = theme();
921 let has_icon = icon.is_some();
922 Row(Modifier::new()
923 .height(56.0)
924 .min_width(80.0)
925 .background(config.container_color)
926 .state_colors(StateColors {
927 default: Color::TRANSPARENT,
928 hovered: config.content_color.with_alpha_f32(0.08),
929 pressed: config.content_color.with_alpha_f32(0.12),
930 disabled: theme().on_surface.with_alpha_f32(0.12),
931 })
932 .state_elevation(config.state_elevation)
933 .clip_rounded(16.0)
934 .padding_values(PaddingValues {
935 left: 16.0,
936 right: 20.0,
937 top: 0.0,
938 bottom: 0.0,
939 })
940 .align_items(AlignItems::Center)
941 .clickable()
942 .on_pointer_down(move |_| on_click())
943 .then(config.modifier))
944 .child((
945 icon.unwrap_or(Box(Modifier::new())),
946 Box(Modifier::new()
947 .width(if has_icon { 12.0 } else { 0.0 })
948 .fill_max_height()),
949 Text(label)
950 .color(config.content_color)
951 .size(th.typography.label_large)
952 .single_line(),
953 ))
954}
955
956#[derive(Clone, Debug)]
958pub struct DividerConfig {
959 pub modifier: Modifier,
960 pub thickness: f32,
961 pub color: Color,
962}
963
964impl Default for DividerConfig {
965 fn default() -> Self {
966 Self {
967 modifier: Modifier::new(),
968 thickness: DividerDefaults::THICKNESS,
969 color: DividerDefaults::color(),
970 }
971 }
972}
973
974pub fn Divider(config: DividerConfig) -> View {
976 Box(Modifier::new()
977 .min_width(200.0)
978 .height(config.thickness)
979 .background(config.color)
980 .then(config.modifier))
981}
982
983pub fn VerticalDivider(config: DividerConfig) -> View {
985 Box(Modifier::new()
986 .width(config.thickness)
987 .fill_max_height()
988 .background(config.color)
989 .then(config.modifier))
990}
991
992#[derive(Clone, Debug)]
994pub struct BadgeConfig {
995 pub modifier: Modifier,
996 pub color: Color,
997 pub label_color: Color,
998}
999
1000impl Default for BadgeConfig {
1001 fn default() -> Self {
1002 Self {
1003 modifier: Modifier::new(),
1004 color: BadgeDefaults::color(),
1005 label_color: BadgeDefaults::label_color(),
1006 }
1007 }
1008}
1009
1010pub fn Badge(label: Option<impl Into<String>>, config: BadgeConfig) -> View {
1013 let th = theme();
1014 match label {
1015 None => Box(Modifier::new()
1016 .size(BadgeDefaults::DOT_SIZE, BadgeDefaults::DOT_SIZE)
1017 .background(config.color)
1018 .clip_rounded(BadgeDefaults::DOT_SIZE * 0.5)
1019 .then(config.modifier)),
1020 Some(text) => {
1021 let text = text.into();
1022 Box(Modifier::new()
1023 .min_width(BadgeDefaults::LABEL_MIN_WIDTH)
1024 .height(BadgeDefaults::LABEL_HEIGHT)
1025 .background(config.color)
1026 .clip_rounded(BadgeDefaults::LABEL_HEIGHT * 0.5)
1027 .padding_values(PaddingValues {
1028 left: 4.0,
1029 right: 4.0,
1030 top: 0.0,
1031 bottom: 0.0,
1032 })
1033 .align_items(AlignItems::Center)
1034 .justify_content(JustifyContent::Center)
1035 .then(config.modifier))
1036 .child(
1037 Text(text)
1038 .color(config.label_color)
1039 .size(th.typography.label_small)
1040 .single_line(),
1041 )
1042 }
1043 }
1044}
1045
1046#[derive(Clone, Debug)]
1048pub struct ListItemConfig {
1049 pub modifier: Modifier,
1050 pub headline_color: Color,
1051 pub supporting_color: Color,
1052 pub horizontal_padding: f32,
1053 pub trailing_padding: f32,
1054 pub one_line_height: f32,
1055 pub two_line_height: f32,
1056}
1057
1058impl Default for ListItemConfig {
1059 fn default() -> Self {
1060 Self {
1061 modifier: Modifier::new(),
1062 headline_color: ListItemDefaults::headline_color(),
1063 supporting_color: ListItemDefaults::supporting_color(),
1064 horizontal_padding: ListItemDefaults::HORIZONTAL_PADDING,
1065 trailing_padding: ListItemDefaults::TRAILING_PADDING,
1066 one_line_height: ListItemDefaults::ONE_LINE_HEIGHT,
1067 two_line_height: ListItemDefaults::TWO_LINE_HEIGHT,
1068 }
1069 }
1070}
1071
1072pub fn ListItem(
1074 headline: impl Into<String>,
1075 supporting_text: Option<String>,
1076 leading: Option<View>,
1077 trailing: Option<View>,
1078 on_click: Option<Rc<dyn Fn()>>,
1079 config: ListItemConfig,
1080) -> View {
1081 let th = theme();
1082 let mut modifier = Modifier::new()
1083 .min_width(200.0)
1084 .min_height(if supporting_text.is_some() {
1085 config.two_line_height
1086 } else {
1087 config.one_line_height
1088 })
1089 .state_colors(StateColors {
1090 default: Color::TRANSPARENT,
1091 hovered: th.on_surface.with_alpha_f32(0.08),
1092 pressed: th.on_surface.with_alpha_f32(0.12),
1093 disabled: Color::TRANSPARENT,
1094 })
1095 .padding_values(PaddingValues {
1096 left: config.horizontal_padding,
1097 right: config.trailing_padding,
1098 top: 8.0,
1099 bottom: 8.0,
1100 })
1101 .align_items(AlignItems::Center)
1102 .then(config.modifier);
1103
1104 if let Some(cb) = on_click {
1105 modifier = modifier.clickable().on_pointer_down(move |_| cb());
1106 }
1107
1108 Row(modifier).child((
1109 leading
1110 .map(|v| {
1111 Box(Modifier::new().padding_values(PaddingValues {
1112 left: 0.0,
1113 right: 16.0,
1114 top: 0.0,
1115 bottom: 0.0,
1116 }))
1117 .child(v)
1118 })
1119 .unwrap_or(Box(Modifier::new())),
1120 Column(
1121 Modifier::new()
1122 .flex_grow(1.0)
1123 .justify_content(JustifyContent::Center),
1124 )
1125 .child((
1126 Text(headline)
1127 .color(config.headline_color)
1128 .size(th.typography.body_large)
1129 .single_line(),
1130 supporting_text
1131 .map(|st| {
1132 Text(st)
1133 .color(config.supporting_color)
1134 .size(th.typography.body_medium)
1135 .max_lines(2)
1136 .overflow_ellipsize()
1137 })
1138 .unwrap_or(Box(Modifier::new())),
1139 )),
1140 trailing
1141 .map(|v| {
1142 Box(Modifier::new().padding_values(PaddingValues {
1143 left: 16.0,
1144 right: 0.0,
1145 top: 0.0,
1146 bottom: 0.0,
1147 }))
1148 .child(v)
1149 })
1150 .unwrap_or(Box(Modifier::new())),
1151 ))
1152}
1153
1154pub struct Tab {
1156 pub label: String,
1157 pub icon: Option<View>,
1158 pub on_click: Rc<dyn Fn()>,
1159}
1160
1161#[derive(Clone, Debug)]
1163pub struct TabRowConfig {
1164 pub modifier: Modifier,
1165 pub container_color: Color,
1166 pub selected_content_color: Color,
1167 pub unselected_content_color: Color,
1168 pub indicator_color: Color,
1169 pub height: f32,
1170 pub indicator_height: f32,
1171}
1172
1173impl Default for TabRowConfig {
1174 fn default() -> Self {
1175 Self {
1176 modifier: Modifier::new(),
1177 container_color: TabDefaults::container_color(),
1178 selected_content_color: TabDefaults::selected_content_color(),
1179 unselected_content_color: TabDefaults::unselected_content_color(),
1180 indicator_color: TabDefaults::indicator_color(),
1181 height: TabDefaults::HEIGHT,
1182 indicator_height: TabDefaults::INDICATOR_HEIGHT,
1183 }
1184 }
1185}
1186
1187static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
1188
1189pub fn TabRow(selected_index: usize, tabs: Vec<Tab>, config: TabRowConfig) -> View {
1192 let th = theme();
1193 let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
1194 let spec = th.motion.color;
1195 Column(Modifier::new().fill_max_width())
1196 .child((
1197 Row(Modifier::new()
1198 .fill_max_width()
1199 .height(config.height)
1200 .background(config.container_color))
1201 .child(
1202 tabs.into_iter()
1203 .enumerate()
1204 .map(|(i, tab)| {
1205 let selected = i == selected_index;
1206 let color = animate_color(
1207 format!("tab_clr_{}_{}", id, i),
1208 if selected {
1209 config.selected_content_color
1210 } else {
1211 config.unselected_content_color
1212 },
1213 spec,
1214 );
1215 let indicator_h = animate_f32(
1216 format!("tab_ind_{}_{}", id, i),
1217 if selected {
1218 config.indicator_height
1219 } else {
1220 0.0
1221 },
1222 spec,
1223 );
1224 let cb = tab.on_click.clone();
1225
1226 Column(
1227 Modifier::new()
1228 .flex_grow(1.0)
1229 .fill_max_height()
1230 .align_items(AlignItems::Center)
1231 .justify_content(JustifyContent::Center)
1232 .state_colors(StateColors {
1233 default: Color::TRANSPARENT,
1234 hovered: th.on_surface.with_alpha_f32(0.08),
1235 pressed: th.on_surface.with_alpha_f32(0.12),
1236 disabled: Color::TRANSPARENT,
1237 })
1238 .clickable()
1239 .on_pointer_down(move |_| cb()),
1240 )
1241 .child((
1242 tab.icon.unwrap_or(Box(Modifier::new())),
1243 Text(tab.label)
1244 .color(color)
1245 .size(th.typography.title_small)
1246 .single_line(),
1247 Box(Modifier::new()
1248 .min_width(200.0)
1249 .height(indicator_h)
1250 .background(config.indicator_color)
1251 .clip_rounded(TabDefaults::INDICATOR_CORNER)),
1252 ))
1253 })
1254 .collect::<Vec<_>>(),
1255 ),
1256 Box(Modifier::new()
1257 .min_width(200.0)
1258 .height(1.0)
1259 .background(th.outline_variant)),
1260 ))
1261 .modifier(config.modifier)
1262}
1263
1264pub struct Segment {
1266 pub label: String,
1267 pub icon: Option<View>,
1268 pub on_click: Rc<dyn Fn()>,
1269}
1270
1271#[derive(Clone, Debug)]
1273pub struct SegmentedButtonConfig {
1274 pub modifier: Modifier,
1275 pub border_color: Color,
1276 pub selected_container_color: Color,
1277 pub selected_content_color: Color,
1278 pub unselected_content_color: Color,
1279 pub height: f32,
1280 pub shape_radius: f32,
1281}
1282
1283impl Default for SegmentedButtonConfig {
1284 fn default() -> Self {
1285 Self {
1286 modifier: Modifier::new(),
1287 border_color: SegmentedButtonDefaults::border_color(),
1288 selected_container_color: SegmentedButtonDefaults::selected_container_color(),
1289 selected_content_color: SegmentedButtonDefaults::selected_content_color(),
1290 unselected_content_color: SegmentedButtonDefaults::unselected_content_color(),
1291 height: SegmentedButtonDefaults::HEIGHT,
1292 shape_radius: SegmentedButtonDefaults::SHAPE_RADIUS,
1293 }
1294 }
1295}
1296
1297static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
1298
1299pub fn SegmentedButton(
1302 selected: &[usize],
1303 segments: Vec<Segment>,
1304 config: SegmentedButtonConfig,
1305) -> View {
1306 let th = theme();
1307 let count = segments.len();
1308 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
1309 let spec = th.motion.color;
1310
1311 Row(Modifier::new()
1312 .height(config.height)
1313 .border(1.0, config.border_color, config.shape_radius)
1314 .clip_rounded(config.shape_radius)
1315 .then(config.modifier))
1316 .child(
1317 segments
1318 .into_iter()
1319 .enumerate()
1320 .map(|(i, seg)| {
1321 let is_selected = selected.contains(&i);
1322
1323 let bg = animate_color(
1324 format!("sb_bg_{}_{}", id, i),
1325 if is_selected {
1326 config.selected_container_color
1327 } else {
1328 Color::TRANSPARENT
1329 },
1330 spec,
1331 );
1332 let fg = animate_color(
1333 format!("sb_fg_{}_{}", id, i),
1334 if is_selected {
1335 config.selected_content_color
1336 } else {
1337 config.unselected_content_color
1338 },
1339 spec,
1340 );
1341
1342 let cb = seg.on_click.clone();
1343
1344 let mut modifier = Modifier::new()
1345 .flex_grow(1.0)
1346 .fill_max_height()
1347 .background(bg)
1348 .align_items(AlignItems::Center)
1349 .justify_content(JustifyContent::Center)
1350 .padding_values(PaddingValues {
1351 left: 12.0,
1352 right: 12.0,
1353 top: 0.0,
1354 bottom: 0.0,
1355 })
1356 .clickable()
1357 .on_pointer_down(move |_| cb());
1358
1359 if i < count - 1 {
1360 modifier = modifier.border(1.0, th.outline, 0.0);
1361 }
1362
1363 Row(modifier).child((
1364 seg.icon.unwrap_or(Box(Modifier::new())),
1365 Text(seg.label)
1366 .color(fg)
1367 .size(th.typography.label_large)
1368 .single_line(),
1369 ))
1370 })
1371 .collect::<Vec<_>>(),
1372 )
1373}
1374
1375#[derive(Clone, Debug)]
1377pub struct CircularProgressIndicatorConfig {
1378 pub color: Color,
1379 pub track_color: Color,
1380}
1381
1382impl Default for CircularProgressIndicatorConfig {
1383 fn default() -> Self {
1384 Self {
1385 color: ProgressIndicatorDefaults::circular_color(),
1386 track_color: ProgressIndicatorDefaults::circular_track_color(),
1387 }
1388 }
1389}
1390
1391pub fn CircularProgressIndicator(
1392 value: Option<f32>,
1393 config: CircularProgressIndicatorConfig,
1394) -> View {
1395 let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
1396 let stroke = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH);
1397 let val = value.unwrap_or(0.0).clamp(0.0, 1.0);
1398
1399 Box(Modifier::new()
1400 .size(sz, sz)
1401 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1402 let mul_c = |c: Color| {
1403 Color(
1404 c.0,
1405 c.1,
1406 c.2,
1407 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1408 )
1409 };
1410 let cx = rect.x + rect.w * 0.5;
1411 let cy = rect.y + rect.h * 0.5;
1412 let r = (rect.w.min(rect.h)) * 0.5 - stroke * 0.5;
1413 let circle = Rect {
1414 x: cx - r,
1415 y: cy - r,
1416 w: r * 2.0,
1417 h: r * 2.0,
1418 };
1419
1420 scene.nodes.push(SceneNode::EllipseBorder {
1422 rect: circle,
1423 color: mul_c(config.track_color),
1424 width: stroke,
1425 });
1426
1427 if val > 0.0 {
1429 let fill_h = r * 2.0 * val;
1430 scene.nodes.push(SceneNode::PushClip {
1431 rect: Rect {
1432 x: cx - r,
1433 y: cy + r - fill_h,
1434 w: r * 2.0,
1435 h: fill_h,
1436 },
1437 radius: 0.0,
1438 });
1439 scene.nodes.push(SceneNode::EllipseBorder {
1440 rect: circle,
1441 color: mul_c(config.color),
1442 width: stroke,
1443 });
1444 scene.nodes.push(SceneNode::PopClip);
1445 }
1446 }))
1447 .semantics(Semantics {
1448 role: Role::ProgressBar,
1449 label: None,
1450 focused: false,
1451 enabled: true,
1452 })
1453}
1454
1455#[derive(Clone, Debug)]
1457pub struct LinearProgressIndicatorConfig {
1458 pub color: Color,
1459 pub track_color: Color,
1460 pub gap_size: f32,
1462 pub stop_size: f32,
1464}
1465
1466impl Default for LinearProgressIndicatorConfig {
1467 fn default() -> Self {
1468 Self {
1469 color: ProgressIndicatorDefaults::linear_color(),
1470 track_color: ProgressIndicatorDefaults::linear_track_color(),
1471 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
1472 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
1473 }
1474 }
1475}
1476
1477pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
1482 Box(Modifier::new()
1483 .fill_max_width()
1484 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
1485 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
1486 let mul_c = |c: Color| {
1487 Color(
1488 c.0,
1489 c.1,
1490 c.2,
1491 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
1492 )
1493 };
1494 let track_h = rect.h;
1495 let corner = track_h * 0.5;
1496 let gap = dp_to_px(config.gap_size);
1497 let dot_r = dp_to_px(config.stop_size) * 0.5;
1498 let cy = rect.y + rect.h * 0.5;
1499 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
1500
1501 if t > 0.0 {
1503 let ind_w = t * rect.w;
1504 scene.nodes.push(SceneNode::Rect {
1505 rect: Rect {
1506 x: rect.x,
1507 y: cy - corner,
1508 w: ind_w,
1509 h: track_h,
1510 },
1511 brush: Brush::Solid(mul_c(config.color)),
1512 radius: corner,
1513 });
1514 }
1515
1516 let track_start = rect.x + t * rect.w + gap;
1518 let track_w = (rect.x + rect.w - track_start).max(0.0);
1519 if t < 1.0 && track_w > 0.0 {
1520 scene.nodes.push(SceneNode::Rect {
1521 rect: Rect {
1522 x: track_start,
1523 y: cy - corner,
1524 w: track_w,
1525 h: track_h,
1526 },
1527 brush: Brush::Solid(mul_c(config.track_color)),
1528 radius: corner,
1529 });
1530 }
1531
1532 if t < 1.0 {
1534 let sx = rect.x + rect.w - dot_r;
1535 scene.nodes.push(SceneNode::Ellipse {
1536 rect: Rect {
1537 x: sx - dot_r,
1538 y: cy - dot_r,
1539 w: dot_r * 2.0,
1540 h: dot_r * 2.0,
1541 },
1542 brush: Brush::Solid(mul_c(config.color)),
1543 });
1544 }
1545 }))
1546 .semantics(Semantics {
1547 role: Role::ProgressBar,
1548 label: None,
1549 focused: false,
1550 enabled: true,
1551 })
1552}
1553
1554#[derive(Clone)]
1556pub struct OutlinedTextFieldConfig {
1557 pub label: Option<String>,
1561 pub placeholder: Option<String>,
1565 pub leading_icon: Option<View>,
1567 pub trailing_icon: Option<View>,
1569 pub single_line: bool,
1571 pub is_error: bool,
1573 pub enabled: bool,
1575 pub on_submit: Option<Rc<dyn Fn(String)>>,
1577}
1578
1579impl Default for OutlinedTextFieldConfig {
1580 fn default() -> Self {
1581 Self {
1582 label: None,
1583 placeholder: None,
1584 leading_icon: None,
1585 trailing_icon: None,
1586 single_line: true,
1587 is_error: false,
1588 enabled: true,
1589 on_submit: None,
1590 }
1591 }
1592}
1593
1594pub fn OutlinedTextField(
1616 modifier: Modifier,
1617 value: String,
1618 on_value_change: impl Fn(String) + 'static,
1619 config: OutlinedTextFieldConfig,
1620) -> View {
1621 let th = theme();
1622 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
1623 let has_label = label_str.is_some();
1624
1625 let anim_key = match &label_str {
1627 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
1628 None => "otf_nolabel".into(),
1629 };
1630
1631 let focus_tracker: Rc<Cell<bool>> =
1635 remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
1636 let is_focused = focus_tracker.get();
1637 let should_float = !value.is_empty() || is_focused;
1638
1639 let float_t = animate_f32(
1640 anim_key.clone(),
1641 if should_float { 1.0 } else { 0.0 },
1642 th.motion.color,
1643 );
1644
1645 let border_color = if config.is_error {
1647 th.error
1648 } else if float_t > 0.5 {
1649 th.primary
1650 } else {
1651 th.outline
1652 };
1653
1654 let label_color = if config.is_error {
1656 th.error
1657 } else if float_t > 0.5 {
1658 th.primary
1659 } else {
1660 th.on_surface_variant
1661 };
1662
1663 let label_size = 16.0 - 4.0 * float_t;
1665
1666 let label_y = 16.0 - 20.0 * float_t;
1668
1669 let tf_placeholder = if has_label {
1672 String::new()
1673 } else {
1674 config.placeholder.unwrap_or_default()
1675 };
1676
1677 Box(modifier
1678 .clip_rounded(th.shapes.small)
1679 .border(1.0, border_color, th.shapes.small)
1680 .background(th.surface))
1681 .child(
1682 Stack(Modifier::new().fill_max_size()).child((
1683 Row(Modifier::new()
1686 .fill_max_size()
1687 .padding_values(PaddingValues {
1688 left: 16.0,
1689 right: 16.0,
1690 top: 16.0,
1691 bottom: 8.0,
1692 })
1693 .align_items(AlignItems::Center))
1694 .child((
1695 config.leading_icon.unwrap_or(Box(Modifier::new())),
1696 View::new(0, ViewKind::Box)
1697 .modifier(
1698 Modifier::new()
1699 .flex_grow(1.0)
1700 .padding_values(PaddingValues {
1701 left: 8.0,
1702 right: 8.0,
1703 top: 0.0,
1704 bottom: 0.0,
1705 })
1706 .text_input(TextInputConfig {
1707 hint: tf_placeholder,
1708 multiline: false,
1709 on_change: Some(Rc::new(on_value_change) as _),
1710 on_submit: config.on_submit.clone().map(|f| {
1711 let f = f.clone();
1712 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
1713 }),
1714 focus_tracker: Some(focus_tracker.clone()),
1715 value: value.clone(),
1716 visual_transformation: None,
1717 keyboard_type: None,
1718 ime_action: None,
1719 }),
1720 )
1721 .semantics(Semantics {
1722 role: Role::TextField,
1723 label: None,
1724 focused: false,
1725 enabled: true,
1726 }),
1727 config.trailing_icon.unwrap_or(Box(Modifier::new())),
1728 )),
1729 if let Some(lbl) = label_str {
1733 Box(Modifier::new()
1734 .min_width(200.0)
1735 .padding_values(PaddingValues {
1736 left: 20.0,
1737 right: 20.0,
1738 top: 0.0,
1739 bottom: 0.0,
1740 })
1741 .absolute()
1742 .offset(Some(0.0), Some(label_y), None, None))
1743 .child(
1744 Box(Modifier::new()
1745 .background(th.surface)
1746 .padding_values(PaddingValues {
1747 left: 4.0,
1748 right: 4.0,
1749 top: 2.0,
1750 bottom: 2.0,
1751 }))
1752 .child(
1753 Text(lbl.as_ref().to_string())
1754 .color(label_color)
1755 .size(label_size),
1756 ),
1757 )
1758 } else {
1759 Box(Modifier::new())
1760 },
1761 )),
1762 )
1763}
1764
1765#[derive(Clone, Debug)]
1767pub struct CheckboxConfig {
1768 pub modifier: Modifier,
1769 pub checked_color: Color,
1770 pub unchecked_color: Color,
1771 pub checkmark_color: Color,
1772}
1773
1774impl Default for CheckboxConfig {
1775 fn default() -> Self {
1776 Self {
1777 modifier: Modifier::new(),
1778 checked_color: CheckboxDefaults::checked_color(),
1779 unchecked_color: CheckboxDefaults::unchecked_color(),
1780 checkmark_color: CheckboxDefaults::checkmark_color(),
1781 }
1782 }
1783}
1784
1785static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
1789pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
1790 let th = theme();
1791 let sz = CheckboxDefaults::BOX_SIZE;
1792
1793 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
1794 let spec = th.motion.color_fast;
1795
1796 let fill = animate_color(
1797 format!("cb_fill_{}", id),
1798 if checked {
1799 config.checked_color
1800 } else {
1801 Color::TRANSPARENT
1802 },
1803 spec,
1804 );
1805 let bd_w = animate_f32(
1806 format!("cb_bw_{}", id),
1807 if checked {
1808 0.0
1809 } else {
1810 CheckboxDefaults::STROKE_WIDTH
1811 },
1812 spec,
1813 );
1814 let bd = animate_color(
1815 format!("cb_bd_{}", id),
1816 if checked {
1817 Color::TRANSPARENT
1818 } else {
1819 config.unchecked_color
1820 },
1821 spec,
1822 );
1823 let check_alpha = animate_f32(
1824 format!("cb_ca_{}", id),
1825 if checked { 1.0 } else { 0.0 },
1826 spec,
1827 );
1828
1829 Box(Modifier::new()
1830 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
1831 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
1832 .padding(0.0)
1833 .clip_rounded(20.0)
1834 .background(Color::TRANSPARENT)
1835 .clickable()
1836 .align_items(AlignItems::Center)
1837 .justify_content(JustifyContent::Center)
1838 .on_pointer_down(move |_| on_change(!checked))
1839 .then(config.modifier))
1840 .child(
1841 Box(Modifier::new()
1842 .size(sz, sz)
1843 .background(fill)
1844 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
1845 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
1846 .align_items(AlignItems::Center)
1847 .justify_content(JustifyContent::Center))
1848 .child(if check_alpha > 0.01 {
1849 Box(Modifier::new().alpha(check_alpha)).child(
1850 Icon(Symbol::new("done", '\u{E876}'))
1851 .color(config.checkmark_color)
1852 .size(CheckboxDefaults::CHECK_ICON_SIZE),
1853 )
1854 } else {
1855 Box(Modifier::new())
1856 }),
1857 )
1858}
1859
1860#[derive(Clone, Debug)]
1862pub struct RadioButtonConfig {
1863 pub modifier: Modifier,
1864 pub selected_color: Color,
1865 pub unselected_color: Color,
1866}
1867
1868impl Default for RadioButtonConfig {
1869 fn default() -> Self {
1870 Self {
1871 modifier: Modifier::new(),
1872 selected_color: RadioButtonDefaults::selected_color(),
1873 unselected_color: RadioButtonDefaults::unselected_color(),
1874 }
1875 }
1876}
1877
1878static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
1882pub fn RadioButton(
1883 selected: bool,
1884 on_select: impl Fn() + 'static,
1885 config: RadioButtonConfig,
1886) -> View {
1887 let th = theme();
1888 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
1889
1890 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
1891 let color_spec = th.motion.color_fast;
1892 let spring = th.motion.spring;
1893
1894 let ring_col = animate_color(
1895 format!("rb_ring_{}", id),
1896 if selected {
1897 config.selected_color
1898 } else {
1899 config.unselected_color
1900 },
1901 color_spec,
1902 );
1903 let dot_size = animate_f32(
1904 format!("rb_dot_{}", id),
1905 if selected {
1906 RadioButtonDefaults::DOT_RADIUS * 2.0
1907 } else {
1908 0.0
1909 },
1910 spring,
1911 );
1912
1913 Box(Modifier::new()
1914 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
1915 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
1916 .padding(0.0)
1917 .clip_rounded(20.0)
1918 .background(Color::TRANSPARENT)
1919 .clickable()
1920 .align_items(AlignItems::Center)
1921 .justify_content(JustifyContent::Center)
1922 .on_pointer_down(move |_| on_select())
1923 .then(config.modifier))
1924 .child(
1925 Box(Modifier::new()
1926 .size(d, d)
1927 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
1928 .clip_rounded(d * 0.5)
1929 .align_items(AlignItems::Center)
1930 .justify_content(JustifyContent::Center))
1931 .child(if dot_size > 0.5 {
1932 Box(Modifier::new()
1933 .size(dot_size, dot_size)
1934 .background(config.selected_color)
1935 .clip_rounded(dot_size * 0.5))
1936 } else {
1937 Box(Modifier::new())
1938 }),
1939 )
1940}
1941
1942#[derive(Clone, Debug)]
1944pub struct SwitchConfig {
1945 pub modifier: Modifier,
1946 pub checked_track_color: Color,
1947 pub unchecked_track_color: Color,
1948 pub checked_thumb_color: Color,
1949 pub unchecked_thumb_color: Color,
1950 pub unchecked_border_color: Color,
1951}
1952
1953impl Default for SwitchConfig {
1954 fn default() -> Self {
1955 Self {
1956 modifier: Modifier::new(),
1957 checked_track_color: SwitchDefaults::checked_track_color(),
1958 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
1959 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
1960 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
1961 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
1962 }
1963 }
1964}
1965
1966static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
1970pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
1971 let th = theme();
1972 let track_w = SwitchDefaults::TRACK_WIDTH;
1973 let track_h = SwitchDefaults::TRACK_HEIGHT;
1974
1975 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
1976
1977 let thumb_target_pos = if checked {
1979 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
1980 } else {
1981 8.0
1982 };
1983 let thumb_target_d = if checked {
1984 SwitchDefaults::THUMB_CHECKED_SIZE
1985 } else {
1986 SwitchDefaults::THUMB_UNCHECKED_SIZE
1987 };
1988 let spring = th.motion.spring;
1989
1990 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
1991 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
1992 let thumb_top = (track_h - thumb_d) * 0.5;
1993
1994 let color_spec = th.motion.color_fast;
1995 let track_bg = animate_color(
1996 format!("sw_tbg_{}", id),
1997 if checked {
1998 config.checked_track_color
1999 } else {
2000 config.unchecked_track_color
2001 },
2002 color_spec,
2003 );
2004 let thumb_bg = animate_color(
2005 format!("sw_tmbg_{}", id),
2006 if checked {
2007 config.checked_thumb_color
2008 } else {
2009 config.unchecked_thumb_color
2010 },
2011 color_spec,
2012 );
2013 let track_border = animate_f32(
2014 format!("sw_tb_{}", id),
2015 if checked { 0.0 } else { 2.0 },
2016 color_spec,
2017 );
2018 let border_color = animate_color(
2019 format!("sw_bc_{}", id),
2020 if checked {
2021 Color::TRANSPARENT
2022 } else {
2023 config.unchecked_border_color
2024 },
2025 color_spec,
2026 );
2027
2028 Box(Modifier::new()
2029 .size(track_w, track_h)
2030 .padding(0.0)
2031 .clip_rounded(track_h * 0.5)
2032 .background(Color::TRANSPARENT)
2033 .clickable()
2034 .on_pointer_down(move |_| on_change(!checked))
2035 .then(config.modifier))
2036 .child(
2037 Box(Modifier::new()
2038 .size(track_w, track_h)
2039 .background(track_bg)
2040 .border(track_border, border_color, track_h * 0.5)
2041 .clip_rounded(track_h * 0.5))
2042 .child(Box(Modifier::new()
2043 .size(thumb_d, thumb_d)
2044 .background(thumb_bg)
2045 .clip_rounded(thumb_d * 0.5)
2046 .absolute()
2047 .offset(Some(thumb_left), Some(thumb_top), None, None))),
2048 )
2049}
2050
2051#[derive(Clone, Debug)]
2053pub struct SliderConfig {
2054 pub modifier: Modifier,
2055 pub active_track_color: Color,
2056 pub inactive_track_color: Color,
2057 pub thumb_color: Color,
2058}
2059
2060impl Default for SliderConfig {
2061 fn default() -> Self {
2062 Self {
2063 modifier: Modifier::new(),
2064 active_track_color: SliderDefaults::active_track_color(),
2065 inactive_track_color: SliderDefaults::inactive_track_color(),
2066 thumb_color: SliderDefaults::thumb_color(),
2067 }
2068 }
2069}
2070
2071static SLIDER_COUNTER: AtomicU64 = AtomicU64::new(0);
2072
2073fn snap_step(v: f32, min: f32, max: f32, step: Option<f32>) -> f32 {
2074 let v = v.clamp(min, max);
2075 if let Some(s) = step.filter(|s| *s > 0.0) {
2076 let t = ((v - min) / s).round();
2077 (min + t * s).clamp(min, max)
2078 } else {
2079 v
2080 }
2081}
2082
2083fn value_from_x(x: f32, rect: Rect, min: f32, max: f32, step: Option<f32>) -> f32 {
2084 let w = rect.w.max(1.0);
2085 let t = ((x - rect.x) / w).clamp(0.0, 1.0);
2086 let v = min + t * (max - min);
2087 snap_step(v, min, max, step)
2088}
2089
2090pub fn M3Slider(
2091 value: f32,
2092 range: (f32, f32),
2093 step: Option<f32>,
2094 on_change: impl Fn(f32) + 'static,
2095 config: SliderConfig,
2096) -> View {
2097 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
2098 let track_rect = remember_state_with_key(format!("ms_rect_{}", id), || Rect::default());
2099 let drag_active = remember_state_with_key(format!("ms_da_{}", id), || false);
2100
2101 let track_rect_p = track_rect.clone();
2102 let drag_active_p = drag_active.clone();
2103
2104 let min = range.0;
2105 let max = range.1;
2106 let oc = Rc::new(on_change);
2107 let range_size = (max - min).max(1e-6);
2108 let t = ((value - min) / range_size).clamp(0.0, 1.0);
2109
2110 let tick_frac: Vec<f32> = if let Some(s) = step {
2111 let n = ((max - min) / s.max(1e-6)).round() as usize;
2112 (0..=n).map(|i| i as f32 / n as f32).collect()
2113 } else {
2114 Vec::new()
2115 };
2116
2117 Box(Modifier::new()
2118 .min_width(200.0)
2119 .height(44.0)
2120 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2121 let mul_c = |c: Color| {
2122 Color(
2123 c.0,
2124 c.1,
2125 c.2,
2126 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2127 )
2128 };
2129 let track_h = dp_to_px(16.0);
2130 let thumb_w = dp_to_px(4.0);
2131 let thumb_h = dp_to_px(44.0);
2132 let dot_r = dp_to_px(2.0);
2133 let corner = track_h * 0.5;
2134 let gap = dp_to_px(8.0);
2135 let pad = thumb_w * 0.5;
2136 let track_x = rect.x + pad;
2137 let track_w = (rect.w - thumb_w).max(0.0);
2138 let cy = rect.y + rect.h * 0.5;
2139
2140 let kx = if step.is_some() && !tick_frac.is_empty() {
2142 let is_first = (t - tick_frac[0]).abs() < 1e-6;
2143 let is_last = (t - tick_frac[tick_frac.len() - 1]).abs() < 1e-6;
2144 if is_first || is_last {
2145 track_x + t * track_w
2146 } else {
2147 track_x + (track_w - track_h) * t + corner
2148 }
2149 } else {
2150 track_x + t * track_w
2151 };
2152
2153 *track_rect_p.borrow_mut() = Rect {
2154 x: track_x,
2155 y: rect.y,
2156 w: track_w,
2157 h: rect.h,
2158 };
2159
2160 let inactive_x = track_x.max(kx + gap);
2162 let inactive_w = (track_x + track_w - inactive_x).max(0.0);
2163 if inactive_w > 0.0 {
2164 scene.nodes.push(SceneNode::Rect {
2165 rect: Rect {
2166 x: inactive_x,
2167 y: cy - track_h * 0.5,
2168 w: inactive_w,
2169 h: track_h,
2170 },
2171 brush: Brush::Solid(mul_c(config.inactive_track_color)),
2172 radius: corner,
2173 });
2174 }
2175 let fill_w = (kx - gap - track_x).max(0.0);
2177 if fill_w > 0.0 {
2178 scene.nodes.push(SceneNode::Rect {
2179 rect: Rect {
2180 x: track_x,
2181 y: cy - track_h * 0.5,
2182 w: fill_w,
2183 h: track_h,
2184 },
2185 brush: Brush::Solid(mul_c(config.active_track_color)),
2186 radius: corner,
2187 });
2188 }
2189 let tick_start = track_x + corner;
2191 let tick_end = track_x + track_w - corner;
2192 for &tf in &tick_frac {
2193 let tx = tick_start + tf * (tick_end - tick_start);
2194 if tx >= kx - gap && tx <= kx + gap {
2195 continue;
2196 }
2197 let on_active = tx <= kx - gap;
2198 scene.nodes.push(SceneNode::Ellipse {
2199 rect: Rect {
2200 x: tx - dot_r,
2201 y: cy - dot_r,
2202 w: dot_r * 2.0,
2203 h: dot_r * 2.0,
2204 },
2205 brush: Brush::Solid(mul_c(if on_active {
2206 config.inactive_track_color
2207 } else {
2208 config.active_track_color
2209 })),
2210 });
2211 }
2212 if inactive_w > 0.0 {
2214 let sx = track_x + track_w - corner;
2215 scene.nodes.push(SceneNode::Ellipse {
2216 rect: Rect {
2217 x: sx - dot_r,
2218 y: cy - dot_r,
2219 w: dot_r * 2.0,
2220 h: dot_r * 2.0,
2221 },
2222 brush: Brush::Solid(mul_c(config.active_track_color)),
2223 });
2224 }
2225 let da = *drag_active_p.borrow();
2227 let tw = if da { thumb_w * 0.5 } else { thumb_w };
2228 scene.nodes.push(SceneNode::Rect {
2229 rect: Rect {
2230 x: kx - tw * 0.5,
2231 y: cy - thumb_h * 0.5,
2232 w: tw,
2233 h: thumb_h,
2234 },
2235 brush: Brush::Solid(mul_c(config.thumb_color)),
2236 radius: tw * 0.5,
2237 });
2238 })
2239 .on_pointer_down({
2240 let oc = oc.clone();
2241 let track_rect = track_rect.clone();
2242 let drag_active = drag_active.clone();
2243 move |pe: PointerEvent| {
2244 *drag_active.borrow_mut() = true;
2245 let r = *track_rect.borrow();
2246 (oc)(value_from_x(pe.position.x, r, min, max, step));
2247 }
2248 })
2249 .on_pointer_move({
2250 let oc = oc.clone();
2251 let track_rect = track_rect.clone();
2252 let drag_active = drag_active.clone();
2253 move |pe: PointerEvent| {
2254 if !*drag_active.borrow() {
2255 return;
2256 }
2257 let r = *track_rect.borrow();
2258 (oc)(value_from_x(pe.position.x, r, min, max, step));
2259 }
2260 })
2261 .on_pointer_up(move |_pe: PointerEvent| {
2262 *drag_active.borrow_mut() = false;
2263 })
2264 .on_scroll({
2265 let oc = oc.clone();
2266 move |d: Vec2| -> Vec2 {
2267 let dir = if d.y < -0.5 {
2268 1
2269 } else if d.y > 0.5 {
2270 -1
2271 } else {
2272 0
2273 };
2274 if dir == 0 {
2275 return d;
2276 }
2277 let step_val = step.unwrap_or(1.0).max(1e-6);
2278 let new_val = snap_step(value + (dir as f32) * step_val, min, max, step);
2279 if (new_val - value).abs() > 1e-6 {
2280 (oc)(new_val);
2281 Vec2 { x: d.x, y: 0.0 }
2282 } else {
2283 d
2284 }
2285 }
2286 })
2287 .then(config.modifier))
2288 .semantics(Semantics {
2289 role: Role::Slider,
2290 label: None,
2291 focused: false,
2292 enabled: true,
2293 })
2294}
2295
2296pub fn M3RangeSlider(
2297 start: f32,
2298 end: f32,
2299 range: (f32, f32),
2300 step: Option<f32>,
2301 on_change: impl Fn(f32, f32) + 'static,
2302 config: SliderConfig,
2303) -> View {
2304 let id = *remember(|| SLIDER_COUNTER.fetch_add(1, Ordering::Relaxed));
2305 let track_rect = remember_state_with_key(format!("mrs_rect_{}", id), || Rect::default());
2306 let drag_active = remember_state_with_key(format!("mrs_da_{}", id), || false);
2307 let active_thumb = remember_state_with_key(format!("mrs_at_{}", id), || false);
2308
2309 let min = range.0;
2310 let max = range.1;
2311 let oc = Rc::new(on_change);
2312 let range_size = (max - min).max(1e-6);
2313 let t0 = ((start - min) / range_size).clamp(0.0, 1.0);
2314 let t1 = ((end - min) / range_size).clamp(0.0, 1.0);
2315
2316 let tick_frac: Vec<f32> = if let Some(s) = step {
2317 let n = ((max - min) / s.max(1e-6)).round() as usize;
2318 (0..=n).map(|i| i as f32 / n as f32).collect()
2319 } else {
2320 Vec::new()
2321 };
2322
2323 let track_rect_p = track_rect.clone();
2324 let drag_active_p = drag_active.clone();
2325 let active_thumb_p = active_thumb.clone();
2326
2327 Box(Modifier::new()
2328 .min_width(200.0)
2329 .height(44.0)
2330 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
2331 let mul_c = |c: Color| {
2332 Color(
2333 c.0,
2334 c.1,
2335 c.2,
2336 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
2337 )
2338 };
2339 let track_h = dp_to_px(16.0);
2340 let thumb_w = dp_to_px(4.0);
2341 let thumb_h = dp_to_px(44.0);
2342 let dot_r = dp_to_px(2.0);
2343 let corner = track_h * 0.5;
2344 let gap = dp_to_px(8.0);
2345 let pad = thumb_w * 0.5;
2346 let track_x = rect.x + pad;
2347 let track_w = (rect.w - thumb_w).max(0.0);
2348 let cy = rect.y + rect.h * 0.5;
2349
2350 let thumb_pos = |tf: f32, fracs: &[f32]| {
2352 if step.is_some() && !fracs.is_empty() {
2353 let is_first = (tf - fracs[0]).abs() < 1e-6;
2354 let is_last = (tf - fracs[fracs.len() - 1]).abs() < 1e-6;
2355 if is_first || is_last {
2356 track_x + tf * track_w
2357 } else {
2358 track_x + (track_w - track_h) * tf + corner
2359 }
2360 } else {
2361 track_x + tf * track_w
2362 }
2363 };
2364 let k0 = thumb_pos(t0, &tick_frac);
2365 let k1 = thumb_pos(t1, &tick_frac);
2366 let active_l = k0.min(k1);
2367 let active_r = k0.max(k1);
2368
2369 *track_rect_p.borrow_mut() = Rect {
2370 x: track_x,
2371 y: rect.y,
2372 w: track_w,
2373 h: rect.h,
2374 };
2375
2376 let linactive_w = (active_l - gap - track_x).max(0.0);
2377 if linactive_w > 0.0 {
2378 scene.nodes.push(SceneNode::Rect {
2379 rect: Rect {
2380 x: track_x,
2381 y: cy - track_h * 0.5,
2382 w: linactive_w,
2383 h: track_h,
2384 },
2385 brush: Brush::Solid(mul_c(config.inactive_track_color)),
2386 radius: corner,
2387 });
2388 }
2389 let rinactive_x = (active_r + gap).min(track_x + track_w);
2390 let rinactive_w = (track_x + track_w - rinactive_x).max(0.0);
2391 if rinactive_w > 0.0 {
2392 scene.nodes.push(SceneNode::Rect {
2393 rect: Rect {
2394 x: rinactive_x,
2395 y: cy - track_h * 0.5,
2396 w: rinactive_w,
2397 h: track_h,
2398 },
2399 brush: Brush::Solid(mul_c(config.inactive_track_color)),
2400 radius: corner,
2401 });
2402 }
2403 let active_w = (active_r - gap - (active_l + gap)).max(0.0);
2405 if active_w > 0.0 {
2406 scene.nodes.push(SceneNode::Rect {
2407 rect: Rect {
2408 x: active_l + gap,
2409 y: cy - track_h * 0.5,
2410 w: active_w,
2411 h: track_h,
2412 },
2413 brush: Brush::Solid(mul_c(config.active_track_color)),
2414 radius: corner,
2415 });
2416 }
2417 let tick_start = track_x + corner;
2419 let tick_end = track_x + track_w - corner;
2420 for &tf in &tick_frac {
2421 let tx = tick_start + tf * (tick_end - tick_start);
2422 if tx >= active_l - gap && tx <= active_r + gap {
2423 continue;
2424 }
2425 let on_active = tx >= active_l + gap && tx <= active_r - gap;
2426 scene.nodes.push(SceneNode::Ellipse {
2427 rect: Rect {
2428 x: tx - dot_r,
2429 y: cy - dot_r,
2430 w: dot_r * 2.0,
2431 h: dot_r * 2.0,
2432 },
2433 brush: Brush::Solid(mul_c(if on_active {
2434 config.inactive_track_color
2435 } else {
2436 config.active_track_color
2437 })),
2438 });
2439 }
2440 if linactive_w > 0.0 {
2442 let sx0 = track_x + corner;
2443 scene.nodes.push(SceneNode::Ellipse {
2444 rect: Rect {
2445 x: sx0 - dot_r,
2446 y: cy - dot_r,
2447 w: dot_r * 2.0,
2448 h: dot_r * 2.0,
2449 },
2450 brush: Brush::Solid(mul_c(config.active_track_color)),
2451 });
2452 }
2453 if rinactive_w > 0.0 {
2454 let sx = track_x + track_w - corner;
2455 scene.nodes.push(SceneNode::Ellipse {
2456 rect: Rect {
2457 x: sx - dot_r,
2458 y: cy - dot_r,
2459 w: dot_r * 2.0,
2460 h: dot_r * 2.0,
2461 },
2462 brush: Brush::Solid(mul_c(config.active_track_color)),
2463 });
2464 }
2465 let da = *drag_active_p.borrow();
2467 let at = *active_thumb_p.borrow();
2468 let thumb_sizes = [
2469 (k0, if da && !at { thumb_w * 0.5 } else { thumb_w }),
2470 (k1, if da && at { thumb_w * 0.5 } else { thumb_w }),
2471 ];
2472 for &(kx, tw) in &thumb_sizes {
2473 scene.nodes.push(SceneNode::Rect {
2474 rect: Rect {
2475 x: kx - tw * 0.5,
2476 y: cy - thumb_h * 0.5,
2477 w: tw,
2478 h: thumb_h,
2479 },
2480 brush: Brush::Solid(mul_c(config.thumb_color)),
2481 radius: tw * 0.5,
2482 });
2483 }
2484 })
2485 .on_pointer_down({
2486 let oc = oc.clone();
2487 let track_rect = track_rect.clone();
2488 let drag_active = drag_active.clone();
2489 let active_thumb = active_thumb.clone();
2490 move |pe: PointerEvent| {
2491 *drag_active.borrow_mut() = true;
2492 let r = *track_rect.borrow();
2493 let v = value_from_x(pe.position.x, r, min, max, step);
2494 let use_end = (v - end).abs() < (v - start).abs();
2495 *active_thumb.borrow_mut() = use_end;
2496 let (a, b) = if use_end {
2497 (start, v.max(start))
2498 } else {
2499 (v.min(end), end)
2500 };
2501 (oc)(a, b);
2502 }
2503 })
2504 .on_pointer_move({
2505 let oc = oc.clone();
2506 let track_rect = track_rect.clone();
2507 let drag_active = drag_active.clone();
2508 let active_thumb = active_thumb.clone();
2509 move |pe: PointerEvent| {
2510 if !*drag_active.borrow() {
2511 return;
2512 }
2513 let r = *track_rect.borrow();
2514 let v = value_from_x(pe.position.x, r, min, max, step);
2515 let use_end = *active_thumb.borrow();
2516 let (a, b) = if use_end {
2517 (start, v.max(start))
2518 } else {
2519 (v.min(end), end)
2520 };
2521 (oc)(a, b);
2522 }
2523 })
2524 .on_pointer_up({
2525 let drag_active = drag_active.clone();
2526 let active_thumb = active_thumb.clone();
2527 move |_pe: PointerEvent| {
2528 *drag_active.borrow_mut() = false;
2529 *active_thumb.borrow_mut() = false;
2530 }
2531 })
2532 .on_scroll({
2533 let oc = oc.clone();
2534 let active_thumb = active_thumb.clone();
2535 move |d: Vec2| -> Vec2 {
2536 let dir = if d.y < -0.5 {
2537 1
2538 } else if d.y > 0.5 {
2539 -1
2540 } else {
2541 0
2542 };
2543 if dir == 0 {
2544 return d;
2545 }
2546 let step_val = step.unwrap_or(1.0).max(1e-6);
2547 let use_end = *active_thumb.borrow();
2548 let (mut a, mut b) = (start, end);
2549 if use_end {
2550 b = snap_step(end + (dir as f32) * step_val, min, max, step).max(a);
2551 } else {
2552 a = snap_step(start + (dir as f32) * step_val, min, max, step).min(b);
2553 }
2554 if (a - start).abs() > 1e-6 || (b - end).abs() > 1e-6 {
2555 (oc)(a, b);
2556 Vec2 { x: d.x, y: 0.0 }
2557 } else {
2558 d
2559 }
2560 }
2561 })
2562 .then(config.modifier))
2563 .semantics(Semantics {
2564 role: Role::Slider,
2565 label: None,
2566 focused: false,
2567 enabled: true,
2568 })
2569}
2570
2571#[derive(Clone, Debug)]
2573pub struct CardConfig {
2574 pub modifier: Modifier,
2575 pub container_color: Color,
2576 pub shape_radius: f32,
2577 pub tonal_elevation: f32,
2578}
2579
2580impl Default for CardConfig {
2581 fn default() -> Self {
2582 Self {
2583 modifier: Modifier::new(),
2584 container_color: CardDefaults::filled_container_color(),
2585 shape_radius: CardDefaults::SHAPE_RADIUS,
2586 tonal_elevation: CardDefaults::ELEVATION,
2587 }
2588 }
2589}
2590
2591pub fn Card(config: CardConfig, content: impl FnOnce() -> View) -> View {
2593 let mut m = Modifier::new()
2594 .background(config.container_color)
2595 .clip_rounded(config.shape_radius)
2596 .then(config.modifier);
2597 if config.tonal_elevation > 0.0 {
2598 m = m.state_elevation(StateElevation {
2599 default: config.tonal_elevation,
2600 hovered: config.tonal_elevation,
2601 pressed: config.tonal_elevation,
2602 disabled: 0.0,
2603 });
2604 }
2605 Box(m).child(content())
2606}
2607
2608#[derive(Clone, Debug)]
2610pub struct SnackbarConfig {
2611 pub modifier: Modifier,
2612 pub container_color: Color,
2613 pub content_color: Color,
2614 pub action_color: Color,
2615 pub min_height: f32,
2616 pub min_width: f32,
2617 pub max_width: f32,
2618}
2619
2620impl Default for SnackbarConfig {
2621 fn default() -> Self {
2622 Self {
2623 modifier: Modifier::new(),
2624 container_color: SnackbarDefaults::container_color(),
2625 content_color: SnackbarDefaults::content_color(),
2626 action_color: SnackbarDefaults::action_color(),
2627 min_height: SnackbarDefaults::MIN_HEIGHT,
2628 min_width: SnackbarDefaults::MIN_WIDTH,
2629 max_width: SnackbarDefaults::MAX_WIDTH,
2630 }
2631 }
2632}
2633
2634#[derive(Clone, Debug)]
2636pub struct ChipConfig {
2637 pub modifier: Modifier,
2638 pub enabled: bool,
2639 pub selected: bool,
2640 pub container_color: Color,
2641 pub selected_container_color: Color,
2642 pub content_color: Color,
2643 pub selected_content_color: Color,
2644 pub border_color: Color,
2645 pub shape_radius: f32,
2646 pub horizontal_padding: f32,
2647}
2648
2649impl Default for ChipConfig {
2650 fn default() -> Self {
2651 Self {
2652 modifier: Modifier::new(),
2653 enabled: true,
2654 selected: false,
2655 container_color: ChipDefaults::surface_color(),
2656 selected_container_color: ChipDefaults::selected_container_color(),
2657 content_color: ChipDefaults::unselected_content_color(),
2658 selected_content_color: ChipDefaults::selected_content_color(),
2659 border_color: ChipDefaults::unselected_border_color(),
2660 shape_radius: ChipDefaults::SHAPE_RADIUS,
2661 horizontal_padding: ChipDefaults::HORIZONTAL_PADDING,
2662 }
2663 }
2664}
2665
2666pub fn AssistChip(
2668 on_click: impl Fn() + 'static,
2669 label: View,
2670 leading_icon: Option<View>,
2671 trailing_icon: Option<View>,
2672 config: ChipConfig,
2673) -> View {
2674 let th = theme();
2675 let shape = config.shape_radius;
2676 Box(Modifier::new()
2677 .state_colors(StateColors {
2678 default: Color::TRANSPARENT,
2679 hovered: th.on_surface.with_alpha_f32(0.08),
2680 pressed: th.on_surface.with_alpha_f32(0.12),
2681 disabled: Color::TRANSPARENT,
2682 })
2683 .padding_values(PaddingValues {
2684 left: config.horizontal_padding,
2685 right: config.horizontal_padding,
2686 top: 8.0,
2687 bottom: 8.0,
2688 })
2689 .clickable()
2690 .on_pointer_down(move |_| on_click())
2691 .background(Color::TRANSPARENT)
2692 .clip_rounded(shape)
2693 .border(1.0, config.border_color, shape)
2694 .then(config.modifier))
2695 .child(
2696 Row(Modifier::new().align_items(AlignItems::Center)).child((
2697 leading_icon
2698 .map(|v| {
2699 Box(Modifier::new().padding_values(PaddingValues {
2700 left: 0.0,
2701 right: 8.0,
2702 top: 0.0,
2703 bottom: 0.0,
2704 }))
2705 .child(with_content_color(config.content_color, move || v))
2706 })
2707 .unwrap_or(Box(Modifier::new())),
2708 with_content_color(config.content_color, move || label),
2709 trailing_icon
2710 .map(|v| {
2711 Box(Modifier::new().padding_values(PaddingValues {
2712 left: 8.0,
2713 right: 0.0,
2714 top: 0.0,
2715 bottom: 0.0,
2716 }))
2717 .child(with_content_color(config.content_color, move || v))
2718 })
2719 .unwrap_or(Box(Modifier::new())),
2720 )),
2721 )
2722}
2723
2724#[derive(Clone, Debug)]
2726pub struct NavigationBarConfig {
2727 pub modifier: Modifier,
2728 pub container_color: Color,
2729 pub selected_icon_color: Color,
2730 pub unselected_icon_color: Color,
2731 pub indicator_color: Color,
2732 pub height: f32,
2733 pub indicator_opacity: f32,
2734 pub item_horizontal_padding: f32,
2735 pub item_vertical_padding: f32,
2736 pub indicator_radius: f32,
2737}
2738
2739impl Default for NavigationBarConfig {
2740 fn default() -> Self {
2741 Self {
2742 modifier: Modifier::new(),
2743 container_color: NavigationBarDefaults::container_color(),
2744 selected_icon_color: NavigationBarDefaults::selected_icon_color(),
2745 unselected_icon_color: NavigationBarDefaults::unselected_icon_color(),
2746 indicator_color: NavigationBarDefaults::indicator_color(),
2747 height: NavigationBarDefaults::HEIGHT,
2748 indicator_opacity: NavigationBarDefaults::ITEM_ACTIVE_INDICATOR_OPACITY,
2749 item_horizontal_padding: 4.0,
2750 item_vertical_padding: 6.0,
2751 indicator_radius: 16.0,
2752 }
2753 }
2754}
2755
2756#[derive(Clone, Debug)]
2758pub struct NavigationRailConfig {
2759 pub modifier: Modifier,
2760 pub container_color: Color,
2761 pub selected_icon_color: Color,
2762 pub unselected_icon_color: Color,
2763 pub selected_container_color: Color,
2764 pub width: f32,
2765 pub item_radius: f32,
2766}
2767
2768impl Default for NavigationRailConfig {
2769 fn default() -> Self {
2770 Self {
2771 modifier: Modifier::new(),
2772 container_color: NavigationRailDefaults::container_color(),
2773 selected_icon_color: NavigationRailDefaults::selected_icon_color(),
2774 unselected_icon_color: NavigationRailDefaults::unselected_icon_color(),
2775 selected_container_color: NavigationRailDefaults::selected_container_color(),
2776 width: NavigationRailDefaults::WIDTH,
2777 item_radius: 16.0,
2778 }
2779 }
2780}
2781
2782#[derive(Clone, Debug)]
2784pub struct ScaffoldConfig {
2785 pub modifier: Modifier,
2786 pub container_color: Color,
2787 pub top_bar_height: f32,
2788 pub bottom_bar_height: f32,
2789 pub fab_margin: f32,
2790}
2791
2792impl Default for ScaffoldConfig {
2793 fn default() -> Self {
2794 Self {
2795 modifier: Modifier::new(),
2796 container_color: ScaffoldDefaults::container_color(),
2797 top_bar_height: ScaffoldDefaults::TOP_BAR_HEIGHT,
2798 bottom_bar_height: ScaffoldDefaults::BOTTOM_BAR_HEIGHT,
2799 fab_margin: ScaffoldDefaults::FAB_MARGIN,
2800 }
2801 }
2802}
2803
2804#[derive(Clone, Debug)]
2806pub struct NavigationDrawerConfig {
2807 pub modifier: Modifier,
2808 pub container_color: Color,
2809 pub scrim_color: Color,
2810 pub width: f32,
2811 pub shape_radius: f32,
2812}
2813
2814impl Default for NavigationDrawerConfig {
2815 fn default() -> Self {
2816 Self {
2817 modifier: Modifier::new(),
2818 container_color: NavigationDrawerDefaults::container_color(),
2819 scrim_color: NavigationDrawerDefaults::scrim_color(),
2820 width: NavigationDrawerDefaults::WIDTH,
2821 shape_radius: NavigationDrawerDefaults::SHAPE_RADIUS,
2822 }
2823 }
2824}
2825
2826#[derive(Clone, Debug)]
2828pub struct BottomSheetConfig {
2829 pub modifier: Modifier,
2830 pub container_color: Color,
2831 pub scrim_color: Color,
2832 pub drag_handle_color: Color,
2833 pub shape_radius: f32,
2834 pub max_width: f32,
2835 pub drag_handle_width: f32,
2836 pub drag_handle_height: f32,
2837 pub peek_height: f32,
2838}
2839
2840impl Default for BottomSheetConfig {
2841 fn default() -> Self {
2842 Self {
2843 modifier: Modifier::new(),
2844 container_color: BottomSheetDefaults::container_color(),
2845 scrim_color: BottomSheetDefaults::scrim_color(),
2846 drag_handle_color: BottomSheetDefaults::drag_handle_color(),
2847 shape_radius: BottomSheetDefaults::SHAPE_RADIUS,
2848 max_width: BottomSheetDefaults::MAX_WIDTH,
2849 drag_handle_width: BottomSheetDefaults::DRAG_HANDLE_WIDTH,
2850 drag_handle_height: BottomSheetDefaults::DRAG_HANDLE_HEIGHT,
2851 peek_height: BottomSheetDefaults::PEEK_HEIGHT,
2852 }
2853 }
2854}
2855
2856#[derive(Clone, Debug)]
2858pub struct SearchBarConfig {
2859 pub modifier: Modifier,
2860 pub container_color: Color,
2861 pub active_container_color: Color,
2862 pub content_color: Color,
2863 pub placeholder_color: Color,
2864 pub height: f32,
2865 pub expanded_width: f32,
2866 pub collapsed_width: f32,
2867}
2868
2869impl Default for SearchBarConfig {
2870 fn default() -> Self {
2871 Self {
2872 modifier: Modifier::new(),
2873 container_color: SearchBarDefaults::container_color(),
2874 active_container_color: SearchBarDefaults::active_container_color(),
2875 content_color: SearchBarDefaults::content_color(),
2876 placeholder_color: SearchBarDefaults::placeholder_color(),
2877 height: SearchBarDefaults::HEIGHT,
2878 expanded_width: SearchBarDefaults::EXPANDED_WIDTH,
2879 collapsed_width: SearchBarDefaults::COLLAPSED_WIDTH,
2880 }
2881 }
2882}
2883
2884#[derive(Clone, Debug)]
2886pub struct DropdownMenuConfig {
2887 pub modifier: Modifier,
2888 pub container_color: Color,
2889 pub item_text_color: Color,
2890 pub disabled_item_text_color: Color,
2891 pub divider_color: Color,
2892 pub min_width: f32,
2893 pub item_height: f32,
2894}
2895
2896impl Default for DropdownMenuConfig {
2897 fn default() -> Self {
2898 Self {
2899 modifier: Modifier::new(),
2900 container_color: DropdownMenuDefaults::container_color(),
2901 item_text_color: DropdownMenuDefaults::item_text_color(),
2902 disabled_item_text_color: DropdownMenuDefaults::disabled_item_text_color(),
2903 divider_color: DropdownMenuDefaults::divider_color(),
2904 min_width: DropdownMenuDefaults::MIN_WIDTH,
2905 item_height: DropdownMenuDefaults::ITEM_HEIGHT,
2906 }
2907 }
2908}
2909
2910#[derive(Clone, Debug)]
2912pub struct TooltipConfig {
2913 pub modifier: Modifier,
2914 pub container_color: Color,
2915 pub content_color: Color,
2916 pub offset_y: f32,
2917 pub horizontal_padding: f32,
2918 pub vertical_padding: f32,
2919}
2920
2921impl Default for TooltipConfig {
2922 fn default() -> Self {
2923 Self {
2924 modifier: Modifier::new(),
2925 container_color: TooltipDefaults::container_color(),
2926 content_color: TooltipDefaults::content_color(),
2927 offset_y: TooltipDefaults::OFFSET_Y,
2928 horizontal_padding: TooltipDefaults::HORIZONTAL_PADDING,
2929 vertical_padding: TooltipDefaults::VERTICAL_PADDING,
2930 }
2931 }
2932}
2933
2934#[derive(Clone, Debug)]
2936pub struct SwipeToDismissConfig {
2937 pub modifier: Modifier,
2938 pub dismiss_threshold: f32,
2939 pub dismissed_offset: f32,
2940 pub animation_spec: AnimationSpec,
2941}
2942
2943impl Default for SwipeToDismissConfig {
2944 fn default() -> Self {
2945 Self {
2946 modifier: Modifier::new(),
2947 dismiss_threshold: 150.0,
2948 dismissed_offset: 300.0,
2949 animation_spec: AnimationSpec::spring_gentle(),
2950 }
2951 }
2952}
2953
2954#[derive(Clone, Debug)]
2956pub struct PullToRefreshConfig {
2957 pub modifier: Modifier,
2958 pub indicator_color: Color,
2959 pub threshold: f32,
2960}
2961
2962impl Default for PullToRefreshConfig {
2963 fn default() -> Self {
2964 Self {
2965 modifier: Modifier::new(),
2966 indicator_color: PullToRefreshDefaults::indicator_color(),
2967 threshold: PullToRefreshDefaults::THRESHOLD,
2968 }
2969 }
2970}
2971
2972#[derive(Clone, Debug)]
2974pub struct AlertDialogConfig {
2975 pub modifier: Modifier,
2976 pub scrim_color: Color,
2977 pub min_width: f32,
2978 pub max_width: f32,
2979 pub horizontal_padding: f32,
2980}
2981
2982impl Default for AlertDialogConfig {
2983 fn default() -> Self {
2984 Self {
2985 modifier: Modifier::new(),
2986 scrim_color: AlertDialogDefaults::scrim_color(),
2987 min_width: AlertDialogDefaults::MIN_WIDTH,
2988 max_width: AlertDialogDefaults::MAX_WIDTH,
2989 horizontal_padding: AlertDialogDefaults::HORIZONTAL_PADDING,
2990 }
2991 }
2992}