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