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