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<Mutex<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::Mutex;
18use tessera_ui::{
19    Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
20    focus_state::Focus, winit::window::CursorIcon,
21};
22use tessera_ui_macros::tessera;
23
24use crate::{
25    fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
26    shape_def::Shape,
27};
28
29/// State for the `glass_slider` component.
30pub struct GlassSliderState {
31    /// True if the user is currently dragging the slider.
32    pub is_dragging: bool,
33    /// The focus handler for the slider.
34    pub focus: Focus,
35}
36
37impl Default for GlassSliderState {
38    fn default() -> Self {
39        Self::new()
40    }
41}
42
43impl GlassSliderState {
44    pub fn new() -> Self {
45        Self {
46            is_dragging: false,
47            focus: Focus::new(),
48        }
49    }
50}
51
52/// Arguments for the `glass_slider` component.
53#[derive(Builder, Clone)]
54#[builder(pattern = "owned")]
55pub struct GlassSliderArgs {
56    /// The current value of the slider, ranging from 0.0 to 1.0.
57    #[builder(default = "0.0")]
58    pub value: f32,
59
60    /// Callback function triggered when the slider's value changes.
61    #[builder(default = "Arc::new(|_| {})")]
62    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
63
64    /// The width of the slider track.
65    #[builder(default = "Dp(200.0)")]
66    pub width: Dp,
67
68    /// The height of the slider track.
69    #[builder(default = "Dp(12.0)")]
70    pub track_height: Dp,
71
72    /// Glass tint color for the track background.
73    #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
74    pub track_tint_color: Color,
75
76    /// Glass tint color for the progress fill.
77    #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
78    pub progress_tint_color: Color,
79
80    /// Glass blur radius for all components.
81    #[builder(default = "8.0")]
82    pub blur_radius: f32,
83
84    /// Border width for the track.
85    #[builder(default = "Px(1).into()")]
86    pub track_border_width: Dp,
87
88    /// Disable interaction.
89    #[builder(default = "false")]
90    pub disabled: bool,
91}
92
93/// Creates a slider component with a frosted glass effect.
94///
95/// The `glass_slider` allows users to select a value from a continuous range (0.0 to 1.0)
96/// by dragging a handle along a track. It features a modern, semi-transparent
97/// "glassmorphism" aesthetic, with a blurred background and subtle highlights.
98///
99/// # Arguments
100///
101/// * `args` - An instance of `GlassSliderArgs` or `GlassSliderArgsBuilder` to configure the slider's appearance and behavior.
102///   - `value`: The current value of the slider, must be between 0.0 and 1.0.
103///   - `on_change`: A callback function that is triggered when the slider's value changes.
104///     It receives the new value as an `f32`.
105/// * `state` - An `Arc<Mutex<GlassSliderState>>` to manage the component's interactive state,
106///   such as dragging and focus.
107///
108/// # Example
109///
110/// ```rust,no_run
111/// use std::sync::Arc;
112/// use parking_lot::Mutex;
113/// use tessera_ui_basic_components::glass_slider::{glass_slider, GlassSliderArgsBuilder, GlassSliderState};
114///
115/// // In your application state
116/// let slider_value = Arc::new(Mutex::new(0.5));
117/// let slider_state = Arc::new(Mutex::new(GlassSliderState::new()));
118///
119/// // In your component function
120/// let value = *slider_value.lock();
121/// let on_change_callback = {
122///     let slider_value = slider_value.clone();
123///     Arc::new(move |new_value| {
124///         *slider_value.lock() = new_value;
125///     })
126/// };
127///
128/// glass_slider(
129///     GlassSliderArgsBuilder::default()
130///         .value(value)
131///         .on_change(on_change_callback)
132///         .build()
133///         .unwrap(),
134///     slider_state.clone(),
135/// );
136/// ```
137#[tessera]
138pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: Arc<Mutex<GlassSliderState>>) {
139    let args: GlassSliderArgs = args.into();
140
141    // External track (background) with border - capsule shape
142    fluid_glass(
143        FluidGlassArgsBuilder::default()
144            .width(DimensionValue::Fixed(args.width.to_px()))
145            .height(DimensionValue::Fixed(args.track_height.to_px()))
146            .tint_color(args.track_tint_color)
147            .blur_radius(args.blur_radius)
148            .shape(Shape::RoundedRectangle {
149                corner_radius: args.track_height.0 as f32 / 2.0,
150                g2_k_value: 2.0, // Capsule shape
151            })
152            .border(GlassBorder::new(args.track_border_width.into()))
153            .padding(args.track_border_width)
154            .build()
155            .unwrap(),
156        None,
157        move || {
158            // Internal progress fill - capsule shape using surface
159            let progress_width = (args.width.to_px().to_f32() * args.value)
160                - (args.track_border_width.to_px().to_f32() * 2.0);
161            let effective_height = args.track_height.to_px().to_f32()
162                - (args.track_border_width.to_px().to_f32() * 2.0);
163            fluid_glass(
164                FluidGlassArgsBuilder::default()
165                    .width(DimensionValue::Fixed(Px(progress_width as i32)))
166                    .height(DimensionValue::Fill {
167                        min: None,
168                        max: None,
169                    })
170                    .tint_color(args.progress_tint_color)
171                    .shape(Shape::RoundedRectangle {
172                        corner_radius: effective_height / 2.0,
173                        g2_k_value: 2.0, // Capsule shape
174                    })
175                    .refraction_amount(0.0)
176                    .build()
177                    .unwrap(),
178                None,
179                || {},
180            );
181        },
182    );
183
184    let on_change = args.on_change.clone();
185    let state_handler_state = state.clone();
186    let disabled = args.disabled;
187
188    state_handler(Box::new(move |input| {
189        if disabled {
190            return;
191        }
192        let mut state = state_handler_state.lock();
193
194        let is_in_component = input.cursor_position_rel.is_some_and(|cursor_pos| {
195            cursor_pos.x.0 >= 0
196                && cursor_pos.x.0 < input.computed_data.width.0
197                && cursor_pos.y.0 >= 0
198                && cursor_pos.y.0 < input.computed_data.height.0
199        });
200
201        // Set cursor to pointer when hovering over the slider
202        if is_in_component {
203            input.requests.cursor_icon = CursorIcon::Pointer;
204        }
205
206        if !is_in_component && !state.is_dragging {
207            return;
208        }
209
210        let mut new_value = None;
211
212        for event in input.cursor_events.iter() {
213            match &event.content {
214                CursorEventContent::Pressed(_) => {
215                    state.focus.request_focus();
216                    state.is_dragging = true;
217
218                    if let Some(pos) = input.cursor_position_rel {
219                        let v =
220                            (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
221                        new_value = Some(v);
222                    }
223                }
224                CursorEventContent::Released(_) => {
225                    state.is_dragging = false;
226                }
227                _ => {}
228            }
229        }
230
231        if state.is_dragging {
232            if let Some(pos) = input.cursor_position_rel {
233                let v = (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
234                new_value = Some(v);
235            }
236        }
237
238        if let Some(v) = new_value {
239            if (v - args.value).abs() > f32::EPSILON {
240                on_change(v);
241            }
242        }
243    }));
244
245    measure(Box::new(move |input| {
246        let self_width = args.width.to_px();
247        let self_height = args.track_height.to_px();
248
249        let track_id = input.children_ids[0];
250
251        // Measure track
252        let track_constraint = Constraint::new(
253            DimensionValue::Fixed(self_width),
254            DimensionValue::Fixed(self_height),
255        );
256        input.measure_child(track_id, &track_constraint)?;
257        input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
258
259        Ok(ComputedData {
260            width: self_width,
261            height: self_height,
262        })
263    }));
264}