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