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::RwLock;
19use tessera_ui::{
20    Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, InputHandlerInput,
21    MeasureInput, MeasurementError, Px, PxPosition, focus_state::Focus, tessera,
22    winit::window::CursorIcon,
23};
24
25use crate::{
26    shape_def::Shape,
27    surface::{SurfaceArgsBuilder, surface},
28};
29
30/// Stores the interactive state for the [`slider`] component, such as whether the slider is currently being dragged by the user.
31/// This struct should be managed via [`Arc<Mutex<SliderState>>`] and passed to the [`slider`] function to enable correct interaction handling.
32///
33/// - `is_dragging`: Indicates whether the user is actively dragging the slider thumb.
34/// - `focus`: Manages keyboard focus for the slider component.
35///
36/// [`slider`]: crate::slider
37pub struct SliderState {
38    /// True if the user is currently dragging the slider.
39    pub is_dragging: bool,
40    /// The focus handler for the slider.
41    pub focus: Focus,
42}
43
44impl Default for SliderState {
45    fn default() -> Self {
46        Self::new()
47    }
48}
49
50impl SliderState {
51    pub fn new() -> Self {
52        Self {
53            is_dragging: false,
54            focus: Focus::new(),
55        }
56    }
57}
58
59/// Arguments for the `slider` component.
60#[derive(Builder, Clone)]
61#[builder(pattern = "owned")]
62pub struct SliderArgs {
63    /// The current value of the slider, ranging from 0.0 to 1.0.
64    #[builder(default = "0.0")]
65    pub value: f32,
66
67    /// Callback function triggered when the slider's value changes.
68    #[builder(default = "Arc::new(|_| {})")]
69    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
70
71    /// The width of the slider track.
72    #[builder(default = "Dp(200.0)")]
73    pub width: Dp,
74
75    /// The height of the slider track.
76    #[builder(default = "Dp(12.0)")]
77    pub track_height: Dp,
78
79    /// The color of the active part of the track (progress fill).
80    #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
81    pub active_track_color: Color,
82
83    /// The color of the inactive part of the track (background).
84    #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
85    pub inactive_track_color: Color,
86
87    /// Disable interaction.
88    #[builder(default = "false")]
89    pub disabled: bool,
90}
91
92/// Helper: check if a cursor position is within the bounds of a component.
93fn cursor_within_component(cursor_pos: Option<PxPosition>, computed: &ComputedData) -> bool {
94    if let Some(pos) = cursor_pos {
95        let within_x = pos.x.0 >= 0 && pos.x.0 < computed.width.0;
96        let within_y = pos.y.0 >= 0 && pos.y.0 < computed.height.0;
97        within_x && within_y
98    } else {
99        false
100    }
101}
102
103/// Helper: compute normalized progress (0.0..1.0) from cursor X and width.
104/// Returns None when cursor is not available.
105fn cursor_progress(cursor_pos: Option<PxPosition>, width_f: f32) -> Option<f32> {
106    cursor_pos.map(|pos| (pos.x.0 as f32 / width_f).clamp(0.0, 1.0))
107}
108
109fn handle_slider_state(
110    input: &mut InputHandlerInput,
111    state: &Arc<RwLock<SliderState>>,
112    args: &SliderArgs,
113) {
114    if args.disabled {
115        return;
116    }
117
118    let is_in_component = cursor_within_component(input.cursor_position_rel, &input.computed_data);
119
120    if is_in_component {
121        input.requests.cursor_icon = CursorIcon::Pointer;
122    }
123
124    if !is_in_component && !state.read().is_dragging {
125        return;
126    }
127
128    let width_f = input.computed_data.width.0 as f32;
129    let mut new_value: Option<f32> = None;
130
131    handle_cursor_events(input, &mut state.write(), &mut new_value, width_f);
132    update_value_on_drag(input, &state.read(), &mut new_value, width_f);
133    notify_on_change(new_value, args);
134}
135
136fn handle_cursor_events(
137    input: &mut InputHandlerInput,
138    state: &mut SliderState,
139    new_value: &mut Option<f32>,
140    width_f: f32,
141) {
142    for event in input.cursor_events.iter() {
143        match &event.content {
144            CursorEventContent::Pressed(_) => {
145                state.focus.request_focus();
146                state.is_dragging = true;
147                if let Some(v) = cursor_progress(input.cursor_position_rel, width_f) {
148                    *new_value = Some(v);
149                }
150            }
151            CursorEventContent::Released(_) => {
152                state.is_dragging = false;
153            }
154            _ => {}
155        }
156    }
157}
158
159fn update_value_on_drag(
160    input: &InputHandlerInput,
161    state: &SliderState,
162    new_value: &mut Option<f32>,
163    width_f: f32,
164) {
165    if state.is_dragging
166        && let Some(v) = cursor_progress(input.cursor_position_rel, width_f)
167    {
168        *new_value = Some(v);
169    }
170}
171
172fn notify_on_change(new_value: Option<f32>, args: &SliderArgs) {
173    if let Some(v) = new_value
174        && (v - args.value).abs() > f32::EPSILON
175    {
176        (args.on_change)(v);
177    }
178}
179
180fn render_track(args: &SliderArgs) {
181    surface(
182        SurfaceArgsBuilder::default()
183            .width(DimensionValue::Fixed(args.width.to_px()))
184            .height(DimensionValue::Fixed(args.track_height.to_px()))
185            .style(args.inactive_track_color.into())
186            .shape({
187                let radius = Dp(args.track_height.0 / 2.0);
188                Shape::RoundedRectangle {
189                    top_left: radius,
190                    top_right: radius,
191                    bottom_right: radius,
192                    bottom_left: radius,
193                    g2_k_value: 2.0, // Capsule shape
194                }
195            })
196            .build()
197            .unwrap(),
198        None,
199        move || {
200            render_progress_fill(args);
201        },
202    );
203}
204
205fn render_progress_fill(args: &SliderArgs) {
206    let progress_width = args.width.to_px().to_f32() * args.value;
207    surface(
208        SurfaceArgsBuilder::default()
209            .width(DimensionValue::Fixed(Px(progress_width as i32)))
210            .height(DimensionValue::Fill {
211                min: None,
212                max: None,
213            })
214            .style(args.active_track_color.into())
215            .shape({
216                let radius = Dp(args.track_height.0 / 2.0);
217                Shape::RoundedRectangle {
218                    top_left: radius,
219                    top_right: radius,
220                    bottom_right: radius,
221                    bottom_left: radius,
222                    g2_k_value: 2.0, // Capsule shape
223                }
224            })
225            .build()
226            .unwrap(),
227        None,
228        || {},
229    );
230}
231
232fn measure_slider(
233    input: &MeasureInput,
234    args: &SliderArgs,
235) -> Result<ComputedData, MeasurementError> {
236    let self_width = args.width.to_px();
237    let self_height = args.track_height.to_px();
238
239    let track_id = input.children_ids[0];
240
241    // Measure track
242    let track_constraint = Constraint::new(
243        DimensionValue::Fixed(self_width),
244        DimensionValue::Fixed(self_height),
245    );
246    input.measure_child(track_id, &track_constraint)?;
247    input.place_child(track_id, PxPosition::new(Px(0), Px(0)));
248
249    Ok(ComputedData {
250        width: self_width,
251        height: self_height,
252    })
253}
254
255/// Renders a slider UI component that allows users to select a value in the range `[0.0, 1.0]`.
256///
257/// The slider displays a horizontal track with a draggable thumb. The current value is visually represented by the filled portion of the track.
258/// The component is fully controlled: you must provide the current value and a callback to handle value changes.
259///
260/// # Parameters
261/// - `args`: Arguments for configuring the slider. See [`SliderArgs`] for all options. The most important are:
262///   - `value` (`f32`): The current value of the slider, in the range `[0.0, 1.0]`.
263///   - `on_change` (`Arc<dyn Fn(f32) + Send + Sync>`): Callback invoked when the user changes the slider's value.
264///   - `width`, `track_height`, `active_track_color`, `inactive_track_color`, `disabled`: Appearance and interaction options.
265/// - `state`: Shared state for the slider, used to track interaction (e.g., dragging, focus).
266///
267/// # State Management
268///
269/// The `state` parameter must be an [`Arc<Mutex<SliderState>>`].
270///
271/// # Example
272///
273/// ```
274/// use std::sync::Arc;
275/// use parking_lot::RwLock;
276/// use tessera_ui::Dp;
277/// use tessera_ui_basic_components::slider::{slider, SliderArgs, SliderState, SliderArgsBuilder};
278///
279/// // In a real application, you would manage the state.
280/// let slider_state = Arc::new(RwLock::new(SliderState::new()));
281///
282/// // Create a slider with a width of 200dp and an initial value of 0.5.
283/// slider(
284///     SliderArgsBuilder::default()
285///         .width(Dp(200.0))
286///         .value(0.5)
287///         .on_change(Arc::new(|new_value| {
288///             // Update your application state here.
289///             println!("Slider value: {}", new_value);
290///         }))
291///         .build()
292///         .unwrap(),
293///     slider_state,
294/// );
295/// ```
296///
297/// This example demonstrates how to create a stateful slider and respond to value changes by updating your own state.
298///
299/// # See Also
300///
301/// - [`SliderArgs`]
302/// - [`SliderState`]
303#[tessera]
304pub fn slider(args: impl Into<SliderArgs>, state: Arc<RwLock<SliderState>>) {
305    let args: SliderArgs = args.into();
306
307    render_track(&args);
308
309    let cloned_args = args.clone();
310    let state_clone = state.clone();
311    input_handler(Box::new(move |mut input| {
312        handle_slider_state(&mut input, &state_clone, &cloned_args);
313    }));
314
315    let cloned_args = args.clone();
316    measure(Box::new(move |input| measure_slider(input, &cloned_args)));
317}