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