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: colors.content_color.with_alpha_f32(0.08),
105 focused: colors.content_color.with_alpha_f32(0.12),
106 pressed: colors.content_color.with_alpha_f32(0.12),
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 if let Some((w, c, r)) = border {
510 m = m.border(w, c, r);
511 }
512 if enabled {
513 let cb = on_checked_change;
514 m = m.clickable().on_click(move || cb(!checked));
515 } else {
516 m = m.alpha(0.38);
517 }
518 with_content_color(fg, || Box(m).child(content(checked)))
519}
520
521pub fn ToggleButton(
523 checked: bool,
524 on_checked_change: impl Fn(bool) + 'static,
525 config: ToggleButtonConfig,
526 content: impl FnOnce(bool) -> View,
527) -> View {
528 let cc = config
529 .content_color
530 .unwrap_or_else(ToggleButtonDefaults::content_color);
531 let checked_cc = config
532 .checked_content_color
533 .unwrap_or_else(ToggleButtonDefaults::checked_content_color);
534 let checked_bg = config
535 .checked_container_color
536 .unwrap_or_else(ToggleButtonDefaults::checked_container_color);
537 let se = config
538 .state_elevation
539 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
540 let pad_l = config
541 .content_padding
542 .map(|p| p.left)
543 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
544 let pad_r = config
545 .content_padding
546 .map(|p| p.right)
547 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING);
548 toggle_button_impl(
549 checked,
550 on_checked_change,
551 content,
552 cc,
553 None,
554 Some(checked_bg),
555 Some(checked_cc),
556 config.state_colors,
557 se,
558 config.border,
559 pad_l,
560 pad_r,
561 config.height,
562 config.shape_radius,
563 config.enabled,
564 config.interaction_source.clone(),
565 )
566}
567
568pub fn TonalToggleButton(
570 checked: bool,
571 on_checked_change: impl Fn(bool) + 'static,
572 config: ToggleButtonConfig,
573 content: impl FnOnce(bool) -> View,
574) -> View {
575 let cc = config
576 .content_color
577 .unwrap_or_else(ToggleButtonDefaults::tonal_content_color);
578 let checked_cc = config
579 .checked_content_color
580 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_content_color);
581 let checked_bg = config
582 .checked_container_color
583 .unwrap_or_else(ToggleButtonDefaults::tonal_checked_container_color);
584 let se = config
585 .state_elevation
586 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
587 toggle_button_impl(
588 checked,
589 on_checked_change,
590 content,
591 cc,
592 None,
593 Some(checked_bg),
594 Some(checked_cc),
595 config.state_colors,
596 se,
597 config.border,
598 config
599 .content_padding
600 .map(|p| p.left)
601 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
602 config
603 .content_padding
604 .map(|p| p.right)
605 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
606 config.height,
607 config.shape_radius,
608 config.enabled,
609 config.interaction_source.clone(),
610 )
611}
612
613pub fn OutlinedToggleButton(
615 checked: bool,
616 on_checked_change: impl Fn(bool) + 'static,
617 config: ToggleButtonConfig,
618 content: impl FnOnce(bool) -> View,
619) -> View {
620 let cc = config
621 .content_color
622 .unwrap_or_else(ToggleButtonDefaults::outlined_content_color);
623 let checked_cc = config
624 .checked_content_color
625 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_content_color);
626 let checked_bg = config
627 .checked_container_color
628 .unwrap_or_else(ToggleButtonDefaults::outlined_checked_container_color);
629 let se = config
630 .state_elevation
631 .unwrap_or_else(ToggleButtonDefaults::state_elevation_default);
632 let border = if !checked {
633 Some(config.border.unwrap_or((
634 1.0,
635 ToggleButtonDefaults::outlined_border_color(),
636 config.shape_radius,
637 )))
638 } else {
639 config.border
640 };
641 toggle_button_impl(
642 checked,
643 on_checked_change,
644 content,
645 cc,
646 None,
647 Some(checked_bg),
648 Some(checked_cc),
649 config.state_colors,
650 se,
651 border,
652 config
653 .content_padding
654 .map(|p| p.left)
655 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
656 config
657 .content_padding
658 .map(|p| p.right)
659 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
660 config.height,
661 config.shape_radius,
662 config.enabled,
663 config.interaction_source.clone(),
664 )
665}
666
667pub fn ElevatedToggleButton(
669 checked: bool,
670 on_checked_change: impl Fn(bool) + 'static,
671 config: ToggleButtonConfig,
672 content: impl FnOnce(bool) -> View,
673) -> View {
674 let cc = config
675 .content_color
676 .unwrap_or_else(ToggleButtonDefaults::elevated_content_color);
677 let checked_cc = config
678 .checked_content_color
679 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_content_color);
680 let checked_bg = config
681 .checked_container_color
682 .unwrap_or_else(ToggleButtonDefaults::elevated_checked_container_color);
683 let se = config
684 .state_elevation
685 .unwrap_or_else(ToggleButtonDefaults::elevated_state_elevation);
686 toggle_button_impl(
687 checked,
688 on_checked_change,
689 content,
690 cc,
691 None,
692 Some(checked_bg),
693 Some(checked_cc),
694 config.state_colors,
695 se,
696 config.border,
697 config
698 .content_padding
699 .map(|p| p.left)
700 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
701 config
702 .content_padding
703 .map(|p| p.right)
704 .unwrap_or(ToggleButtonDefaults::HORIZONTAL_PADDING),
705 config.height,
706 config.shape_radius,
707 config.enabled,
708 config.interaction_source.clone(),
709 )
710}