use std::sync::Arc;
use derive_setters::Setters;
use tessera_ui::{Color, Dp, Modifier, accesskit::Role, tessera, use_context};
use crate::{
alignment::Alignment,
modifier::ModifierExt,
shape_def::Shape,
surface::{SurfaceArgs, surface},
theme::{
ContentColor, MaterialAlpha, MaterialColorScheme, MaterialTheme, content_color_for,
provide_text_style,
},
};
pub struct ButtonDefaults;
impl ButtonDefaults {
pub const PRESSED_ALPHA: f32 = MaterialAlpha::PRESSED;
pub const DISABLED_CONTAINER_ALPHA: f32 = MaterialAlpha::DISABLED_CONTAINER;
pub const DISABLED_CONTENT_ALPHA: f32 = MaterialAlpha::DISABLED_CONTENT;
pub const FILLED_DISABLED_CONTAINER_ALPHA: f32 = 0.1;
pub const DISABLED_LABEL_ALPHA: f32 = 0.38;
pub const MIN_WIDTH: Dp = Dp(58.0);
pub const MIN_HEIGHT: Dp = Dp(40.0);
pub const CONTENT_HORIZONTAL_PADDING: Dp = Dp(16.0);
pub const CONTENT_VERTICAL_PADDING: Dp = Dp(8.0);
pub fn disabled_container_color(scheme: &MaterialColorScheme) -> Color {
scheme
.on_surface
.with_alpha(Self::FILLED_DISABLED_CONTAINER_ALPHA)
}
pub fn disabled_content_color(scheme: &MaterialColorScheme) -> Color {
scheme
.on_surface_variant
.with_alpha(Self::DISABLED_LABEL_ALPHA)
}
pub fn disabled_border_color(scheme: &MaterialColorScheme) -> Color {
scheme
.outline_variant
.with_alpha(Self::FILLED_DISABLED_CONTAINER_ALPHA)
}
}
#[derive(Clone, Setters)]
pub struct ButtonArgs {
pub enabled: bool,
pub modifier: Modifier,
pub color: Color,
#[setters(strip_option)]
pub content_color: Option<Color>,
pub shape: Shape,
pub padding: Dp,
#[setters(skip)]
pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
pub ripple_color: Color,
pub border_width: Dp,
pub border_color: Option<Color>,
#[setters(strip_option)]
pub elevation: Option<Dp>,
pub tonal_elevation: Dp,
pub disabled_container_color: Color,
pub disabled_content_color: Color,
pub disabled_border_color: Color,
#[setters(strip_option, into)]
pub accessibility_label: Option<String>,
#[setters(strip_option, into)]
pub accessibility_description: Option<String>,
}
impl ButtonArgs {
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Arc::new(on_click));
self
}
pub fn on_click_shared(mut self, on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
self.on_click = Some(on_click);
self
}
}
impl Default for ButtonArgs {
fn default() -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
Self {
enabled: true,
modifier: Modifier::new(),
color: scheme.primary,
content_color: None,
shape: Shape::capsule(),
padding: ButtonDefaults::CONTENT_VERTICAL_PADDING,
on_click: Some(Arc::new(|| {})),
ripple_color: scheme.on_primary,
border_width: Dp(0.0),
border_color: None,
elevation: None,
tonal_elevation: Dp(0.0),
disabled_container_color: ButtonDefaults::disabled_container_color(&scheme),
disabled_content_color: ButtonDefaults::disabled_content_color(&scheme),
disabled_border_color: ButtonDefaults::disabled_border_color(&scheme),
accessibility_label: None,
accessibility_description: None,
}
}
}
#[tessera]
pub fn button(args: impl Into<ButtonArgs>, child: impl FnOnce() + Send + Sync + 'static) {
let button_args: ButtonArgs = args.into();
let typography = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.typography;
surface(create_surface_args(&button_args), move || {
Modifier::new()
.padding_all(button_args.padding)
.run(move || {
provide_text_style(typography.label_large, child);
});
});
}
fn create_surface_args(args: &ButtonArgs) -> crate::surface::SurfaceArgs {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
let inherited_content_color = use_context::<ContentColor>()
.map(|c| c.get().current)
.unwrap_or(ContentColor::default().current);
let container_color = if args.enabled {
args.color
} else {
args.disabled_container_color
};
let content_color = if args.enabled {
args.content_color.unwrap_or_else(|| {
content_color_for(args.color, &scheme).unwrap_or(inherited_content_color)
})
} else {
args.disabled_content_color
};
let style = if args.border_width.to_pixels_f32() > 0.0 {
let border_color = if args.enabled {
args.border_color.unwrap_or(container_color)
} else {
args.disabled_border_color
};
crate::surface::SurfaceStyle::FilledOutlined {
fill_color: container_color,
border_color,
border_width: args.border_width,
}
} else {
crate::surface::SurfaceStyle::Filled {
color: container_color,
}
};
let mut surface_args = SurfaceArgs::default();
if let Some(elevation) = args.elevation {
surface_args = surface_args.elevation(elevation);
}
if args.enabled
&& let Some(on_click) = args.on_click.clone()
{
surface_args = surface_args.on_click_shared(on_click);
}
if let Some(label) = args.accessibility_label.clone() {
surface_args = surface_args.accessibility_label(label);
}
if let Some(description) = args.accessibility_description.clone() {
surface_args = surface_args.accessibility_description(description);
}
surface_args
.style(style)
.shape(args.shape)
.modifier(args.modifier.size_in(
Some(ButtonDefaults::MIN_WIDTH),
None,
Some(ButtonDefaults::MIN_HEIGHT),
None,
))
.ripple_color(args.ripple_color)
.content_alignment(Alignment::Center)
.content_color(content_color)
.enabled(args.enabled)
.tonal_elevation(args.tonal_elevation)
.accessibility_role(Role::Button)
.accessibility_focusable(true)
}
impl ButtonArgs {
pub fn filled(on_click: impl Fn() + Send + Sync + 'static) -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
ButtonArgs::default()
.color(scheme.primary)
.content_color(scheme.on_primary)
.ripple_color(scheme.on_primary)
.disabled_container_color(
scheme
.on_surface
.with_alpha(ButtonDefaults::FILLED_DISABLED_CONTAINER_ALPHA),
)
.disabled_content_color(
scheme
.on_surface_variant
.with_alpha(ButtonDefaults::DISABLED_LABEL_ALPHA),
)
.on_click(on_click)
}
pub fn elevated(on_click: impl Fn() + Send + Sync + 'static) -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
ButtonArgs::default()
.color(scheme.surface_container_low)
.content_color(scheme.primary)
.ripple_color(scheme.primary)
.elevation(Dp(1.0))
.disabled_container_color(
scheme
.on_surface
.with_alpha(ButtonDefaults::FILLED_DISABLED_CONTAINER_ALPHA),
)
.disabled_content_color(
scheme
.on_surface_variant
.with_alpha(ButtonDefaults::DISABLED_LABEL_ALPHA),
)
.on_click(on_click)
}
pub fn tonal(on_click: impl Fn() + Send + Sync + 'static) -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
ButtonArgs::default()
.color(scheme.secondary_container)
.content_color(scheme.on_secondary_container)
.ripple_color(scheme.on_secondary_container)
.disabled_container_color(
scheme
.on_surface
.with_alpha(ButtonDefaults::DISABLED_CONTAINER_ALPHA),
)
.disabled_content_color(
scheme
.on_surface
.with_alpha(ButtonDefaults::DISABLED_CONTENT_ALPHA),
)
.on_click(on_click)
}
pub fn outlined(on_click: impl Fn() + Send + Sync + 'static) -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
ButtonArgs::default()
.color(Color::TRANSPARENT)
.content_color(scheme.on_surface_variant)
.disabled_container_color(Color::TRANSPARENT)
.ripple_color(scheme.on_surface_variant)
.border_width(Dp(1.0))
.border_color(Some(scheme.outline_variant))
.disabled_border_color(
scheme
.outline_variant
.with_alpha(ButtonDefaults::FILLED_DISABLED_CONTAINER_ALPHA),
)
.disabled_content_color(
scheme
.on_surface_variant
.with_alpha(ButtonDefaults::DISABLED_LABEL_ALPHA),
)
.on_click(on_click)
}
pub fn text(on_click: impl Fn() + Send + Sync + 'static) -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
ButtonArgs::default()
.color(Color::TRANSPARENT)
.content_color(scheme.primary)
.disabled_container_color(Color::TRANSPARENT)
.ripple_color(scheme.primary)
.disabled_content_color(
scheme
.on_surface_variant
.with_alpha(ButtonDefaults::DISABLED_LABEL_ALPHA),
)
.on_click(on_click)
}
}
impl ButtonArgs {
pub fn with_color(mut self, color: Color) -> Self {
self.color = color;
self
}
pub fn with_padding(mut self, padding: Dp) -> Self {
self.padding = padding;
self
}
pub fn with_shape(mut self, shape: Shape) -> Self {
self.shape = shape;
self
}
pub fn with_ripple_color(mut self, ripple_color: Color) -> Self {
self.ripple_color = ripple_color;
self
}
pub fn with_border(mut self, width: Dp, color: Option<Color>) -> Self {
self.border_width = width;
self.border_color = color;
self
}
}