Skip to main content

azul_layout/widgets/
slider.rs

1//! Slider / range widget — a horizontal track with a draggable circular thumb
2//! that maps a position along the track to a numeric value in `[min, max]`.
3//! Combines the value/min/max state + `on_value_change` callback shape of
4//! [`crate::widgets::number_input::NumberInput`] with the pointer-drag handling
5//! of [`crate::widgets::map`] (cursor-relative-to-node → value), and the
6//! switch's "track + knob slid via `margin-left`" rendering.
7//!
8//! Behaviour: pressing or dragging anywhere on the track sets the value from the
9//! cursor's X position (relative to the track, in logical px), slides the thumb
10//! live via `set_css_property`, and invokes the user's `on_value_change`.
11//!
12//! Key types: [`Slider`], [`SliderState`], [`SliderOnValueChange`].
13
14use azul_core::{
15    callbacks::{CoreCallbackData, Update},
16    dom::{Dom, IdOrClass, IdOrClass::Class, IdOrClassVec, TabIndex},
17    refany::RefAny,
18};
19use azul_css::dynamic_selector::{CssPropertyWithConditions, CssPropertyWithConditionsVec};
20use azul_css::{
21    props::{
22        basic::{color::ColorU, *},
23        layout::{LayoutDisplay, LayoutFlexDirection, LayoutAlignItems, LayoutAlignSelf, LayoutFlexGrow, LayoutWidth, LayoutHeight, LayoutMarginLeft},
24        property::{CssProperty, *},
25        style::{StyleBackgroundContent, StyleBackgroundContentVec, StyleBorderTopLeftRadius, StyleBorderTopRightRadius, StyleBorderBottomLeftRadius, StyleBorderBottomRightRadius, StyleCursor},
26    },
27    impl_option_inner, AzString,
28};
29
30use crate::callbacks::{Callback, CallbackInfo};
31
32static SLIDER_TRACK_CLASS: &[IdOrClass] =
33    &[Class(AzString::from_const_str("__azul-native-slider"))];
34static SLIDER_THUMB_CLASS: &[IdOrClass] =
35    &[Class(AzString::from_const_str("__azul-native-slider-thumb"))];
36
37/// Callback function type invoked when the slider value changes.
38pub type SliderOnValueChangeCallbackType =
39    extern "C" fn(RefAny, CallbackInfo, SliderState) -> Update;
40impl_widget_callback!(
41    SliderOnValueChange,
42    OptionSliderOnValueChange,
43    SliderOnValueChangeCallback,
44    SliderOnValueChangeCallbackType
45);
46
47azul_core::impl_managed_callback! {
48    wrapper:        SliderOnValueChangeCallback,
49    info_ty:        CallbackInfo,
50    return_ty:      Update,
51    default_ret:    Update::DoNothing,
52    invoker_static: SLIDER_ON_VALUE_CHANGE_INVOKER,
53    invoker_ty:     AzSliderOnValueChangeCallbackInvoker,
54    thunk_fn:       az_slider_on_value_change_callback_thunk,
55    setter_fn:      AzApp_setSliderOnValueChangeCallbackInvoker,
56    from_handle_fn: AzSliderOnValueChangeCallback_createFromHostHandle,
57    extra_args:     [ state: SliderState ],
58}
59
60/// A horizontal slider with a draggable thumb and a value-change callback.
61#[derive(Debug, Clone, PartialEq)]
62#[repr(C)]
63pub struct Slider {
64    pub slider_state: SliderStateWrapper,
65    /// Style for the slider track (the horizontal rail).
66    pub track_style: CssPropertyWithConditionsVec,
67    /// Style for the draggable thumb.
68    pub thumb_style: CssPropertyWithConditionsVec,
69}
70
71#[derive(Debug, Default, Clone, PartialEq)]
72#[repr(C)]
73pub struct SliderStateWrapper {
74    /// Optional: function to call when the value changes.
75    pub on_value_change: OptionSliderOnValueChange,
76    /// The value/range of this Slider.
77    pub inner: SliderState,
78    /// `true` while a pointer-drag is in flight (mirrors `map::MapTileCache::drag_anchor`).
79    /// Transient; not part of the user-visible [`SliderState`].
80    pub dragging: bool,
81}
82
83/// State of a [`Slider`]: the current value and the allowed `[min, max]` range.
84#[derive(Debug, Copy, Clone, PartialEq)]
85#[repr(C)]
86pub struct SliderState {
87    /// The current value (always within `[min, max]`).
88    pub value: f32,
89    /// Minimum allowed value (inclusive) — thumb at the far left.
90    pub min: f32,
91    /// Maximum allowed value (inclusive) — thumb at the far right.
92    pub max: f32,
93}
94
95impl Default for SliderState {
96    fn default() -> Self {
97        Self {
98            value: 0.0,
99            min: 0.0,
100            max: 100.0,
101        }
102    }
103}
104
105// ---- dimensions (logical px) ----
106const TRACK_WIDTH: isize = 200;
107const TRACK_HEIGHT: isize = 16;
108const TRACK_RADIUS: isize = 8;
109const THUMB_SIZE: isize = 16;
110const THUMB_RADIUS: isize = 8;
111
112// ---- colours ----
113/// Rail colour (#cccccc).
114const RAIL_COLOR: ColorU = ColorU {
115    r: 204,
116    g: 204,
117    b: 204,
118    a: 255,
119};
120/// Thumb colour (#0d6efd, accent blue).
121const THUMB_COLOR: ColorU = ColorU {
122    r: 13,
123    g: 110,
124    b: 253,
125    a: 255,
126};
127
128const RAIL_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(RAIL_COLOR)];
129const RAIL_BG: StyleBackgroundContentVec =
130    StyleBackgroundContentVec::from_const_slice(RAIL_BG_ITEMS);
131const THUMB_BG_ITEMS: &[StyleBackgroundContent] = &[StyleBackgroundContent::Color(THUMB_COLOR)];
132const THUMB_BG: StyleBackgroundContentVec =
133    StyleBackgroundContentVec::from_const_slice(THUMB_BG_ITEMS);
134
135/// The track (rail) style is parameter-independent, so it lives in a const slice.
136static SLIDER_TRACK_STYLE: &[CssPropertyWithConditions] = &[
137    CssPropertyWithConditions::simple(CssProperty::const_display(LayoutDisplay::Flex)),
138    CssPropertyWithConditions::simple(CssProperty::const_flex_direction(LayoutFlexDirection::Row)),
139    CssPropertyWithConditions::simple(CssProperty::const_align_items(LayoutAlignItems::Center)),
140    CssPropertyWithConditions::simple(CssProperty::align_self(LayoutAlignSelf::Center)),
141    CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(0))),
142    CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(TRACK_WIDTH))),
143    CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
144        TRACK_HEIGHT,
145    ))),
146    CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
147        StyleBorderTopLeftRadius::const_px(TRACK_RADIUS),
148    )),
149    CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
150        StyleBorderTopRightRadius::const_px(TRACK_RADIUS),
151    )),
152    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
153        StyleBorderBottomLeftRadius::const_px(TRACK_RADIUS),
154    )),
155    CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
156        StyleBorderBottomRightRadius::const_px(TRACK_RADIUS),
157    )),
158    CssPropertyWithConditions::simple(CssProperty::const_cursor(StyleCursor::Pointer)),
159    CssPropertyWithConditions::simple(CssProperty::const_background_content(RAIL_BG)),
160];
161
162/// Maps a value to a `[0, 1]` fraction along the track.
163fn value_to_fraction(value: f32, min: f32, max: f32) -> f32 {
164    if max <= min {
165        0.0
166    } else {
167        ((value - min) / (max - min)).clamp(0.0, 1.0)
168    }
169}
170
171/// Builds the thumb style; the `margin-left` is the only position-dependent
172/// property and slides the thumb between the left (`min`) and right (`max`) ends.
173#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // bounded layout/render numeric cast
174fn build_thumb_style(fraction: f32) -> CssPropertyWithConditionsVec {
175    let margin = (fraction * (TRACK_WIDTH - THUMB_SIZE) as f32).round() as isize;
176    CssPropertyWithConditionsVec::from_vec(alloc::vec![
177        CssPropertyWithConditions::simple(CssProperty::const_width(LayoutWidth::const_px(
178            THUMB_SIZE,
179        ))),
180        CssPropertyWithConditions::simple(CssProperty::const_height(LayoutHeight::const_px(
181            THUMB_SIZE,
182        ))),
183        CssPropertyWithConditions::simple(CssProperty::const_flex_grow(LayoutFlexGrow::const_new(
184            0,
185        ))),
186        CssPropertyWithConditions::simple(CssProperty::const_border_top_left_radius(
187            StyleBorderTopLeftRadius::const_px(THUMB_RADIUS),
188        )),
189        CssPropertyWithConditions::simple(CssProperty::const_border_top_right_radius(
190            StyleBorderTopRightRadius::const_px(THUMB_RADIUS),
191        )),
192        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_left_radius(
193            StyleBorderBottomLeftRadius::const_px(THUMB_RADIUS),
194        )),
195        CssPropertyWithConditions::simple(CssProperty::const_border_bottom_right_radius(
196            StyleBorderBottomRightRadius::const_px(THUMB_RADIUS),
197        )),
198        CssPropertyWithConditions::simple(CssProperty::const_background_content(THUMB_BG)),
199        CssPropertyWithConditions::simple(CssProperty::const_margin_left(
200            LayoutMarginLeft::const_px(margin),
201        )),
202    ])
203}
204
205impl Slider {
206    /// Creates a slider with the given current value and `[min, max]` range.
207    #[must_use] pub fn create(value: f32, min: f32, max: f32) -> Self {
208        let value = value.clamp(min, max);
209        Self {
210            slider_state: SliderStateWrapper {
211                inner: SliderState { value, min, max },
212                ..Default::default()
213            },
214            track_style: CssPropertyWithConditionsVec::from_const_slice(SLIDER_TRACK_STYLE),
215            thumb_style: build_thumb_style(value_to_fraction(value, min, max)),
216        }
217    }
218
219    /// Sets the current value (clamped to the range), recomputing the thumb position.
220    #[inline]
221    pub fn set_value(&mut self, value: f32) {
222        let min = self.slider_state.inner.min;
223        let max = self.slider_state.inner.max;
224        let value = value.clamp(min, max);
225        self.slider_state.inner.value = value;
226        self.thumb_style = build_thumb_style(value_to_fraction(value, min, max));
227    }
228
229    /// Builder-style setter for the current value.
230    #[inline]
231    #[must_use] pub fn with_value(mut self, value: f32) -> Self {
232        self.set_value(value);
233        self
234    }
235
236    #[inline]
237    #[must_use] pub fn swap_with_default(&mut self) -> Self {
238        let mut s = Self::create(0.0, 0.0, 100.0);
239        core::mem::swap(&mut s, self);
240        s
241    }
242
243    #[inline]
244    pub fn set_on_value_change<C: Into<SliderOnValueChangeCallback>>(
245        &mut self,
246        data: RefAny,
247        on_value_change: C,
248    ) {
249        self.slider_state.on_value_change = Some(SliderOnValueChange {
250            callback: on_value_change.into(),
251            refany: data,
252        })
253        .into();
254    }
255
256    #[inline]
257    #[must_use] pub fn with_on_value_change<C: Into<SliderOnValueChangeCallback>>(
258        mut self,
259        data: RefAny,
260        on_value_change: C,
261    ) -> Self {
262        self.set_on_value_change(data, on_value_change);
263        self
264    }
265
266    #[inline]
267    #[must_use] pub fn dom(self) -> Dom {
268        use azul_core::{
269            callbacks::CoreCallback,
270            dom::{EventFilter, HoverEventFilter},
271            refany::OptionRefAny,
272        };
273
274        // One shared RefAny across all pointer callbacks so the transient
275        // `dragging` flag set on press is visible to the move/release handlers
276        // (RefAny::clone shares the underlying data — same pattern as map.rs).
277        let state = RefAny::new(self.slider_state);
278        let mk = |event: EventFilter, cb: usize| CoreCallbackData {
279            event,
280            callback: CoreCallback {
281                cb,
282                ctx: OptionRefAny::None,
283            },
284            refany: state.clone(),
285        };
286        let callbacks = vec![
287            mk(
288                EventFilter::Hover(HoverEventFilter::MouseDown),
289                on_slider_pointer_down as usize,
290            ),
291            mk(
292                EventFilter::Hover(HoverEventFilter::MouseOver),
293                on_slider_pointer_move as usize,
294            ),
295            mk(
296                EventFilter::Hover(HoverEventFilter::MouseUp),
297                on_slider_pointer_up as usize,
298            ),
299            mk(
300                EventFilter::Hover(HoverEventFilter::MouseLeave),
301                on_slider_pointer_up as usize,
302            ),
303            mk(
304                EventFilter::Hover(HoverEventFilter::TouchStart),
305                on_slider_pointer_down as usize,
306            ),
307            mk(
308                EventFilter::Hover(HoverEventFilter::TouchMove),
309                on_slider_pointer_move as usize,
310            ),
311            mk(
312                EventFilter::Hover(HoverEventFilter::TouchEnd),
313                on_slider_pointer_up as usize,
314            ),
315        ];
316
317        Dom::create_div()
318            .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_TRACK_CLASS))
319            .with_css_props(self.track_style)
320            .with_callbacks(callbacks.into())
321            .with_tab_index(TabIndex::Auto)
322            .with_children(
323                vec![Dom::create_div()
324                    .with_ids_and_classes(IdOrClassVec::from_const_slice(SLIDER_THUMB_CLASS))
325                    .with_css_props(self.thumb_style)]
326                .into(),
327            )
328    }
329}
330
331impl Default for Slider {
332    fn default() -> Self {
333        Self::create(0.0, 0.0, 100.0)
334    }
335}
336
337/// Shared logic for press + drag: compute the value from the cursor's X position
338/// relative to the track, slide the thumb live, and invoke the user callback.
339#[allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)] // bounded layout/render numeric cast
340fn apply_cursor_value(slider: &mut SliderStateWrapper, info: &mut CallbackInfo) -> Update {
341    let Some(pos) = info.get_cursor_relative_to_node().into_option() else {
342        return Update::DoNothing;
343    };
344    // Track width in LOGICAL px (falls back to the design width before first layout).
345    let width = info
346        .get_hit_node_rect()
347        .map(|r| r.size.width)
348        .filter(|w| *w > 0.0)
349        .unwrap_or(TRACK_WIDTH as f32);
350
351    let fraction = (pos.x / width).clamp(0.0, 1.0);
352    let min = slider.inner.min;
353    let max = slider.inner.max;
354    slider.inner.value = fraction.mul_add(max - min, min);
355
356    // Slide the thumb (first child of the track) to the new position.
357    let track_id = info.get_hit_node();
358    if let Some(thumb_id) = info.get_first_child(track_id) {
359        let margin = (fraction * (width - THUMB_SIZE as f32)).round() as isize;
360        info.set_css_property(
361            thumb_id,
362            CssProperty::const_margin_left(LayoutMarginLeft::const_px(margin)),
363        );
364    }
365
366    let inner = slider.inner;
367    match slider.on_value_change.as_mut() {
368        Some(SliderOnValueChange { callback, refany }) => (callback.cb)(refany.clone(), *info, inner),
369        None => Update::DoNothing,
370    }
371}
372
373/// Pointer down → begin a drag and set the value from the press position.
374extern "C" fn on_slider_pointer_down(mut data: RefAny, mut info: CallbackInfo) -> Update {
375    let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
376        return Update::DoNothing;
377    };
378    slider.dragging = true;
379    apply_cursor_value(&mut slider, &mut info)
380}
381
382/// Pointer move → if a drag is active, track the value to the cursor.
383extern "C" fn on_slider_pointer_move(mut data: RefAny, mut info: CallbackInfo) -> Update {
384    let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() else {
385        return Update::DoNothing;
386    };
387    if !slider.dragging {
388        return Update::DoNothing;
389    }
390    apply_cursor_value(&mut slider, &mut info)
391}
392
393/// Pointer up / leave → end the drag.
394extern "C" fn on_slider_pointer_up(mut data: RefAny, _info: CallbackInfo) -> Update {
395    if let Some(mut slider) = data.downcast_mut::<SliderStateWrapper>() {
396        slider.dragging = false;
397    }
398    Update::DoNothing
399}
400
401impl From<Slider> for Dom {
402    fn from(s: Slider) -> Self {
403        s.dom()
404    }
405}