1#![allow(non_snake_case)]
2
3use std::rc::Rc;
4
5use super::SplitButtonDefaults;
6use super::util::{apply_m3_clickable, with_button_semantics};
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 = apply_m3_clickable(m, &source, content_color, enabled, on_click);
111 m = with_button_semantics(m, enabled);
112 m = m.then(outer_modifier);
113 with_content_color(content_color, || Box(m).child(content()))
114}
115
116pub fn SplitButtonLeadingButton(
117 modifier: Modifier,
118 on_click: impl Fn() + 'static,
119 config: super::ButtonConfig,
120 content: impl FnOnce() -> View,
121) -> View {
122 let def = super::ButtonColors {
123 container_color: super::ButtonDefaults::container_color(),
124 content_color: super::ButtonDefaults::content_color(),
125 disabled_container_color: super::ButtonDefaults::container_color()
126 .with_alpha_f32(0.12)
127 .composite_over(theme().surface_container_low),
128 disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
129 };
130 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
131 let pad = config
132 .content_padding
133 .unwrap_or(SplitButtonDefaults::small_leading_content_padding());
134 split_button_impl(
135 modifier.clip_rounded_radii(split_leading_shape_radii()),
136 on_click,
137 content,
138 cc,
139 bg,
140 sc,
141 se.or(Some(super::ButtonDefaults::state_elevation_default())),
142 config.border,
143 pad.left,
144 pad.right,
145 config.height,
146 config.enabled,
147 config.interaction_source.clone(),
148 )
149}
150
151pub fn SplitButtonTrailingButton(
152 modifier: Modifier,
153 on_click: impl Fn() + 'static,
154 config: super::ButtonConfig,
155 content: impl FnOnce() -> View,
156) -> View {
157 let def = super::ButtonColors {
158 container_color: super::ButtonDefaults::container_color(),
159 content_color: super::ButtonDefaults::content_color(),
160 disabled_container_color: super::ButtonDefaults::container_color()
161 .with_alpha_f32(0.12)
162 .composite_over(theme().surface_container_low),
163 disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
164 };
165 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
166 let pad = config
167 .content_padding
168 .unwrap_or(SplitButtonDefaults::small_trailing_content_padding());
169 split_button_impl(
170 modifier.clip_rounded_radii(split_trailing_shape_radii()),
171 on_click,
172 content,
173 cc,
174 bg,
175 sc,
176 se.or(Some(super::ButtonDefaults::state_elevation_default())),
177 config.border,
178 pad.left,
179 pad.right,
180 config.height,
181 config.enabled,
182 config.interaction_source.clone(),
183 )
184}
185
186pub fn SplitButtonTrailingToggleButton(
187 checked: bool,
188 on_checked_change: impl Fn(bool) + 'static,
189 modifier: Modifier,
190 config: super::ToggleButtonConfig,
191 content: impl FnOnce(bool) -> View,
192) -> View {
193 let _th = theme();
194 let cc = config
195 .content_color
196 .unwrap_or_else(super::ToggleButtonDefaults::content_color);
197 let checked_cc = config
198 .checked_content_color
199 .unwrap_or_else(super::ToggleButtonDefaults::checked_content_color);
200 let checked_bg = config
201 .checked_container_color
202 .unwrap_or_else(super::ToggleButtonDefaults::checked_container_color);
203 let bg = if checked {
204 checked_bg
205 } else {
206 config.container_color.unwrap_or(Color::TRANSPARENT)
207 };
208 let fg = if checked { checked_cc } else { cc };
209 let se = config
210 .state_elevation
211 .unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
212 let pad_l = config
213 .content_padding
214 .map(|p| p.left)
215 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
216 let pad_r = config
217 .content_padding
218 .map(|p| p.right)
219 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
220 let mut m = Modifier::new()
221 .height(config.height)
222 .min_width(48.0)
223 .padding_values(PaddingValues {
224 left: pad_l,
225 right: pad_r,
226 top: 0.0,
227 bottom: 0.0,
228 })
229 .background(bg)
230 .align_items(AlignItems::CENTER)
231 .justify_content(JustifyContent::CENTER)
232 .state_colors(config.state_colors)
233 .state_elevation(se);
234 let tg_source: Rc<MutableInteractionSource> = config
235 .interaction_source
236 .clone()
237 .map(Rc::new)
238 .unwrap_or_else(|| remember(MutableInteractionSource::new));
239 if let Some((w, c, r)) = config.border {
240 m = m.border(w, c, r);
241 }
242 let cb = on_checked_change;
243 m = apply_m3_clickable(m, &tg_source, fg, config.enabled, move || cb(!checked));
244 if !config.enabled {
245 m = m.alpha(0.38);
246 }
247 m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
248 with_content_color(fg, || Box(m).child(content(checked)))
249}
250
251pub fn SplitButtonTonalLeadingButton(
252 modifier: Modifier,
253 on_click: impl Fn() + 'static,
254 config: super::ButtonConfig,
255 content: impl FnOnce() -> View,
256) -> View {
257 let def = super::ButtonColors {
258 container_color: super::ButtonDefaults::tonal_container_color(),
259 content_color: super::ButtonDefaults::tonal_content_color(),
260 disabled_container_color: theme()
261 .on_surface
262 .with_alpha_f32(0.12)
263 .composite_over(theme().surface_container_low),
264 disabled_content_color: theme().on_surface.with_alpha_f32(0.38),
265 };
266 let (cc, bg, sc, se) = resolve_button_colors(&config, def);
267 let pad = config
268 .content_padding
269 .unwrap_or(SplitButtonDefaults::small_leading_content_padding());
270 split_button_impl(
271 modifier.clip_rounded_radii(split_leading_shape_radii()),
272 on_click,
273 content,
274 cc,
275 bg,
276 sc,
277 se.or(Some(super::ButtonDefaults::elevated_state_elevation())),
278 config.border,
279 pad.left,
280 pad.right,
281 config.height,
282 config.enabled,
283 config.interaction_source.clone(),
284 )
285}
286
287pub fn SplitButtonTonalTrailingToggleButton(
288 checked: bool,
289 on_checked_change: impl Fn(bool) + 'static,
290 modifier: Modifier,
291 config: super::ToggleButtonConfig,
292 content: impl FnOnce(bool) -> View,
293) -> View {
294 let _th = theme();
295 let cc = config
296 .content_color
297 .unwrap_or_else(super::ToggleButtonDefaults::tonal_content_color);
298 let checked_cc = config
299 .checked_content_color
300 .unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_content_color);
301 let checked_bg = config
302 .checked_container_color
303 .unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_container_color);
304 let bg = if checked {
305 checked_bg
306 } else {
307 config.container_color.unwrap_or(Color::TRANSPARENT)
308 };
309 let fg = if checked { checked_cc } else { cc };
310 let se = config
311 .state_elevation
312 .unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
313 let pad_l = config
314 .content_padding
315 .map(|p| p.left)
316 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
317 let pad_r = config
318 .content_padding
319 .map(|p| p.right)
320 .unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
321 let mut m = Modifier::new()
322 .height(config.height)
323 .min_width(48.0)
324 .padding_values(PaddingValues {
325 left: pad_l,
326 right: pad_r,
327 top: 0.0,
328 bottom: 0.0,
329 })
330 .background(bg)
331 .align_items(AlignItems::CENTER)
332 .justify_content(JustifyContent::CENTER)
333 .state_colors(config.state_colors)
334 .state_elevation(se);
335 let tg_source: Rc<MutableInteractionSource> = config
336 .interaction_source
337 .clone()
338 .map(Rc::new)
339 .unwrap_or_else(|| remember(MutableInteractionSource::new));
340 if let Some((w, c, r)) = config.border {
341 m = m.border(w, c, r);
342 }
343 let cb = on_checked_change;
344 m = apply_m3_clickable(m, &tg_source, fg, config.enabled, move || cb(!checked));
345 if !config.enabled {
346 m = m.alpha(0.38);
347 }
348 m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
349 with_content_color(fg, || Box(m).child(content(checked)))
350}
351
352pub struct ButtonGroupMenuState {
354 pub is_showing: bool,
355}
356
357impl ButtonGroupMenuState {
358 pub fn dismiss(&mut self) {
359 self.is_showing = false;
360 }
361 pub fn show(&mut self) {
362 self.is_showing = true;
363 }
364}
365
366pub struct ButtonGroupScope {
368 items: Vec<ButtonGroupItem>,
369}
370
371#[allow(dead_code)] struct ButtonGroupItem {
374 button_group_content: Box<dyn FnOnce() -> View>,
375 menu_content: Option<Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View>>,
376}
377
378impl ButtonGroupScope {
379 fn new() -> Self {
380 Self { items: Vec::new() }
381 }
382
383 pub fn clickable_item(
385 &mut self,
386 on_click: impl Fn() + 'static,
387 label: String,
388 icon: Option<View>,
389 ) {
390 let cb = Rc::new(on_click);
391 let cb2 = cb.clone();
392 self.items.push(ButtonGroupItem {
393 button_group_content: Box::new(move || {
394 let cb = cb2.clone();
395 let config = super::ButtonConfig {
396 shape_radius: 0.0,
397 ..Default::default()
398 };
399 super::Button(
400 Modifier::new().flex_grow(1.0),
401 move || (cb)(),
402 config,
403 move || {
404 let label = label.clone();
405 let t = Text(label).single_line();
406 match icon.clone() {
407 Some(ic) => Box(Modifier::new()).child((ic, t)),
408 None => t,
409 }
410 },
411 )
412 }),
413 menu_content: None,
414 });
415 }
416
417 pub fn toggleable_item(
419 &mut self,
420 checked: bool,
421 on_checked_change: impl Fn(bool) + 'static,
422 label: String,
423 icon: Option<View>,
424 ) {
425 let cb = Rc::new(on_checked_change);
426 let cb2 = cb.clone();
427 self.items.push(ButtonGroupItem {
428 button_group_content: Box::new(move || {
429 let cb = cb2.clone();
430 let config = super::ToggleButtonConfig {
431 shape_radius: 0.0,
432 ..Default::default()
433 };
434 super::ToggleButton(
435 checked,
436 move |b| (cb)(b),
437 config,
438 move |_| {
439 let label = label.clone();
440 let t = Text(label).single_line();
441 match icon.clone() {
442 Some(ic) => Box(Modifier::new()).child((ic, t)),
443 None => t,
444 }
445 },
446 )
447 }),
448 menu_content: None,
449 });
450 }
451
452 pub fn custom_item(
454 &mut self,
455 button_group_content: impl FnOnce() -> View + 'static,
456 menu_content: Option<impl FnOnce(&mut ButtonGroupMenuState) -> View + 'static>,
457 ) {
458 self.items.push(ButtonGroupItem {
459 button_group_content: Box::new(button_group_content),
460 menu_content: menu_content.map(|f| {
461 let b: Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View> = Box::new(f);
462 b
463 }),
464 });
465 }
466}
467
468pub fn ButtonGroup(
473 modifier: Modifier,
474 gap: f32,
475 content: impl FnOnce(&mut ButtonGroupScope),
476) -> View {
477 let mut scope = ButtonGroupScope::new();
478 content(&mut scope);
479 Row(modifier.gap(gap).align_items(AlignItems::CENTER)).with_children(
480 scope
481 .items
482 .into_iter()
483 .map(|item| (item.button_group_content)())
484 .collect::<Vec<View>>(),
485 )
486}
487
488fn resolve_button_colors(
489 config: &super::ButtonConfig,
490 def: super::ButtonColors,
491) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
492 if let Some(colors) = &config.colors {
493 let bg = if config.enabled {
494 colors.container_color
495 } else {
496 colors.disabled_container_color
497 };
498 let cc = if config.enabled {
499 colors.content_color
500 } else {
501 colors.disabled_content_color
502 };
503 let sc = StateColors {
504 default: Color::TRANSPARENT,
505 hovered: Color::TRANSPARENT,
506 focused: Color::TRANSPARENT,
507 pressed: Color::TRANSPARENT,
508 dragged: colors.content_color.with_alpha_f32(0.12),
509 disabled: Color::TRANSPARENT,
510 };
511 let se = config.elevation.map(|e| StateElevation {
512 default: e.default,
513 hovered: e.hovered,
514 focused: e.focused,
515 pressed: e.pressed,
516 dragged: e.pressed,
517 disabled: e.disabled,
518 });
519 (cc, Some(bg), sc, se)
520 } else {
521 let cc = config.content_color.unwrap_or(def.content_color);
522 let bg = Some(config.container_color.unwrap_or(def.container_color));
523 let sc = if config.enabled {
524 config.state_colors
525 } else {
526 StateColors {
527 default: Color::TRANSPARENT,
528 hovered: Color::TRANSPARENT,
529 focused: Color::TRANSPARENT,
530 pressed: Color::TRANSPARENT,
531 dragged: Color::TRANSPARENT,
532 disabled: config.state_colors.disabled,
533 }
534 };
535 let se = config.state_elevation;
536 (cc, bg, sc, se)
537 }
538}