tessera_ui_basic_components/
button.rs

1//! An interactive button component.
2//!
3//! ## Usage
4//!
5//! Use for triggering actions, submitting forms, or navigation.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use tessera_ui::{Color, DimensionValue, Dp, accesskit::Role, tessera};
10
11use crate::{
12    ShadowProps,
13    material_color::global_material_scheme,
14    ripple_state::RippleState,
15    shape_def::Shape,
16    surface::{SurfaceArgsBuilder, surface},
17};
18
19/// Arguments for the `button` component.
20#[derive(Builder, Clone)]
21#[builder(pattern = "owned")]
22pub struct ButtonArgs {
23    /// The fill color of the button (RGBA).
24    #[builder(default = "crate::material_color::global_material_scheme().primary")]
25    pub color: Color,
26    /// The hover color of the button (RGBA). If None, no hover effect is applied.
27    #[builder(
28        default = "Some(crate::material_color::blend_over(crate::material_color::global_material_scheme().primary, crate::material_color::global_material_scheme().on_primary, 0.08))"
29    )]
30    pub hover_color: Option<Color>,
31    /// The shape of the button.
32    #[builder(default = "Shape::rounded_rectangle(Dp(20.0))")]
33    pub shape: Shape,
34    /// The padding of the button.
35    #[builder(default = "Dp(10.0)")]
36    pub padding: Dp,
37    /// Optional explicit width behavior for the button.
38    #[builder(default = "DimensionValue::WRAP", setter(into))]
39    pub width: DimensionValue,
40    /// Optional explicit height behavior for the button.
41    #[builder(default = "DimensionValue::WRAP", setter(into))]
42    pub height: DimensionValue,
43    /// The click callback function
44    #[builder(default, setter(strip_option))]
45    pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
46    /// The ripple color (RGB) for the button.
47    #[builder(
48        default = "crate::material_color::global_material_scheme().on_primary.with_alpha(0.12)"
49    )]
50    pub ripple_color: Color,
51    /// Width of the border. If > 0, an outline will be drawn.
52    #[builder(default = "Dp(0.0)")]
53    pub border_width: Dp,
54    /// Optional color for the border (RGBA). If None and border_width > 0, `color` will be used.
55    #[builder(default)]
56    pub border_color: Option<Color>,
57    /// Shadow of the button. If None, no shadow is applied.
58    #[builder(default, setter(strip_option))]
59    pub shadow: Option<ShadowProps>,
60    /// Optional label announced by assistive technologies (e.g., screen readers).
61    #[builder(default, setter(strip_option, into))]
62    pub accessibility_label: Option<String>,
63    /// Optional longer description or hint for assistive technologies.
64    #[builder(default, setter(strip_option, into))]
65    pub accessibility_description: Option<String>,
66}
67
68impl Default for ButtonArgs {
69    fn default() -> Self {
70        ButtonArgsBuilder::default()
71            .on_click(Arc::new(|| {}))
72            .build()
73            .expect("ButtonArgsBuilder default build should succeed")
74    }
75}
76
77/// # button
78///
79/// Provides a clickable button with customizable style and ripple feedback.
80///
81/// ## Usage
82///
83/// Use to trigger an action when the user clicks or taps.
84///
85/// ## Parameters
86///
87/// - `args` — configures the button's appearance and `on_click` handler; see [`ButtonArgs`].
88/// - `ripple_state` — a clonable [`RippleState`] to manage the ripple animation.
89/// - `child` — a closure that renders the button's content (e.g., text or an icon).
90///
91/// ## Examples
92///
93/// ```
94/// use std::sync::Arc;
95/// use tessera_ui::Color;
96/// use tessera_ui_basic_components::{
97///     button::{button, ButtonArgsBuilder},
98///     ripple_state::RippleState,
99///     text::{text, TextArgsBuilder},
100/// };
101///
102/// let ripple = RippleState::new();
103/// let args = ButtonArgsBuilder::default()
104///     .on_click(Arc::new(|| {}))
105///     .build()
106///     .unwrap();
107///
108/// button(args, ripple, || {
109///     text(TextArgsBuilder::default().text("Click Me".to_string()).build().expect("builder construction failed"));
110/// });
111/// ```
112#[tessera]
113pub fn button(args: impl Into<ButtonArgs>, ripple_state: RippleState, child: impl FnOnce()) {
114    let button_args: ButtonArgs = args.into();
115
116    // Create interactive surface for button
117    surface(create_surface_args(&button_args), Some(ripple_state), child);
118}
119
120/// Create surface arguments based on button configuration
121fn create_surface_args(args: &ButtonArgs) -> crate::surface::SurfaceArgs {
122    let style = if args.border_width.to_pixels_f32() > 0.0 {
123        crate::surface::SurfaceStyle::FilledOutlined {
124            fill_color: args.color,
125            border_color: args.border_color.unwrap_or(args.color),
126            border_width: args.border_width,
127        }
128    } else {
129        crate::surface::SurfaceStyle::Filled { color: args.color }
130    };
131
132    let hover_style = if let Some(hover_color) = args.hover_color {
133        let style = if args.border_width.to_pixels_f32() > 0.0 {
134            crate::surface::SurfaceStyle::FilledOutlined {
135                fill_color: hover_color,
136                border_color: args.border_color.unwrap_or(hover_color),
137                border_width: args.border_width,
138            }
139        } else {
140            crate::surface::SurfaceStyle::Filled { color: hover_color }
141        };
142        Some(style)
143    } else {
144        None
145    };
146
147    let mut builder = SurfaceArgsBuilder::default();
148
149    // Set shadow if available
150    if let Some(shadow) = args.shadow {
151        builder = builder.shadow(shadow);
152    }
153
154    // Set on_click handler if available
155    if let Some(on_click) = args.on_click.clone() {
156        builder = builder.on_click(on_click);
157    }
158
159    if let Some(label) = args.accessibility_label.clone() {
160        builder = builder.accessibility_label(label);
161    }
162
163    if let Some(description) = args.accessibility_description.clone() {
164        builder = builder.accessibility_description(description);
165    }
166
167    builder
168        .style(style)
169        .hover_style(hover_style)
170        .shape(args.shape)
171        .padding(args.padding)
172        .ripple_color(args.ripple_color)
173        .width(args.width)
174        .height(args.height)
175        .accessibility_role(Role::Button)
176        .accessibility_focusable(true)
177        .build()
178        .expect("SurfaceArgsBuilder failed with required button fields set")
179}
180
181/// Convenience constructors for standard Material Design 3 button styles
182impl ButtonArgs {
183    /// Create a standard "Filled" button (High emphasis).
184    /// Uses Primary color for container and OnPrimary for content.
185    pub fn filled(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
186        let scheme = global_material_scheme();
187        ButtonArgsBuilder::default()
188            .color(scheme.primary)
189            .hover_color(Some(crate::material_color::blend_over(
190                scheme.primary,
191                scheme.on_primary,
192                0.08,
193            )))
194            .ripple_color(scheme.on_primary.with_alpha(0.12))
195            .on_click(on_click)
196            .build()
197            .expect("ButtonArgsBuilder failed for filled button")
198    }
199
200    /// Create an "Elevated" button (Medium emphasis).
201    /// Uses Surface color (or SurfaceContainerLow if available) with a shadow.
202    pub fn elevated(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
203        let scheme = global_material_scheme();
204        ButtonArgsBuilder::default()
205            .color(scheme.surface)
206            .hover_color(Some(crate::material_color::blend_over(
207                scheme.surface,
208                scheme.primary,
209                0.08,
210            )))
211            .ripple_color(scheme.primary.with_alpha(0.12))
212            .shadow(ShadowProps::default())
213            .on_click(on_click)
214            .build()
215            .expect("ButtonArgsBuilder failed for elevated button")
216    }
217
218    /// Create a "Tonal" button (Medium emphasis).
219    /// Uses SecondaryContainer color for container and OnSecondaryContainer for content.
220    pub fn tonal(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
221        let scheme = global_material_scheme();
222        ButtonArgsBuilder::default()
223            .color(scheme.secondary_container)
224            .hover_color(Some(crate::material_color::blend_over(
225                scheme.secondary_container,
226                scheme.on_secondary_container,
227                0.08,
228            )))
229            .ripple_color(scheme.on_secondary_container.with_alpha(0.12))
230            .on_click(on_click)
231            .build()
232            .expect("ButtonArgsBuilder failed for tonal button")
233    }
234
235    /// Create an "Outlined" button (Medium emphasis).
236    /// Transparent container with an Outline border.
237    pub fn outlined(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
238        let scheme = global_material_scheme();
239        ButtonArgsBuilder::default()
240            .color(Color::TRANSPARENT)
241            .hover_color(Some(crate::material_color::blend_over(
242                Color::TRANSPARENT,
243                scheme.primary,
244                0.08,
245            )))
246            .ripple_color(scheme.primary.with_alpha(0.12))
247            .border_width(Dp(1.0))
248            .border_color(Some(scheme.outline))
249            .on_click(on_click)
250            .build()
251            .expect("ButtonArgsBuilder failed for outlined button")
252    }
253
254    /// Create a "Text" button (Low emphasis).
255    /// Transparent container and no border.
256    pub fn text(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
257        let scheme = global_material_scheme();
258        ButtonArgsBuilder::default()
259            .color(Color::TRANSPARENT)
260            .hover_color(Some(crate::material_color::blend_over(
261                Color::TRANSPARENT,
262                scheme.primary,
263                0.08,
264            )))
265            .ripple_color(scheme.primary.with_alpha(0.12))
266            .on_click(on_click)
267            .build()
268            .expect("ButtonArgsBuilder failed for text button")
269    }
270}
271
272/// Builder methods for fluent API
273impl ButtonArgs {
274    /// Sets the fill color for the button.
275    pub fn with_color(mut self, color: Color) -> Self {
276        self.color = color;
277        self
278    }
279
280    /// Sets the hover color applied when the pointer is over the button.
281    pub fn with_hover_color(mut self, hover_color: Color) -> Self {
282        self.hover_color = Some(hover_color);
283        self
284    }
285
286    /// Updates the padding inside the button.
287    pub fn with_padding(mut self, padding: Dp) -> Self {
288        self.padding = padding;
289        self
290    }
291
292    /// Overrides the button's shape.
293    pub fn with_shape(mut self, shape: Shape) -> Self {
294        self.shape = shape;
295        self
296    }
297
298    /// Sets a fixed or flexible width constraint.
299    pub fn with_width(mut self, width: DimensionValue) -> Self {
300        self.width = width;
301        self
302    }
303
304    /// Sets a fixed or flexible height constraint.
305    pub fn with_height(mut self, height: DimensionValue) -> Self {
306        self.height = height;
307        self
308    }
309
310    /// Adjusts the ripple color tint.
311    pub fn with_ripple_color(mut self, ripple_color: Color) -> Self {
312        self.ripple_color = ripple_color;
313        self
314    }
315
316    /// Configures the border width and optional color.
317    pub fn with_border(mut self, width: Dp, color: Option<Color>) -> Self {
318        self.border_width = width;
319        self.border_color = color;
320        self
321    }
322}