tessera_ui_basic_components/
button.rs

1//! Provides a highly customizable and interactive button component for Tessera UI.
2//!
3//! This module defines the [`button`] component and its configuration via [`ButtonArgs`].
4//! The button supports custom colors, shapes, padding, border, ripple effects, and hover states.
5//! It is designed to wrap arbitrary child content and handle user interactions such as clicks
6//! with visual feedback. Typical use cases include triggering actions, submitting forms, or
7//! serving as a core interactive element in user interfaces.
8//!
9//! The API offers builder patterns and convenience constructors for common button styles
10//! (primary, secondary, success, danger), making it easy to create consistent and accessible
11//! buttons throughout your application.
12//!
13//! Example usage and customization patterns are provided in the [`button`] documentation.
14//!
15//! # Features
16//! - Customizable appearance: color, shape, border, padding, ripple, hover
17//! - Flexible sizing: explicit width/height or content-based
18//! - Event handling: on_click callback
19//! - Composable: can wrap any child component
20//! - Builder and fluent APIs for ergonomic usage
21//!
22//! See [`button`] and [`ButtonArgs`] for details.
23use std::sync::Arc;
24
25use derive_builder::Builder;
26use tessera_ui::{Color, DimensionValue, Dp};
27use tessera_ui_macros::tessera;
28
29use crate::{
30    ripple_state::RippleState,
31    shape_def::Shape,
32    surface::{SurfaceArgsBuilder, surface},
33};
34
35/// Arguments for the `button` component.
36#[derive(Builder, Clone)]
37#[builder(pattern = "owned")]
38pub struct ButtonArgs {
39    /// The fill color of the button (RGBA).
40    #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
41    pub color: Color,
42    /// The hover color of the button (RGBA). If None, no hover effect is applied.
43    #[builder(default)]
44    pub hover_color: Option<Color>,
45    /// The shape of the button.
46    #[builder(default = "Shape::RoundedRectangle { corner_radius: 25.0, g2_k_value: 3.0 }")]
47    pub shape: Shape,
48    /// The padding of the button.
49    #[builder(default = "Dp(12.0)")]
50    pub padding: Dp,
51    /// Optional explicit width behavior for the button.
52    #[builder(default, setter(strip_option))]
53    pub width: Option<DimensionValue>,
54    /// Optional explicit height behavior for the button.
55    #[builder(default, setter(strip_option))]
56    pub height: Option<DimensionValue>,
57    /// The click callback function
58    pub on_click: Arc<dyn Fn() + Send + Sync>,
59    /// The ripple color (RGB) for the button.
60    #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
61    pub ripple_color: Color,
62    /// Width of the border. If > 0, an outline will be drawn.
63    #[builder(default = "0.0")]
64    pub border_width: f32,
65    /// Optional color for the border (RGBA). If None and border_width > 0, `color` will be used.
66    #[builder(default)]
67    pub border_color: Option<Color>,
68}
69
70impl std::fmt::Debug for ButtonArgs {
71    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
72        f.debug_struct("ButtonArgs")
73            .field("color", &self.color)
74            .field("hover_color", &self.hover_color)
75            .field("shape", &self.shape)
76            .field("padding", &self.padding)
77            .field("width", &self.width)
78            .field("height", &self.height)
79            .field("on_click", &"<callback>")
80            .field("ripple_color", &self.ripple_color)
81            .field("border_width", &self.border_width)
82            .field("border_color", &self.border_color)
83            .finish()
84    }
85}
86
87impl Default for ButtonArgs {
88    fn default() -> Self {
89        ButtonArgsBuilder::default()
90            .on_click(Arc::new(|| {}))
91            .build()
92            .unwrap()
93    }
94}
95
96/// Creates an interactive button component that can wrap any custom child content.
97///
98/// The `button` component provides a clickable surface with a ripple effect,
99/// customizable appearance, and event handling. It's built on top of the `surface`
100/// component and handles user interactions like clicks and hover states.
101///
102/// # Parameters
103///
104/// - `args`: An instance of `ButtonArgs` or `ButtonArgsBuilder` that defines the button's
105///   properties, such as color, shape, padding, and the `on_click` callback.
106/// - `ripple_state`: An `Arc<RippleState>` that manages the visual state of the ripple
107///   effect. This should be created and managed by the parent component to persist
108///   the ripple animation state across recompositions.
109/// - `child`: A closure that defines the content to be displayed inside the button.
110///   This can be any other component, such as `text`, `image`, or a combination of them.
111///
112/// # Example
113///
114/// ```
115/// # use std::sync::Arc;
116/// # use tessera_ui::{Color, Dp};
117/// # use tessera_ui_basic_components::{
118/// #     button::{button, ButtonArgsBuilder},
119/// #     ripple_state::RippleState,
120/// #     text::{text, TextArgsBuilder},
121/// # };
122/// #
123/// // 1. Create a ripple state to manage the effect.
124/// let ripple_state = Arc::new(RippleState::new());
125///
126/// // 2. Define the button's properties using the builder pattern.
127/// let args = ButtonArgsBuilder::default()
128///     .color(Color::new(0.2, 0.5, 0.8, 1.0)) // A nice blue
129///     .padding(Dp(12.0))
130///     .on_click(Arc::new(|| {
131///         println!("Button was clicked!");
132///     }))
133///     .build()
134///     .unwrap();
135///
136/// // 3. Call the button component, passing the args, state, and a child content closure.
137/// button(args, ripple_state, || {
138///     text(
139///         TextArgsBuilder::default()
140///             .text("Click Me".to_string())
141///             .color(Color::WHITE)
142///             .build()
143///             .unwrap(),
144///     );
145/// });
146/// ```
147#[tessera]
148pub fn button(args: impl Into<ButtonArgs>, ripple_state: Arc<RippleState>, child: impl FnOnce()) {
149    let button_args: ButtonArgs = args.into();
150
151    // Create interactive surface for button
152    surface(create_surface_args(&button_args), Some(ripple_state), child);
153}
154
155/// Create surface arguments based on button configuration
156fn create_surface_args(args: &ButtonArgs) -> crate::surface::SurfaceArgs {
157    let mut builder = SurfaceArgsBuilder::default();
158
159    // Set width if available
160    if let Some(width) = args.width {
161        builder = builder.width(width);
162    }
163
164    // Set height if available
165    if let Some(height) = args.height {
166        builder = builder.height(height);
167    }
168
169    builder
170        .color(args.color)
171        .hover_color(args.hover_color)
172        .shape(args.shape)
173        .padding(args.padding)
174        .border_width(args.border_width)
175        .border_color(args.border_color)
176        .ripple_color(args.ripple_color)
177        .on_click(Some(args.on_click.clone()))
178        .build()
179        .unwrap()
180}
181
182/// Convenience constructors for common button styles
183impl ButtonArgs {
184    /// Create a primary button with default blue styling
185    pub fn primary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
186        ButtonArgsBuilder::default()
187            .color(Color::new(0.2, 0.5, 0.8, 1.0)) // Blue
188            .on_click(on_click)
189            .build()
190            .unwrap()
191    }
192
193    /// Create a secondary button with gray styling
194    pub fn secondary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
195        ButtonArgsBuilder::default()
196            .color(Color::new(0.6, 0.6, 0.6, 1.0)) // Gray
197            .on_click(on_click)
198            .build()
199            .unwrap()
200    }
201
202    /// Create a success button with green styling
203    pub fn success(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
204        ButtonArgsBuilder::default()
205            .color(Color::new(0.1, 0.7, 0.3, 1.0)) // Green
206            .on_click(on_click)
207            .build()
208            .unwrap()
209    }
210
211    /// Create a danger button with red styling
212    pub fn danger(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
213        ButtonArgsBuilder::default()
214            .color(Color::new(0.8, 0.2, 0.2, 1.0)) // Red
215            .on_click(on_click)
216            .build()
217            .unwrap()
218    }
219}
220
221/// Builder methods for fluent API
222impl ButtonArgs {
223    pub fn with_color(mut self, color: Color) -> Self {
224        self.color = color;
225        self
226    }
227
228    pub fn with_hover_color(mut self, hover_color: Color) -> Self {
229        self.hover_color = Some(hover_color);
230        self
231    }
232
233    pub fn with_padding(mut self, padding: Dp) -> Self {
234        self.padding = padding;
235        self
236    }
237
238    pub fn with_shape(mut self, shape: Shape) -> Self {
239        self.shape = shape;
240        self
241    }
242
243    pub fn with_width(mut self, width: DimensionValue) -> Self {
244        self.width = Some(width);
245        self
246    }
247
248    pub fn with_height(mut self, height: DimensionValue) -> Self {
249        self.height = Some(height);
250        self
251    }
252
253    pub fn with_ripple_color(mut self, ripple_color: Color) -> Self {
254        self.ripple_color = ripple_color;
255        self
256    }
257
258    pub fn with_border(mut self, width: f32, color: Option<Color>) -> Self {
259        self.border_width = width;
260        self.border_color = color;
261        self
262    }
263}