1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4use std::sync::atomic::{AtomicU64, Ordering};
5
6use crate::{Icon, Symbol};
7use repose_core::*;
8use repose_ui::{
9 Box, TextStyle, ViewExt,
10 anim::{animate_color, animate_f32},
11};
12
13use super::util::{apply_enabled_click, apply_m3_clickable_ex};
14
15use super::*;
16
17#[derive(Clone, Debug)]
19pub struct CheckboxConfig {
20 pub modifier: Modifier,
21 pub enabled: bool,
23 pub checked_color: Color,
24 pub unchecked_color: Color,
25 pub checkmark_color: Color,
26 pub checked_border_color: Color,
28 pub unchecked_border_color: Color,
30 pub disabled_checked_box_color: Color,
31 pub disabled_unchecked_box_color: Color,
32 pub disabled_indeterminate_box_color: Color,
33 pub disabled_checkmark_color: Color,
34 pub disabled_checked_border_color: Color,
35 pub disabled_unchecked_border_color: Color,
36 pub disabled_indeterminate_border_color: Color,
37 pub state_colors: StateColors,
38 pub interaction_source: Option<MutableInteractionSource>,
39}
40
41impl Default for CheckboxConfig {
42 fn default() -> Self {
43 Self {
44 modifier: Modifier::new(),
45 enabled: true,
46 checked_color: CheckboxDefaults::checked_color(),
47 unchecked_color: CheckboxDefaults::unchecked_color(),
48 checkmark_color: CheckboxDefaults::checkmark_color(),
49 checked_border_color: CheckboxDefaults::checked_color(),
50 unchecked_border_color: CheckboxDefaults::unchecked_color(),
51 disabled_checked_box_color: CheckboxDefaults::disabled_checked_box_color(),
52 disabled_unchecked_box_color: Color::TRANSPARENT,
53 disabled_indeterminate_box_color: CheckboxDefaults::disabled_checked_box_color(),
54 disabled_checkmark_color: CheckboxDefaults::disabled_checkmark_color(),
55 disabled_checked_border_color: CheckboxDefaults::disabled_checked_box_color(),
56 disabled_unchecked_border_color: CheckboxDefaults::disabled_unchecked_border_color(),
57 disabled_indeterminate_border_color: CheckboxDefaults::disabled_checked_box_color(),
58 state_colors: CheckboxDefaults::state_colors_default(),
59 interaction_source: None,
60 }
61 }
62}
63
64static CHECKBOX_COUNTER: AtomicU64 = AtomicU64::new(0);
68pub fn Checkbox(checked: bool, on_change: impl Fn(bool) + 'static, config: CheckboxConfig) -> View {
69 let th = theme();
70 let sz = CheckboxDefaults::BOX_SIZE;
71
72 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
73 let spec = th.motion.color_fast;
74
75 let is_enabled = config.enabled;
76
77 let fill = animate_color(
78 format!("cb_fill_{}", id),
79 if !is_enabled {
80 if checked {
81 config.disabled_checked_box_color
82 } else {
83 config.disabled_unchecked_box_color
84 }
85 } else if checked {
86 config.checked_color
87 } else {
88 Color::TRANSPARENT
89 },
90 spec,
91 );
92 let bd_w = animate_f32(
93 format!("cb_bw_{}", id),
94 if !is_enabled && checked {
95 0.0
96 } else if !is_enabled {
97 CheckboxDefaults::STROKE_WIDTH
98 } else if checked {
99 0.0
100 } else {
101 CheckboxDefaults::STROKE_WIDTH
102 },
103 spec,
104 );
105 let bd = animate_color(
106 format!("cb_bd_{}", id),
107 if !is_enabled {
108 if checked {
109 config.disabled_checked_border_color
110 } else {
111 config.disabled_unchecked_border_color
112 }
113 } else if checked {
114 Color::TRANSPARENT
115 } else {
116 config.unchecked_border_color
117 },
118 spec,
119 );
120 let check_alpha = animate_f32(
121 format!("cb_ca_{}", id),
122 if checked { 1.0 } else { 0.0 },
123 spec,
124 );
125 let check_col = if !is_enabled {
126 config.disabled_checkmark_color
127 } else {
128 config.checkmark_color
129 };
130
131 let cb = move || {
132 if config.enabled {
133 on_change(!checked)
134 }
135 };
136
137 let cb_source: Rc<MutableInteractionSource> = config
138 .interaction_source
139 .clone()
140 .map(Rc::new)
141 .unwrap_or_else(|| remember(MutableInteractionSource::new));
142
143 let ripple_col = if checked {
146 config.checked_color
147 } else {
148 th.on_surface
149 };
150
151 let mut m = Modifier::new()
152 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
153 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
154 .padding(0.0)
155 .background(Color::TRANSPARENT)
156 .state_colors(config.state_colors) .align_items(AlignItems::CENTER)
158 .justify_content(JustifyContent::CENTER);
159
160 m = apply_m3_clickable_ex(
161 m,
162 &cb_source,
163 ripple_col,
164 is_enabled,
165 cb,
166 false, Some(20.0),
168 );
169 m = m.semantics(Semantics {
170 role: Role::Checkbox,
171 label: None,
172 focused: false,
173 enabled: is_enabled,
174 selectable_group: false,
175 });
176 Box(m.then(config.modifier)).child(
177 Box(Modifier::new()
178 .size(sz, sz)
179 .background(fill)
180 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
181 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
182 .align_items(AlignItems::CENTER)
183 .justify_content(JustifyContent::CENTER))
184 .child(if check_alpha > 0.01 {
185 Box(Modifier::new().alpha(check_alpha)).child(
186 Icon(Symbol::new("done", '\u{E876}'))
187 .color(check_col)
188 .size(CheckboxDefaults::CHECK_ICON_SIZE),
189 )
190 } else {
191 Box(Modifier::new())
192 }),
193 )
194}
195
196#[derive(Clone, Copy, Debug, PartialEq)]
198pub enum TriState {
199 Checked,
200 Unchecked,
201 Indeterminate,
202}
203
204pub fn TriStateCheckbox(
207 state: TriState,
208 on_change: impl Fn(TriState) + 'static,
209 config: CheckboxConfig,
210) -> View {
211 let th = theme();
212 let sz = CheckboxDefaults::BOX_SIZE;
213
214 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
215 let spec = th.motion.color_fast;
216
217 let is_checked = state == TriState::Checked;
218 let is_indeterminate = state == TriState::Indeterminate;
219 let has_fill = is_checked || is_indeterminate;
220 let is_enabled = config.enabled;
221
222 let fill = animate_color(
223 format!("tc_fill_{}", id),
224 if !is_enabled {
225 if has_fill {
226 config.disabled_indeterminate_box_color
227 } else {
228 config.disabled_unchecked_box_color
229 }
230 } else if has_fill {
231 config.checked_color
232 } else {
233 Color::TRANSPARENT
234 },
235 spec,
236 );
237 let bd_w = animate_f32(
238 format!("tc_bw_{}", id),
239 if !is_enabled {
240 if has_fill {
241 0.0
242 } else {
243 CheckboxDefaults::STROKE_WIDTH
244 }
245 } else if has_fill {
246 0.0
247 } else {
248 CheckboxDefaults::STROKE_WIDTH
249 },
250 spec,
251 );
252 let bd = animate_color(
253 format!("tc_bd_{}", id),
254 if !is_enabled {
255 if has_fill {
256 config.disabled_indeterminate_border_color
257 } else {
258 config.disabled_unchecked_border_color
259 }
260 } else if has_fill {
261 Color::TRANSPARENT
262 } else {
263 config.unchecked_border_color
264 },
265 spec,
266 );
267 let symbol_alpha = animate_f32(
268 format!("tc_sa_{}", id),
269 if has_fill { 1.0 } else { 0.0 },
270 spec,
271 );
272 let symbol_col = if !is_enabled {
273 config.disabled_checkmark_color
274 } else {
275 config.checkmark_color
276 };
277
278 let tc_source: Rc<MutableInteractionSource> = config
279 .interaction_source
280 .clone()
281 .map(Rc::new)
282 .unwrap_or_else(|| remember(MutableInteractionSource::new));
283
284 let ripple_col = if has_fill {
286 config.checked_color
287 } else {
288 th.on_surface
289 };
290
291 let mut m = Modifier::new()
292 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
293 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
294 .padding(0.0)
295 .background(Color::TRANSPARENT)
296 .state_colors(config.state_colors)
297 .align_items(AlignItems::CENTER)
298 .justify_content(JustifyContent::CENTER);
299
300 m = apply_m3_clickable_ex(
301 m,
302 &tc_source,
303 ripple_col,
304 is_enabled,
305 move || {
306 on_change(match state {
307 TriState::Checked => TriState::Unchecked,
308 TriState::Indeterminate => TriState::Checked,
309 TriState::Unchecked => TriState::Checked,
310 })
311 },
312 false, Some(20.0),
314 );
315 m = m.semantics(Semantics {
316 role: Role::Checkbox,
317 label: None,
318 focused: false,
319 enabled: is_enabled,
320 selectable_group: false,
321 });
322 Box(m.then(config.modifier)).child(
323 Box(Modifier::new()
324 .size(sz, sz)
325 .background(fill)
326 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
327 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
328 .align_items(AlignItems::CENTER)
329 .justify_content(JustifyContent::CENTER))
330 .child(if symbol_alpha > 0.01 {
331 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
332 Box(Modifier::new()
334 .width(10.0)
335 .height(2.0)
336 .background(symbol_col)
337 .clip_rounded(1.0))
338 } else {
339 Icon(Symbol::new("done", '\u{E876}'))
340 .color(symbol_col)
341 .size(CheckboxDefaults::CHECK_ICON_SIZE)
342 })
343 } else {
344 Box(Modifier::new())
345 }),
346 )
347}
348
349#[derive(Clone, Debug)]
351pub struct RadioButtonConfig {
352 pub modifier: Modifier,
353 pub enabled: bool,
355 pub selected_color: Color,
356 pub unselected_color: Color,
357 pub disabled_selected_color: Color,
358 pub disabled_unselected_color: Color,
359 pub state_colors: StateColors,
360 pub interaction_source: Option<MutableInteractionSource>,
361}
362
363impl Default for RadioButtonConfig {
364 fn default() -> Self {
365 Self {
366 modifier: Modifier::new(),
367 enabled: true,
368 selected_color: RadioButtonDefaults::selected_color(),
369 unselected_color: RadioButtonDefaults::unselected_color(),
370 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
371 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
372 state_colors: RadioButtonDefaults::state_colors_default(),
373 interaction_source: None,
374 }
375 }
376}
377
378static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
382pub fn RadioButton(
383 selected: bool,
384 on_select: impl Fn() + 'static,
385 config: RadioButtonConfig,
386) -> View {
387 let th = theme();
388 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
389
390 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
391 let color_spec = th.motion.color_fast;
392 let spring = th.motion.spring;
393
394 let ring_col = animate_color(
395 format!("rb_ring_{}", id),
396 if !config.enabled {
397 if selected {
398 config.disabled_selected_color
399 } else {
400 config.disabled_unselected_color
401 }
402 } else if selected {
403 config.selected_color
404 } else {
405 config.unselected_color
406 },
407 color_spec,
408 );
409 let dot_size = animate_f32(
410 format!("rb_dot_{}", id),
411 if selected {
412 RadioButtonDefaults::DOT_RADIUS * 2.0
413 } else {
414 0.0
415 },
416 spring,
417 );
418 let dot_col = if !config.enabled {
419 config.disabled_selected_color
420 } else {
421 config.selected_color
422 };
423
424 let cb = move || {
425 if config.enabled {
426 on_select()
427 }
428 };
429
430 let rb_source: Rc<MutableInteractionSource> = config
431 .interaction_source
432 .clone()
433 .map(Rc::new)
434 .unwrap_or_else(|| remember(MutableInteractionSource::new));
435
436 let ripple_col = if selected {
438 config.selected_color
439 } else {
440 th.on_surface
441 };
442
443 let mut m = Modifier::new()
444 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
445 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
446 .padding(0.0)
447 .background(Color::TRANSPARENT)
448 .state_colors(config.state_colors)
449 .align_items(AlignItems::CENTER)
450 .justify_content(JustifyContent::CENTER);
451
452 m = apply_m3_clickable_ex(
453 m,
454 &rb_source,
455 ripple_col,
456 config.enabled,
457 cb,
458 false,
459 Some(20.0),
460 );
461 m = m.semantics(Semantics {
462 role: Role::RadioButton,
463 label: None,
464 focused: false,
465 enabled: config.enabled,
466 selectable_group: false,
467 });
468 Box(m.then(config.modifier)).child(
469 Box(Modifier::new()
470 .size(d, d)
471 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
472 .clip_rounded(d * 0.5)
473 .align_items(AlignItems::CENTER)
474 .justify_content(JustifyContent::CENTER))
475 .child(if dot_size > 0.5 {
476 Box(Modifier::new()
477 .size(dot_size, dot_size)
478 .background(dot_col)
479 .clip_rounded(dot_size * 0.5))
480 } else {
481 Box(Modifier::new())
482 }),
483 )
484}
485
486#[derive(Clone, Debug)]
488pub struct SwitchConfig {
489 pub modifier: Modifier,
490 pub enabled: bool,
492 pub checked_track_color: Color,
493 pub unchecked_track_color: Color,
494 pub checked_thumb_color: Color,
495 pub unchecked_thumb_color: Color,
496 pub checked_icon_color: Color,
498 pub unchecked_icon_color: Color,
500 pub checked_border_color: Color,
502 pub unchecked_border_color: Color,
504 pub disabled_checked_thumb_color: Color,
505 pub disabled_checked_track_color: Color,
506 pub disabled_checked_border_color: Color,
507 pub disabled_checked_icon_color: Color,
508 pub disabled_unchecked_thumb_color: Color,
509 pub disabled_unchecked_track_color: Color,
510 pub disabled_unchecked_border_color: Color,
511 pub disabled_unchecked_icon_color: Color,
512 pub state_colors: StateColors,
513 pub thumb_content: Option<View>,
514 pub interaction_source: Option<MutableInteractionSource>,
515}
516
517impl Default for SwitchConfig {
518 fn default() -> Self {
519 Self {
520 modifier: Modifier::new(),
521 enabled: true,
522 checked_track_color: SwitchDefaults::checked_track_color(),
523 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
524 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
525 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
526 checked_icon_color: SwitchDefaults::checked_icon_color(),
527 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
528 checked_border_color: Color::TRANSPARENT,
529 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
530 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
531 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
532 disabled_checked_border_color: Color::TRANSPARENT,
533 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
534 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
535 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
536 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
537 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
538 state_colors: SwitchDefaults::state_colors_default(),
539 thumb_content: None,
540 interaction_source: None,
541 }
542 }
543}
544
545static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
549pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
550 let th = theme();
551 let track_w = SwitchDefaults::TRACK_WIDTH;
552 let track_h = SwitchDefaults::TRACK_HEIGHT;
553
554 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
555
556 let thumb_target_pos = if checked {
558 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
559 } else {
560 8.0
561 };
562 let thumb_target_d = if checked {
563 SwitchDefaults::THUMB_CHECKED_SIZE
564 } else {
565 SwitchDefaults::THUMB_UNCHECKED_SIZE
566 };
567 let spring = th.motion.spring;
568
569 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
570 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
571 let thumb_top = (track_h - thumb_d) * 0.5;
572
573 let color_spec = th.motion.color_fast;
574 let is_enabled = config.enabled;
575
576 let track_bg = animate_color(
577 format!("sw_tbg_{}", id),
578 if !is_enabled {
579 if checked {
580 config.disabled_checked_track_color
581 } else {
582 config.disabled_unchecked_track_color
583 }
584 } else if checked {
585 config.checked_track_color
586 } else {
587 config.unchecked_track_color
588 },
589 color_spec,
590 );
591 let thumb_bg = animate_color(
592 format!("sw_tmbg_{}", id),
593 if !is_enabled {
594 if checked {
595 config.disabled_checked_thumb_color
596 } else {
597 config.disabled_unchecked_thumb_color
598 }
599 } else if checked {
600 config.checked_thumb_color
601 } else {
602 config.unchecked_thumb_color
603 },
604 color_spec,
605 );
606 let track_border = animate_f32(
607 format!("sw_tb_{}", id),
608 if !is_enabled {
609 if checked { 0.0 } else { 2.0 }
610 } else if checked {
611 0.0
612 } else {
613 2.0
614 },
615 color_spec,
616 );
617 let border_color = animate_color(
618 format!("sw_bc_{}", id),
619 if !is_enabled {
620 if checked {
621 config.disabled_checked_border_color
622 } else {
623 config.disabled_unchecked_border_color
624 }
625 } else if checked {
626 config.checked_border_color
627 } else {
628 config.unchecked_border_color
629 },
630 color_spec,
631 );
632
633 let sw_source: Rc<MutableInteractionSource> = config
634 .interaction_source
635 .clone()
636 .map(Rc::new)
637 .unwrap_or_else(|| remember(MutableInteractionSource::new));
638
639 let mut track = Modifier::new()
640 .size(track_w, track_h)
641 .padding(0.0)
642 .clip_rounded(track_h * 0.5)
643 .background(track_bg)
644 .border(track_border, border_color, track_h * 0.5)
645 .interaction_source(&sw_source);
646
647 track = apply_enabled_click(track, is_enabled, {
648 let cb = on_change;
649 move || cb(!checked)
650 });
651
652 Box(track.then(config.modifier)).child((
653 Box(Modifier::new()
654 .size(thumb_d, thumb_d)
655 .background(thumb_bg)
656 .clip_rounded(thumb_d * 0.5)
657 .hit_passthrough()
658 .align_items(AlignItems::CENTER)
659 .justify_content(JustifyContent::CENTER)
660 .absolute()
661 .offset(Some(thumb_left), Some(thumb_top), None, None))
662 .child(
663 config
664 .thumb_content
665 .map(|v| {
666 let icon_col = if !is_enabled {
667 if checked {
668 config.disabled_checked_icon_color
669 } else {
670 config.disabled_unchecked_icon_color
671 }
672 } else if checked {
673 config.checked_icon_color
674 } else {
675 config.unchecked_icon_color
676 };
677 with_content_color(icon_col, move || v)
678 })
679 .unwrap_or(Box(Modifier::new())),
680 ),
681 Box(Modifier::new()
682 .size(40.0, 40.0)
683 .hit_passthrough()
684 .interaction_source(&sw_source)
685 .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
686 color: Some(th.on_surface),
687 bounded: false,
688 radius: Some(20.0),
689 ..Default::default()
690 }))
691 .absolute()
692 .offset(
693 Some(thumb_left + thumb_d * 0.5 - 20.0),
694 Some(track_h * 0.5 - 20.0),
695 None,
696 None,
697 )),
698 ))
699}