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, Button, Column, Row, Stack, Text, TextStyle, ViewExt};
10
11use crate::{Icon, Symbol};
12
13pub fn TopAppBar(
16 title: impl Into<String>,
17 navigation_icon: Option<View>,
18 actions: Vec<View>,
19) -> View {
20 let th = theme();
21 Row(Modifier::new()
22 .fill_max_width()
23 .height(64.0)
24 .background(th.surface)
25 .padding_values(PaddingValues {
26 left: 4.0,
27 right: 4.0,
28 top: 0.0,
29 bottom: 0.0,
30 })
31 .align_items(AlignItems::Center))
32 .child((
33 navigation_icon.unwrap_or(Box(Modifier::new().size(16.0, 1.0))),
34 Box(Modifier::new()
35 .padding_values(PaddingValues {
36 left: 16.0,
37 right: 0.0,
38 top: 0.0,
39 bottom: 0.0,
40 })
41 .flex_grow(1.0))
42 .child(
43 Text(title)
44 .color(th.on_surface)
45 .size(th.typography.title_large),
46 ),
47 Row(Modifier::new().align_items(AlignItems::Center)).child(actions),
48 ))
49}
50
51pub fn IconButton(icon: View, on_click: impl Fn() + 'static) -> View {
53 let th = theme();
54 let _bg = Color::TRANSPARENT;
55 Box(Modifier::new()
56 .size(40.0, 40.0)
57 .clip_rounded(20.0)
58 .state_colors(StateColors {
59 default: Color::TRANSPARENT,
60 hovered: th.on_surface.with_alpha_f32(0.08),
61 pressed: th.on_surface.with_alpha_f32(0.12),
62 disabled: Color::TRANSPARENT,
63 })
64 .align_items(AlignItems::Center)
65 .justify_content(JustifyContent::Center)
66 .clickable()
67 .on_pointer_down(move |_| on_click()))
68 .child(icon)
69}
70
71pub fn FilledIconButton(icon: View, on_click: impl Fn() + 'static) -> View {
73 let th = theme();
74 let bg = th.primary;
75 Box(Modifier::new()
76 .size(40.0, 40.0)
77 .clip_rounded(20.0)
78 .background(bg)
79 .state_colors(StateColors {
80 default: Color::TRANSPARENT,
81 hovered: th.on_primary.with_alpha_f32(0.08),
82 pressed: th.on_primary.with_alpha_f32(0.12),
83 disabled: th.on_surface.with_alpha_f32(0.12),
84 })
85 .align_items(AlignItems::Center)
86 .justify_content(JustifyContent::Center)
87 .clickable()
88 .on_pointer_down(move |_| on_click()))
89 .child(icon)
90}
91
92pub fn FilledButton(
94 modifier: Modifier,
95 on_click: impl Fn() + 'static,
96 content: impl FnOnce() -> View,
97) -> View {
98 let th = theme();
99 let content = with_content_color(th.on_primary, content);
100 let bg = th.primary;
101 Box(Modifier::new()
102 .height(40.0)
103 .min_width(48.0)
104 .background(bg)
105 .state_colors(StateColors {
106 default: Color::TRANSPARENT,
107 hovered: th.on_primary.with_alpha_f32(0.08),
108 pressed: th.on_primary.with_alpha_f32(0.12),
109 disabled: th.on_surface.with_alpha_f32(0.12),
110 })
111 .state_elevation(StateElevation {
112 default: 0.0,
113 hovered: 1.0,
114 pressed: 8.0,
115 disabled: 0.0,
116 })
117 .clip_rounded(20.0)
118 .padding_values(PaddingValues {
119 left: 24.0,
120 right: 24.0,
121 top: 0.0,
122 bottom: 0.0,
123 })
124 .align_items(AlignItems::Center)
125 .justify_content(JustifyContent::Center)
126 .clickable()
127 .on_pointer_down(move |_| on_click())
128 .then(modifier))
129 .child(content)
130}
131
132pub fn FilledTonalButton(
134 modifier: Modifier,
135 on_click: impl Fn() + 'static,
136 content: impl FnOnce() -> View,
137) -> View {
138 let th = theme();
139 let content = with_content_color(th.on_secondary_container, content);
140 let bg = th.secondary_container;
141 Box(Modifier::new()
142 .height(40.0)
143 .min_width(48.0)
144 .background(bg)
145 .state_colors(StateColors {
146 default: Color::TRANSPARENT,
147 hovered: th.on_secondary_container.with_alpha_f32(0.08),
148 pressed: th.on_secondary_container.with_alpha_f32(0.12),
149 disabled: th.on_surface.with_alpha_f32(0.12),
150 })
151 .state_elevation(StateElevation {
152 default: 0.0,
153 hovered: 1.0,
154 pressed: 8.0,
155 disabled: 0.0,
156 })
157 .clip_rounded(20.0)
158 .padding_values(PaddingValues {
159 left: 24.0,
160 right: 24.0,
161 top: 0.0,
162 bottom: 0.0,
163 })
164 .align_items(AlignItems::Center)
165 .justify_content(JustifyContent::Center)
166 .clickable()
167 .on_pointer_down(move |_| on_click())
168 .then(modifier))
169 .child(content)
170}
171
172pub fn OutlinedButton(
174 modifier: Modifier,
175 on_click: impl Fn() + 'static,
176 content: impl FnOnce() -> View,
177) -> View {
178 let th = theme();
179 let content = with_content_color(th.on_surface, content);
180 let _bg = Color::TRANSPARENT;
181 Box(Modifier::new()
182 .height(40.0)
183 .min_width(48.0)
184 .state_colors(StateColors {
185 default: Color::TRANSPARENT,
186 hovered: th.on_surface.with_alpha_f32(0.08),
187 pressed: th.on_surface.with_alpha_f32(0.12),
188 disabled: Color::TRANSPARENT,
189 })
190 .border(1.0, th.outline_variant, 20.0)
191 .clip_rounded(20.0)
192 .padding_values(PaddingValues {
193 left: 24.0,
194 right: 24.0,
195 top: 0.0,
196 bottom: 0.0,
197 })
198 .align_items(AlignItems::Center)
199 .justify_content(JustifyContent::Center)
200 .clickable()
201 .on_pointer_down(move |_| on_click())
202 .then(modifier))
203 .child(content)
204}
205
206pub fn TextButton(
208 modifier: Modifier,
209 on_click: impl Fn() + 'static,
210 content: impl FnOnce() -> View,
211) -> View {
212 let th = theme();
213 let content = with_content_color(th.on_surface, content);
214 let _bg = Color::TRANSPARENT;
215 Box(Modifier::new()
216 .height(40.0)
217 .min_width(48.0)
218 .state_colors(StateColors {
219 default: Color::TRANSPARENT,
220 hovered: th.on_surface.with_alpha_f32(0.08),
221 pressed: th.on_surface.with_alpha_f32(0.12),
222 disabled: Color::TRANSPARENT,
223 })
224 .clip_rounded(20.0)
225 .padding_values(PaddingValues {
226 left: 12.0,
227 right: 12.0,
228 top: 0.0,
229 bottom: 0.0,
230 })
231 .align_items(AlignItems::Center)
232 .justify_content(JustifyContent::Center)
233 .clickable()
234 .on_pointer_down(move |_| on_click())
235 .then(modifier))
236 .child(content)
237}
238
239pub fn ElevatedButton(
241 modifier: Modifier,
242 on_click: impl Fn() + 'static,
243 content: impl FnOnce() -> View,
244) -> View {
245 let th = theme();
246 let content = with_content_color(th.primary, content);
247 let bg = th.surface_container_low;
248 Box(Modifier::new()
249 .height(40.0)
250 .min_width(48.0)
251 .background(bg)
252 .state_colors(StateColors {
253 default: Color::TRANSPARENT,
254 hovered: th.primary.with_alpha_f32(0.08),
255 pressed: th.primary.with_alpha_f32(0.12),
256 disabled: th.on_surface.with_alpha_f32(0.12),
257 })
258 .state_elevation(StateElevation {
259 default: th.elevation.level1,
260 hovered: th.elevation.level2,
261 pressed: th.elevation.level3,
262 disabled: 0.0,
263 })
264 .clip_rounded(20.0)
265 .padding_values(PaddingValues {
266 left: 24.0,
267 right: 24.0,
268 top: 0.0,
269 bottom: 0.0,
270 })
271 .align_items(AlignItems::Center)
272 .justify_content(JustifyContent::Center)
273 .clickable()
274 .on_pointer_down(move |_| on_click())
275 .then(modifier))
276 .child(content)
277}
278
279pub fn FAB(icon: View, on_click: impl Fn() + 'static) -> View {
281 let th = theme();
282 let bg = th.primary_container;
283 Box(Modifier::new()
284 .size(56.0, 56.0)
285 .background(bg)
286 .state_colors(StateColors {
287 default: Color::TRANSPARENT,
288 hovered: th.on_primary_container.with_alpha_f32(0.08),
289 pressed: th.on_primary_container.with_alpha_f32(0.12),
290 disabled: th.on_surface.with_alpha_f32(0.12),
291 })
292 .state_elevation(StateElevation {
293 default: 6.0,
294 hovered: 8.0,
295 pressed: 12.0,
296 disabled: 0.0,
297 })
298 .clip_rounded(28.0)
299 .align_items(AlignItems::Center)
300 .justify_content(JustifyContent::Center)
301 .clickable()
302 .on_pointer_down(move |_| on_click()))
303 .child(icon)
304}
305
306pub fn LargeFAB(icon: View, on_click: impl Fn() + 'static) -> View {
308 let th = theme();
309 let bg = th.primary_container;
310 Box(Modifier::new()
311 .size(96.0, 96.0)
312 .background(bg)
313 .state_colors(StateColors {
314 default: Color::TRANSPARENT,
315 hovered: th.on_primary_container.with_alpha_f32(0.08),
316 pressed: th.on_primary_container.with_alpha_f32(0.12),
317 disabled: th.on_surface.with_alpha_f32(0.12),
318 })
319 .state_elevation(StateElevation {
320 default: 6.0,
321 hovered: 8.0,
322 pressed: 12.0,
323 disabled: 0.0,
324 })
325 .clip_rounded(28.0)
326 .align_items(AlignItems::Center)
327 .justify_content(JustifyContent::Center)
328 .clickable()
329 .on_pointer_down(move |_| on_click()))
330 .child(icon)
331}
332
333pub fn ExtendedFAB(
335 icon: Option<View>,
336 label: impl Into<String>,
337 on_click: impl Fn() + 'static,
338) -> View {
339 let th = theme();
340 let has_icon = icon.is_some();
341 let bg = th.primary_container;
342 Row(Modifier::new()
343 .height(56.0)
344 .min_width(80.0)
345 .background(bg)
346 .state_colors(StateColors {
347 default: Color::TRANSPARENT,
348 hovered: th.on_primary_container.with_alpha_f32(0.08),
349 pressed: th.on_primary_container.with_alpha_f32(0.12),
350 disabled: th.on_surface.with_alpha_f32(0.12),
351 })
352 .state_elevation(StateElevation {
353 default: 6.0,
354 hovered: 8.0,
355 pressed: 12.0,
356 disabled: 0.0,
357 })
358 .clip_rounded(16.0)
359 .padding_values(PaddingValues {
360 left: 16.0,
361 right: 20.0,
362 top: 0.0,
363 bottom: 0.0,
364 })
365 .align_items(AlignItems::Center)
366 .clickable()
367 .on_pointer_down(move |_| on_click()))
368 .child((
369 icon.unwrap_or(Box(Modifier::new())),
370 Box(Modifier::new().size(if has_icon { 12.0 } else { 0.0 }, 1.0)),
371 Text(label)
372 .color(th.on_primary_container)
373 .size(th.typography.label_large)
374 .single_line(),
375 ))
376}
377
378pub fn Divider() -> View {
380 let th = theme();
381 Box(Modifier::new()
382 .fill_max_width()
383 .height(1.0)
384 .background(th.outline_variant))
385}
386
387pub fn VerticalDivider() -> View {
389 let th = theme();
390 Box(Modifier::new()
391 .width(1.0)
392 .fill_max_height()
393 .background(th.outline_variant))
394}
395
396pub fn Badge(label: Option<impl Into<String>>) -> View {
399 let th = theme();
400 match label {
401 None => Box(Modifier::new()
402 .size(6.0, 6.0)
403 .background(th.error)
404 .clip_rounded(3.0)),
405 Some(text) => {
406 let text = text.into();
407 Box(Modifier::new()
408 .min_width(16.0)
409 .height(16.0)
410 .background(th.error)
411 .clip_rounded(8.0)
412 .padding_values(PaddingValues {
413 left: 4.0,
414 right: 4.0,
415 top: 0.0,
416 bottom: 0.0,
417 })
418 .align_items(AlignItems::Center)
419 .justify_content(JustifyContent::Center))
420 .child(
421 Text(text)
422 .color(th.on_error)
423 .size(th.typography.label_small)
424 .single_line(),
425 )
426 }
427 }
428}
429
430pub fn ListItem(
432 headline: impl Into<String>,
433 supporting_text: Option<String>,
434 leading: Option<View>,
435 trailing: Option<View>,
436 on_click: Option<Rc<dyn Fn()>>,
437) -> View {
438 let th = theme();
439 let mut modifier = Modifier::new()
440 .fill_max_width()
441 .min_height(if supporting_text.is_some() {
442 72.0
443 } else {
444 56.0
445 })
446 .state_colors(StateColors {
447 default: Color::TRANSPARENT,
448 hovered: th.on_surface.with_alpha_f32(0.08),
449 pressed: th.on_surface.with_alpha_f32(0.12),
450 disabled: Color::TRANSPARENT,
451 })
452 .padding_values(PaddingValues {
453 left: 16.0,
454 right: 24.0,
455 top: 8.0,
456 bottom: 8.0,
457 })
458 .align_items(AlignItems::Center);
459
460 if let Some(cb) = on_click {
461 modifier = modifier.clickable().on_pointer_down(move |_| cb());
462 }
463
464 Row(modifier).child((
465 leading
466 .map(|v| {
467 Box(Modifier::new().padding_values(PaddingValues {
468 left: 0.0,
469 right: 16.0,
470 top: 0.0,
471 bottom: 0.0,
472 }))
473 .child(v)
474 })
475 .unwrap_or(Box(Modifier::new())),
476 Column(
477 Modifier::new()
478 .flex_grow(1.0)
479 .justify_content(JustifyContent::Center),
480 )
481 .child((
482 Text(headline)
483 .color(th.on_surface)
484 .size(th.typography.body_large)
485 .single_line(),
486 supporting_text
487 .map(|st| {
488 Text(st)
489 .color(th.on_surface_variant)
490 .size(th.typography.body_medium)
491 .max_lines(2)
492 .overflow_ellipsize()
493 })
494 .unwrap_or(Box(Modifier::new())),
495 )),
496 trailing
497 .map(|v| {
498 Box(Modifier::new().padding_values(PaddingValues {
499 left: 16.0,
500 right: 0.0,
501 top: 0.0,
502 bottom: 0.0,
503 }))
504 .child(v)
505 })
506 .unwrap_or(Box(Modifier::new())),
507 ))
508}
509
510pub struct Tab {
512 pub label: String,
513 pub icon: Option<View>,
514 pub on_click: Rc<dyn Fn()>,
515}
516
517static TABROW_COUNTER: AtomicU64 = AtomicU64::new(0);
518
519pub fn TabRow(selected_index: usize, tabs: Vec<Tab>) -> View {
522 let th = theme();
523 let id = remember(|| TABROW_COUNTER.fetch_add(1, Ordering::Relaxed));
524 let spec = th.motion.color;
525 Column(Modifier::new().fill_max_width()).child((
526 Row(Modifier::new()
527 .fill_max_width()
528 .height(48.0)
529 .background(th.surface))
530 .child(
531 tabs.into_iter()
532 .enumerate()
533 .map(|(i, tab)| {
534 let selected = i == selected_index;
535 let color = animate_color(
536 format!("tab_clr_{}_{}", id, i),
537 if selected {
538 th.primary
539 } else {
540 th.on_surface_variant
541 },
542 spec,
543 );
544 let indicator_h = animate_f32(
545 format!("tab_ind_{}_{}", id, i),
546 if selected { 3.0 } else { 0.0 },
547 spec,
548 );
549 let cb = tab.on_click.clone();
550
551 Column(
552 Modifier::new()
553 .flex_grow(1.0)
554 .fill_max_height()
555 .align_items(AlignItems::Center)
556 .justify_content(JustifyContent::Center)
557 .state_colors(StateColors {
558 default: Color::TRANSPARENT,
559 hovered: th.on_surface.with_alpha_f32(0.08),
560 pressed: th.on_surface.with_alpha_f32(0.12),
561 disabled: Color::TRANSPARENT,
562 })
563 .clickable()
564 .on_pointer_down(move |_| cb()),
565 )
566 .child((
567 tab.icon.unwrap_or(Box(Modifier::new())),
568 Text(tab.label)
569 .color(color)
570 .size(th.typography.title_small)
571 .single_line(),
572 Box(Modifier::new()
573 .fill_max_width()
574 .height(indicator_h)
575 .background(th.primary)
576 .clip_rounded(1.5)),
577 ))
578 })
579 .collect::<Vec<_>>(),
580 ),
581 Box(Modifier::new()
582 .fill_max_width()
583 .height(1.0)
584 .background(th.outline_variant)),
585 ))
586}
587
588pub struct Segment {
590 pub label: String,
591 pub icon: Option<View>,
592 pub on_click: Rc<dyn Fn()>,
593}
594
595static SEGBUTTON_COUNTER: AtomicU64 = AtomicU64::new(0);
596
597pub fn SegmentedButton(selected: &[usize], segments: Vec<Segment>) -> View {
600 let th = theme();
601 let count = segments.len();
602 let id = remember(|| SEGBUTTON_COUNTER.fetch_add(1, Ordering::Relaxed));
603 let spec = th.motion.color;
604
605 Row(Modifier::new()
606 .height(40.0)
607 .border(1.0, th.outline, 20.0)
608 .clip_rounded(20.0))
609 .child(
610 segments
611 .into_iter()
612 .enumerate()
613 .map(|(i, seg)| {
614 let is_selected = selected.contains(&i);
615
616 let bg = animate_color(
617 format!("sb_bg_{}_{}", id, i),
618 if is_selected {
619 th.secondary_container
620 } else {
621 Color::TRANSPARENT
622 },
623 spec,
624 );
625 let fg = animate_color(
626 format!("sb_fg_{}_{}", id, i),
627 if is_selected {
628 th.on_secondary_container
629 } else {
630 th.on_surface
631 },
632 spec,
633 );
634
635 let cb = seg.on_click.clone();
636
637 let mut modifier = Modifier::new()
638 .flex_grow(1.0)
639 .fill_max_height()
640 .background(bg)
641 .align_items(AlignItems::Center)
642 .justify_content(JustifyContent::Center)
643 .padding_values(PaddingValues {
644 left: 12.0,
645 right: 12.0,
646 top: 0.0,
647 bottom: 0.0,
648 })
649 .clickable()
650 .on_pointer_down(move |_| cb());
651
652 if i < count - 1 {
653 modifier = modifier.border(1.0, th.outline, 0.0);
654 }
655
656 Row(modifier).child((
657 seg.icon.unwrap_or(Box(Modifier::new())),
658 Text(seg.label)
659 .color(fg)
660 .size(th.typography.label_large)
661 .single_line(),
662 ))
663 })
664 .collect::<Vec<_>>(),
665 )
666}
667
668pub fn CircularProgressIndicator(value: Option<f32>) -> View {
673 View::new(
674 0,
675 ViewKind::ProgressBar {
676 value: value.unwrap_or(0.0),
677 min: 0.0,
678 max: 1.0,
679 circular: true,
680 },
681 )
682 .modifier(Modifier::new().size(48.0, 48.0))
683 .semantics(Semantics {
684 role: Role::ProgressBar,
685 label: None,
686 focused: false,
687 enabled: true,
688 })
689}
690
691pub fn LinearProgressIndicator(value: Option<f32>) -> View {
693 View::new(
694 0,
695 ViewKind::ProgressBar {
696 value: value.unwrap_or(0.0),
697 min: 0.0,
698 max: 1.0,
699 circular: false,
700 },
701 )
702 .modifier(Modifier::new().fill_max_width().height(4.0))
703 .semantics(Semantics {
704 role: Role::ProgressBar,
705 label: None,
706 focused: false,
707 enabled: true,
708 })
709}
710
711#[derive(Clone)]
713pub struct OutlinedTextFieldConfig {
714 pub label: Option<String>,
718 pub placeholder: Option<String>,
722 pub leading_icon: Option<View>,
724 pub trailing_icon: Option<View>,
726 pub single_line: bool,
728 pub is_error: bool,
730 pub enabled: bool,
732 pub on_submit: Option<Rc<dyn Fn(String)>>,
734}
735
736impl Default for OutlinedTextFieldConfig {
737 fn default() -> Self {
738 Self {
739 label: None,
740 placeholder: None,
741 leading_icon: None,
742 trailing_icon: None,
743 single_line: true,
744 is_error: false,
745 enabled: true,
746 on_submit: None,
747 }
748 }
749}
750
751pub fn OutlinedTextField(
773 modifier: Modifier,
774 value: String,
775 on_value_change: impl Fn(String) + 'static,
776 config: OutlinedTextFieldConfig,
777) -> View {
778 let th = theme();
779 let label_str: Option<Rc<str>> = config.label.map(Rc::from);
780 let has_label = label_str.is_some();
781
782 let anim_key = match &label_str {
784 Some(l) => format!("otf_{}", &l[..l.len().min(32)]),
785 None => "otf_nolabel".into(),
786 };
787
788 let focus_tracker: Rc<Cell<bool>> =
792 remember_with_key(format!("otf_focus_{}", anim_key), || Cell::new(false));
793 let is_focused = focus_tracker.get();
794 let should_float = !value.is_empty() || is_focused;
795
796 let float_t = animate_f32(
797 anim_key.clone(),
798 if should_float { 1.0 } else { 0.0 },
799 th.motion.color,
800 );
801
802 let border_color = if config.is_error {
804 th.error
805 } else if float_t > 0.5 {
806 th.primary
807 } else {
808 th.outline
809 };
810
811 let label_color = if config.is_error {
813 th.error
814 } else if float_t > 0.5 {
815 th.primary
816 } else {
817 th.on_surface_variant
818 };
819
820 let label_size = 16.0 - 4.0 * float_t;
822
823 let label_y = 16.0 - 20.0 * float_t;
825
826 let tf_placeholder = if has_label {
829 String::new()
830 } else {
831 config.placeholder.unwrap_or_default()
832 };
833
834 Box(modifier
835 .clip_rounded(th.shapes.small)
836 .border(1.0, border_color, th.shapes.small)
837 .background(th.surface))
838 .child(
839 Stack(Modifier::new().fill_max_size()).child((
840 Row(Modifier::new()
843 .fill_max_size()
844 .padding_values(PaddingValues {
845 left: 16.0,
846 right: 16.0,
847 top: 16.0,
848 bottom: 8.0,
849 })
850 .align_items(AlignItems::Center))
851 .child((
852 config.leading_icon.unwrap_or(Box(Modifier::new())),
853 View::new(
854 0,
855 ViewKind::TextField {
856 state_key: 0,
857 hint: tf_placeholder,
858 multiline: false,
859 on_change: Some(Rc::new(on_value_change) as _),
860 on_submit: config.on_submit.clone().map(|f| {
861 let f = f.clone();
862 Rc::new(move |s| f(s)) as Rc<dyn Fn(String)>
863 }),
864 focus_tracker: Some(focus_tracker.clone()),
865 value: value.clone(),
866 visual_transformation: None,
867 keyboard_type: None,
868 ime_action: None,
869 },
870 )
871 .modifier(
872 Modifier::new()
873 .flex_grow(1.0)
874 .padding_values(PaddingValues {
875 left: 8.0,
876 right: 8.0,
877 top: 0.0,
878 bottom: 0.0,
879 }),
880 )
881 .semantics(Semantics {
882 role: Role::TextField,
883 label: None,
884 focused: false,
885 enabled: true,
886 }),
887 config.trailing_icon.unwrap_or(Box(Modifier::new())),
888 )),
889 if let Some(lbl) = label_str {
893 Box(Modifier::new()
894 .fill_max_width()
895 .padding_values(PaddingValues {
896 left: 20.0,
897 right: 20.0,
898 top: 0.0,
899 bottom: 0.0,
900 })
901 .absolute()
902 .offset(Some(0.0), Some(label_y), None, None))
903 .child(
904 Box(Modifier::new()
905 .background(th.surface)
906 .padding_values(PaddingValues {
907 left: 4.0,
908 right: 4.0,
909 top: 2.0,
910 bottom: 2.0,
911 }))
912 .child(
913 Text(lbl.as_ref().to_string())
914 .color(label_color)
915 .size(label_size),
916 ),
917 )
918 } else {
919 Box(Modifier::new())
920 },
921 )),
922 )
923}
924
925static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
929pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
930 let th = theme();
931 let sz = 18.0;
932
933 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
934 let spec = th.motion.color_fast;
935
936 let fill = animate_color(
937 format!("cb_fill_{}", id),
938 if checked {
939 th.primary
940 } else {
941 Color::TRANSPARENT
942 },
943 spec,
944 );
945 let bd_w = animate_f32(
946 format!("cb_bw_{}", id),
947 if checked { 0.0 } else { 2.0 },
948 spec,
949 );
950 let bd = animate_color(
951 format!("cb_bd_{}", id),
952 if checked {
953 Color::TRANSPARENT
954 } else {
955 th.on_surface_variant
956 },
957 spec,
958 );
959 let check_alpha = animate_f32(
960 format!("cb_ca_{}", id),
961 if checked { 1.0 } else { 0.0 },
962 spec,
963 );
964
965 Button(
966 Box(Modifier::new()
967 .size(sz, sz)
968 .background(fill)
969 .border(bd_w, bd, 2.0)
970 .clip_rounded(2.0)
971 .align_items(AlignItems::Center)
972 .justify_content(JustifyContent::Center))
973 .child(if check_alpha > 0.01 {
974 Box(Modifier::new().alpha(check_alpha)).child(
975 Icon(Symbol::new("done", '\u{E876}'))
976 .color(th.on_primary)
977 .size(14.0),
978 )
979 } else {
980 Box(Modifier::new())
981 }),
982 move || on_change(!checked),
983 )
984 .modifier(
985 Modifier::new()
986 .width(40.0)
987 .height(40.0)
988 .padding(0.0)
989 .clip_rounded(20.0)
990 .background(Color::TRANSPARENT),
991 )
992}
993
994static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
998pub fn RadioButton(selected: bool, on_select: impl Fn() + 'static) -> View {
999 let th = theme();
1000 let d = 20.0;
1001
1002 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
1003 let color_spec = th.motion.color_fast;
1004 let spring = th.motion.spring;
1005
1006 let ring_col = animate_color(
1007 format!("rb_ring_{}", id),
1008 if selected {
1009 th.primary
1010 } else {
1011 th.on_surface_variant
1012 },
1013 color_spec,
1014 );
1015 let dot_size = animate_f32(
1016 format!("rb_dot_{}", id),
1017 if selected { 10.0 } else { 0.0 },
1018 spring,
1019 );
1020
1021 Button(
1022 Box(Modifier::new()
1023 .size(d, d)
1024 .border(2.0, ring_col, d * 0.5)
1025 .clip_rounded(d * 0.5)
1026 .align_items(AlignItems::Center)
1027 .justify_content(JustifyContent::Center))
1028 .child(if dot_size > 0.5 {
1029 Box(Modifier::new()
1030 .size(dot_size, dot_size)
1031 .background(th.primary)
1032 .clip_rounded(dot_size * 0.5))
1033 } else {
1034 Box(Modifier::new())
1035 }),
1036 on_select,
1037 )
1038 .modifier(
1039 Modifier::new()
1040 .width(40.0)
1041 .height(40.0)
1042 .padding(0.0)
1043 .clip_rounded(20.0)
1044 .background(Color::TRANSPARENT),
1045 )
1046}
1047
1048static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
1052pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static) -> View {
1053 let th = theme();
1054 let track_w = 52.0;
1055 let track_h = 32.0;
1056
1057 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
1058
1059 let thumb_target_pos = if checked { track_w - 24.0 - 4.0 } else { 8.0 };
1061 let thumb_target_d = if checked { 24.0 } else { 16.0 };
1062 let spring = th.motion.spring;
1063
1064 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
1065 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
1066 let thumb_top = (track_h - thumb_d) * 0.5;
1067
1068 let color_spec = th.motion.color_fast;
1069 let track_bg = animate_color(
1070 format!("sw_tbg_{}", id),
1071 if checked {
1072 th.primary
1073 } else {
1074 th.surface_container_highest
1075 },
1076 color_spec,
1077 );
1078 let thumb_bg = animate_color(
1079 format!("sw_tmbg_{}", id),
1080 if checked { th.on_primary } else { th.outline },
1081 color_spec,
1082 );
1083 let track_border = animate_f32(
1084 format!("sw_tb_{}", id),
1085 if checked { 0.0 } else { 2.0 },
1086 color_spec,
1087 );
1088 let border_color = animate_color(
1089 format!("sw_bc_{}", id),
1090 if checked {
1091 Color::TRANSPARENT
1092 } else {
1093 th.outline
1094 },
1095 color_spec,
1096 );
1097
1098 Button(
1099 Box(Modifier::new()
1100 .size(track_w, track_h)
1101 .background(track_bg)
1102 .border(track_border, border_color, track_h * 0.5)
1103 .clip_rounded(track_h * 0.5))
1104 .child(Box(Modifier::new()
1105 .size(thumb_d, thumb_d)
1106 .background(thumb_bg)
1107 .clip_rounded(thumb_d * 0.5)
1108 .absolute()
1109 .offset(Some(thumb_left), Some(thumb_top), None, None))),
1110 move || on_change(!checked),
1111 )
1112 .modifier(
1113 Modifier::new()
1114 .size(track_w, track_h)
1115 .padding(0.0)
1116 .clip_rounded(track_h * 0.5)
1117 .background(Color::TRANSPARENT),
1118 )
1119}
1120
1121pub fn M3Slider(
1124 value: f32,
1125 range: (f32, f32),
1126 step: Option<f32>,
1127 on_change: impl Fn(f32) + 'static,
1128) -> View {
1129 View::new(
1130 0,
1131 ViewKind::Slider {
1132 value,
1133 min: range.0,
1134 max: range.1,
1135 step,
1136 on_change: Some(Rc::new(on_change)),
1137 },
1138 )
1139 .modifier(Modifier::new().height(28.0))
1140 .semantics(Semantics {
1141 role: Role::Slider,
1142 label: None,
1143 focused: false,
1144 enabled: true,
1145 })
1146}
1147
1148pub fn M3RangeSlider(
1151 start: f32,
1152 end: f32,
1153 range: (f32, f32),
1154 step: Option<f32>,
1155 on_change: impl Fn(f32, f32) + 'static,
1156) -> View {
1157 View::new(
1158 0,
1159 ViewKind::RangeSlider {
1160 start,
1161 end,
1162 min: range.0,
1163 max: range.1,
1164 step,
1165 on_change: Some(Rc::new(on_change)),
1166 },
1167 )
1168 .modifier(Modifier::new().height(28.0))
1169 .semantics(Semantics {
1170 role: Role::Slider,
1171 label: None,
1172 focused: false,
1173 enabled: true,
1174 })
1175}