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}