tessera_ui_basic_components/
slider.rs

1//! An interactive slider component for selecting a value in a range.
2//!
3//! ## Usage
4//!
5//! Use to allow users to select a value from a continuous range.
6use std::sync::Arc;
7
8use derive_builder::Builder;
9use parking_lot::{RwLock, RwLockReadGuard, RwLockWriteGuard};
10use tessera_ui::{
11    Color, ComputedData, Constraint, DimensionValue, Dp, MeasureInput, MeasurementError, Px,
12    PxPosition, focus_state::Focus, tessera,
13};
14
15use crate::{material_color, pipelines::image_vector::command::VectorTintMode};
16
17use interaction::{
18    apply_range_slider_accessibility, apply_slider_accessibility, handle_range_slider_state,
19    handle_slider_state,
20};
21use layout::{
22    CenteredSliderLayout, RangeSliderLayout, SliderLayout, centered_slider_layout,
23    fallback_component_width, range_slider_layout, resolve_component_width, slider_layout,
24};
25use render::{
26    render_active_segment, render_centered_stops, render_centered_tracks, render_focus,
27    render_handle, render_inactive_segment, render_range_stops, render_range_tracks,
28    render_stop_indicator,
29};
30
31pub use interaction::RangeSliderState;
32
33mod interaction;
34mod layout;
35mod render;
36
37const ACCESSIBILITY_STEP: f32 = 0.05;
38const MIN_TOUCH_TARGET: Dp = Dp(40.0);
39const HANDLE_GAP: Dp = Dp(6.0);
40const STOP_INDICATOR_DIAMETER: Dp = Dp(4.0);
41
42/// Stores the interactive state for the [`slider`] component, such as whether the slider is currently being dragged by the user.
43/// The [`SliderState`] handle owns the necessary locking internally, so callers can simply clone and pass it between components.
44pub(crate) struct SliderStateInner {
45    /// True if the user is currently dragging the slider.
46    pub is_dragging: bool,
47    /// The focus handler for the slider.
48    pub focus: Focus,
49    /// True when the cursor is hovering inside the slider bounds.
50    pub is_hovered: bool,
51}
52
53impl Default for SliderStateInner {
54    fn default() -> Self {
55        Self::new()
56    }
57}
58
59impl SliderStateInner {
60    pub fn new() -> Self {
61        Self {
62            is_dragging: false,
63            focus: Focus::new(),
64            is_hovered: false,
65        }
66    }
67}
68
69/// External state for the `slider` component.
70///
71/// # Example
72///
73/// ```
74/// use tessera_ui_basic_components::slider::SliderState;
75///
76/// let slider_state = SliderState::new();
77/// ```
78#[derive(Clone)]
79pub struct SliderState {
80    inner: Arc<RwLock<SliderStateInner>>,
81}
82
83impl SliderState {
84    /// Creates a new slider state handle.
85    pub fn new() -> Self {
86        Self {
87            inner: Arc::new(RwLock::new(SliderStateInner::new())),
88        }
89    }
90
91    pub(crate) fn read(&self) -> RwLockReadGuard<'_, SliderStateInner> {
92        self.inner.read()
93    }
94
95    pub(crate) fn write(&self) -> RwLockWriteGuard<'_, SliderStateInner> {
96        self.inner.write()
97    }
98
99    /// Returns whether the slider handle is currently being dragged.
100    pub fn is_dragging(&self) -> bool {
101        self.inner.read().is_dragging
102    }
103
104    /// Manually sets the dragging flag. Useful for custom gesture integrations.
105    pub fn set_dragging(&self, dragging: bool) {
106        self.inner.write().is_dragging = dragging;
107    }
108
109    /// Requests focus for the slider.
110    pub fn request_focus(&self) {
111        self.inner.write().focus.request_focus();
112    }
113
114    /// Clears focus from the slider if it is currently focused.
115    pub fn clear_focus(&self) {
116        self.inner.write().focus.unfocus();
117    }
118
119    /// Returns `true` if this slider currently holds focus.
120    pub fn is_focused(&self) -> bool {
121        self.inner.read().focus.is_focused()
122    }
123
124    /// Returns `true` if the cursor is hovering over this slider.
125    pub fn is_hovered(&self) -> bool {
126        self.inner.read().is_hovered
127    }
128}
129
130impl Default for SliderState {
131    fn default() -> Self {
132        Self::new()
133    }
134}
135
136/// Size variants for the slider component.
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum SliderSize {
139    /// Extra Small (default).
140    #[default]
141    ExtraSmall,
142    /// Small.
143    Small,
144    /// Medium.
145    Medium,
146    /// Large.
147    Large,
148    /// Extra Large.
149    ExtraLarge,
150}
151
152/// Arguments for the `slider` component.
153#[derive(Builder, Clone)]
154#[builder(pattern = "owned")]
155pub struct SliderArgs {
156    /// The current value of the slider, ranging from 0.0 to 1.0.
157    #[builder(default = "0.0")]
158    pub value: f32,
159    /// Callback function triggered when the slider's value changes.
160    #[builder(default = "Arc::new(|_| {})")]
161    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
162    /// Size variant of the slider.
163    #[builder(default)]
164    pub size: SliderSize,
165    /// Total width of the slider control.
166    #[builder(default = "DimensionValue::Fixed(Dp(260.0).to_px())")]
167    pub width: DimensionValue,
168    /// The color of the active part of the track (progress fill).
169    #[builder(default = "crate::material_color::global_material_scheme().primary")]
170    pub active_track_color: Color,
171    /// The color of the inactive part of the track (background).
172    #[builder(default = "crate::material_color::global_material_scheme().secondary_container")]
173    pub inactive_track_color: Color,
174    /// The thickness of the handle indicator.
175    #[builder(default = "Dp(4.0)")]
176    pub thumb_diameter: Dp,
177    /// Color of the handle indicator.
178    #[builder(default = "crate::material_color::global_material_scheme().primary")]
179    pub thumb_color: Color,
180    /// Height of the handle focus layer (hover/drag halo).
181    #[builder(default = "Dp(18.0)")]
182    pub state_layer_diameter: Dp,
183    /// Base color for the state layer; alpha will be adjusted per interaction state.
184    #[builder(
185        default = "crate::material_color::global_material_scheme().primary.with_alpha(0.18)"
186    )]
187    pub state_layer_color: Color,
188    /// Disable interaction.
189    #[builder(default = "false")]
190    pub disabled: bool,
191    /// Optional accessibility label read by assistive technologies.
192    #[builder(default, setter(strip_option, into))]
193    pub accessibility_label: Option<String>,
194    /// Optional accessibility description.
195    #[builder(default, setter(strip_option, into))]
196    pub accessibility_description: Option<String>,
197    /// Whether to show the stop indicators at the ends of the track.
198    #[builder(default = "true")]
199    pub show_stop_indicator: bool,
200    /// Optional icon content to display at the start of the slider (only for Medium sizes and above).
201    #[builder(default, setter(strip_option, into))]
202    pub inset_icon: Option<crate::icon::IconContent>,
203}
204
205/// Arguments for the `range_slider` component.
206#[derive(Builder, Clone)]
207#[builder(pattern = "owned")]
208pub struct RangeSliderArgs {
209    /// The current range values (start, end), each between 0.0 and 1.0.
210    #[builder(default = "(0.0, 1.0)")]
211    pub value: (f32, f32),
212
213    /// Callback function triggered when the range values change.
214    #[builder(default = "Arc::new(|_| {})")]
215    pub on_change: Arc<dyn Fn((f32, f32)) + Send + Sync>,
216
217    /// Size variant of the slider.
218    #[builder(default)]
219    pub size: SliderSize,
220
221    /// Total width of the slider control.
222    #[builder(default = "DimensionValue::Fixed(Dp(260.0).to_px())")]
223    pub width: DimensionValue,
224
225    /// The color of the active part of the track (range fill).
226    #[builder(default = "crate::material_color::global_material_scheme().primary")]
227    pub active_track_color: Color,
228
229    /// The color of the inactive part of the track (background).
230    #[builder(default = "crate::material_color::global_material_scheme().secondary_container")]
231    pub inactive_track_color: Color,
232
233    /// The thickness of the handle indicators.
234    #[builder(default = "Dp(4.0)")]
235    pub thumb_diameter: Dp,
236
237    /// Color of the handle indicators.
238    #[builder(default = "crate::material_color::global_material_scheme().primary")]
239    pub thumb_color: Color,
240
241    /// Height of the handle focus layer.
242    #[builder(default = "Dp(18.0)")]
243    pub state_layer_diameter: Dp,
244
245    /// Base color for the state layer.
246    #[builder(
247        default = "crate::material_color::global_material_scheme().primary.with_alpha(0.18)"
248    )]
249    pub state_layer_color: Color,
250
251    /// Disable interaction.
252    #[builder(default = "false")]
253    pub disabled: bool,
254    /// Optional accessibility label.
255    #[builder(default, setter(strip_option, into))]
256    pub accessibility_label: Option<String>,
257    /// Optional accessibility description.
258    #[builder(default, setter(strip_option, into))]
259    pub accessibility_description: Option<String>,
260
261    /// Whether to show the stop indicators at the ends of the track.
262    #[builder(default = "true")]
263    pub show_stop_indicator: bool,
264}
265
266fn measure_slider(
267    input: &MeasureInput,
268    layout: SliderLayout,
269    clamped_value: f32,
270    has_inset_icon: bool,
271) -> Result<ComputedData, MeasurementError> {
272    let self_width = layout.component_width;
273    let self_height = layout.component_height;
274
275    let active_id = input.children_ids[0];
276    let inactive_id = input.children_ids[1];
277
278    // Order in render: active, inactive, [icon], focus, handle, [stop]
279    let mut current_index = 2;
280
281    let icon_id = if has_inset_icon {
282        let id = input.children_ids.get(current_index).copied();
283        current_index += 1;
284        id
285    } else {
286        None
287    };
288
289    let focus_id = input.children_ids[current_index];
290    current_index += 1;
291    let handle_id = input.children_ids[current_index];
292    current_index += 1;
293
294    let stop_id = if layout.show_stop_indicator {
295        input.children_ids.get(current_index).copied()
296    } else {
297        None
298    };
299
300    let active_width = layout.active_width(clamped_value);
301    let inactive_width = layout.inactive_width(clamped_value);
302
303    let active_constraint = Constraint::new(
304        DimensionValue::Fixed(active_width),
305        DimensionValue::Fixed(layout.track_height),
306    );
307    input.measure_child(active_id, &active_constraint)?;
308    input.place_child(active_id, PxPosition::new(Px(0), layout.track_y));
309
310    let inactive_constraint = Constraint::new(
311        DimensionValue::Fixed(inactive_width),
312        DimensionValue::Fixed(layout.track_height),
313    );
314    input.measure_child(inactive_id, &inactive_constraint)?;
315    input.place_child(
316        inactive_id,
317        PxPosition::new(
318            Px(active_width.0 + layout.handle_gap.0 * 2 + layout.handle_width.0),
319            layout.track_y,
320        ),
321    );
322
323    let focus_constraint = Constraint::new(
324        DimensionValue::Fixed(layout.focus_width),
325        DimensionValue::Fixed(layout.focus_height),
326    );
327    input.measure_child(focus_id, &focus_constraint)?;
328
329    let handle_constraint = Constraint::new(
330        DimensionValue::Fixed(layout.handle_width),
331        DimensionValue::Fixed(layout.handle_height),
332    );
333    input.measure_child(handle_id, &handle_constraint)?;
334
335    let handle_center = layout.handle_center(clamped_value);
336    let focus_offset = layout.center_child_offset(layout.focus_width);
337    input.place_child(
338        focus_id,
339        PxPosition::new(Px(handle_center.x.0 - focus_offset.0), layout.focus_y),
340    );
341
342    let handle_offset = layout.center_child_offset(layout.handle_width);
343    input.place_child(
344        handle_id,
345        PxPosition::new(Px(handle_center.x.0 - handle_offset.0), layout.handle_y),
346    );
347
348    if let Some(stop_id) = stop_id {
349        let stop_size = layout.stop_indicator_diameter;
350        let stop_constraint = Constraint::new(
351            DimensionValue::Fixed(stop_size),
352            DimensionValue::Fixed(stop_size),
353        );
354        input.measure_child(stop_id, &stop_constraint)?;
355        let stop_offset = layout.center_child_offset(layout.stop_indicator_diameter);
356        let inactive_start = active_width.0 + layout.handle_gap.0 * 2 + layout.handle_width.0;
357        let padding = Dp(8.0).to_px() - stop_size / Px(2);
358        let stop_center_x = Px(inactive_start + inactive_width.0 - padding.0);
359        input.place_child(
360            stop_id,
361            PxPosition::new(Px(stop_center_x.0 - stop_offset.0), layout.stop_indicator_y),
362        );
363    }
364
365    if let Some(icon_id) = icon_id
366        && let Some(icon_size) = layout.icon_size
367    {
368        let icon_constraint = Constraint::new(
369            DimensionValue::Wrap {
370                min: None,
371                max: Some(icon_size.into()),
372            },
373            DimensionValue::Wrap {
374                min: None,
375                max: Some(icon_size.into()),
376            },
377        );
378        let icon_measured = input.measure_child(icon_id, &icon_constraint)?;
379
380        // Icon placement: 8dp padding from left edge, vertically centered within the track
381        let icon_padding = Dp(8.0).to_px();
382        let icon_y = layout.track_y + Px((layout.track_height.0 - icon_measured.height.0) / 2);
383        input.place_child(icon_id, PxPosition::new(icon_padding, icon_y));
384    }
385
386    Ok(ComputedData {
387        width: self_width,
388        height: self_height,
389    })
390}
391
392#[derive(Clone, Copy)]
393struct SliderColors {
394    active_track: Color,
395    inactive_track: Color,
396    handle: Color,
397    handle_focus: Color,
398}
399
400fn slider_colors(args: &SliderArgs, is_hovered: bool, is_dragging: bool) -> SliderColors {
401    if args.disabled {
402        let scheme = material_color::global_material_scheme();
403        return SliderColors {
404            active_track: scheme.on_surface.with_alpha(0.38),
405            inactive_track: scheme.on_surface.with_alpha(0.12),
406            handle: scheme.on_surface.with_alpha(0.38),
407            handle_focus: Color::new(0.0, 0.0, 0.0, 0.0),
408        };
409    }
410
411    let mut state_layer_alpha_scale = 0.0;
412    if is_dragging {
413        state_layer_alpha_scale = 1.0;
414    } else if is_hovered {
415        state_layer_alpha_scale = 0.7;
416    }
417    let base_state = args.state_layer_color;
418    let state_layer_alpha = (base_state.a * state_layer_alpha_scale).clamp(0.0, 1.0);
419    let handle_focus = Color::new(base_state.r, base_state.g, base_state.b, state_layer_alpha);
420
421    SliderColors {
422        active_track: args.active_track_color,
423        inactive_track: args.inactive_track_color,
424        handle: args.thumb_color,
425        handle_focus,
426    }
427}
428
429/// # slider
430///
431/// Renders an interactive slider with a bar-style handle for selecting a value between 0.0 and 1.0.
432///
433/// ## Usage
434///
435/// Use for settings like volume or brightness, or for any user-adjustable value.
436///
437/// ## Parameters
438///
439/// - `args` — configures the slider's value, appearance, and callbacks; see [`SliderArgs`].
440/// - `state` — a clonable [`SliderState`] to manage interaction state like dragging and focus.
441///
442/// ## Examples
443///
444/// ```
445/// use std::sync::Arc;
446/// use tessera_ui::{DimensionValue, Dp};
447/// use tessera_ui_basic_components::slider::{slider, SliderArgsBuilder, SliderState};
448///
449/// // In a real application, you would manage this state.
450/// let slider_state = SliderState::new();
451///
452/// slider(
453///     SliderArgsBuilder::default()
454///         .width(DimensionValue::Fixed(Dp(200.0).to_px()))
455///         .value(0.5)
456///         .on_change(Arc::new(|new_value| {
457///             // In a real app, you would update your state here.
458///             println!("Slider value changed to: {}", new_value);
459///         }))
460///         .build()
461///         .unwrap(),
462///     slider_state,
463/// );
464/// ```
465#[tessera]
466pub fn slider(args: impl Into<SliderArgs>, state: SliderState) {
467    let args: SliderArgs = args.into();
468    let initial_width = fallback_component_width(&args);
469    let layout = slider_layout(&args, initial_width);
470    let clamped_value = args.value.clamp(0.0, 1.0);
471    let state_snapshot = state.read();
472    let colors = slider_colors(&args, state_snapshot.is_hovered, state_snapshot.is_dragging);
473    drop(state_snapshot);
474
475    render_active_segment(layout, &colors);
476    render_inactive_segment(layout, &colors);
477
478    if let Some(icon_size) = layout.icon_size
479        && let Some(inset_icon) = args.inset_icon.as_ref()
480    {
481        let scheme = material_color::global_material_scheme();
482        let tint = if args.disabled {
483            scheme.on_surface.with_alpha(0.38)
484        } else {
485            scheme.on_primary
486        };
487
488        crate::icon::icon(
489            crate::icon::IconArgsBuilder::default()
490                .content(inset_icon.clone())
491                .tint(tint)
492                .tint_mode(VectorTintMode::Solid)
493                .size(icon_size)
494                .build()
495                .expect("Failed to build icon args"),
496        );
497    }
498
499    render_focus(layout, &colors);
500    render_handle(layout, &colors);
501    if layout.show_stop_indicator {
502        render_stop_indicator(layout, &colors);
503    }
504
505    let cloned_args = args.clone();
506    let state_clone = state.clone();
507    let clamped_value_for_accessibility = clamped_value;
508    input_handler(Box::new(move |mut input| {
509        let resolved_layout = slider_layout(&cloned_args, input.computed_data.width);
510        handle_slider_state(&mut input, &state_clone, &cloned_args, &resolved_layout);
511        apply_slider_accessibility(
512            &mut input,
513            &cloned_args,
514            clamped_value_for_accessibility,
515            &cloned_args.on_change,
516        );
517    }));
518
519    measure(Box::new(move |input| {
520        let component_width = resolve_component_width(&args, input.parent_constraint);
521        let resolved_layout = slider_layout(&args, component_width);
522        let has_inset_icon = args.inset_icon.is_some();
523        measure_slider(input, resolved_layout, clamped_value, has_inset_icon)
524    }));
525}
526
527fn measure_centered_slider(
528    input: &MeasureInput,
529    layout: CenteredSliderLayout,
530    value: f32,
531) -> Result<ComputedData, MeasurementError> {
532    let self_width = layout.base.component_width;
533    let self_height = layout.base.component_height;
534    let track_y = layout.base.track_y;
535
536    let left_inactive_id = input.children_ids[0];
537    let active_id = input.children_ids[1];
538    let right_inactive_id = input.children_ids[2];
539    let focus_id = input.children_ids[3];
540    let handle_id = input.children_ids[4];
541    let left_stop_id = input.children_ids[5];
542    let right_stop_id = input.children_ids[6];
543
544    let segments = layout.segments(value);
545
546    // 1. Left Inactive
547    input.measure_child(
548        left_inactive_id,
549        &Constraint::new(
550            DimensionValue::Fixed(segments.left_inactive.1),
551            DimensionValue::Fixed(layout.base.track_height),
552        ),
553    )?;
554    input.place_child(
555        left_inactive_id,
556        PxPosition::new(segments.left_inactive.0, track_y),
557    );
558
559    // 2. Active
560    input.measure_child(
561        active_id,
562        &Constraint::new(
563            DimensionValue::Fixed(segments.active.1),
564            DimensionValue::Fixed(layout.base.track_height),
565        ),
566    )?;
567    input.place_child(active_id, PxPosition::new(segments.active.0, track_y));
568
569    // 3. Right Inactive
570    input.measure_child(
571        right_inactive_id,
572        &Constraint::new(
573            DimensionValue::Fixed(segments.right_inactive.1),
574            DimensionValue::Fixed(layout.base.track_height),
575        ),
576    )?;
577    input.place_child(
578        right_inactive_id,
579        PxPosition::new(segments.right_inactive.0, track_y),
580    );
581
582    // 4. Focus
583    let focus_offset = layout.base.center_child_offset(layout.base.focus_width);
584    input.measure_child(
585        focus_id,
586        &Constraint::new(
587            DimensionValue::Fixed(layout.base.focus_width),
588            DimensionValue::Fixed(layout.base.focus_height),
589        ),
590    )?;
591    input.place_child(
592        focus_id,
593        PxPosition::new(
594            Px(segments.handle_center.x.0 - focus_offset.0),
595            layout.base.focus_y,
596        ),
597    );
598
599    // 5. Handle
600    let handle_offset = layout.base.center_child_offset(layout.base.handle_width);
601    input.measure_child(
602        handle_id,
603        &Constraint::new(
604            DimensionValue::Fixed(layout.base.handle_width),
605            DimensionValue::Fixed(layout.base.handle_height),
606        ),
607    )?;
608    input.place_child(
609        handle_id,
610        PxPosition::new(
611            Px(segments.handle_center.x.0 - handle_offset.0),
612            layout.base.handle_y,
613        ),
614    );
615
616    if layout.base.show_stop_indicator {
617        // 6. Left Stop
618        let stop_size = layout.base.stop_indicator_diameter;
619        let stop_constraint = Constraint::new(
620            DimensionValue::Fixed(stop_size),
621            DimensionValue::Fixed(stop_size),
622        );
623        input.measure_child(left_stop_id, &stop_constraint)?;
624
625        let stop_offset = layout.base.center_child_offset(stop_size);
626        let stop_padding = layout.stop_indicator_offset();
627
628        let left_stop_x = Px(stop_padding.0);
629
630        input.place_child(
631            left_stop_id,
632            PxPosition::new(
633                Px(left_stop_x.0 - stop_offset.0),
634                layout.base.stop_indicator_y,
635            ),
636        );
637
638        // 7. Right Stop
639        input.measure_child(right_stop_id, &stop_constraint)?;
640        let right_stop_x = Px(self_width.0 - stop_padding.0);
641
642        input.place_child(
643            right_stop_id,
644            PxPosition::new(
645                Px(right_stop_x.0 - stop_offset.0),
646                layout.base.stop_indicator_y,
647            ),
648        );
649    }
650
651    Ok(ComputedData {
652        width: self_width,
653        height: self_height,
654    })
655}
656
657/// # centered_slider
658///
659/// Renders an interactive slider that originates from the center (0.5), allowing selection of a value
660/// between 0.0 and 1.0. The active track extends from the center to the handle, while inactive
661/// tracks fill the remaining space.
662///
663/// ## Usage
664///
665/// Use for adjustments that have a neutral midpoint, such as balance controls or deviation settings.
666///
667/// ## Parameters
668///
669/// - `args` — configures the slider's value, appearance, and callbacks; see [`SliderArgs`].
670/// - `state` — a clonable [`SliderState`] to manage interaction state like dragging and focus.
671///
672/// ## Examples
673///
674/// ```
675/// use std::sync::{Arc, Mutex};
676/// use tessera_ui::{DimensionValue, Dp};
677/// use tessera_ui_basic_components::slider::{centered_slider, SliderArgsBuilder, SliderState};
678///
679/// let slider_state = SliderState::new();
680/// let current_value = Arc::new(Mutex::new(0.5));
681///
682/// // Simulate a value change
683/// {
684///     let mut value_guard = current_value.lock().unwrap();
685///     *value_guard = 0.75;
686///     assert_eq!(*value_guard, 0.75);
687/// }
688///
689/// centered_slider(
690///     SliderArgsBuilder::default()
691///         .width(DimensionValue::Fixed(Dp(200.0).to_px()))
692///         .value(*current_value.lock().unwrap())
693///         .on_change(Arc::new(move |new_value| {
694///             // In a real app, you would update your state here.
695///             // For this example, we'll just check it after the simulated change.
696///             println!("Centered slider value changed to: {}", new_value);
697///         }))
698///         .build()
699///         .unwrap(),
700///     slider_state.clone(),
701/// );
702///
703/// // Simulate another value change and check the state
704/// {
705///     let mut value_guard = current_value.lock().unwrap();
706///     *value_guard = 0.25;
707///     assert_eq!(*value_guard, 0.25);
708/// }
709/// ```
710#[tessera]
711pub fn centered_slider(args: impl Into<SliderArgs>, state: SliderState) {
712    let args: SliderArgs = args.into();
713    let initial_width = fallback_component_width(&args);
714    let layout = centered_slider_layout(&args, initial_width);
715    let clamped_value = args.value.clamp(0.0, 1.0);
716    let state_snapshot = state.read();
717    let colors = slider_colors(&args, state_snapshot.is_hovered, state_snapshot.is_dragging);
718    drop(state_snapshot);
719
720    render_centered_tracks(layout, &colors);
721    render_focus(layout.base, &colors);
722    render_handle(layout.base, &colors);
723    if layout.base.show_stop_indicator {
724        render_centered_stops(layout, &colors);
725    }
726
727    let cloned_args = args.clone();
728    let state_clone = state.clone();
729    let clamped_value_for_accessibility = clamped_value;
730    input_handler(Box::new(move |mut input| {
731        let resolved_layout = centered_slider_layout(&cloned_args, input.computed_data.width);
732        handle_slider_state(
733            &mut input,
734            &state_clone,
735            &cloned_args,
736            &resolved_layout.base,
737        );
738        apply_slider_accessibility(
739            &mut input,
740            &cloned_args,
741            clamped_value_for_accessibility,
742            &cloned_args.on_change,
743        );
744    }));
745
746    measure(Box::new(move |input| {
747        let component_width = resolve_component_width(&args, input.parent_constraint);
748        let resolved_layout = centered_slider_layout(&args, component_width);
749        measure_centered_slider(input, resolved_layout, clamped_value)
750    }));
751}
752
753fn measure_range_slider(
754    input: &MeasureInput,
755    layout: RangeSliderLayout,
756    start: f32,
757    end: f32,
758) -> Result<ComputedData, MeasurementError> {
759    let self_width = layout.base.component_width;
760    let self_height = layout.base.component_height;
761    let track_y = layout.base.track_y;
762
763    let left_inactive_id = input.children_ids[0];
764    let active_id = input.children_ids[1];
765    let right_inactive_id = input.children_ids[2];
766    let focus_start_id = input.children_ids[3];
767    let focus_end_id = input.children_ids[4];
768    let handle_start_id = input.children_ids[5];
769    let handle_end_id = input.children_ids[6];
770    let stop_start_id = input.children_ids[7];
771    let stop_end_id = input.children_ids[8];
772
773    let segments = layout.segments(start, end);
774
775    input.measure_child(
776        left_inactive_id,
777        &Constraint::new(
778            DimensionValue::Fixed(segments.left_inactive.1),
779            DimensionValue::Fixed(layout.base.track_height),
780        ),
781    )?;
782    input.place_child(
783        left_inactive_id,
784        PxPosition::new(segments.left_inactive.0, track_y),
785    );
786
787    input.measure_child(
788        active_id,
789        &Constraint::new(
790            DimensionValue::Fixed(segments.active.1),
791            DimensionValue::Fixed(layout.base.track_height),
792        ),
793    )?;
794    input.place_child(active_id, PxPosition::new(segments.active.0, track_y));
795
796    input.measure_child(
797        right_inactive_id,
798        &Constraint::new(
799            DimensionValue::Fixed(segments.right_inactive.1),
800            DimensionValue::Fixed(layout.base.track_height),
801        ),
802    )?;
803    input.place_child(
804        right_inactive_id,
805        PxPosition::new(segments.right_inactive.0, track_y),
806    );
807
808    let focus_constraint = Constraint::new(
809        DimensionValue::Fixed(layout.base.focus_width),
810        DimensionValue::Fixed(layout.base.focus_height),
811    );
812    let handle_constraint = Constraint::new(
813        DimensionValue::Fixed(layout.base.handle_width),
814        DimensionValue::Fixed(layout.base.handle_height),
815    );
816    let focus_offset = layout.base.center_child_offset(layout.base.focus_width);
817    let handle_offset = layout.base.center_child_offset(layout.base.handle_width);
818
819    input.measure_child(focus_start_id, &focus_constraint)?;
820    input.place_child(
821        focus_start_id,
822        PxPosition::new(
823            Px(segments.start_handle_center.x.0 - focus_offset.0),
824            layout.base.focus_y,
825        ),
826    );
827
828    input.measure_child(handle_start_id, &handle_constraint)?;
829    input.place_child(
830        handle_start_id,
831        PxPosition::new(
832            Px(segments.start_handle_center.x.0 - handle_offset.0),
833            layout.base.handle_y,
834        ),
835    );
836
837    input.measure_child(focus_end_id, &focus_constraint)?;
838    input.place_child(
839        focus_end_id,
840        PxPosition::new(
841            Px(segments.end_handle_center.x.0 - focus_offset.0),
842            layout.base.focus_y,
843        ),
844    );
845
846    input.measure_child(handle_end_id, &handle_constraint)?;
847    input.place_child(
848        handle_end_id,
849        PxPosition::new(
850            Px(segments.end_handle_center.x.0 - handle_offset.0),
851            layout.base.handle_y,
852        ),
853    );
854
855    if layout.base.show_stop_indicator {
856        let stop_size = layout.base.stop_indicator_diameter;
857        let stop_constraint = Constraint::new(
858            DimensionValue::Fixed(stop_size),
859            DimensionValue::Fixed(stop_size),
860        );
861        input.measure_child(stop_start_id, &stop_constraint)?;
862
863        let stop_offset = layout.base.center_child_offset(stop_size);
864        // We can reuse stop_indicator_offset logic if we expose it or reimplement it.
865        // layout.base doesn't have it, CenteredSliderLayout does.
866        // Let's reimplement simple padding: Dp(8.0) - size/2
867        let padding = Dp(8.0).to_px() - stop_size / Px(2);
868        let start_stop_x = Px(padding.0);
869
870        input.place_child(
871            stop_start_id,
872            PxPosition::new(
873                Px(start_stop_x.0 - stop_offset.0),
874                layout.base.stop_indicator_y,
875            ),
876        );
877
878        input.measure_child(stop_end_id, &stop_constraint)?;
879        let end_stop_x = Px(self_width.0 - padding.0);
880
881        input.place_child(
882            stop_end_id,
883            PxPosition::new(
884                Px(end_stop_x.0 - stop_offset.0),
885                layout.base.stop_indicator_y,
886            ),
887        );
888    }
889
890    Ok(ComputedData {
891        width: self_width,
892        height: self_height,
893    })
894}
895
896/// # range_slider
897///
898/// Renders an interactive slider with two handles, allowing selection of a range (start, end)
899/// between 0.0 and 1.0.
900///
901/// ## Usage
902///
903/// Use for filtering by range, setting minimum and maximum values, or defining an interval.
904///
905/// ## Parameters
906///
907/// - `args` — configures the slider's range, appearance, and callbacks; see [`RangeSliderArgs`].
908/// - `state` — a clonable [`RangeSliderState`] to manage interaction state for both handles.
909///
910/// ## Examples
911///
912/// ```
913/// use std::sync::{Arc, Mutex};
914/// use tessera_ui::{DimensionValue, Dp};
915/// use tessera_ui_basic_components::slider::{range_slider, RangeSliderArgsBuilder, RangeSliderState};
916///
917/// let slider_state = RangeSliderState::new();
918/// let range_value = Arc::new(Mutex::new((0.2, 0.8)));
919///
920/// range_slider(
921///     RangeSliderArgsBuilder::default()
922///         .width(DimensionValue::Fixed(Dp(200.0).to_px()))
923///         .value(*range_value.lock().unwrap())
924///         .on_change(Arc::new(move |(start, end)| {
925///             println!("Range changed: {} - {}", start, end);
926///         }))
927///         .build()
928///         .unwrap(),
929///     slider_state,
930/// );
931/// ```
932#[tessera]
933pub fn range_slider(args: impl Into<RangeSliderArgs>, state: RangeSliderState) {
934    let args: RangeSliderArgs = args.into();
935    // Convert RangeSliderArgs to SliderArgs for layout helpers where possible,
936    // or rely on the dedicated range_slider_layout which handles this.
937    let dummy_slider_args = SliderArgsBuilder::default()
938        .width(args.width)
939        .size(args.size)
940        .build()
941        .expect("Failed to build dummy args");
942    let initial_width = fallback_component_width(&dummy_slider_args);
943    let layout = range_slider_layout(&args, initial_width);
944
945    let start = args.value.0.clamp(0.0, 1.0);
946    let end = args.value.1.clamp(start, 1.0);
947
948    let state_snapshot = state.read();
949    // Determine colors based on interaction.
950    // We check if *either* handle is interacted with to highlight the active tracks/handles?
951    // Or ideally, we highlight specific handles.
952    // For simplicity, let's use a unified color struct but apply focus colors selectively.
953
954    let is_dragging_any = state_snapshot.is_dragging_start || state_snapshot.is_dragging_end;
955
956    // Override colors from specific RangeSliderArgs
957    // We need a helper to convert RangeSliderArgs colors to SliderColors if they differ
958    // But for now we just reused the dummy args construction above which didn't copy colors.
959    // Let's reconstruct colors properly.
960    let mut state_layer_alpha_scale = 0.0;
961    if is_dragging_any {
962        state_layer_alpha_scale = 1.0;
963    } else if state_snapshot.is_hovered {
964        state_layer_alpha_scale = 0.7;
965    }
966
967    let base_state = args.state_layer_color;
968    let state_layer_alpha = (base_state.a * state_layer_alpha_scale).clamp(0.0, 1.0);
969    let handle_focus_color =
970        Color::new(base_state.r, base_state.g, base_state.b, state_layer_alpha);
971
972    let colors = if args.disabled {
973        let scheme = material_color::global_material_scheme();
974        SliderColors {
975            active_track: scheme.on_surface.with_alpha(0.38),
976            inactive_track: scheme.on_surface.with_alpha(0.12),
977            handle: scheme.on_surface.with_alpha(0.38),
978            handle_focus: Color::new(0.0, 0.0, 0.0, 0.0),
979        }
980    } else {
981        SliderColors {
982            active_track: args.active_track_color,
983            inactive_track: args.inactive_track_color,
984            handle: args.thumb_color,
985            handle_focus: handle_focus_color,
986        }
987    };
988
989    drop(state_snapshot);
990
991    render_range_tracks(layout, &colors);
992
993    // Render Start Focus & Handle
994    render_focus(layout.base, &colors);
995    // Note: render_focus uses layout.focus_width/height. Position is handled by measure/place.
996    // But we need two focus indicators.
997
998    // Render End Focus
999    render_focus(layout.base, &colors);
1000
1001    // Render Start Handle
1002    render_handle(layout.base, &colors);
1003
1004    // Render End Handle
1005    render_handle(layout.base, &colors);
1006
1007    if layout.base.show_stop_indicator {
1008        render_range_stops(layout, &colors);
1009    }
1010
1011    let cloned_args = args.clone();
1012    let state_clone = state.clone();
1013    let start_val = start;
1014    let end_val = end;
1015
1016    input_handler(Box::new(move |mut input| {
1017        let resolved_layout = range_slider_layout(&cloned_args, input.computed_data.width);
1018        handle_range_slider_state(
1019            &mut input,
1020            &state_clone,
1021            &cloned_args,
1022            &resolved_layout.base,
1023        );
1024        apply_range_slider_accessibility(
1025            &mut input,
1026            &cloned_args,
1027            start_val,
1028            end_val,
1029            &cloned_args.on_change,
1030        );
1031    }));
1032
1033    measure(Box::new(move |input| {
1034        let dummy_args_for_resolve = SliderArgsBuilder::default()
1035            .width(args.width)
1036            .size(args.size)
1037            .build()
1038            .expect("Failed to build dummy args");
1039        let component_width =
1040            resolve_component_width(&dummy_args_for_resolve, input.parent_constraint);
1041        let resolved_layout = range_slider_layout(&args, component_width);
1042        measure_range_slider(input, resolved_layout, start, end)
1043    }));
1044}