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}