tessera_ui_basic_components/slider.rs
1//! A reusable, interactive slider UI component for selecting a value within the range [0.0, 1.0].
2//!
3//! This module provides a customizable horizontal slider, suitable for use in forms, settings panels,
4//! media controls, or any scenario where users need to adjust a continuous value. The slider supports
5//! mouse and keyboard interaction, visual feedback for dragging and focus, and allows full control over
6//! appearance and behavior via configuration options and callbacks.
7//!
8//! Typical use cases include volume controls, progress bars, brightness adjustments, and other parameter selection tasks.
9//!
10//! The slider is fully controlled: you provide the current value and handle updates via a callback.
11//! State management (e.g., dragging, focus) is handled externally and passed in, enabling integration with various UI frameworks.
12//!
13//! See [`SliderArgs`] and [`SliderState`] for configuration and state management details.
14
15use std::sync::Arc;
16
17use derive_builder::Builder;
18use parking_lot::Mutex;
19use tessera_ui::{
20 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
21 focus_state::Focus, winit::window::CursorIcon,
22};
23use tessera_ui_macros::tessera;
24
25use crate::{
26 shape_def::Shape,
27 surface::{SurfaceArgsBuilder, surface},
28};
29
30///
31/// Stores the interactive state for the [`slider`] component, such as whether the slider is currently being dragged by the user.
32/// This struct should be managed via [`Arc<Mutex<SliderState>>`] and passed to the [`slider`] function to enable correct interaction handling.
33///
34/// # Fields
35/// - `is_dragging`: Indicates whether the user is actively dragging the slider thumb.
36/// - `focus`: Manages keyboard focus for the slider component.
37///
38/// Typically, you create and manage this state using [`use_state`] or similar state management utilities.
39///
40/// [`slider`]: crate::slider
41pub struct SliderState {
42 /// True if the user is currently dragging the slider.
43 pub is_dragging: bool,
44 /// The focus handler for the slider.
45 pub focus: Focus,
46}
47
48impl Default for SliderState {
49 fn default() -> Self {
50 Self::new()
51 }
52}
53
54impl SliderState {
55 pub fn new() -> Self {
56 Self {
57 is_dragging: false,
58 focus: Focus::new(),
59 }
60 }
61}
62
63/// Arguments for the `slider` component.
64#[derive(Builder, Clone)]
65#[builder(pattern = "owned")]
66pub struct SliderArgs {
67 /// The current value of the slider, ranging from 0.0 to 1.0.
68 #[builder(default = "0.0")]
69 pub value: f32,
70
71 /// Callback function triggered when the slider's value changes.
72 #[builder(default = "Arc::new(|_| {})")]
73 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
74
75 /// The width of the slider track.
76 #[builder(default = "Dp(200.0)")]
77 pub width: Dp,
78
79 /// The height of the slider track.
80 #[builder(default = "Dp(12.0)")]
81 pub track_height: Dp,
82
83 /// The color of the active part of the track (progress fill).
84 #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
85 pub active_track_color: Color,
86
87 /// The color of the inactive part of the track (background).
88 #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
89 pub inactive_track_color: Color,
90
91 /// Disable interaction.
92 #[builder(default = "false")]
93 pub disabled: bool,
94}
95
96#[tessera]
97///
98/// Renders a slider UI component that allows users to select a value in the range `[0.0, 1.0]`.
99///
100/// The slider displays a horizontal track with a draggable thumb. The current value is visually represented by the filled portion of the track.
101/// The component is fully controlled: you must provide the current value and a callback to handle value changes.
102///
103/// # Parameters
104/// - `args`: Arguments for configuring the slider. See [`SliderArgs`] for all options. The most important are:
105/// - `value` (`f32`): The current value of the slider, in the range `[0.0, 1.0]`.
106/// - `on_change` (`Arc<dyn Fn(f32) + Send + Sync>`): Callback invoked when the user changes the slider's value.
107/// - `width`, `track_height`, `active_track_color`, `inactive_track_color`, `disabled`: Appearance and interaction options.
108/// - `state`: Shared state for the slider, used to track interaction (e.g., dragging, focus). Create and manage this using [`use_state`] or similar, and pass it to the slider for correct behavior.
109///
110/// # State Management
111/// The `state` parameter must be an [`Arc<Mutex<SliderState>>`]. You can create and manage it using the `use_state` hook or any other state management approach compatible with your application.
112///
113/// # Example
114/// ```
115/// use std::sync::Arc;
116/// use parking_lot::Mutex;
117/// use tessera_ui::Dp;
118/// use tessera_ui_basic_components::slider::{slider, SliderArgs, SliderState, SliderArgsBuilder};
119///
120/// // In a real application, you would manage the state.
121/// let slider_state = Arc::new(Mutex::new(SliderState::new()));
122///
123/// // Create a slider with a width of 200dp and an initial value of 0.5.
124/// slider(
125/// SliderArgsBuilder::default()
126/// .width(Dp(200.0))
127/// .value(0.5)
128/// .on_change(Arc::new(|new_value| {
129/// // Update your application state here.
130/// println!("Slider value: {}", new_value);
131/// }))
132/// .build()
133/// .unwrap(),
134/// slider_state,
135/// );
136/// ```
137///
138/// This example demonstrates how to create a stateful slider and respond to value changes by updating your own state.
139///
140/// # See Also
141/// - [`SliderArgs`]
142/// - [`SliderState`]
143pub fn slider(args: impl Into<SliderArgs>, state: Arc<Mutex<SliderState>>) {
144 let args: SliderArgs = args.into();
145
146 // Background track (inactive part) - capsule shape
147 surface(
148 SurfaceArgsBuilder::default()
149 .width(DimensionValue::Fixed(args.width.to_px()))
150 .height(DimensionValue::Fixed(args.track_height.to_px()))
151 .color(args.inactive_track_color)
152 .shape(Shape::RoundedRectangle {
153 corner_radius: args.track_height.to_px().to_f32() / 2.0,
154 g2_k_value: 2.0, // Capsule shape
155 })
156 .build()
157 .unwrap(),
158 None,
159 move || {
160 // Progress fill (active part) - capsule shape
161 let progress_width = args.width.to_px().to_f32() * args.value;
162 surface(
163 SurfaceArgsBuilder::default()
164 .width(DimensionValue::Fixed(Px(progress_width as i32)))
165 .height(DimensionValue::Fill {
166 min: None,
167 max: None,
168 })
169 .color(args.active_track_color)
170 .shape(Shape::RoundedRectangle {
171 corner_radius: args.track_height.to_px().to_f32() / 2.0,
172 g2_k_value: 2.0, // Capsule shape
173 })
174 .build()
175 .unwrap(),
176 None,
177 || {},
178 );
179 },
180 );
181
182 let on_change = args.on_change.clone();
183 let state_handler_state = state.clone();
184 let disabled = args.disabled;
185
186 state_handler(Box::new(move |input| {
187 if disabled {
188 return;
189 }
190 let mut state = state_handler_state.lock();
191
192 let is_in_component = input.cursor_position_rel.is_some_and(|cursor_pos| {
193 cursor_pos.x.0 >= 0
194 && cursor_pos.x.0 < input.computed_data.width.0
195 && cursor_pos.y.0 >= 0
196 && cursor_pos.y.0 < input.computed_data.height.0
197 });
198
199 // Set cursor to pointer when hovering over the slider
200 if is_in_component {
201 input.requests.cursor_icon = CursorIcon::Pointer;
202 }
203
204 if !is_in_component && !state.is_dragging {
205 return;
206 }
207
208 let mut new_value = None;
209
210 for event in input.cursor_events.iter() {
211 match &event.content {
212 CursorEventContent::Pressed(_) => {
213 state.focus.request_focus();
214 state.is_dragging = true;
215
216 if let Some(pos) = input.cursor_position_rel {
217 let v =
218 (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
219 new_value = Some(v);
220 }
221 }
222 CursorEventContent::Released(_) => {
223 state.is_dragging = false;
224 }
225 _ => {}
226 }
227 }
228
229 if state.is_dragging {
230 if let Some(pos) = input.cursor_position_rel {
231 let v = (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
232 new_value = Some(v);
233 }
234 }
235
236 if let Some(v) = new_value {
237 if (v - args.value).abs() > f32::EPSILON {
238 on_change(v);
239 }
240 }
241 }));
242
243 measure(Box::new(move |input| {
244 let self_width = args.width.to_px();
245 let self_height = args.track_height.to_px();
246
247 let track_id = input.children_ids[0];
248
249 // Measure track
250 let track_constraint = Constraint::new(
251 DimensionValue::Fixed(self_width),
252 DimensionValue::Fixed(self_height),
253 );
254 input.measure_child(track_id, &track_constraint)?;
255 input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
256
257 Ok(ComputedData {
258 width: self_width,
259 height: self_height,
260 })
261 }));
262}