1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use super::SplitButtonDefaults;
6use super::util::apply_m3_clickable;
7use repose_core::{locals::with_content_color, *};
8use repose_ui::{Box, Row, Text, TextStyle, ViewExt};
9
10#[derive(Clone)]
12pub struct SplitButtonConfig {
13 pub modifier: Modifier,
14 pub spacing: f32,
15}
16
17impl Default for SplitButtonConfig {
18 fn default() -> Self {
19 Self {
20 modifier: Modifier::new(),
21 spacing: SplitButtonDefaults::SPACING,
22 }
23 }
24}
25
26pub fn SplitButtonLayout(
31 leading_button: View,
32 trailing_button: View,
33 config: SplitButtonConfig,
34) -> View {
35 Row(config
36 .modifier
37 .gap(config.spacing)
38 .align_items(AlignItems::CENTER))
39 .child((leading_button, trailing_button))
40}
41
42fn split_leading_shape_radii() -> [f32; 4] {
43 [
44 SplitButtonDefaults::OUTER_CORNER_SIZE,
45 SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
46 SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
47 SplitButtonDefaults::OUTER_CORNER_SIZE,
48 ]
49}
50
51fn split_trailing_shape_radii() -> [f32; 4] {
52 [
53 SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
54 SplitButtonDefaults::OUTER_CORNER_SIZE,
55 SplitButtonDefaults::OUTER_CORNER_SIZE,
56 SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
57 ]
58}
59
60fn split_button_impl(
61 outer_modifier: Modifier,
62 on_click: impl Fn() + 'static,
63 content: impl FnOnce() -> View,
64 content_color: Color,
65 container_color: Option<Color>,
66 state_colors: StateColors,
67 state_elevation: Option<StateElevation>,
68 border: Option<(f32, Color, f32)>,
69 pad_left: f32,
70 pad_right: f32,
71 height: f32,
72 enabled: bool,
73 interaction_source: Option<MutableInteractionSource>,
74) -> View {
75 let mut m = Modifier::new()
76 .height(height)
77 .min_width(48.0)
78 .padding_values(PaddingValues {
79 left: pad_left,
80 right: pad_right,
81 top: 0.0,
82 bottom: 0.0,
83 })
84 .align_items(AlignItems::CENTER)
85 .justify_content(JustifyContent::CENTER);
86 if let Some(bg) = container_color {
87 m = m.background(bg);
88 }
89 m = m.state_colors(if enabled {
90 state_colors
91 } else {
92 StateColors {
93 default: Color::TRANSPARENT,
94 hovered: Color::TRANSPARENT,
95 focused: Color::TRANSPARENT,
96 pressed: Color::TRANSPARENT,
97 dragged: Color::TRANSPARENT,
98 disabled: state_colors.disabled,
99 }
100 });
101 if let Some(se) = state_elevation {
102 m = m.state_elevation(se);
103 }
104 if let Some((w, c, r)) = border {
105 m = m.border(w, c, r);
106 }
107 let source: Rc<MutableInteractionSource> = interaction_source
108 .map(Rc::new)
109 .unwrap_or_else(|| remember(MutableInteractionSource::new));
110 m = m.interaction_source(&source);
111 m = m.indication(crate::ripple::ripple(crate::ripple::RippleConfig {
112 color: Some(content_color),
113 bounded: true,
114 ..Default::default()
115 }));
116 if enabled {
117 m = m.clickable().on_click(on_click);
118 }
119 m = m.then(outer_modifier);
120 let effective = if enabled {
121 content_color
122 } else {
123 content_color.with_alpha_f32(0.38)
124 };
125 with_content_color(effective, || Box(m).child(content()))
126}
127
128pub fn SplitButtonLeadingButton(
129 modifier: Modifier,
130 on_click: impl Fn() + 'static,
131 config: super::ButtonConfig,
132 content: impl FnOnce() -> View,
133) -> View {
134 let def = super::ButtonColors {
135 container_color: super::ButtonDefaults::container_color(),
136 content_color: super::ButtonDefaults::content_color(),
137 disabled_container_color: super::ButtonDefaults::container_color()
138 .with_alpha_f32(0.12)
139 .composite_over(theme().surface_container_low),
140 disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
141 };
142 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
143 let pad = config
144 .content_padding
145 .unwrap_or(SplitButtonDefaults::small_leading_content_padding());
146 split_button_impl(
147 modifier.clip_rounded_radii(split_leading_shape_radii()),
148 on_click,
149 content,
150 cc,
151 bg,
152 sc,
153 se.or(Some(super::ButtonDefaults::state_elevation_default())),
154 config.border,
155 pad.left,
156 pad.right,
157 config.height,
158 config.enabled,
159 config.interaction_source.clone(),
160 )
161}
162
163pub fn SplitButtonTrailingButton(
164 modifier: Modifier,
165 on_click: impl Fn() + 'static,
166 config: super::ButtonConfig,
167 content: impl FnOnce() -> View,
168) -> View {
169 let def = super::ButtonColors {
170 container_color: super::ButtonDefaults::container_color(),
171 content_color: super::ButtonDefaults::content_color(),
172 disabled_container_color: super::ButtonDefaults::container_color()
173 .with_alpha_f32(0.12)
174 .composite_over(theme().surface_container_low),
175 disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
176 };
177 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
178 let pad = config
179 .content_padding
180 .unwrap_or(SplitButtonDefaults::small_trailing_content_padding());
181 split_button_impl(
182 modifier.clip_rounded_radii(split_trailing_shape_radii()),
183 on_click,
184 content,
185 cc,
186 bg,
187 sc,
188 se.or(Some(super::ButtonDefaults::state_elevation_default())),
189 config.border,
190 pad.left,
191 pad.right,
192 config.height,
193 config.enabled,
194 config.interaction_source.clone(),
195 )
196}
197
198pub fn SplitButtonTrailingToggleButton(
199 checked: bool,
200 on_checked_change: impl Fn(bool) + 'static,
201 modifier: Modifier,
202 config: super::ToggleButtonConfig,
203 content: impl FnOnce(bool) -> View,
204) -> View {
205 let _th = theme();
206 let cc = config
207 .content_color
208 .unwrap_or_else(super::ToggleButtonDefaults::content_color);
209 let checked_cc = config
210 .checked_content_color
211 .unwrap_or_else(super::ToggleButtonDefaults::checked_content_color);
212 let checked_bg = config
213 .checked_container_color
214 .unwrap_or_else(super::ToggleButtonDefaults::checked_container_color);
215 let bg = if checked {
216 checked_bg
217 } else {
218 config.container_color.unwrap_or(Color::TRANSPARENT)
219 };
220 let fg = if checked { checked_cc } else { cc };
221 let se = config
222 .state_elevation
223 .unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
224 let pad_l = config
225 .content_padding
226 .map(|p| p.left)
227 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
228 let pad_r = config
229 .content_padding
230 .map(|p| p.right)
231 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
232 let mut m = Modifier::new()
233 .height(config.height)
234 .min_width(48.0)
235 .padding_values(PaddingValues {
236 left: pad_l,
237 right: pad_r,
238 top: 0.0,
239 bottom: 0.0,
240 })
241 .background(bg)
242 .align_items(AlignItems::CENTER)
243 .justify_content(JustifyContent::CENTER)
244 .state_colors(config.state_colors)
245 .state_elevation(se);
246 let tg_source: Rc<MutableInteractionSource> = config
247 .interaction_source
248 .clone()
249 .map(Rc::new)
250 .unwrap_or_else(|| remember(MutableInteractionSource::new));
251 m = m.interaction_source(&tg_source);
252 if let Some((w, c, r)) = config.border {
253 m = m.border(w, c, r);
254 }
255 if config.enabled {
256 let cb = on_checked_change;
257 m = apply_m3_clickable(m, &tg_source, fg, true, move || cb(!checked));
258 } else {
259 m = m.alpha(0.38);
260 }
261 m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
262 with_content_color(fg, || Box(m).child(content(checked)))
263}
264
265pub fn SplitButtonTonalLeadingButton(
266 modifier: Modifier,
267 on_click: impl Fn() + 'static,
268 config: super::ButtonConfig,
269 content: impl FnOnce() -> View,
270) -> View {
271 let def = super::ButtonColors {
272 container_color: super::ButtonDefaults::tonal_container_color(),
273 content_color: super::ButtonDefaults::tonal_content_color(),
274 disabled_container_color: theme()
275 .on_surface
276 .with_alpha_f32(0.12)
277 .composite_over(theme().surface_container_low),
278 disabled_content_color: theme().on_surface.with_alpha_f32(0.38),
279 };
280 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
281 let pad = config
282 .content_padding
283 .unwrap_or(SplitButtonDefaults::small_leading_content_padding());
284 split_button_impl(
285 modifier.clip_rounded_radii(split_leading_shape_radii()),
286 on_click,
287 content,
288 cc,
289 bg,
290 sc,
291 se.or(Some(super::ButtonDefaults::elevated_state_elevation())),
292 config.border,
293 pad.left,
294 pad.right,
295 config.height,
296 config.enabled,
297 config.interaction_source.clone(),
298 )
299}
300
301pub fn SplitButtonTonalTrailingToggleButton(
302 checked: bool,
303 on_checked_change: impl Fn(bool) + 'static,
304 modifier: Modifier,
305 config: super::ToggleButtonConfig,
306 content: impl FnOnce(bool) -> View,
307) -> View {
308 let _th = theme();
309 let cc = config
310 .content_color
311 .unwrap_or_else(super::ToggleButtonDefaults::tonal_content_color);
312 let checked_cc = config
313 .checked_content_color
314 .unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_content_color);
315 let checked_bg = config
316 .checked_container_color
317 .unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_container_color);
318 let bg = if checked {
319 checked_bg
320 } else {
321 config.container_color.unwrap_or(Color::TRANSPARENT)
322 };
323 let fg = if checked { checked_cc } else { cc };
324 let se = config
325 .state_elevation
326 .unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
327 let pad_l = config
328 .content_padding
329 .map(|p| p.left)
330 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
331 let pad_r = config
332 .content_padding
333 .map(|p| p.right)
334 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
335 let mut m = Modifier::new()
336 .height(config.height)
337 .min_width(48.0)
338 .padding_values(PaddingValues {
339 left: pad_l,
340 right: pad_r,
341 top: 0.0,
342 bottom: 0.0,
343 })
344 .background(bg)
345 .align_items(AlignItems::CENTER)
346 .justify_content(JustifyContent::CENTER)
347 .state_colors(config.state_colors)
348 .state_elevation(se);
349 let tg_source: Rc<MutableInteractionSource> = config
350 .interaction_source
351 .clone()
352 .map(Rc::new)
353 .unwrap_or_else(|| remember(MutableInteractionSource::new));
354 m = m.interaction_source(&tg_source);
355 if let Some((w, c, r)) = config.border {
356 m = m.border(w, c, r);
357 }
358 if config.enabled {
359 let cb = on_checked_change;
360 m = apply_m3_clickable(m, &tg_source, fg, true, move || cb(!checked));
361 } else {
362 m = m.alpha(0.38);
363 }
364 m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
365 with_content_color(fg, || Box(m).child(content(checked)))
366}
367
368pub struct ButtonGroupMenuState {
370 pub is_showing: bool,
371}
372
373impl ButtonGroupMenuState {
374 pub fn dismiss(&mut self) {
375 self.is_showing = false;
376 }
377 pub fn show(&mut self) {
378 self.is_showing = true;
379 }
380}
381
382pub struct ButtonGroupScope {
384 items: Vec<ButtonGroupItem>,
385}
386
387#[allow(dead_code)] struct ButtonGroupItem {
390 button_group_content: Box<dyn FnOnce() -> View>,
391 menu_content: Option<Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View>>,
392}
393
394impl ButtonGroupScope {
395 fn new() -> Self {
396 Self { items: Vec::new() }
397 }
398
399 pub fn clickable_item(
401 &mut self,
402 on_click: impl Fn() + 'static,
403 label: String,
404 icon: Option<View>,
405 ) {
406 let cb = Rc::new(on_click);
407 let cb2 = cb.clone();
408 self.items.push(ButtonGroupItem {
409 button_group_content: Box::new(move || {
410 let cb = cb2.clone();
411 let config = super::ButtonConfig {
412 shape_radius: 0.0,
413 ..Default::default()
414 };
415 super::Button(
416 Modifier::new().flex_grow(1.0),
417 move || (cb)(),
418 config,
419 move || {
420 let label = label.clone();
421 let t = Text(label).single_line();
422 match icon.clone() {
423 Some(ic) => Box(Modifier::new()).child((ic, t)),
424 None => t,
425 }
426 },
427 )
428 }),
429 menu_content: None,
430 });
431 }
432
433 pub fn toggleable_item(
435 &mut self,
436 checked: bool,
437 on_checked_change: impl Fn(bool) + 'static,
438 label: String,
439 icon: Option<View>,
440 ) {
441 let cb = Rc::new(on_checked_change);
442 let cb2 = cb.clone();
443 self.items.push(ButtonGroupItem {
444 button_group_content: Box::new(move || {
445 let cb = cb2.clone();
446 let config = super::ToggleButtonConfig {
447 shape_radius: 0.0,
448 ..Default::default()
449 };
450 super::ToggleButton(
451 checked,
452 move |b| (cb)(b),
453 config,
454 move |_| {
455 let label = label.clone();
456 let t = Text(label).single_line();
457 match icon.clone() {
458 Some(ic) => Box(Modifier::new()).child((ic, t)),
459 None => t,
460 }
461 },
462 )
463 }),
464 menu_content: None,
465 });
466 }
467
468 pub fn custom_item(
470 &mut self,
471 button_group_content: impl FnOnce() -> View + 'static,
472 menu_content: Option<impl FnOnce(&mut ButtonGroupMenuState) -> View + 'static>,
473 ) {
474 self.items.push(ButtonGroupItem {
475 button_group_content: Box::new(button_group_content),
476 menu_content: menu_content.map(|f| {
477 let b: Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View> = Box::new(f);
478 b
479 }),
480 });
481 }
482}
483
484pub fn ButtonGroup(
489 modifier: Modifier,
490 gap: f32,
491 content: impl FnOnce(&mut ButtonGroupScope),
492) -> View {
493 let mut scope = ButtonGroupScope::new();
494 content(&mut scope);
495 Row(modifier.gap(gap).align_items(AlignItems::CENTER)).with_children(
496 scope
497 .items
498 .into_iter()
499 .map(|item| (item.button_group_content)())
500 .collect::<Vec<View>>(),
501 )
502}
503
504fn resolve_button_colors(
505 config: &super::ButtonConfig,
506 def: super::ButtonColors,
507) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
508 if let Some(colors) = &config.colors {
509 let bg = if config.enabled {
510 colors.container_color
511 } else {
512 colors.disabled_container_color
513 };
514 let cc = if config.enabled {
515 colors.content_color
516 } else {
517 colors.disabled_content_color
518 };
519 let sc = StateColors {
520 default: Color::TRANSPARENT,
521 hovered: Color::TRANSPARENT,
522 focused: Color::TRANSPARENT,
523 pressed: Color::TRANSPARENT,
524 dragged: colors.content_color.with_alpha_f32(0.12),
525 disabled: Color::TRANSPARENT,
526 };
527 let se = config.elevation.map(|e| StateElevation {
528 default: e.default,
529 hovered: e.hovered,
530 focused: e.focused,
531 pressed: e.pressed,
532 dragged: e.pressed,
533 disabled: e.disabled,
534 });
535 (cc, Some(bg), sc, se)
536 } else {
537 let cc = config.content_color.unwrap_or(def.content_color);
538 let bg = Some(config.container_color.unwrap_or(def.container_color));
539 let sc = if config.enabled {
540 config.state_colors
541 } else {
542 StateColors {
543 default: Color::TRANSPARENT,
544 hovered: Color::TRANSPARENT,
545 focused: Color::TRANSPARENT,
546 pressed: Color::TRANSPARENT,
547 dragged: Color::TRANSPARENT,
548 disabled: config.state_colors.disabled,
549 }
550 };
551 let se = config.state_elevation;
552 (cc, bg, sc, se)
553 }
554}