tessera_ui_basic_components/
glass_slider.rs

1//! Provides a glassmorphism-style slider component for selecting a value in modern UI applications.
2//!
3//! The `glass_slider` module implements a customizable, frosted glass effect slider with support for
4//! blurred backgrounds, tint colors, borders, and interactive state management. It enables users to
5//! select a continuous value between 0.0 and 1.0 by dragging a thumb along a track, and is suitable
6//! for dashboards, settings panels, or any interface requiring visually appealing value selection.
7//!
8//! Typical usage involves integrating the slider into a component tree, passing state via `Arc<RwLock<GlassSliderState>>`,
9//! and customizing appearance through `GlassSliderArgs`. The component is designed to fit seamlessly into
10//! glassmorphism-themed user interfaces.
11//!
12//! See the module-level documentation and examples for details.
13
14use std::sync::Arc;
15
16use derive_builder::Builder;
17use parking_lot::RwLock;
18use tessera_ui::{
19    Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
20    focus_state::Focus, tessera, winit::window::CursorIcon,
21};
22
23use crate::{
24    fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
25    shape_def::Shape,
26};
27
28/// State for the `glass_slider` component.
29pub struct GlassSliderState {
30    /// True if the user is currently dragging the slider.
31    pub is_dragging: bool,
32    /// The focus handler for the slider.
33    pub focus: Focus,
34}
35
36impl Default for GlassSliderState {
37    fn default() -> Self {
38        Self::new()
39    }
40}
41
42impl GlassSliderState {
43    pub fn new() -> Self {
44        Self {
45            is_dragging: false,
46            focus: Focus::new(),
47        }
48    }
49}
50
51/// Arguments for the `glass_slider` component.
52#[derive(Builder, Clone)]
53#[builder(pattern = "owned")]
54pub struct GlassSliderArgs {
55    /// The current value of the slider, ranging from 0.0 to 1.0.
56    #[builder(default = "0.0")]
57    pub value: f32,
58
59    /// Callback function triggered when the slider's value changes.
60    #[builder(default = "Arc::new(|_| {})")]
61    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
62
63    /// The width of the slider track.
64    #[builder(default = "Dp(200.0)")]
65    pub width: Dp,
66
67    /// The height of the slider track.
68    #[builder(default = "Dp(12.0)")]
69    pub track_height: Dp,
70
71    /// Glass tint color for the track background.
72    #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
73    pub track_tint_color: Color,
74
75    /// Glass tint color for the progress fill.
76    #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
77    pub progress_tint_color: Color,
78
79    /// Glass blur radius for all components.
80    #[builder(default = "8.0")]
81    pub blur_radius: f32,
82
83    /// Border width for the track.
84    #[builder(default = "Dp(1.0)")]
85    pub track_border_width: Dp,
86
87    /// Disable interaction.
88    #[builder(default = "false")]
89    pub disabled: bool,
90}
91
92/// Helper: check if a cursor position is inside a measured component area.
93/// Extracted to reduce duplication and keep the input handler concise.
94fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
95    if let Some(pos) = cursor_pos {
96        let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
97        let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
98        within_x && within_y
99    } else {
100        false
101    }
102}
103
104/// Helper: compute normalized progress (0.0..1.0) from cursor X and width.
105/// Returns None when cursor is not available.
106fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
107    cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
108}
109
110/// Helper: compute progress fill width in Px, clamped to >= 0.
111fn compute_progress_width(total_width: Px, value: f32, border_padding_px: f32) -> Px {
112    let total_f = total_width.0 as f32;
113    let mut w = total_f * value - border_padding_px;
114    if w < 0.0 {
115        w = 0.0;
116    }
117    Px(w as i32)
118}
119
120/// Process cursor events and update the slider state accordingly.
121/// Returns the new value (0.0..1.0) if a change should be emitted.
122fn process_cursor_events(
123    state: &mut GlassSliderState,
124    input: &tessera_ui::InputHandlerInput,
125    width_f: f32,
126) -> Option<f32> {
127    let mut new_value: Option<f32> = None;
128
129    for event in input.cursor_events.iter() {
130        match &event.content {
131            CursorEventContent::Pressed(_) => {
132                state.focus.request_focus();
133                state.is_dragging = true;
134                if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
135                    new_value = Some(v);
136                }
137            }
138            CursorEventContent::Released(_) => {
139                state.is_dragging = false;
140            }
141            _ => {}
142        }
143    }
144
145    if state.is_dragging
146        && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
147    {
148        new_value = Some(v);
149    }
150
151    new_value
152}
153
154/// Creates a slider component with a frosted glass effect.
155///
156/// The `glass_slider` allows users to select a value from a continuous range (0.0 to 1.0)
157/// by dragging a handle along a track. It features a modern, semi-transparent
158/// "glassmorphism" aesthetic, with a blurred background and subtle highlights.
159///
160/// # Arguments
161///
162/// * `args` - An instance of `GlassSliderArgs` or `GlassSliderArgsBuilder` to configure the slider's appearance and behavior.
163///   - `value`: The current value of the slider, must be between 0.0 and 1.0.
164///   - `on_change`: A callback function that is triggered when the slider's value changes.
165///     It receives the new value as an `f32`.
166/// * `state` - An `Arc<RwLock<GlassSliderState>>` to manage the component's interactive state,
167///   such as dragging and focus.
168///
169/// # Example
170///
171/// ```
172/// use std::sync::Arc;
173/// use parking_lot::RwLock;
174/// use tessera_ui_basic_components::glass_slider::{glass_slider, GlassSliderArgsBuilder, GlassSliderState};
175///
176/// // In your application state
177/// let slider_value = Arc::new(RwLock::new(0.5));
178/// let slider_state = Arc::new(RwLock::new(GlassSliderState::new()));
179///
180/// // In your component function
181/// let on_change_callback = {
182///     let slider_value = slider_value.clone();
183///     Arc::new(move |new_value| {
184///         *slider_value.write() = new_value;
185///     })
186/// };
187///
188/// glass_slider(
189///     GlassSliderArgsBuilder::default()
190///         .value(*slider_value.read())
191///         .on_change(on_change_callback)
192///         .build()
193///         .unwrap(),
194///     slider_state.clone(),
195/// );
196/// ```
197#[tessera]
198pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: Arc<RwLock<GlassSliderState>>) {
199    let args: GlassSliderArgs = args.into();
200    let border_padding_px = args.track_border_width.to_px().to_f32() * 2.0;
201
202    // External track (background) with border - capsule shape
203    fluid_glass(
204        FluidGlassArgsBuilder::default()
205            .width(DimensionValue::Fixed(args.width.to_px()))
206            .height(DimensionValue::Fixed(args.track_height.to_px()))
207            .tint_color(args.track_tint_color)
208            .blur_radius(args.blur_radius)
209            .shape({
210                let track_radius_dp = Dp(args.track_height.0 / 2.0);
211                Shape::RoundedRectangle {
212                    top_left: track_radius_dp,
213                    top_right: track_radius_dp,
214                    bottom_right: track_radius_dp,
215                    bottom_left: track_radius_dp,
216                    g2_k_value: 2.0, // Capsule shape
217                }
218            })
219            .border(GlassBorder::new(args.track_border_width.into()))
220            .padding(args.track_border_width)
221            .build()
222            .unwrap(),
223        None,
224        move || {
225            // Internal progress fill - capsule shape using surface
226            let progress_width_px =
227                compute_progress_width(args.width.to_px(), args.value, border_padding_px);
228            let effective_height = args.track_height.to_px().to_f32() - border_padding_px;
229            fluid_glass(
230                FluidGlassArgsBuilder::default()
231                    .width(DimensionValue::Fixed(progress_width_px))
232                    .height(DimensionValue::Fill {
233                        min: None,
234                        max: None,
235                    })
236                    .tint_color(args.progress_tint_color)
237                    .shape({
238                        let effective_height_dp = Dp::from_pixels_f32(effective_height);
239                        let radius_dp = Dp(effective_height_dp.0 / 2.0);
240                        Shape::RoundedRectangle {
241                            top_left: radius_dp,
242                            top_right: radius_dp,
243                            bottom_right: radius_dp,
244                            bottom_left: radius_dp,
245                            g2_k_value: 2.0, // Capsule shape
246                        }
247                    })
248                    .refraction_amount(0.0)
249                    .build()
250                    .unwrap(),
251                None,
252                || {},
253            );
254        },
255    );
256
257    let on_change = args.on_change.clone();
258    let disabled = args.disabled;
259
260    input_handler(Box::new(move |input| {
261        if disabled {
262            return;
263        }
264
265        let is_in_component =
266            cursor_within_component(input.cursor_position_rel, &input.computed_data);
267
268        // Set cursor to pointer when hovering over the slider
269        if is_in_component {
270            input.requests.cursor_icon = CursorIcon::Pointer;
271        }
272
273        if !is_in_component && !state.read().is_dragging {
274            return;
275        }
276
277        let width_f = input.computed_data.width.0 as f32;
278
279        if let Some(v) = process_cursor_events(&mut state.write(), &input, width_f)
280            && (v - args.value).abs() > f32::EPSILON
281        {
282            on_change(v);
283        }
284    }));
285
286    measure(Box::new(move |input| {
287        let self_width = args.width.to_px();
288        let self_height = args.track_height.to_px();
289
290        let track_id = input.children_ids[0];
291
292        // Measure track
293        let track_constraint = Constraint::new(
294            DimensionValue::Fixed(self_width),
295            DimensionValue::Fixed(self_height),
296        );
297        input.measure_child(track_id, &track_constraint)?;
298        input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
299
300        Ok(ComputedData {
301            width: self_width,
302            height: self_height,
303        })
304    }));
305}