tessera_ui_basic_components/
fluid_glass.rs

1//! Fluid glass effect module for Tessera UI Basic Components.
2//!
3//! This module provides the core implementation for the "fluid glass" (frosted/distorted glass) visual effect,
4//! including parameter structures, builder patterns, and the main `fluid_glass` component.
5//! It enables highly customizable backgrounds with blur, tint, chromatic dispersion, noise, and ripple effects,
6//! suitable for creating modern, layered user interfaces with enhanced depth and focus.
7//! Typical use cases include backgrounds for buttons, sliders, switches, and other interactive UI elements
8//! where a dynamic, visually appealing glass-like surface is desired.
9
10use std::sync::Arc;
11
12use derive_builder::Builder;
13use tessera_ui::{
14    BarrierRequirement, Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp,
15    PressKeyEventType, Px, PxPosition, renderer::DrawCommand, tessera, winit::window::CursorIcon,
16};
17
18use crate::{
19    padding_utils::remove_padding_from_dimension,
20    pipelines::{blur::command::BlurCommand, contrast::ContrastCommand, mean::MeanCommand},
21    pos_misc::is_position_in_component,
22    ripple_state::RippleState,
23    shape_def::Shape,
24};
25
26#[derive(Clone, Copy, Debug, Default, PartialEq)]
27pub struct GlassBorder {
28    pub width: Px,
29}
30
31impl GlassBorder {
32    pub fn new(width: Px) -> Self {
33        Self { width }
34    }
35}
36
37/// Arguments for the `fluid_glass` component, providing extensive control over its appearance.
38///
39/// This struct uses the builder pattern for easy construction.
40#[derive(Builder, Clone)]
41#[builder(build_fn(validate = "Self::validate"), pattern = "owned", setter(into))]
42pub struct FluidGlassArgs {
43    /// The tint color of the glass.
44    /// The alpha channel uniquely and directly controls the tint strength.
45    /// `A=0.0` means no tint (100% background visibility).
46    /// `A=1.0` means full tint (100% color visibility).
47    #[builder(default = "Color::TRANSPARENT")]
48    pub tint_color: Color,
49    /// The shape of the component, an enum that can be `RoundedRectangle` or `Ellipse`.
50    #[builder(
51        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 }"
52    )]
53    pub shape: Shape,
54    /// The radius for the background blur effect. A value of `0.0` disables the blur.
55    #[builder(default = "0.0")]
56    pub blur_radius: f32,
57    /// The height of the chromatic dispersion effect.
58    #[builder(default = "25.0")]
59    pub dispersion_height: f32,
60    /// Multiplier for the chromatic aberration, enhancing the color separation effect.
61    #[builder(default = "1.0")]
62    pub chroma_multiplier: f32,
63    /// The height of the refraction effect, simulating light bending through the glass.
64    #[builder(default = "24.0")]
65    pub refraction_height: f32,
66    /// The amount of refraction to apply.
67    #[builder(default = "32.0")]
68    pub refraction_amount: f32,
69    /// Controls the shape and eccentricity of the highlight.
70    #[builder(default = "0.2")]
71    pub eccentric_factor: f32,
72    /// The amount of noise to apply over the surface, adding texture.
73    #[builder(default = "0.0")]
74    pub noise_amount: f32,
75    /// The scale of the noise pattern.
76    #[builder(default = "1.0")]
77    pub noise_scale: f32,
78    /// A time value, typically used to animate the noise or other effects.
79    #[builder(default = "0.0")]
80    pub time: f32,
81    /// The contrast adjustment factor.
82    #[builder(default, setter(strip_option))]
83    pub contrast: Option<f32>,
84    /// The optional width of the component, defined as a `DimensionValue`.
85    #[builder(default = "DimensionValue::WRAP", setter(into))]
86    pub width: DimensionValue,
87    /// The optional height of the component, defined as a `DimensionValue`.
88    #[builder(default = "DimensionValue::WRAP", setter(into))]
89    pub height: DimensionValue,
90
91    #[builder(default = "Dp(0.0)")]
92    pub padding: Dp,
93
94    // Ripple effect properties
95    #[builder(default, setter(strip_option))]
96    pub ripple_center: Option<[f32; 2]>,
97    #[builder(default, setter(strip_option))]
98    pub ripple_radius: Option<f32>,
99    #[builder(default, setter(strip_option))]
100    pub ripple_alpha: Option<f32>,
101    #[builder(default, setter(strip_option))]
102    pub ripple_strength: Option<f32>,
103
104    #[builder(default, setter(strip_option, into = false))]
105    pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
106
107    #[builder(default = "Some(GlassBorder { width: Dp(1.0).into() })")]
108    pub border: Option<GlassBorder>,
109
110    /// Whether to block input events on the glass surface.
111    /// When `true`, the surface will consume all input events, preventing interaction with underlying components.
112    #[builder(default = "false")]
113    pub block_input: bool,
114}
115
116impl PartialEq for FluidGlassArgs {
117    fn eq(&self, other: &Self) -> bool {
118        self.tint_color == other.tint_color
119            && self.shape == other.shape
120            && self.blur_radius == other.blur_radius
121            && self.dispersion_height == other.dispersion_height
122            && self.chroma_multiplier == other.chroma_multiplier
123            && self.refraction_height == other.refraction_height
124            && self.refraction_amount == other.refraction_amount
125            && self.eccentric_factor == other.eccentric_factor
126            && self.noise_amount == other.noise_amount
127            && self.noise_scale == other.noise_scale
128            && self.time == other.time
129            && self.contrast == other.contrast
130            && self.width == other.width
131            && self.height == other.height
132            && self.padding == other.padding
133            && self.ripple_center == other.ripple_center
134            && self.ripple_radius == other.ripple_radius
135            && self.ripple_alpha == other.ripple_alpha
136            && self.ripple_strength == other.ripple_strength
137            && self.border == other.border
138            && self.block_input == other.block_input
139    }
140}
141
142impl FluidGlassArgsBuilder {
143    fn validate(&self) -> Result<(), String> {
144        Ok(())
145    }
146}
147
148// Manual implementation of Default because derive_builder's default conflicts with our specific defaults
149impl Default for FluidGlassArgs {
150    fn default() -> Self {
151        FluidGlassArgsBuilder::default().build().unwrap()
152    }
153}
154
155#[derive(Clone, PartialEq)]
156pub struct FluidGlassCommand {
157    pub args: FluidGlassArgs,
158}
159
160impl DrawCommand for FluidGlassCommand {
161    fn barrier(&self) -> Option<BarrierRequirement> {
162        Some(BarrierRequirement::ZERO_PADDING_LOCAL)
163    }
164}
165
166// Helper: input handler logic extracted to reduce complexity of `fluid_glass`
167// These helpers operate on the injected InputHandlerInput type from the core crate.
168fn handle_click_state(
169    args: &FluidGlassArgs,
170    ripple_state: Option<Arc<RippleState>>,
171    on_click: Arc<dyn Fn() + Send + Sync>,
172    input: &mut tessera_ui::InputHandlerInput,
173) {
174    let size = input.computed_data;
175    let cursor_pos_option = input.cursor_position_rel;
176    let is_cursor_in = cursor_pos_option
177        .map(|pos| is_position_in_component(size, pos))
178        .unwrap_or(false);
179
180    if is_cursor_in {
181        input.requests.cursor_icon = CursorIcon::Pointer;
182
183        if let Some(_event) = input.cursor_events.iter().find(|e| {
184            matches!(
185                e.content,
186                CursorEventContent::Released(PressKeyEventType::Left)
187            )
188        }) {
189            if let Some(ripple_state) = &ripple_state
190                && let Some(pos) = input.cursor_position_rel
191            {
192                let size = input.computed_data;
193                let normalized_pos = [
194                    pos.x.to_f32() / size.width.to_f32(),
195                    pos.y.to_f32() / size.height.to_f32(),
196                ];
197                ripple_state.start_animation(normalized_pos);
198            }
199            on_click();
200        }
201
202        if args.block_input {
203            // Consume all input events to prevent interaction with underlying components
204            input.block_all();
205        }
206    }
207}
208
209fn handle_block_input(input: &mut tessera_ui::InputHandlerInput) {
210    let size = input.computed_data;
211    let cursor_pos_option = input.cursor_position_rel;
212    let is_cursor_in = cursor_pos_option
213        .map(|pos| is_position_in_component(size, pos))
214        .unwrap_or(false);
215
216    if is_cursor_in {
217        // Consume all input events to prevent interaction with underlying components
218        input.block_all();
219    }
220}
221
222/// Creates a fluid glass effect component, which serves as a dynamic and visually appealing background.
223///
224/// The `fluid_glass` component simulates the look of frosted or distorted glass with a fluid,
225/// animated texture. It can be used to create modern, layered user interfaces where the background
226/// content is blurred and stylized, enhancing depth and focus. The effect is highly customizable
227/// through `FluidGlassArgs`.
228///
229/// # Example
230///
231/// ```
232/// use tessera_ui_basic_components::{
233///     fluid_glass::{fluid_glass, FluidGlassArgs},
234///     text::text,
235/// };
236///
237/// fluid_glass(FluidGlassArgs::default(), None, || {
238///     text("Content on glass".to_string());
239/// });
240/// ```
241///
242/// # Arguments
243///
244/// * `args` - A `FluidGlassArgs` struct that specifies the appearance and behavior of the glass
245///   effect. This includes properties like tint color, shape, blur radius, and noise level.
246///   The builder pattern is recommended for constructing the arguments.
247///
248/// * `ripple_state` - An optional `Arc<RippleState>` to enable and manage a ripple effect on user
249///   interaction, such as a click. When `None`, no ripple effect is applied.
250///
251/// * `child` - A closure that defines the child components to be rendered on top of the glass surface.
252///   These children will be contained within the bounds of the `fluid_glass` component.
253#[tessera]
254pub fn fluid_glass(
255    mut args: FluidGlassArgs,
256    ripple_state: Option<Arc<RippleState>>,
257    child: impl FnOnce(),
258) {
259    if let Some(ripple_state) = &ripple_state
260        && let Some((progress, center)) = ripple_state.get_animation_progress()
261    {
262        args.ripple_center = Some(center);
263        args.ripple_radius = Some(progress);
264        args.ripple_alpha = Some((1.0 - progress) * 0.3);
265        args.ripple_strength = Some(progress);
266    }
267    (child)();
268    let args_measure_clone = args.clone();
269    measure(Box::new(move |input| {
270        let glass_intrinsic_width = args_measure_clone.width;
271        let glass_intrinsic_height = args_measure_clone.height;
272        let glass_intrinsic_constraint =
273            Constraint::new(glass_intrinsic_width, glass_intrinsic_height);
274        let effective_glass_constraint = glass_intrinsic_constraint.merge(input.parent_constraint);
275
276        let child_constraint = Constraint::new(
277            remove_padding_from_dimension(
278                effective_glass_constraint.width,
279                args_measure_clone.padding.into(),
280            ),
281            remove_padding_from_dimension(
282                effective_glass_constraint.height,
283                args_measure_clone.padding.into(),
284            ),
285        );
286
287        let child_measurement = if !input.children_ids.is_empty() {
288            let child_measurement =
289                input.measure_child(input.children_ids[0], &child_constraint)?;
290            input.place_child(
291                input.children_ids[0],
292                PxPosition {
293                    x: args.padding.into(),
294                    y: args.padding.into(),
295                },
296            );
297            child_measurement
298        } else {
299            ComputedData {
300                width: Px(0),
301                height: Px(0),
302            }
303        };
304
305        if args.blur_radius > 0.0 {
306            let blur_command = BlurCommand {
307                radius: args.blur_radius,
308                direction: (1.0, 0.0), // Horizontal
309                padding: Px(args.refraction_height as i32),
310            };
311            let blur_command2 = BlurCommand {
312                radius: args.blur_radius,
313                direction: (0.0, 1.0), // Vertical
314                padding: Px(args.refraction_height as i32),
315            };
316            let mut metadata = input.metadata_mut();
317            metadata.push_compute_command(blur_command);
318            metadata.push_compute_command(blur_command2);
319        }
320
321        if let Some(contrast_value) = args.contrast {
322            let mean_command =
323                MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
324            let contrast_command =
325                ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
326            let mut metadata = input.metadata_mut();
327            metadata.push_compute_command(mean_command);
328            metadata.push_compute_command(contrast_command);
329        }
330
331        let drawable = FluidGlassCommand {
332            args: args_measure_clone.clone(),
333        };
334
335        input.metadata_mut().push_draw_command(drawable);
336
337        let padding_px: Px = args_measure_clone.padding.into();
338        let min_width = child_measurement.width + padding_px * 2;
339        let min_height = child_measurement.height + padding_px * 2;
340        let width = match effective_glass_constraint.width {
341            DimensionValue::Fixed(value) => value,
342            DimensionValue::Wrap { min, max } => min
343                .unwrap_or(Px(0))
344                .max(min_width)
345                .min(max.unwrap_or(Px::MAX)),
346            DimensionValue::Fill { min, max } => max
347                .expect("Seems that you are trying to fill an infinite width, which is not allowed")
348                .max(min_width)
349                .max(min.unwrap_or(Px(0))),
350        };
351        let height = match effective_glass_constraint.height {
352            DimensionValue::Fixed(value) => value,
353            DimensionValue::Wrap { min, max } => min
354                .unwrap_or(Px(0))
355                .max(min_height)
356                .min(max.unwrap_or(Px::MAX)),
357            DimensionValue::Fill { min, max } => max
358                .expect(
359                    "Seems that you are trying to fill an infinite height, which is not allowed",
360                )
361                .max(min_height)
362                .max(min.unwrap_or(Px(0))),
363        };
364        Ok(ComputedData { width, height })
365    }));
366
367    if let Some(ref on_click) = args.on_click {
368        let ripple_state = ripple_state.clone();
369        let on_click_arc = on_click.clone();
370        let args_for_handler = args.clone();
371        input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
372            // Delegate to extracted helper to reduce closure complexity.
373            handle_click_state(
374                &args_for_handler,
375                ripple_state.clone(),
376                on_click_arc.clone(),
377                &mut input,
378            );
379        }));
380    } else if args.block_input {
381        input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
382            // Delegate to extracted helper for input blocking behavior.
383            handle_block_input(&mut input);
384        }));
385    }
386}