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::{RoundedCorner, 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    /// The ripple color (RGB) for the button.
25    #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
26    pub ripple_color: Color,
27    /// The padding of the button.
28    #[builder(default = "Dp(12.0)")]
29    pub padding: Dp,
30    /// Explicit width behavior for the button. Defaults to `WRAP`.
31    #[builder(default = "DimensionValue::WRAP", setter(into))]
32    pub width: DimensionValue,
33    /// Explicit height behavior for the button. Defaults to `WRAP`.
34    #[builder(default = "DimensionValue::WRAP", setter(into))]
35    pub height: DimensionValue,
36    /// Tint color applied to the glass surface.
37    #[builder(default = "Color::new(0.5, 0.5, 0.5, 0.1)")]
38    pub tint_color: Color,
39    /// Shape used for the button background.
40    #[builder(default = "Shape::RoundedRectangle {
41            top_left: RoundedCorner::manual(Dp(25.0), 3.0),
42            top_right: RoundedCorner::manual(Dp(25.0), 3.0),
43            bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
44            bottom_left: RoundedCorner::manual(Dp(25.0), 3.0),
45        }")]
46    pub shape: Shape,
47    /// Blur radius applied to the captured background.
48    #[builder(default = "Dp(0.0)")]
49    pub blur_radius: Dp,
50    /// Virtual height of the chromatic dispersion effect.
51    #[builder(default = "Dp(25.0)")]
52    pub dispersion_height: Dp,
53    /// Multiplier controlling the strength of chromatic aberration.
54    #[builder(default = "1.1")]
55    pub chroma_multiplier: f32,
56    /// Virtual height used when calculating refraction distortion.
57    #[builder(default = "Dp(24.0)")]
58    pub refraction_height: Dp,
59    /// Amount of refraction to apply to the background.
60    #[builder(default = "32.0")]
61    pub refraction_amount: f32,
62    /// Strength of the grain/noise applied across the surface.
63    #[builder(default = "0.0")]
64    pub noise_amount: f32,
65    /// Scale factor for the generated noise texture.
66    #[builder(default = "1.0")]
67    pub noise_scale: f32,
68    /// Time value for animating noise or other procedural effects.
69    #[builder(default = "0.0")]
70    pub time: f32,
71    /// Optional contrast adjustment applied to the glass rendering.
72    #[builder(default, setter(strip_option))]
73    pub contrast: Option<f32>,
74    /// Optional outline configuration for the glass shape.
75    #[builder(default, setter(strip_option))]
76    pub border: Option<GlassBorder>,
77    /// Optional label announced by assistive technologies.
78    #[builder(default, setter(strip_option, into))]
79    pub accessibility_label: Option<String>,
80    /// Optional longer description for assistive technologies.
81    #[builder(default, setter(strip_option, into))]
82    pub accessibility_description: Option<String>,
83    /// Whether the button should remain focusable even when no click handler is provided.
84    #[builder(default)]
85    pub accessibility_focusable: bool,
86}
87
88/// Convenience constructors for common glass button styles
89impl GlassButtonArgs {
90    /// Create a primary glass button with default blue tint
91    pub fn primary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
92        GlassButtonArgsBuilder::default()
93            .on_click(on_click)
94            .tint_color(Color::new(0.2, 0.5, 0.8, 0.2)) // Blue tint
95            .border(GlassBorder::new(Dp(1.0).into()))
96            .build()
97            .expect("builder construction failed")
98    }
99
100    /// Create a secondary glass button with gray tint
101    pub fn secondary(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
102        GlassButtonArgsBuilder::default()
103            .on_click(on_click)
104            .tint_color(Color::new(0.6, 0.6, 0.6, 0.2)) // Gray tint
105            .border(GlassBorder::new(Dp(1.0).into()))
106            .build()
107            .expect("builder construction failed")
108    }
109
110    /// Create a success glass button with green tint
111    pub fn success(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
112        GlassButtonArgsBuilder::default()
113            .on_click(on_click)
114            .tint_color(Color::new(0.1, 0.7, 0.3, 0.2)) // Green tint
115            .border(GlassBorder::new(Dp(1.0).into()))
116            .build()
117            .expect("builder construction failed")
118    }
119
120    /// Create a danger glass button with red tint
121    pub fn danger(on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
122        GlassButtonArgsBuilder::default()
123            .on_click(on_click)
124            .tint_color(Color::new(0.8, 0.2, 0.2, 0.2)) // Red tint
125            .border(GlassBorder::new(Dp(1.0).into()))
126            .build()
127            .expect("builder construction failed")
128    }
129}
130
131/// # glass_button
132///
133/// Renders an interactive button with a customizable glass effect and ripple animation.
134///
135/// ## Usage
136///
137/// Use as a primary action button where a modern, layered look is desired.
138///
139/// ## Parameters
140///
141/// - `args` — configures the button's glass appearance and `on_click` handler; see [`GlassButtonArgs`].
142/// - `ripple_state` — a clonable [`RippleState`] to manage the ripple animation.
143/// - `child` — a closure that renders the button's content (e.g., text or an icon).
144///
145/// ## Examples
146///
147/// ```
148/// use std::sync::Arc;
149/// use tessera_ui::Color;
150/// use tessera_ui_basic_components::{
151///     glass_button::{glass_button, GlassButtonArgs},
152///     ripple_state::RippleState,
153///     text::{text, TextArgsBuilder},
154/// };
155///
156/// let ripple_state = RippleState::new();
157///
158/// glass_button(
159///     GlassButtonArgs {
160///         on_click: Some(Arc::new(|| println!("Button clicked!"))),
161///         tint_color: Color::new(0.2, 0.3, 0.8, 0.3),
162///         ..Default::default()
163///     },
164///     ripple_state,
165///     || text(TextArgsBuilder::default().text("Click Me".to_string()).build().expect("builder construction failed")),
166/// );
167/// ```
168#[tessera]
169pub fn glass_button(
170    args: impl Into<GlassButtonArgs>,
171    ripple_state: RippleState,
172    child: impl FnOnce() + Send + Sync + 'static,
173) {
174    let args: GlassButtonArgs = args.into();
175
176    let mut glass_args_builder = FluidGlassArgsBuilder::default();
177    if let Some((progress, center)) = ripple_state.get_animation_progress() {
178        let ripple_alpha = (1.0 - progress) * 0.3; // Fade out
179        glass_args_builder = glass_args_builder
180            .ripple_center(center)
181            .ripple_radius(progress)
182            .ripple_alpha(ripple_alpha)
183            .ripple_strength(progress);
184    }
185
186    if let Some(contrast) = args.contrast {
187        glass_args_builder = glass_args_builder.contrast(contrast);
188    }
189
190    let mut glass_args = glass_args_builder
191        .tint_color(args.tint_color)
192        .shape(args.shape)
193        .blur_radius(args.blur_radius)
194        .dispersion_height(args.dispersion_height)
195        .chroma_multiplier(args.chroma_multiplier)
196        .refraction_height(args.refraction_height)
197        .refraction_amount(args.refraction_amount)
198        .noise_amount(args.noise_amount)
199        .noise_scale(args.noise_scale)
200        .width(args.width)
201        .height(args.height)
202        .time(args.time)
203        .padding(args.padding);
204
205    if let Some(on_click) = args.on_click {
206        glass_args = glass_args.on_click(on_click);
207    }
208
209    if let Some(border) = args.border {
210        glass_args = glass_args.border(border);
211    }
212
213    if let Some(label) = args.accessibility_label {
214        glass_args = glass_args.accessibility_label(label);
215    }
216
217    if let Some(description) = args.accessibility_description {
218        glass_args = glass_args.accessibility_description(description);
219    }
220
221    if args.accessibility_focusable {
222        glass_args = glass_args.accessibility_focusable(true);
223    }
224
225    let glass_args = glass_args.build().expect("builder construction failed");
226
227    fluid_glass(glass_args, Some(ripple_state), child);
228}