tessera_ui_basic_components/
glass_button.rs

1//! An interactive button with a glassmorphic background.
2//!
3//! ## Usage
4//!
5//! Use for visually distinctive actions in layered or modern UIs.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use tessera_ui::{Color, DimensionValue, Dp, tessera};
10
11use crate::{
12    fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
13    ripple_state::RippleState,
14    shape_def::Shape,
15};
16
17/// Arguments for the `glass_button` component.
18#[derive(Builder, Clone, Default)]
19#[builder(pattern = "owned", setter(into, strip_option), default)]
20pub struct GlassButtonArgs {
21    /// The click callback function
22    #[builder(setter(strip_option, into = false))]
23    pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
24
25    // Ripple effect properties
26    /// The ripple color (RGB) for the button.
27    #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
28    pub ripple_color: Color,
29
30    // Layout properties
31    /// The padding of the button.
32    #[builder(default = "Dp(12.0)")]
33    pub padding: Dp,
34    /// Explicit width behavior for the button. Defaults to `WRAP`.
35    #[builder(default = "DimensionValue::WRAP", setter(into))]
36    pub width: DimensionValue,
37    /// Explicit height behavior for the button. Defaults to `WRAP`.
38    #[builder(default = "DimensionValue::WRAP", setter(into))]
39    pub height: DimensionValue,
40
41    // Glass visual properties
42    #[builder(default = "Color::new(0.5, 0.5, 0.5, 0.1)")]
43    pub tint_color: Color,
44    #[builder(
45        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 }"
46    )]
47    pub shape: Shape,
48    #[builder(default = "Dp(0.0)")]
49    pub blur_radius: Dp,
50    #[builder(default = "Dp(25.0)")]
51    pub dispersion_height: Dp,
52    #[builder(default = "1.1")]
53    pub chroma_multiplier: f32,
54    #[builder(default = "Dp(24.0)")]
55    pub refraction_height: Dp,
56    #[builder(default = "32.0")]
57    pub refraction_amount: f32,
58    #[builder(default = "0.0")]
59    pub noise_amount: f32,
60    #[builder(default = "1.0")]
61    pub noise_scale: f32,
62    #[builder(default = "0.0")]
63    pub time: f32,
64    #[builder(default, setter(strip_option))]
65    pub contrast: Option<f32>,
66    #[builder(default, setter(strip_option))]
67    pub border: Option<GlassBorder>,
68    /// Optional label announced by assistive technologies.
69    #[builder(default, setter(strip_option, into))]
70    pub accessibility_label: Option<String>,
71    /// Optional longer description for assistive technologies.
72    #[builder(default, setter(strip_option, into))]
73    pub accessibility_description: Option<String>,
74    /// Whether the button should remain focusable even when no click handler is provided.
75    #[builder(default)]
76    pub accessibility_focusable: bool,
77}
78
79/// Convenience constructors for common glass button styles
80impl GlassButtonArgs {
81    /// Create a primary glass button with default blue tint
82    pub fn primary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
83        GlassButtonArgsBuilder::default()
84            .on_click(on_click)
85            .tint_color(Color::new(0.2, 0.5, 0.8, 0.2)) // Blue tint
86            .border(GlassBorder::new(Dp(1.0).into()))
87            .build()
88            .unwrap()
89    }
90
91    /// Create a secondary glass button with gray tint
92    pub fn secondary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
93        GlassButtonArgsBuilder::default()
94            .on_click(on_click)
95            .tint_color(Color::new(0.6, 0.6, 0.6, 0.2)) // Gray tint
96            .border(GlassBorder::new(Dp(1.0).into()))
97            .build()
98            .unwrap()
99    }
100
101    /// Create a success glass button with green tint
102    pub fn success(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
103        GlassButtonArgsBuilder::default()
104            .on_click(on_click)
105            .tint_color(Color::new(0.1, 0.7, 0.3, 0.2)) // Green tint
106            .border(GlassBorder::new(Dp(1.0).into()))
107            .build()
108            .unwrap()
109    }
110
111    /// Create a danger glass button with red tint
112    pub fn danger(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
113        GlassButtonArgsBuilder::default()
114            .on_click(on_click)
115            .tint_color(Color::new(0.8, 0.2, 0.2, 0.2)) // Red tint
116            .border(GlassBorder::new(Dp(1.0).into()))
117            .build()
118            .unwrap()
119    }
120}
121
122/// # glass_button
123///
124/// Renders an interactive button with a customizable glass effect and ripple animation.
125///
126/// ## Usage
127///
128/// Use as a primary action button where a modern, layered look is desired.
129///
130/// ## Parameters
131///
132/// - `args` — configures the button's glass appearance and `on_click` handler; see [`GlassButtonArgs`].
133/// - `ripple_state` — a clonable [`RippleState`] to manage the ripple animation.
134/// - `child` — a closure that renders the button's content (e.g., text or an icon).
135///
136/// ## Examples
137///
138/// ```
139/// use std::sync::Arc;
140/// use tessera_ui::Color;
141/// use tessera_ui_basic_components::{
142///     glass_button::{glass_button, GlassButtonArgs},
143///     ripple_state::RippleState,
144///     text::{text, TextArgsBuilder},
145/// };
146///
147/// let ripple_state = RippleState::new();
148///
149/// glass_button(
150///     GlassButtonArgs {
151///         on_click: Some(Arc::new(|| println!("Button clicked!"))),
152///         tint_color: Color::new(0.2, 0.3, 0.8, 0.3),
153///         ..Default::default()
154///     },
155///     ripple_state,
156///     || text(TextArgsBuilder::default().text("Click Me".to_string()).build().unwrap()),
157/// );
158/// ```
159#[tessera]
160pub fn glass_button(
161    args: impl Into<GlassButtonArgs>,
162    ripple_state: RippleState,
163    child: impl FnOnce() + Send + Sync + 'static,
164) {
165    let args: GlassButtonArgs = args.into();
166
167    let mut glass_args_builder = FluidGlassArgsBuilder::default();
168    if let Some((progress, center)) = ripple_state.get_animation_progress() {
169        let ripple_alpha = (1.0 - progress) * 0.3; // Fade out
170        glass_args_builder = glass_args_builder
171            .ripple_center(center)
172            .ripple_radius(progress)
173            .ripple_alpha(ripple_alpha)
174            .ripple_strength(progress);
175    }
176
177    if let Some(contrast) = args.contrast {
178        glass_args_builder = glass_args_builder.contrast(contrast);
179    }
180
181    let mut glass_args = glass_args_builder
182        .tint_color(args.tint_color)
183        .shape(args.shape)
184        .blur_radius(args.blur_radius)
185        .dispersion_height(args.dispersion_height)
186        .chroma_multiplier(args.chroma_multiplier)
187        .refraction_height(args.refraction_height)
188        .refraction_amount(args.refraction_amount)
189        .noise_amount(args.noise_amount)
190        .noise_scale(args.noise_scale)
191        .width(args.width)
192        .height(args.height)
193        .time(args.time)
194        .padding(args.padding);
195
196    if let Some(on_click) = args.on_click {
197        glass_args = glass_args.on_click(on_click);
198    }
199
200    if let Some(border) = args.border {
201        glass_args = glass_args.border(border);
202    }
203
204    if let Some(label) = args.accessibility_label {
205        glass_args = glass_args.accessibility_label(label);
206    }
207
208    if let Some(description) = args.accessibility_description {
209        glass_args = glass_args.accessibility_description(description);
210    }
211
212    if args.accessibility_focusable {
213        glass_args = glass_args.accessibility_focusable(true);
214    }
215
216    let glass_args = glass_args.build().unwrap();
217
218    fluid_glass(glass_args, Some(ripple_state), child);
219}