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 .clickable()
149 .align_items(AlignItems::CENTER)
150 .justify_content(JustifyContent::CENTER)
151 .on_click(cb)
152 .then(config.modifier))
153 .child(
154 Box(Modifier::new()
155 .size(sz, sz)
156 .background(fill)
157 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
158 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
159 .align_items(AlignItems::CENTER)
160 .justify_content(JustifyContent::CENTER))
161 .child(if check_alpha > 0.01 {
162 Box(Modifier::new().alpha(check_alpha)).child(
163 Icon(Symbol::new("done", '\u{E876}'))
164 .color(check_col)
165 .size(CheckboxDefaults::CHECK_ICON_SIZE),
166 )
167 } else {
168 Box(Modifier::new())
169 }),
170 )
171}
172
173#[derive(Clone, Copy, Debug, PartialEq)]
175pub enum TriState {
176 Checked,
177 Unchecked,
178 Indeterminate,
179}
180
181pub fn TriStateCheckbox(
184 state: TriState,
185 on_change: impl Fn(TriState) + 'static,
186 config: CheckboxConfig,
187) -> View {
188 let th = theme();
189 let sz = CheckboxDefaults::BOX_SIZE;
190
191 let id = remember(|| CHECKBOX_COUNTER.fetch_add(1, Ordering::Relaxed));
192 let spec = th.motion.color_fast;
193
194 let is_checked = state == TriState::Checked;
195 let is_indeterminate = state == TriState::Indeterminate;
196 let has_fill = is_checked || is_indeterminate;
197 let is_enabled = config.enabled;
198
199 let fill = animate_color(
200 format!("tc_fill_{}", id),
201 if !is_enabled {
202 if has_fill {
203 config.disabled_indeterminate_box_color
204 } else {
205 config.disabled_unchecked_box_color
206 }
207 } else if has_fill {
208 config.checked_color
209 } else {
210 Color::TRANSPARENT
211 },
212 spec,
213 );
214 let bd_w = animate_f32(
215 format!("tc_bw_{}", id),
216 if !is_enabled {
217 if has_fill {
218 0.0
219 } else {
220 CheckboxDefaults::STROKE_WIDTH
221 }
222 } else if has_fill {
223 0.0
224 } else {
225 CheckboxDefaults::STROKE_WIDTH
226 },
227 spec,
228 );
229 let bd = animate_color(
230 format!("tc_bd_{}", id),
231 if !is_enabled {
232 if has_fill {
233 config.disabled_indeterminate_border_color
234 } else {
235 config.disabled_unchecked_border_color
236 }
237 } else if has_fill {
238 Color::TRANSPARENT
239 } else {
240 config.unchecked_border_color
241 },
242 spec,
243 );
244 let symbol_alpha = animate_f32(
245 format!("tc_sa_{}", id),
246 if has_fill { 1.0 } else { 0.0 },
247 spec,
248 );
249 let symbol_col = if !is_enabled {
250 config.disabled_checkmark_color
251 } else {
252 config.checkmark_color
253 };
254
255 Box(Modifier::new()
256 .width(CheckboxDefaults::TOUCH_TARGET_SIZE)
257 .height(CheckboxDefaults::TOUCH_TARGET_SIZE)
258 .padding(0.0)
259 .clip_rounded(20.0)
260 .background(Color::TRANSPARENT)
261 .clickable()
262 .align_items(AlignItems::CENTER)
263 .justify_content(JustifyContent::CENTER)
264 .on_click(move || {
265 if is_enabled {
266 on_change(match state {
267 TriState::Checked => TriState::Unchecked,
268 TriState::Indeterminate => TriState::Checked,
269 TriState::Unchecked => TriState::Checked,
270 })
271 }
272 })
273 .then(config.modifier))
274 .child(
275 Box(Modifier::new()
276 .size(sz, sz)
277 .background(fill)
278 .border(bd_w, bd, CheckboxDefaults::CORNER_RADIUS)
279 .clip_rounded(CheckboxDefaults::CORNER_RADIUS)
280 .align_items(AlignItems::CENTER)
281 .justify_content(JustifyContent::CENTER))
282 .child(if symbol_alpha > 0.01 {
283 Box(Modifier::new().alpha(symbol_alpha)).child(if is_indeterminate {
284 Box(Modifier::new()
286 .width(10.0)
287 .height(2.0)
288 .background(symbol_col)
289 .clip_rounded(1.0))
290 } else {
291 Icon(Symbol::new("done", '\u{E876}'))
292 .color(symbol_col)
293 .size(CheckboxDefaults::CHECK_ICON_SIZE)
294 })
295 } else {
296 Box(Modifier::new())
297 }),
298 )
299}
300
301#[derive(Clone, Debug)]
303pub struct RadioButtonConfig {
304 pub modifier: Modifier,
305 pub enabled: bool,
307 pub selected_color: Color,
308 pub unselected_color: Color,
309 pub disabled_selected_color: Color,
310 pub disabled_unselected_color: Color,
311 pub state_colors: StateColors,
312 pub interaction_source: Option<MutableInteractionSource>,
313}
314
315impl Default for RadioButtonConfig {
316 fn default() -> Self {
317 Self {
318 modifier: Modifier::new(),
319 enabled: true,
320 selected_color: RadioButtonDefaults::selected_color(),
321 unselected_color: RadioButtonDefaults::unselected_color(),
322 disabled_selected_color: RadioButtonDefaults::disabled_selected_color(),
323 disabled_unselected_color: RadioButtonDefaults::disabled_unselected_color(),
324 state_colors: RadioButtonDefaults::state_colors_default(),
325 interaction_source: None,
326 }
327 }
328}
329
330static RADIO_COUNTER: AtomicU64 = AtomicU64::new(0);
334pub fn RadioButton(
335 selected: bool,
336 on_select: impl Fn() + 'static,
337 config: RadioButtonConfig,
338) -> View {
339 let th = theme();
340 let d = RadioButtonDefaults::OUTER_RADIUS * 2.0;
341
342 let id = remember(|| RADIO_COUNTER.fetch_add(1, Ordering::Relaxed));
343 let color_spec = th.motion.color_fast;
344 let spring = th.motion.spring;
345
346 let ring_col = animate_color(
347 format!("rb_ring_{}", id),
348 if !config.enabled {
349 if selected {
350 config.disabled_selected_color
351 } else {
352 config.disabled_unselected_color
353 }
354 } else if selected {
355 config.selected_color
356 } else {
357 config.unselected_color
358 },
359 color_spec,
360 );
361 let dot_size = animate_f32(
362 format!("rb_dot_{}", id),
363 if selected {
364 RadioButtonDefaults::DOT_RADIUS * 2.0
365 } else {
366 0.0
367 },
368 spring,
369 );
370 let dot_col = if !config.enabled {
371 config.disabled_selected_color
372 } else {
373 config.selected_color
374 };
375
376 let cb = move || {
377 if config.enabled {
378 on_select()
379 }
380 };
381
382 let rb_source: Rc<MutableInteractionSource> = config
383 .interaction_source
384 .clone()
385 .map(Rc::new)
386 .unwrap_or_else(|| remember(MutableInteractionSource::new));
387 Box(Modifier::new()
388 .width(RadioButtonDefaults::TOUCH_TARGET_SIZE)
389 .height(RadioButtonDefaults::TOUCH_TARGET_SIZE)
390 .padding(0.0)
391 .clip_rounded(20.0)
392 .background(Color::TRANSPARENT)
393 .state_colors(config.state_colors)
394 .interaction_source(&rb_source)
395 .clickable()
396 .align_items(AlignItems::CENTER)
397 .justify_content(JustifyContent::CENTER)
398 .on_click(cb)
399 .then(config.modifier))
400 .child(
401 Box(Modifier::new()
402 .size(d, d)
403 .border(RadioButtonDefaults::STROKE_WIDTH, ring_col, d * 0.5)
404 .clip_rounded(d * 0.5)
405 .align_items(AlignItems::CENTER)
406 .justify_content(JustifyContent::CENTER))
407 .child(if dot_size > 0.5 {
408 Box(Modifier::new()
409 .size(dot_size, dot_size)
410 .background(dot_col)
411 .clip_rounded(dot_size * 0.5))
412 } else {
413 Box(Modifier::new())
414 }),
415 )
416}
417
418#[derive(Clone, Debug)]
420pub struct SwitchConfig {
421 pub modifier: Modifier,
422 pub enabled: bool,
424 pub checked_track_color: Color,
425 pub unchecked_track_color: Color,
426 pub checked_thumb_color: Color,
427 pub unchecked_thumb_color: Color,
428 pub checked_icon_color: Color,
430 pub unchecked_icon_color: Color,
432 pub checked_border_color: Color,
434 pub unchecked_border_color: Color,
436 pub disabled_checked_thumb_color: Color,
437 pub disabled_checked_track_color: Color,
438 pub disabled_checked_border_color: Color,
439 pub disabled_checked_icon_color: Color,
440 pub disabled_unchecked_thumb_color: Color,
441 pub disabled_unchecked_track_color: Color,
442 pub disabled_unchecked_border_color: Color,
443 pub disabled_unchecked_icon_color: Color,
444 pub state_colors: StateColors,
445 pub thumb_content: Option<View>,
446 pub interaction_source: Option<MutableInteractionSource>,
447}
448
449impl Default for SwitchConfig {
450 fn default() -> Self {
451 Self {
452 modifier: Modifier::new(),
453 enabled: true,
454 checked_track_color: SwitchDefaults::checked_track_color(),
455 unchecked_track_color: SwitchDefaults::unchecked_track_color(),
456 checked_thumb_color: SwitchDefaults::checked_thumb_color(),
457 unchecked_thumb_color: SwitchDefaults::unchecked_thumb_color(),
458 checked_icon_color: SwitchDefaults::checked_icon_color(),
459 unchecked_icon_color: SwitchDefaults::unchecked_icon_color(),
460 checked_border_color: Color::TRANSPARENT,
461 unchecked_border_color: SwitchDefaults::unchecked_border_color(),
462 disabled_checked_thumb_color: SwitchDefaults::disabled_checked_thumb_color(),
463 disabled_checked_track_color: SwitchDefaults::disabled_checked_track_color(),
464 disabled_checked_border_color: Color::TRANSPARENT,
465 disabled_checked_icon_color: SwitchDefaults::disabled_checked_icon_color(),
466 disabled_unchecked_thumb_color: SwitchDefaults::disabled_unchecked_thumb_color(),
467 disabled_unchecked_track_color: SwitchDefaults::disabled_unchecked_track_color(),
468 disabled_unchecked_border_color: SwitchDefaults::disabled_unchecked_border_color(),
469 disabled_unchecked_icon_color: SwitchDefaults::disabled_unchecked_icon_color(),
470 state_colors: SwitchDefaults::state_colors_default(),
471 thumb_content: None,
472 interaction_source: None,
473 }
474 }
475}
476
477static SWITCH_COUNTER: AtomicU64 = AtomicU64::new(0);
481pub fn Switch(checked: bool, on_change: impl Fn(bool) + 'static, config: SwitchConfig) -> View {
482 let th = theme();
483 let track_w = SwitchDefaults::TRACK_WIDTH;
484 let track_h = SwitchDefaults::TRACK_HEIGHT;
485
486 let id = remember(|| SWITCH_COUNTER.fetch_add(1, Ordering::Relaxed));
487
488 let hovered = remember(|| Signal::new(false));
489 let pressed = remember(|| Signal::new(false));
490
491 let thumb_target_pos = if checked {
493 track_w - SwitchDefaults::THUMB_CHECKED_SIZE - 4.0
494 } else {
495 8.0
496 };
497 let thumb_target_d = if checked {
498 SwitchDefaults::THUMB_CHECKED_SIZE
499 } else {
500 SwitchDefaults::THUMB_UNCHECKED_SIZE
501 };
502 let spring = th.motion.spring;
503
504 let thumb_left = animate_f32(format!("sw_pos_{}", id), thumb_target_pos, spring);
505 let thumb_d = animate_f32(format!("sw_d_{}", id), thumb_target_d, spring);
506 let thumb_top = (track_h - thumb_d) * 0.5;
507
508 let color_spec = th.motion.color_fast;
509 let is_enabled = config.enabled;
510
511 let track_bg = animate_color(
512 format!("sw_tbg_{}", id),
513 if !is_enabled {
514 if checked {
515 config.disabled_checked_track_color
516 } else {
517 config.disabled_unchecked_track_color
518 }
519 } else if checked {
520 config.checked_track_color
521 } else {
522 config.unchecked_track_color
523 },
524 color_spec,
525 );
526 let thumb_bg = animate_color(
527 format!("sw_tmbg_{}", id),
528 if !is_enabled {
529 if checked {
530 config.disabled_checked_thumb_color
531 } else {
532 config.disabled_unchecked_thumb_color
533 }
534 } else if checked {
535 config.checked_thumb_color
536 } else {
537 config.unchecked_thumb_color
538 },
539 color_spec,
540 );
541 let track_border = animate_f32(
542 format!("sw_tb_{}", id),
543 if !is_enabled {
544 if checked { 0.0 } else { 2.0 }
545 } else if checked {
546 0.0
547 } else {
548 2.0
549 },
550 color_spec,
551 );
552 let border_color = animate_color(
553 format!("sw_bc_{}", id),
554 if !is_enabled {
555 if checked {
556 config.disabled_checked_border_color
557 } else {
558 config.disabled_unchecked_border_color
559 }
560 } else if checked {
561 config.checked_border_color
562 } else {
563 config.unchecked_border_color
564 },
565 color_spec,
566 );
567
568 let state_overlay = animate_color(
569 format!("sw_ol_{}", id),
570 if !is_enabled {
571 Color::TRANSPARENT
572 } else if pressed.get() {
573 config.state_colors.pressed
574 } else if hovered.get() {
575 config.state_colors.hovered
576 } else {
577 config.state_colors.default
578 },
579 color_spec,
580 );
581
582 let sw_source: Rc<MutableInteractionSource> = config
583 .interaction_source
584 .clone()
585 .map(Rc::new)
586 .unwrap_or_else(|| remember(MutableInteractionSource::new));
587 Box(Modifier::new()
588 .size(track_w, track_h)
589 .padding(0.0)
590 .clip_rounded(track_h * 0.5)
591 .background(track_bg)
592 .border(track_border, border_color, track_h * 0.5)
593 .interaction_source(&sw_source)
594 .clickable()
595 .on_pointer_enter({
596 let h = hovered.clone();
597 move |_| h.set(true)
598 })
599 .on_pointer_leave({
600 let h = hovered.clone();
601 let p = pressed.clone();
602 move |_| {
603 h.set(false);
604 p.set(false);
605 }
606 })
607 .on_pointer_down({
608 let p = pressed.clone();
609 move |_| p.set(true)
610 })
611 .on_click({
612 let cb = on_change;
613 move || cb(!checked)
614 })
615 .on_pointer_up({
616 let p = pressed.clone();
617 move |_| p.set(false)
618 })
619 .then(config.modifier))
620 .child((
621 Box(Modifier::new()
622 .size(thumb_d, thumb_d)
623 .background(thumb_bg)
624 .clip_rounded(thumb_d * 0.5)
625 .hit_passthrough()
626 .absolute()
627 .offset(Some(thumb_left), Some(thumb_top), None, None)),
628 Box(Modifier::new()
629 .size(40.0, 40.0)
630 .clip_rounded(20.0)
631 .background(state_overlay)
632 .hit_passthrough()
633 .absolute()
634 .offset(
635 Some(thumb_left + thumb_d * 0.5 - 20.0),
636 Some(track_h * 0.5 - 20.0),
637 None,
638 None,
639 )),
640 ))
641}