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, tessera};
27
28use crate::{
29    pipelines::ShadowProps,
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(
47        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 }"
48    )]
49    pub shape: Shape,
50    /// The padding of the button.
51    #[builder(default = "Dp(12.0)")]
52    pub padding: Dp,
53    /// Optional explicit width behavior for the button.
54    #[builder(default = "DimensionValue::WRAP", setter(into))]
55    pub width: DimensionValue,
56    /// Optional explicit height behavior for the button.
57    #[builder(default = "DimensionValue::WRAP", setter(into))]
58    pub height: DimensionValue,
59    /// The click callback function
60    #[builder(default, setter(strip_option))]
61    pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
62    /// The ripple color (RGB) for the button.
63    #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
64    pub ripple_color: Color,
65    /// Width of the border. If > 0, an outline will be drawn.
66    #[builder(default = "Dp(0.0)")]
67    pub border_width: Dp,
68    /// Optional color for the border (RGBA). If None and border_width > 0, `color` will be used.
69    #[builder(default)]
70    pub border_color: Option<Color>,
71    /// Shadow of the button. If None, no shadow is applied.
72    #[builder(default, setter(strip_option))]
73    pub shadow: Option<ShadowProps>,
74}
75
76impl Default for ButtonArgs {
77    fn default() -> Self {
78        ButtonArgsBuilder::default()
79            .on_click(Arc::new(|| {}))
80            .build()
81            .unwrap()
82    }
83}
84
85/// Creates an interactive button component that can wrap any custom child content.
86///
87/// The `button` component provides a clickable surface with a ripple effect,
88/// customizable appearance, and event handling. It's built on top of the `surface`
89/// component and handles user interactions like clicks and hover states.
90///
91/// # Parameters
92///
93/// - `args`: An instance of `ButtonArgs` or `ButtonArgsBuilder` that defines the button's
94///   properties, such as color, shape, padding, and the `on_click` callback.
95/// - `ripple_state`: An `Arc<RippleState>` that manages the visual state of the ripple
96///   effect. This should be created and managed by the parent component to persist
97///   the ripple animation state across recompositions.
98/// - `child`: A closure that defines the content to be displayed inside the button.
99///   This can be any other component, such as `text`, `image`, or a combination of them.
100///
101/// # Example
102///
103/// ```
104/// # use std::sync::Arc;
105/// # use tessera_ui::{Color, Dp};
106/// # use tessera_ui_basic_components::{
107/// #     button::{button, ButtonArgsBuilder},
108/// #     ripple_state::RippleState,
109/// #     text::{text, TextArgsBuilder},
110/// # };
111/// #
112/// // 1. Create a ripple state to manage the effect.
113/// let ripple_state = Arc::new(RippleState::new());
114///
115/// // 2. Define the button's properties using the builder pattern.
116/// let args = ButtonArgsBuilder::default()
117///     .color(Color::new(0.2, 0.5, 0.8, 1.0)) // A nice blue
118///     .padding(Dp(12.0))
119///     .on_click(Arc::new(|| {
120///         println!("Button was clicked!");
121///     }))
122///     .build()
123///     .unwrap();
124///
125/// // 3. Call the button component, passing the args, state, and a child content closure.
126/// button(args, ripple_state, || {
127///     text(
128///         TextArgsBuilder::default()
129///             .text("Click Me".to_string())
130///             .color(Color::WHITE)
131///             .build()
132///             .unwrap(),
133///     );
134/// });
135/// ```
136#[tessera]
137pub fn button(args: impl Into<ButtonArgs>, ripple_state: Arc<RippleState>, child: impl FnOnce()) {
138    let button_args: ButtonArgs = args.into();
139
140    // Create interactive surface for button
141    surface(create_surface_args(&button_args), Some(ripple_state), child);
142}
143
144/// Create surface arguments based on button configuration
145fn create_surface_args(args: &ButtonArgs) -> crate::surface::SurfaceArgs {
146    let style = if args.border_width.to_pixels_f32() > 0.0 {
147        crate::surface::SurfaceStyle::FilledOutlined {
148            fill_color: args.color,
149            border_color: args.border_color.unwrap_or(args.color),
150            border_width: args.border_width,
151        }
152    } else {
153        crate::surface::SurfaceStyle::Filled { color: args.color }
154    };
155
156    let hover_style = if let Some(hover_color) = args.hover_color {
157        let style = if args.border_width.to_pixels_f32() > 0.0 {
158            crate::surface::SurfaceStyle::FilledOutlined {
159                fill_color: hover_color,
160                border_color: args.border_color.unwrap_or(hover_color),
161                border_width: args.border_width,
162            }
163        } else {
164            crate::surface::SurfaceStyle::Filled { color: hover_color }
165        };
166        Some(style)
167    } else {
168        None
169    };
170
171    let mut builder = SurfaceArgsBuilder::default();
172
173    // Set shadow if available
174    if let Some(shadow) = args.shadow {
175        builder = builder.shadow(shadow);
176    }
177
178    // Set on_click handler if available
179    if let Some(on_click) = args.on_click.clone() {
180        builder = builder.on_click(on_click);
181    }
182
183    builder
184        .style(style)
185        .hover_style(hover_style)
186        .shape(args.shape)
187        .padding(args.padding)
188        .ripple_color(args.ripple_color)
189        .width(args.width)
190        .height(args.height)
191        .build()
192        .unwrap()
193}
194
195/// Convenience constructors for common button styles
196impl ButtonArgs {
197    /// Create a primary button with default blue styling
198    pub fn primary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
199        ButtonArgsBuilder::default()
200            .color(Color::new(0.2, 0.5, 0.8, 1.0)) // Blue
201            .on_click(on_click)
202            .build()
203            .unwrap()
204    }
205
206    /// Create a secondary button with gray styling
207    pub fn secondary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
208        ButtonArgsBuilder::default()
209            .color(Color::new(0.6, 0.6, 0.6, 1.0)) // Gray
210            .on_click(on_click)
211            .build()
212            .unwrap()
213    }
214
215    /// Create a success button with green styling
216    pub fn success(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
217        ButtonArgsBuilder::default()
218            .color(Color::new(0.1, 0.7, 0.3, 1.0)) // Green
219            .on_click(on_click)
220            .build()
221            .unwrap()
222    }
223
224    /// Create a danger button with red styling
225    pub fn danger(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
226        ButtonArgsBuilder::default()
227            .color(Color::new(0.8, 0.2, 0.2, 1.0)) // Red
228            .on_click(on_click)
229            .build()
230            .unwrap()
231    }
232}
233
234/// Builder methods for fluent API
235impl ButtonArgs {
236    pub fn with_color(mut self, color: Color) -> Self {
237        self.color = color;
238        self
239    }
240
241    pub fn with_hover_color(mut self, hover_color: Color) -> Self {
242        self.hover_color = Some(hover_color);
243        self
244    }
245
246    pub fn with_padding(mut self, padding: Dp) -> Self {
247        self.padding = padding;
248        self
249    }
250
251    pub fn with_shape(mut self, shape: Shape) -> Self {
252        self.shape = shape;
253        self
254    }
255
256    pub fn with_width(mut self, width: DimensionValue) -> Self {
257        self.width = width;
258        self
259    }
260
261    pub fn with_height(mut self, height: DimensionValue) -> Self {
262        self.height = height;
263        self
264    }
265
266    pub fn with_ripple_color(mut self, ripple_color: Color) -> Self {
267        self.ripple_color = ripple_color;
268        self
269    }
270
271    pub fn with_border(mut self, width: Dp, color: Option<Color>) -> Self {
272        self.border_width = width;
273        self.border_color = color;
274        self
275    }
276}