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    pipelines::ShadowProps,
13    ripple_state::RippleState,
14    shape_def::Shape,
15    surface::{SurfaceArgsBuilder, surface},
16};
17
18/// Arguments for the `button` component.
19#[derive(Builder, Clone)]
20#[builder(pattern = "owned")]
21pub struct ButtonArgs {
22    /// The fill color of the button (RGBA).
23    #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
24    pub color: Color,
25    /// The hover color of the button (RGBA). If None, no hover effect is applied.
26    #[builder(default)]
27    pub hover_color: Option<Color>,
28    /// The shape of the button.
29    #[builder(
30        default = "Shape::RoundedRectangle { top_left: Dp(25.0), top_right: Dp(25.0), bottom_right: Dp(25.0), bottom_left: Dp(25.0), g2_k_value: 3.0 }"
31    )]
32    pub shape: Shape,
33    /// The padding of the button.
34    #[builder(default = "Dp(12.0)")]
35    pub padding: Dp,
36    /// Optional explicit width behavior for the button.
37    #[builder(default = "DimensionValue::WRAP", setter(into))]
38    pub width: DimensionValue,
39    /// Optional explicit height behavior for the button.
40    #[builder(default = "DimensionValue::WRAP", setter(into))]
41    pub height: DimensionValue,
42    /// The click callback function
43    #[builder(default, setter(strip_option))]
44    pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
45    /// The ripple color (RGB) for the button.
46    #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
47    pub ripple_color: Color,
48    /// Width of the border. If > 0, an outline will be drawn.
49    #[builder(default = "Dp(0.0)")]
50    pub border_width: Dp,
51    /// Optional color for the border (RGBA). If None and border_width > 0, `color` will be used.
52    #[builder(default)]
53    pub border_color: Option<Color>,
54    /// Shadow of the button. If None, no shadow is applied.
55    #[builder(default, setter(strip_option))]
56    pub shadow: Option<ShadowProps>,
57    /// Optional label announced by assistive technologies (e.g., screen readers).
58    #[builder(default, setter(strip_option, into))]
59    pub accessibility_label: Option<String>,
60    /// Optional longer description or hint for assistive technologies.
61    #[builder(default, setter(strip_option, into))]
62    pub accessibility_description: Option<String>,
63}
64
65impl Default for ButtonArgs {
66    fn default() -> Self {
67        ButtonArgsBuilder::default()
68            .on_click(Arc::new(|| {}))
69            .build()
70            .unwrap()
71    }
72}
73
74/// # button
75///
76/// Provides a clickable button with customizable style and ripple feedback.
77///
78/// ## Usage
79///
80/// Use to trigger an action when the user clicks or taps.
81///
82/// ## Parameters
83///
84/// - `args` — configures the button's appearance and `on_click` handler; see [`ButtonArgs`].
85/// - `ripple_state` — a clonable [`RippleState`] to manage the ripple animation.
86/// - `child` — a closure that renders the button's content (e.g., text or an icon).
87///
88/// ## Examples
89///
90/// ```
91/// use std::sync::Arc;
92/// use tessera_ui::Color;
93/// use tessera_ui_basic_components::{
94///     button::{button, ButtonArgsBuilder},
95///     ripple_state::RippleState,
96///     text::{text, TextArgsBuilder},
97/// };
98///
99/// let ripple = RippleState::new();
100/// let args = ButtonArgsBuilder::default()
101///     .on_click(Arc::new(|| {}))
102///     .build()
103///     .unwrap();
104///
105/// button(args, ripple, || {
106///     text(TextArgsBuilder::default().text("Click Me".to_string()).build().unwrap());
107/// });
108/// ```
109#[tessera]
110pub fn button(args: impl Into<ButtonArgs>, ripple_state: RippleState, child: impl FnOnce()) {
111    let button_args: ButtonArgs = args.into();
112
113    // Create interactive surface for button
114    surface(create_surface_args(&button_args), Some(ripple_state), child);
115}
116
117/// Create surface arguments based on button configuration
118fn create_surface_args(args: &ButtonArgs) -> crate::surface::SurfaceArgs {
119    let style = if args.border_width.to_pixels_f32() > 0.0 {
120        crate::surface::SurfaceStyle::FilledOutlined {
121            fill_color: args.color,
122            border_color: args.border_color.unwrap_or(args.color),
123            border_width: args.border_width,
124        }
125    } else {
126        crate::surface::SurfaceStyle::Filled { color: args.color }
127    };
128
129    let hover_style = if let Some(hover_color) = args.hover_color {
130        let style = if args.border_width.to_pixels_f32() > 0.0 {
131            crate::surface::SurfaceStyle::FilledOutlined {
132                fill_color: hover_color,
133                border_color: args.border_color.unwrap_or(hover_color),
134                border_width: args.border_width,
135            }
136        } else {
137            crate::surface::SurfaceStyle::Filled { color: hover_color }
138        };
139        Some(style)
140    } else {
141        None
142    };
143
144    let mut builder = SurfaceArgsBuilder::default();
145
146    // Set shadow if available
147    if let Some(shadow) = args.shadow {
148        builder = builder.shadow(shadow);
149    }
150
151    // Set on_click handler if available
152    if let Some(on_click) = args.on_click.clone() {
153        builder = builder.on_click(on_click);
154    }
155
156    if let Some(label) = args.accessibility_label.clone() {
157        builder = builder.accessibility_label(label);
158    }
159
160    if let Some(description) = args.accessibility_description.clone() {
161        builder = builder.accessibility_description(description);
162    }
163
164    builder
165        .style(style)
166        .hover_style(hover_style)
167        .shape(args.shape)
168        .padding(args.padding)
169        .ripple_color(args.ripple_color)
170        .width(args.width)
171        .height(args.height)
172        .accessibility_role(Role::Button)
173        .accessibility_focusable(true)
174        .build()
175        .unwrap()
176}
177
178/// Convenience constructors for common button styles
179impl ButtonArgs {
180    /// Create a primary button with default blue styling
181    pub fn primary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
182        ButtonArgsBuilder::default()
183            .color(Color::new(0.2, 0.5, 0.8, 1.0)) // Blue
184            .on_click(on_click)
185            .build()
186            .unwrap()
187    }
188
189    /// Create a secondary button with gray styling
190    pub fn secondary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
191        ButtonArgsBuilder::default()
192            .color(Color::new(0.6, 0.6, 0.6, 1.0)) // Gray
193            .on_click(on_click)
194            .build()
195            .unwrap()
196    }
197
198    /// Create a success button with green styling
199    pub fn success(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
200        ButtonArgsBuilder::default()
201            .color(Color::new(0.1, 0.7, 0.3, 1.0)) // Green
202            .on_click(on_click)
203            .build()
204            .unwrap()
205    }
206
207    /// Create a danger button with red styling
208    pub fn danger(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
209        ButtonArgsBuilder::default()
210            .color(Color::new(0.8, 0.2, 0.2, 1.0)) // Red
211            .on_click(on_click)
212            .build()
213            .unwrap()
214    }
215}
216
217/// Builder methods for fluent API
218impl ButtonArgs {
219    pub fn with_color(mut self, color: Color) -> Self {
220        self.color = color;
221        self
222    }
223
224    pub fn with_hover_color(mut self, hover_color: Color) -> Self {
225        self.hover_color = Some(hover_color);
226        self
227    }
228
229    pub fn with_padding(mut self, padding: Dp) -> Self {
230        self.padding = padding;
231        self
232    }
233
234    pub fn with_shape(mut self, shape: Shape) -> Self {
235        self.shape = shape;
236        self
237    }
238
239    pub fn with_width(mut self, width: DimensionValue) -> Self {
240        self.width = width;
241        self
242    }
243
244    pub fn with_height(mut self, height: DimensionValue) -> Self {
245        self.height = height;
246        self
247    }
248
249    pub fn with_ripple_color(mut self, ripple_color: Color) -> Self {
250        self.ripple_color = ripple_color;
251        self
252    }
253
254    pub fn with_border(mut self, width: Dp, color: Option<Color>) -> Self {
255        self.border_width = width;
256        self.border_color = color;
257        self
258    }
259}