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