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}