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