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