#![allow(non_snake_case)]
use std::rc::Rc;
use super::{ButtonGroupDefaults, SplitButtonDefaults};
use repose_core::{locals::with_content_color, *};
use repose_ui::{Box, Row, Text, TextStyle, ViewExt};
#[derive(Clone)]
pub struct SplitButtonConfig {
pub modifier: Modifier,
pub spacing: f32,
}
impl Default for SplitButtonConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
spacing: SplitButtonDefaults::SPACING,
}
}
}
pub fn SplitButtonLayout(
leading_button: View,
trailing_button: View,
config: SplitButtonConfig,
) -> View {
Row(config
.modifier
.gap(config.spacing)
.align_items(AlignItems::CENTER))
.child((leading_button, trailing_button))
}
fn split_leading_shape_radii() -> [f32; 4] {
[
SplitButtonDefaults::OUTER_CORNER_SIZE,
SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
SplitButtonDefaults::OUTER_CORNER_SIZE,
]
}
fn split_trailing_shape_radii() -> [f32; 4] {
[
SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
SplitButtonDefaults::OUTER_CORNER_SIZE,
SplitButtonDefaults::OUTER_CORNER_SIZE,
SplitButtonDefaults::SMALL_INNER_CORNER_SIZE,
]
}
fn split_button_impl(
outer_modifier: Modifier,
on_click: impl Fn() + 'static,
content: impl FnOnce() -> View,
content_color: Color,
container_color: Option<Color>,
state_colors: StateColors,
state_elevation: Option<StateElevation>,
border: Option<(f32, Color, f32)>,
pad_left: f32,
pad_right: f32,
height: f32,
enabled: bool,
interaction_source: Option<MutableInteractionSource>,
) -> View {
let mut m = Modifier::new()
.height(height)
.min_width(48.0)
.padding_values(PaddingValues {
left: pad_left,
right: pad_right,
top: 0.0,
bottom: 0.0,
})
.align_items(AlignItems::CENTER)
.justify_content(JustifyContent::CENTER);
if let Some(bg) = container_color {
m = m.background(bg);
}
m = m.state_colors(if enabled {
state_colors
} else {
StateColors {
default: Color::TRANSPARENT,
hovered: Color::TRANSPARENT,
pressed: Color::TRANSPARENT,
disabled: state_colors.disabled,
}
});
if let Some(se) = state_elevation {
m = m.state_elevation(se);
}
if let Some((w, c, r)) = border {
m = m.border(w, c, r);
}
let source: Rc<MutableInteractionSource> = interaction_source
.map(Rc::new)
.unwrap_or_else(|| remember(MutableInteractionSource::new));
m = m.interaction_source(&*source);
m = m.indication(crate::ripple::ripple(crate::ripple::RippleConfig {
color: Some(content_color),
bounded: true,
..Default::default()
}));
if enabled {
m = m.clickable().on_click(move || on_click());
}
m = m.then(outer_modifier);
let effective = if enabled {
content_color
} else {
content_color.with_alpha_f32(0.38)
};
with_content_color(effective, || Box(m).child(content()))
}
pub fn SplitButtonLeadingButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
config: super::ButtonConfig,
content: impl FnOnce() -> View,
) -> View {
let def = super::ButtonColors {
container_color: super::ButtonDefaults::container_color(),
content_color: super::ButtonDefaults::content_color(),
disabled_container_color: super::ButtonDefaults::container_color()
.with_alpha_f32(0.12)
.composite_over(theme().surface_container_low),
disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
};
let (cc, bg, sc, se) = resolve_button_colors(&config, def);
let pad = config
.content_padding
.unwrap_or(SplitButtonDefaults::small_leading_content_padding());
split_button_impl(
modifier.clip_rounded_radii(split_leading_shape_radii()),
on_click,
content,
cc,
bg,
sc,
se.or(Some(super::ButtonDefaults::state_elevation_default())),
config.border,
pad.left,
pad.right,
config.height,
config.enabled,
config.interaction_source.clone(),
)
}
pub fn SplitButtonTrailingButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
config: super::ButtonConfig,
content: impl FnOnce() -> View,
) -> View {
let def = super::ButtonColors {
container_color: super::ButtonDefaults::container_color(),
content_color: super::ButtonDefaults::content_color(),
disabled_container_color: super::ButtonDefaults::container_color()
.with_alpha_f32(0.12)
.composite_over(theme().surface_container_low),
disabled_content_color: super::ButtonDefaults::content_color().with_alpha_f32(0.38),
};
let (cc, bg, sc, se) = resolve_button_colors(&config, def);
let pad = config
.content_padding
.unwrap_or(SplitButtonDefaults::small_trailing_content_padding());
split_button_impl(
modifier.clip_rounded_radii(split_trailing_shape_radii()),
on_click,
content,
cc,
bg,
sc,
se.or(Some(super::ButtonDefaults::state_elevation_default())),
config.border,
pad.left,
pad.right,
config.height,
config.enabled,
config.interaction_source.clone(),
)
}
pub fn SplitButtonTrailingToggleButton(
checked: bool,
on_checked_change: impl Fn(bool) + 'static,
modifier: Modifier,
config: super::ToggleButtonConfig,
content: impl FnOnce(bool) -> View,
) -> View {
let th = theme();
let cc = config
.content_color
.unwrap_or_else(super::ToggleButtonDefaults::content_color);
let checked_cc = config
.checked_content_color
.unwrap_or_else(super::ToggleButtonDefaults::checked_content_color);
let checked_bg = config
.checked_container_color
.unwrap_or_else(super::ToggleButtonDefaults::checked_container_color);
let bg = if checked {
checked_bg
} else {
config.container_color.unwrap_or(Color::TRANSPARENT)
};
let fg = if checked { checked_cc } else { cc };
let se = config
.state_elevation
.unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
let pad_l = config
.content_padding
.map(|p| p.left)
.unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
let pad_r = config
.content_padding
.map(|p| p.right)
.unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
let mut m = Modifier::new()
.height(config.height)
.min_width(48.0)
.padding_values(PaddingValues {
left: pad_l,
right: pad_r,
top: 0.0,
bottom: 0.0,
})
.background(bg)
.align_items(AlignItems::CENTER)
.justify_content(JustifyContent::CENTER)
.state_colors(config.state_colors)
.state_elevation(se);
let tg_source: Rc<MutableInteractionSource> = config
.interaction_source
.clone()
.map(Rc::new)
.unwrap_or_else(|| remember(MutableInteractionSource::new));
m = m.interaction_source(&*tg_source);
if let Some((w, c, r)) = config.border {
m = m.border(w, c, r);
}
if config.enabled {
let cb = on_checked_change;
m = m.clickable().on_click(move || cb(!checked));
} else {
m = m.alpha(0.38);
}
m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
with_content_color(fg, || Box(m).child(content(checked)))
}
pub fn SplitButtonTonalLeadingButton(
modifier: Modifier,
on_click: impl Fn() + 'static,
config: super::ButtonConfig,
content: impl FnOnce() -> View,
) -> View {
let def = super::ButtonColors {
container_color: super::ButtonDefaults::tonal_container_color(),
content_color: super::ButtonDefaults::tonal_content_color(),
disabled_container_color: theme()
.on_surface
.with_alpha_f32(0.12)
.composite_over(theme().surface_container_low),
disabled_content_color: theme().on_surface.with_alpha_f32(0.38),
};
let (cc, bg, sc, se) = resolve_button_colors(&config, def);
let pad = config
.content_padding
.unwrap_or(SplitButtonDefaults::small_leading_content_padding());
split_button_impl(
modifier.clip_rounded_radii(split_leading_shape_radii()),
on_click,
content,
cc,
bg,
sc,
se.or(Some(super::ButtonDefaults::elevated_state_elevation())),
config.border,
pad.left,
pad.right,
config.height,
config.enabled,
config.interaction_source.clone(),
)
}
pub fn SplitButtonTonalTrailingToggleButton(
checked: bool,
on_checked_change: impl Fn(bool) + 'static,
modifier: Modifier,
config: super::ToggleButtonConfig,
content: impl FnOnce(bool) -> View,
) -> View {
let th = theme();
let cc = config
.content_color
.unwrap_or_else(super::ToggleButtonDefaults::tonal_content_color);
let checked_cc = config
.checked_content_color
.unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_content_color);
let checked_bg = config
.checked_container_color
.unwrap_or_else(super::ToggleButtonDefaults::tonal_checked_container_color);
let bg = if checked {
checked_bg
} else {
config.container_color.unwrap_or(Color::TRANSPARENT)
};
let fg = if checked { checked_cc } else { cc };
let se = config
.state_elevation
.unwrap_or_else(super::ToggleButtonDefaults::state_elevation_default);
let pad_l = config
.content_padding
.map(|p| p.left)
.unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
let pad_r = config
.content_padding
.map(|p| p.right)
.unwrap_or(super::ToggleButtonDefaults::HORIZONTAL_PADDING);
let mut m = Modifier::new()
.height(config.height)
.min_width(48.0)
.padding_values(PaddingValues {
left: pad_l,
right: pad_r,
top: 0.0,
bottom: 0.0,
})
.background(bg)
.align_items(AlignItems::CENTER)
.justify_content(JustifyContent::CENTER)
.state_colors(config.state_colors)
.state_elevation(se);
let tg_source: Rc<MutableInteractionSource> = config
.interaction_source
.clone()
.map(Rc::new)
.unwrap_or_else(|| remember(MutableInteractionSource::new));
m = m.interaction_source(&*tg_source);
if let Some((w, c, r)) = config.border {
m = m.border(w, c, r);
}
if config.enabled {
let cb = on_checked_change;
m = m.clickable().on_click(move || cb(!checked));
} else {
m = m.alpha(0.38);
}
m = m.then(modifier.clip_rounded_radii(split_trailing_shape_radii()));
with_content_color(fg, || Box(m).child(content(checked)))
}
pub struct ButtonGroupMenuState {
pub is_showing: bool,
}
impl ButtonGroupMenuState {
pub fn dismiss(&mut self) {
self.is_showing = false;
}
pub fn show(&mut self) {
self.is_showing = true;
}
}
pub struct ButtonGroupScope {
items: Vec<ButtonGroupItem>,
connected: bool,
}
struct ButtonGroupItem {
button_group_content: Box<dyn FnOnce() -> View>,
menu_content: Option<Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View>>,
}
impl ButtonGroupScope {
fn new() -> Self {
Self {
items: Vec::new(),
connected: false,
}
}
pub fn clickable_item(
&mut self,
on_click: impl Fn() + 'static,
label: String,
icon: Option<View>,
) {
let cb = Rc::new(on_click);
let cb2 = cb.clone();
self.items.push(ButtonGroupItem {
button_group_content: Box::new(move || {
let cb = cb2.clone();
let config = super::ButtonConfig {
shape_radius: 0.0,
..Default::default()
};
super::Button(
Modifier::new().flex_grow(1.0),
move || (cb)(),
config,
move || {
let label = label.clone();
let t = Text(label).single_line();
match icon.clone() {
Some(ic) => Box(Modifier::new()).child((ic, t)),
None => t,
}
},
)
}),
menu_content: None,
});
}
pub fn toggleable_item(
&mut self,
checked: bool,
on_checked_change: impl Fn(bool) + 'static,
label: String,
icon: Option<View>,
) {
let cb = Rc::new(on_checked_change);
let cb2 = cb.clone();
self.items.push(ButtonGroupItem {
button_group_content: Box::new(move || {
let cb = cb2.clone();
let config = super::ToggleButtonConfig {
shape_radius: 0.0,
..Default::default()
};
super::ToggleButton(
checked,
move |b| (cb)(b),
config,
move |_| {
let label = label.clone();
let t = Text(label).single_line();
match icon.clone() {
Some(ic) => Box(Modifier::new()).child((ic, t)),
None => t,
}
},
)
}),
menu_content: None,
});
}
pub fn custom_item(
&mut self,
button_group_content: impl FnOnce() -> View + 'static,
menu_content: Option<impl FnOnce(&mut ButtonGroupMenuState) -> View + 'static>,
) {
self.items.push(ButtonGroupItem {
button_group_content: Box::new(button_group_content),
menu_content: menu_content.map(|f| {
let b: Box<dyn FnOnce(&mut ButtonGroupMenuState) -> View> = Box::new(f);
b
}),
});
}
}
pub fn ButtonGroup(
modifier: Modifier,
gap: f32,
content: impl FnOnce(&mut ButtonGroupScope),
) -> View {
let mut scope = ButtonGroupScope::new();
content(&mut scope);
Row(modifier.gap(gap).align_items(AlignItems::CENTER)).with_children(
scope
.items
.into_iter()
.map(|item| (item.button_group_content)())
.collect::<Vec<View>>(),
)
}
fn resolve_button_colors(
config: &super::ButtonConfig,
def: super::ButtonColors,
) -> (Color, Option<Color>, StateColors, Option<StateElevation>) {
if let Some(colors) = &config.colors {
let bg = if config.enabled {
colors.container_color
} else {
colors.disabled_container_color
};
let cc = if config.enabled {
colors.content_color
} else {
colors.disabled_content_color
};
let sc = StateColors {
default: Color::TRANSPARENT,
hovered: colors.content_color.with_alpha_f32(0.08),
pressed: colors.content_color.with_alpha_f32(0.12),
disabled: Color::TRANSPARENT,
};
let se = config.elevation.map(|e| StateElevation {
default: e.default,
hovered: e.hovered,
pressed: e.pressed,
disabled: e.disabled,
});
(cc, Some(bg), sc, se)
} else {
let cc = config.content_color.unwrap_or(def.content_color);
let bg = Some(config.container_color.unwrap_or(def.container_color));
let sc = if config.enabled {
config.state_colors
} else {
StateColors {
default: Color::TRANSPARENT,
hovered: Color::TRANSPARENT,
pressed: Color::TRANSPARENT,
disabled: config.state_colors.disabled,
}
};
let se = config.state_elevation;
(cc, bg, sc, se)
}
}