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