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
160m = 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 enabled: is_enabled,
172 checked: Some(checked),
173 ..Default::default()
174 });
175 Box(m.then(config.modifier)).child(
176 Box(Modifier::new()
177 .size(sz, sz)
178 .background(fill)
179 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
180 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
181 .align_items(AlignItems::CENTER)
182 .justify_content(JustifyContent::CENTER))
183 .child(if check_alpha > 0.01 {
184 Box(Modifier::new().alpha(check_alpha)).child(
185 Icon(Symbol::new("done", '\u{E876}'))
186 .color(check_col)
187 .size(CheckboxDefaults::CHECK_ICON_SIZE),
188 )
189 } else {
190 Box(Modifier::new())
191 }),
192 )
193}
194
195#[derive(Clone, Copy, Debug, PartialEq)]
197pub enum TriState {
198 Checked,
199 Unchecked,
200 Indeterminate,
201}
202
203pub fn TriStateCheckbox(
206 state: TriState,
207 on_change: impl Fn(TriState) + 'static,
208 config: CheckboxConfig,
209) -> View {
210 let th = theme();
211 let sz = CheckboxDefaults::BOX_SIZE;
212
213 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
214 let spec = th.motion.color_fast;
215
216 let is_checked = state == TriState::Checked;
217 let is_indeterminate = state == TriState::Indeterminate;
218 let has_fill = is_checked || is_indeterminate;
219 let is_enabled = config.enabled;
220
221 let fill = animate_color(
222 format!("tc_fill_{}", id),
223 if !is_enabled {
224 if has_fill {
225 config.disabled_indeterminate_box_color
226 } else {
227 config.disabled_unchecked_box_color
228 }
229 } else if has_fill {
230 config.checked_color
231 } else {
232 Color::TRANSPARENT
233 },
234 spec,
235 );
236 let bd_w = animate_f32(
237 format!("tc_bw_{}", id),
238 if !is_enabled {
239 if has_fill {
240 0.0
241 } else {
242 CheckboxDefaults::STROKE_WIDTH
243 }
244 } else if has_fill {
245 0.0
246 } else {
247 CheckboxDefaults::STROKE_WIDTH
248 },
249 spec,
250 );
251 let bd = animate_color(
252 format!("tc_bd_{}", id),
253 if !is_enabled {
254 if has_fill {
255 config.disabled_indeterminate_border_color
256 } else {
257 config.disabled_unchecked_border_color
258 }
259 } else if has_fill {
260 Color::TRANSPARENT
261 } else {
262 config.unchecked_border_color
263 },
264 spec,
265 );
266 let symbol_alpha = animate_f32(
267 format!("tc_sa_{}", id),
268 if has_fill { 1.0 } else { 0.0 },
269 spec,
270 );
271 let symbol_col = if !is_enabled {
272 config.disabled_checkmark_color
273 } else {
274 config.checkmark_color
275 };
276
277 let tc_source: Rc<MutableInteractionSource> = config
278 .interaction_source
279 .clone()
280 .map(Rc::new)
281 .unwrap_or_else(|| remember(MutableInteractionSource::new));
282
283 let ripple_col = if has_fill {
285 config.checked_color
286 } else {
287 th.on_surface
288 };
289
290 let mut m = Modifier::new()
291 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
292 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
293 .padding(0.0)
294 .background(Color::TRANSPARENT)
295 .state_colors(config.state_colors)
296 .align_items(AlignItems::CENTER)
297 .justify_content(JustifyContent::CENTER);
298
299 m = apply_m3_clickable_ex(
300 m,
301 &tc_source,
302 ripple_col,
303 is_enabled,
304 move || {
305 on_change(match state {
306 TriState::Checked => TriState::Unchecked,
307 TriState::Indeterminate => TriState::Checked,
308 TriState::Unchecked => TriState::Checked,
309 })
310 },
311 false, Some(20.0),
313 );
314 m = m.semantics(Semantics {
315 role: Role::Checkbox,
316 enabled: is_enabled,
317 checked: if is_indeterminate {
318 None
319 } else {
320 Some(is_checked)
321 },
322 ..Default::default()
323 });
324 Box(m.then(config.modifier)).child(
325 Box(Modifier::new()
326 .size(sz, sz)
327 .background(fill)
328 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
329 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
330 .align_items(AlignItems::CENTER)
331 .justify_content(JustifyContent::CENTER))
332 .child(if symbol_alpha > 0.01 {
333 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
334 Box(Modifier::new()
336 .width(10.0)
337 .height(2.0)
338 .background(symbol_col)
339 .clip_rounded(1.0))
340 } else {
341 Icon(Symbol::new("done", '\u{E876}'))
342 .color(symbol_col)
343 .size(CheckboxDefaults::CHECK_ICON_SIZE)
344 })
345 } else {
346 Box(Modifier::new())
347 }),
348 )
349}
350
351#[derive(Clone, Debug)]
353pub struct RadioButtonConfig {
354 pub modifier: Modifier,
355 pub enabled: bool,
357 pub selected_color: Color,
358 pub unselected_color: Color,
359 pub disabled_selected_color: Color,
360 pub disabled_unselected_color: Color,
361 pub state_colors: StateColors,
362 pub interaction_source: Option<MutableInteractionSource>,
363}
364
365impl Default for RadioButtonConfig {
366 fn default() -> Self {
367 Self {
368 modifier: Modifier::new(),
369 enabled: true,
370 selected_color: RadioButtonDefaults::selected_color(),
371 unselected_color: RadioButtonDefaults::unselected_color(),
372 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
373 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
374 state_colors: RadioButtonDefaults::state_colors_default(),
375 interaction_source: None,
376 }
377 }
378}
379
380static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
384pub fn RadioButton(
385 selected: bool,
386 on_select: impl Fn() + 'static,
387 config: RadioButtonConfig,
388) -> View {
389 let th = theme();
390 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
391
392 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
393 let color_spec = th.motion.color_fast;
394 let spring = th.motion.spring;
395
396 let ring_col = animate_color(
397 format!("rb_ring_{}", id),
398 if !config.enabled {
399 if selected {
400 config.disabled_selected_color
401 } else {
402 config.disabled_unselected_color
403 }
404 } else if selected {
405 config.selected_color
406 } else {
407 config.unselected_color
408 },
409 color_spec,
410 );
411 let dot_size = animate_f32(
412 format!("rb_dot_{}", id),
413 if selected {
414 RadioButtonDefaults::DOT_RADIUS * 2.0
415 } else {
416 0.0
417 },
418 spring,
419 );
420 let dot_col = if !config.enabled {
421 config.disabled_selected_color
422 } else {
423 config.selected_color
424 };
425
426 let cb = move || {
427 if config.enabled {
428 on_select()
429 }
430 };
431
432 let rb_source: Rc<MutableInteractionSource> = config
433 .interaction_source
434 .clone()
435 .map(Rc::new)
436 .unwrap_or_else(|| remember(MutableInteractionSource::new));
437
438 let ripple_col = if selected {
440 config.selected_color
441 } else {
442 th.on_surface
443 };
444
445 let mut m = Modifier::new()
446 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
447 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
448 .padding(0.0)
449 .background(Color::TRANSPARENT)
450 .state_colors(config.state_colors)
451 .align_items(AlignItems::CENTER)
452 .justify_content(JustifyContent::CENTER);
453
454 m = apply_m3_clickable_ex(
455 m,
456 &rb_source,
457 ripple_col,
458 config.enabled,
459 cb,
460 false,
461 Some(20.0),
462 );
463 m = m.semantics(Semantics {
464 role: Role::RadioButton,
465 enabled: config.enabled,
466 selected: Some(selected),
467 ..Default::default()
468 });
469 Box(m.then(config.modifier)).child(
470 Box(Modifier::new()
471 .size(d, d)
472 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
473 .clip_rounded(d * 0.5)
474 .align_items(AlignItems::CENTER)
475 .justify_content(JustifyContent::CENTER))
476 .child(if dot_size > 0.5 {
477 Box(Modifier::new()
478 .size(dot_size, dot_size)
479 .background(dot_col)
480 .clip_rounded(dot_size * 0.5))
481 } else {
482 Box(Modifier::new())
483 }),
484 )
485}
486
487#[derive(Clone, Debug)]
489pub struct SwitchConfig {
490 pub modifier: Modifier,
491 pub enabled: bool,
493 pub checked_track_color: Color,
494 pub unchecked_track_color: Color,
495 pub checked_thumb_color: Color,
496 pub unchecked_thumb_color: Color,
497 pub checked_icon_color: Color,
499 pub unchecked_icon_color: Color,
501 pub checked_border_color: Color,
503 pub unchecked_border_color: Color,
505 pub disabled_checked_thumb_color: Color,
506 pub disabled_checked_track_color: Color,
507 pub disabled_checked_border_color: Color,
508 pub disabled_checked_icon_color: Color,
509 pub disabled_unchecked_thumb_color: Color,
510 pub disabled_unchecked_track_color: Color,
511 pub disabled_unchecked_border_color: Color,
512 pub disabled_unchecked_icon_color: Color,
513 pub state_colors: StateColors,
514 pub thumb_content: Option<View>,
515 pub interaction_source: Option<MutableInteractionSource>,
516}
517
518impl Default for SwitchConfig {
519 fn default() -> Self {
520 Self {
521 modifier: Modifier::new(),
522 enabled: true,
523 checked_track_color: SwitchDefaults::checked_track_color(),
524 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
525 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
526 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
527 checked_icon_color: SwitchDefaults::checked_icon_color(),
528 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
529 checked_border_color: Color::TRANSPARENT,
530 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
531 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
532 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
533 disabled_checked_border_color: Color::TRANSPARENT,
534 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
535 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
536 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
537 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
538 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
539 state_colors: SwitchDefaults::state_colors_default(),
540 thumb_content: None,
541 interaction_source: None,
542 }
543 }
544}
545
546static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
550pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
551 let th = theme();
552 let track_w = SwitchDefaults::TRACK_WIDTH;
553 let track_h = SwitchDefaults::TRACK_HEIGHT;
554
555 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
556
557 let thumb_target_pos = if checked {
559 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
560 } else {
561 8.0
562 };
563 let thumb_target_d = if checked {
564 SwitchDefaults::THUMB_CHECKED_SIZE
565 } else {
566 SwitchDefaults::THUMB_UNCHECKED_SIZE
567 };
568 let spring = th.motion.spring;
569
570 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
571 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
572 let thumb_top = (track_h - thumb_d) * 0.5;
573
574 let color_spec = th.motion.color_fast;
575 let is_enabled = config.enabled;
576
577 let track_bg = animate_color(
578 format!("sw_tbg_{}", id),
579 if !is_enabled {
580 if checked {
581 config.disabled_checked_track_color
582 } else {
583 config.disabled_unchecked_track_color
584 }
585 } else if checked {
586 config.checked_track_color
587 } else {
588 config.unchecked_track_color
589 },
590 color_spec,
591 );
592 let thumb_bg = animate_color(
593 format!("sw_tmbg_{}", id),
594 if !is_enabled {
595 if checked {
596 config.disabled_checked_thumb_color
597 } else {
598 config.disabled_unchecked_thumb_color
599 }
600 } else if checked {
601 config.checked_thumb_color
602 } else {
603 config.unchecked_thumb_color
604 },
605 color_spec,
606 );
607 let track_border = animate_f32(
608 format!("sw_tb_{}", id),
609 if !is_enabled {
610 if checked { 0.0 } else { 2.0 }
611 } else if checked {
612 0.0
613 } else {
614 2.0
615 },
616 color_spec,
617 );
618 let border_color = animate_color(
619 format!("sw_bc_{}", id),
620 if !is_enabled {
621 if checked {
622 config.disabled_checked_border_color
623 } else {
624 config.disabled_unchecked_border_color
625 }
626 } else if checked {
627 config.checked_border_color
628 } else {
629 config.unchecked_border_color
630 },
631 color_spec,
632 );
633
634 let sw_source: Rc<MutableInteractionSource> = config
635 .interaction_source
636 .clone()
637 .map(Rc::new)
638 .unwrap_or_else(|| remember(MutableInteractionSource::new));
639
640 let mut track = Modifier::new()
641 .size(track_w, track_h)
642 .padding(0.0)
643 .clip_rounded(track_h * 0.5)
644 .background(track_bg)
645 .border(track_border, border_color, track_h * 0.5)
646 .interaction_source(&sw_source);
647
648 track = apply_enabled_click(track, is_enabled, {
649 let cb = on_change;
650 move || cb(!checked)
651 });
652 track = track.semantics(Semantics {
653 role: Role::Switch,
654 enabled: is_enabled,
655 checked: Some(checked),
656 ..Default::default()
657 });
658
659 Box(track.then(config.modifier)).child((
660 Box(Modifier::new()
661 .size(thumb_d, thumb_d)
662 .background(thumb_bg)
663 .clip_rounded(thumb_d * 0.5)
664 .hit_passthrough()
665 .align_items(AlignItems::CENTER)
666 .justify_content(JustifyContent::CENTER)
667 .absolute()
668 .offset(Some(thumb_left), Some(thumb_top), None, None))
669 .child(
670 config
671 .thumb_content
672 .map(|v| {
673 let icon_col = if !is_enabled {
674 if checked {
675 config.disabled_checked_icon_color
676 } else {
677 config.disabled_unchecked_icon_color
678 }
679 } else if checked {
680 config.checked_icon_color
681 } else {
682 config.unchecked_icon_color
683 };
684 with_content_color(icon_col, move || v)
685 })
686 .unwrap_or(Box(Modifier::new())),
687 ),
688 Box(Modifier::new()
689 .size(40.0, 40.0)
690 .hit_passthrough()
691 .interaction_source(&sw_source)
692 .indication(crate::ripple::ripple(crate::ripple::RippleConfig {
693 color: Some(th.on_surface),
694 bounded: false,
695 radius: Some(20.0),
696 ..Default::default()
697 }))
698 .absolute()
699 .offset(
700 Some(thumb_left + thumb_d * 0.5 - 20.0),
701 Some(track_h * 0.5 - 20.0),
702 None,
703 None,
704 )),
705 ))
706}