1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use crate::ripple::{RippleConfig, ripple};
6use repose_core::*;
7use repose_ui::{Box, ViewExt};
8
9use super::*;
10
11#[derive(Clone, Copy, Debug)]
13pub struct ButtonColors {
14 pub container_color: Color,
15 pub content_color: Color,
16 pub disabled_container_color: Color,
17 pub disabled_content_color: Color,
18}
19
20impl ButtonColors {
21 pub fn container(&self, enabled: bool) -> Color {
22 if enabled {
23 self.container_color
24 } else {
25 self.disabled_container_color
26 }
27 }
28 pub fn content(&self, enabled: bool) -> Color {
29 if enabled {
30 self.content_color
31 } else {
32 self.disabled_content_color
33 }
34 }
35}
36
37#[derive(Clone, Copy, Debug)]
39pub struct ButtonElevation {
40 pub default: f32,
41 pub pressed: f32,
42 pub focused: f32,
43 pub hovered: f32,
44 pub disabled: f32,
45}
46
47#[derive(Clone, Debug)]
49pub struct ButtonConfig {
50 pub modifier: Modifier,
51 pub enabled: bool,
52 pub content_color: Option<Color>,
53 pub container_color: Option<Color>,
54 pub state_colors: StateColors,
55 pub state_elevation: Option<StateElevation>,
56 pub border: Option<(f32, Color, f32)>,
57 pub shape_radius: f32,
58 pub content_padding: Option<PaddingValues>,
59 pub height: f32,
60 pub colors: Option<ButtonColors>,
61 pub elevation: Option<ButtonElevation>,
62 pub interaction_source: Option<MutableInteractionSource>,
63}
64
65impl Default for ButtonConfig {
66 fn default() -> Self {
67 Self {
68 modifier: Modifier::new(),
69 enabled: true,
70 content_color: None,
71 container_color: None,
72 state_colors: ButtonDefaults::state_colors_default(),
73 state_elevation: None,
74 border: None,
75 shape_radius: ButtonDefaults::SHAPE_RADIUS,
76 content_padding: None,
77 height: ButtonDefaults::HEIGHT,
78 colors: None,
79 elevation: None,
80 interaction_source: None,
81 }
82 }
83}
84
85fn resolve_button_colors(
88 config: &ButtonConfig,
89 def: ButtonColors,
90) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
91 if let Some(colors) = &config.colors {
92 let bg = if config.enabled {
93 colors.container_color
94 } else {
95 colors.disabled_container_color
96 };
97 let cc = if config.enabled {
98 colors.content_color
99 } else {
100 colors.disabled_content_color
101 };
102 let sc = StateColors {
103 default: Color::TRANSPARENT,
104 hovered: Color::TRANSPARENT,
105 focused: Color::TRANSPARENT,
106 pressed: Color::TRANSPARENT,
107 dragged: colors.content_color.with_alpha_f32(0.12),
108 disabled: Color::TRANSPARENT,
109 };
110 let se = config.elevation.map(|e| StateElevation {
111 default: e.default,
112 hovered: e.hovered,
113 focused: e.focused,
114 pressed: e.pressed,
115 dragged: e.pressed,
116 disabled: e.disabled,
117 });
118 (cc, Some(bg), sc, se)
119 } else {
120 let cc = config.content_color.unwrap_or(def.content_color);
121 let bg = Some(config.container_color.unwrap_or(def.container_color));
122 let sc = if config.enabled {
123 config.state_colors
124 } else {
125 StateColors {
126 default: Color::TRANSPARENT,
127 hovered: Color::TRANSPARENT,
128 focused: Color::TRANSPARENT,
129 pressed: Color::TRANSPARENT,
130 dragged: Color::TRANSPARENT,
131 disabled: config.state_colors.disabled,
132 }
133 };
134 let se = config.state_elevation;
135 (cc, bg, sc, se)
136 }
137}
138
139fn button_impl(
140 outer_modifier: Modifier,
141 on_click: impl Fn() + 'static,
142 content: impl FnOnce() -> View,
143 content_color: Color,
144 container_color: Option<Color>,
145 state_colors: StateColors,
146 state_elevation: Option<StateElevation>,
147 border: Option<(f32, Color, f32)>,
148 padding_left: f32,
149 padding_right: f32,
150 height: f32,
151 shape_radius: f32,
152 enabled: bool,
153 interaction_source: Option<MutableInteractionSource>,
154) -> View {
155 let mut m = Modifier::new()
156 .min_height(height)
157 .min_width(48.0)
158 .flex_shrink(0.0);
159 if let Some(bg) = container_color {
160 m = m.background(bg);
161 }
162 m = m.state_colors(if enabled {
163 state_colors
164 } else {
165 StateColors {
166 default: Color::TRANSPARENT,
167 hovered: Color::TRANSPARENT,
168 focused: Color::TRANSPARENT,
169 pressed: Color::TRANSPARENT,
170 dragged: Color::TRANSPARENT,
171 disabled: state_colors.disabled,
172 }
173 });
174 if let Some(se) = state_elevation {
175 m = m.state_elevation(se);
176 }
177 if let Some((w, c, r)) = border {
178 m = m.border(w, c, r);
179 }
180 m = m
181 .clip_rounded(shape_radius)
182 .padding_values(PaddingValues {
183 left: padding_left,
184 right: padding_right,
185 top: 8.0,
186 bottom: 8.0,
187 })
188 .align_items(AlignItems::CENTER)
189 .justify_content(JustifyContent::CENTER);
190
191 let source: Rc<MutableInteractionSource> =
193 interaction_source
194 .map(Rc::new)
195 .unwrap_or_else(|| match outer_modifier.key {
196 Some(k) => {
197 remember_with_key(format!("m3_btn_src:{k}"), MutableInteractionSource::new)
198 }
199 None => remember(MutableInteractionSource::new),
200 });
201 m = m.interaction_source(&source);
202 m = m.indication(ripple(RippleConfig {
203 color: Some(content_color),
204 bounded: true,
205 ..Default::default()
206 }));
207
208 if enabled {
209 m = m.clickable().on_click(on_click);
210 }
211 m = m.then(outer_modifier);
212 let effective = if enabled {
213 content_color
214 } else {
215 content_color.with_alpha_f32(0.38)
216 };
217 let content = with_content_color(effective, content);
218 Box(m).child(content)
219}
220
221pub fn Button(
224 modifier: Modifier,
225 on_click: impl Fn() + 'static,
226 config: ButtonConfig,
227 content: impl FnOnce() -> View,
228) -> View {
229 let def = ButtonColors {
230 container_color: ButtonDefaults::container_color(),
231 content_color: ButtonDefaults::content_color(),
232 disabled_container_color: ButtonDefaults::container_color()
233 .with_alpha_f32(0.12)
234 .composite_over(theme().surface_container_low),
235 disabled_content_color: ButtonDefaults::content_color().with_alpha_f32(0.38),
236 };
237 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
238 let pad = config.content_padding.unwrap_or(PaddingValues {
239 left: 24.0,
240 right: 24.0,
241 top: 0.0,
242 bottom: 0.0,
243 });
244 button_impl(
245 modifier.then(config.modifier),
246 on_click,
247 content,
248 cc,
249 bg,
250 sc,
251 se.or(Some(ButtonDefaults::state_elevation_default())),
252 config.border,
253 pad.left,
254 pad.right,
255 config.height,
256 config.shape_radius,
257 config.enabled,
258 config.interaction_source.clone(),
259 )
260}
261
262pub fn FilledTonalButton(
264 modifier: Modifier,
265 on_click: impl Fn() + 'static,
266 config: ButtonConfig,
267 content: impl FnOnce() -> View,
268) -> View {
269 let th = theme();
270 let def = ButtonColors {
271 container_color: ButtonDefaults::tonal_container_color(),
272 content_color: ButtonDefaults::tonal_content_color(),
273 disabled_container_color: th
274 .on_surface
275 .with_alpha_f32(0.12)
276 .composite_over(th.surface_container_low),
277 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
278 };
279 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
280 let pad = config.content_padding.unwrap_or(PaddingValues {
281 left: 24.0,
282 right: 24.0,
283 top: 0.0,
284 bottom: 0.0,
285 });
286 button_impl(
287 modifier.then(config.modifier),
288 on_click,
289 content,
290 cc,
291 bg,
292 sc,
293 se.or(Some(ButtonDefaults::state_elevation_default())),
294 config.border,
295 pad.left,
296 pad.right,
297 config.height,
298 config.shape_radius,
299 config.enabled,
300 config.interaction_source.clone(),
301 )
302}
303
304pub fn OutlinedButton(
306 modifier: Modifier,
307 on_click: impl Fn() + 'static,
308 config: ButtonConfig,
309 content: impl FnOnce() -> View,
310) -> View {
311 let th = theme();
312 let def = ButtonColors {
313 container_color: Color::TRANSPARENT,
314 content_color: ButtonDefaults::outlined_content_color(),
315 disabled_container_color: Color::TRANSPARENT,
316 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
317 };
318 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
319 let border = config
320 .border
321 .unwrap_or((1.0, ButtonDefaults::outlined_border_color(), 20.0));
322 let pad = config.content_padding.unwrap_or(PaddingValues {
323 left: 24.0,
324 right: 24.0,
325 top: 0.0,
326 bottom: 0.0,
327 });
328 button_impl(
329 modifier.then(config.modifier),
330 on_click,
331 content,
332 cc,
333 bg,
334 sc,
335 se,
336 Some(border),
337 pad.left,
338 pad.right,
339 config.height,
340 config.shape_radius,
341 config.enabled,
342 config.interaction_source.clone(),
343 )
344}
345
346pub fn TextButton(
348 modifier: Modifier,
349 on_click: impl Fn() + 'static,
350 config: ButtonConfig,
351 content: impl FnOnce() -> View,
352) -> View {
353 let th = theme();
354 let def = ButtonColors {
355 container_color: Color::TRANSPARENT,
356 content_color: ButtonDefaults::text_content_color(),
357 disabled_container_color: Color::TRANSPARENT,
358 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
359 };
360 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
361 let pad = config.content_padding.unwrap_or(PaddingValues {
362 left: 12.0,
363 right: 12.0,
364 top: 0.0,
365 bottom: 0.0,
366 });
367 button_impl(
368 modifier.then(config.modifier),
369 on_click,
370 content,
371 cc,
372 bg,
373 sc,
374 se,
375 None,
376 pad.left,
377 pad.right,
378 config.height,
379 config.shape_radius,
380 config.enabled,
381 config.interaction_source.clone(),
382 )
383}
384
385pub fn ElevatedButton(
387 modifier: Modifier,
388 on_click: impl Fn() + 'static,
389 config: ButtonConfig,
390 content: impl FnOnce() -> View,
391) -> View {
392 let th = theme();
393 let def = ButtonColors {
394 container_color: ButtonDefaults::elevated_container_color(),
395 content_color: ButtonDefaults::elevated_content_color(),
396 disabled_container_color: th.on_surface.with_alpha_f32(0.04),
397 disabled_content_color: th.on_surface.with_alpha_f32(0.38),
398 };
399 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
400 let pad = config.content_padding.unwrap_or(PaddingValues {
401 left: 24.0,
402 right: 24.0,
403 top: 0.0,
404 bottom: 0.0,
405 });
406 button_impl(
407 modifier.then(config.modifier),
408 on_click,
409 content,
410 cc,
411 bg,
412 sc,
413 se.or(Some(ButtonDefaults::elevated_state_elevation())),
414 config.border,
415 pad.left,
416 pad.right,
417 config.height,
418 config.shape_radius,
419 config.enabled,
420 config.interaction_source.clone(),
421 )
422}
423
424#[derive(Clone, Debug)]
426pub struct ToggleButtonConfig {
427 pub modifier: Modifier,
428 pub enabled: bool,
429 pub container_color: Option<Color>,
430 pub content_color: Option<Color>,
431 pub checked_container_color: Option<Color>,
432 pub checked_content_color: Option<Color>,
433 pub state_colors: StateColors,
434 pub state_elevation: Option<StateElevation>,
435 pub border: Option<(f32, Color, f32)>,
436 pub shape_radius: f32,
437 pub height: f32,
438 pub content_padding: Option<PaddingValues>,
439 pub interaction_source: Option<MutableInteractionSource>,
440}
441
442impl Default for ToggleButtonConfig {
443 fn default() -> Self {
444 Self {
445 modifier: Modifier::new(),
446 enabled: true,
447 container_color: None,
448 content_color: None,
449 checked_container_color: None,
450 checked_content_color: None,
451 state_colors: ToggleButtonDefaults::state_colors_default(),
452 state_elevation: None,
453 border: None,
454 shape_radius: ToggleButtonDefaults::SHAPE_RADIUS,
455 height: ToggleButtonDefaults::HEIGHT,
456 content_padding: None,
457 interaction_source: None,
458 }
459 }
460}
461
462fn toggle_button_impl(
463 checked: bool,
464 on_checked_change: impl Fn(bool) + 'static,
465 content: impl FnOnce(bool) -> View,
466 content_color: Color,
467 container_color: Option<Color>,
468 checked_container_color: Option<Color>,
469 checked_content_color: Option<Color>,
470 state_colors: StateColors,
471 state_elevation: StateElevation,
472 border: Option<(f32, Color, f32)>,
473 pad_left: f32,
474 pad_right: f32,
475 height: f32,
476 shape_radius: f32,
477 enabled: bool,
478 interaction_source: Option<MutableInteractionSource>,
479) -> View {
480 let th = theme();
481 let bg = if checked {
482 checked_container_color.unwrap_or(th.primary)
483 } else {
484 container_color.unwrap_or(Color::TRANSPARENT)
485 };
486 let fg = if checked {
487 checked_content_color.unwrap_or(th.on_primary)
488 } else {
489 content_color
490 };
491 let mut m = Modifier::new()
492 .min_height(height)
493 .padding_values(PaddingValues {
494 left: pad_left,
495 right: pad_right,
496 top: 8.0,
497 bottom: 8.0,
498 })
499 .background(bg)
500 .clip_rounded(shape_radius)
501 .align_items(AlignItems::CENTER)
502 .justify_content(JustifyContent::CENTER)
503 .state_colors(state_colors)
504 .state_elevation(state_elevation);
505 let tg_source: Rc<MutableInteractionSource> = interaction_source
506 .map(Rc::new)
507 .unwrap_or_else(|| remember(MutableInteractionSource::new));
508 m = m.interaction_source(&tg_source);
509 m = m.indication(ripple(RippleConfig {
510 color: Some(fg),
511 bounded: true,
512 ..Default::default()
513 }));
514 if let Some((w, c, r)) = border {
515 m = m.border(w, c, r);
516 }
517 if enabled {
518 let cb = on_checked_change;
519 m = m.clickable().on_click(move || cb(!checked));
520 } else {
521 m = m.alpha(0.38);
522 }
523 with_content_color(fg, || Box(m).child(content(checked)))
524}
525
526pub fn ToggleButton(
528 checked: bool,
529 on_checked_change: impl Fn(bool) + 'static,
530 config: ToggleButtonConfig,
531 content: impl FnOnce(bool) -> View,
532) -> View {
533 let cc = config
534 .content_color
535 .unwrap_or_else(ToggleButtonDefaults::content_color);
536 let checked_cc = config
537 .checked_content_color
538 .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
539 let checked_bg = config
540 .checked_container_color
541 .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
542 let se = config
543 .state_elevation
544 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
545 let pad_l = config
546 .content_padding
547 .map(|p| p.left)
548 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
549 let pad_r = config
550 .content_padding
551 .map(|p| p.right)
552 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
553 toggle_button_impl(
554 checked,
555 on_checked_change,
556 content,
557 cc,
558 None,
559 Some(checked_bg),
560 Some(checked_cc),
561 config.state_colors,
562 se,
563 config.border,
564 pad_l,
565 pad_r,
566 config.height,
567 config.shape_radius,
568 config.enabled,
569 config.interaction_source.clone(),
570 )
571}
572
573pub fn TonalToggleButton(
575 checked: bool,
576 on_checked_change: impl Fn(bool) + 'static,
577 config: ToggleButtonConfig,
578 content: impl FnOnce(bool) -> View,
579) -> View {
580 let cc = config
581 .content_color
582 .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
583 let checked_cc = config
584 .checked_content_color
585 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
586 let checked_bg = config
587 .checked_container_color
588 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
589 let se = config
590 .state_elevation
591 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
592 toggle_button_impl(
593 checked,
594 on_checked_change,
595 content,
596 cc,
597 None,
598 Some(checked_bg),
599 Some(checked_cc),
600 config.state_colors,
601 se,
602 config.border,
603 config
604 .content_padding
605 .map(|p| p.left)
606 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
607 config
608 .content_padding
609 .map(|p| p.right)
610 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
611 config.height,
612 config.shape_radius,
613 config.enabled,
614 config.interaction_source.clone(),
615 )
616}
617
618pub fn OutlinedToggleButton(
620 checked: bool,
621 on_checked_change: impl Fn(bool) + 'static,
622 config: ToggleButtonConfig,
623 content: impl FnOnce(bool) -> View,
624) -> View {
625 let cc = config
626 .content_color
627 .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
628 let checked_cc = config
629 .checked_content_color
630 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
631 let checked_bg = config
632 .checked_container_color
633 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
634 let se = config
635 .state_elevation
636 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
637 let border = if !checked {
638 Some(config.border.unwrap_or((
639 1.0,
640 ToggleButtonDefaults::outlined_border_color(),
641 config.shape_radius,
642 )))
643 } else {
644 config.border
645 };
646 toggle_button_impl(
647 checked,
648 on_checked_change,
649 content,
650 cc,
651 None,
652 Some(checked_bg),
653 Some(checked_cc),
654 config.state_colors,
655 se,
656 border,
657 config
658 .content_padding
659 .map(|p| p.left)
660 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
661 config
662 .content_padding
663 .map(|p| p.right)
664 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
665 config.height,
666 config.shape_radius,
667 config.enabled,
668 config.interaction_source.clone(),
669 )
670}
671
672pub fn ElevatedToggleButton(
674 checked: bool,
675 on_checked_change: impl Fn(bool) + 'static,
676 config: ToggleButtonConfig,
677 content: impl FnOnce(bool) -> View,
678) -> View {
679 let cc = config
680 .content_color
681 .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
682 let checked_cc = config
683 .checked_content_color
684 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
685 let checked_bg = config
686 .checked_container_color
687 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
688 let se = config
689 .state_elevation
690 .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
691 toggle_button_impl(
692 checked,
693 on_checked_change,
694 content,
695 cc,
696 None,
697 Some(checked_bg),
698 Some(checked_cc),
699 config.state_colors,
700 se,
701 config.border,
702 config
703 .content_padding
704 .map(|p| p.left)
705 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
706 config
707 .content_padding
708 .map(|p| p.right)
709 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
710 config.height,
711 config.shape_radius,
712 config.enabled,
713 config.interaction_source.clone(),
714 )
715}