Skip to main content

floem/views/
slider.rs

1//! A toggle button widget. An example can be found in widget-gallery/button in the floem examples.
2
3use std::ops::RangeInclusive;
4
5use floem_reactive::{SignalGet, SignalUpdate, UpdaterEffect};
6use peniko::Brush;
7use peniko::color::palette;
8use peniko::kurbo::{Circle, Point, RoundedRect, RoundedRectRadii};
9use ui_events::keyboard::{Key, KeyState, KeyboardEvent, NamedKey};
10use ui_events::pointer::{PointerButtonEvent, PointerEvent};
11
12use crate::style::{BorderRadiusProp, CustomStyle};
13use crate::unit::Pct;
14use crate::{
15    Renderer,
16    event::EventPropagation,
17    prop, prop_extractor,
18    style::{Background, CustomStylable, Foreground, Height, Style},
19    style_class,
20    unit::{PxPct, PxPctAuto},
21    view::View,
22    view::ViewId,
23    views::Decorators,
24};
25
26/// Creates a new [Slider] with a function that returns a percentage value.
27/// See [Slider] for more documentation
28pub fn slider<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Slider {
29    Slider::new(percent)
30}
31
32enum SliderUpdate {
33    Percent(f64),
34}
35
36prop!(pub EdgeAlign: bool {} = false);
37prop!(pub HandleRadius: PxPct {} = PxPct::Pct(98.));
38
39prop_extractor! {
40    SliderStyle {
41        foreground: Foreground,
42        handle_radius: HandleRadius,
43        edge_align: EdgeAlign,
44    }
45}
46style_class!(pub SliderClass);
47style_class!(pub BarClass);
48style_class!(pub AccentBarClass);
49
50prop_extractor! {
51    BarStyle {
52        border_radius: BorderRadiusProp,
53        color: Background,
54        height: Height
55
56    }
57}
58
59fn border_radius(style: &BarStyle, size: f64) -> RoundedRectRadii {
60    let border_radius = style.border_radius();
61    RoundedRectRadii {
62        top_left: crate::view::border_radius(
63            border_radius.top_left.unwrap_or(PxPct::Px(0.0)),
64            size,
65        ),
66        top_right: crate::view::border_radius(
67            border_radius.top_right.unwrap_or(PxPct::Px(0.0)),
68            size,
69        ),
70        bottom_left: crate::view::border_radius(
71            border_radius.bottom_left.unwrap_or(PxPct::Px(0.0)),
72            size,
73        ),
74        bottom_right: crate::view::border_radius(
75            border_radius.bottom_right.unwrap_or(PxPct::Px(0.0)),
76            size,
77        ),
78    }
79}
80
81/// **A reactive slider.**
82///
83/// You can set the slider to a percent value between 0 and 100.
84///
85/// The slider is composed of four parts. The main view, the background bar, an accent bar and a handle.
86/// The background bar is separate from the main view because it is shortened when [`EdgeAlign`] is set to false;
87///
88/// **Responding to events**:
89/// You can respond to events by calling the [`Slider::on_change_pct`], and [`Slider::on_change_px`] methods on [`Slider`] and passing in a callback. Both of these callbacks are called whenever a change is effected by either clicking or by the arrow keys.
90/// These callbacks will not be called on reactive updates, only on a mouse event or by using the arrow keys.
91///
92/// You can also disable event handling [`Decorators::disabled`]. If you want to use this slider as a progress bar this may be useful.
93///
94/// **Styling**:
95/// You can use the [`Slider::slider_style`] method to get access to a [`SliderCustomStyle`] which has convenient functions with documentation for styling all of the properties of the slider.
96///
97/// Styling Example:
98/// ```rust
99/// # use floem::prelude::*;
100/// # use floem::peniko::Brush;
101/// # use floem::style::Foreground;
102/// slider::Slider::new(|| 40.pct())
103///     .slider_style(|s| {
104///         s.edge_align(true)
105///             .handle_radius(50.pct())
106///             .bar_color(palette::css::BLACK)
107///             .bar_radius(100.pct())
108///             .accent_bar_color(palette::css::GREEN)
109///             .accent_bar_radius(100.pct())
110///             .accent_bar_height(100.pct())
111///     });
112///```
113pub struct Slider {
114    id: ViewId,
115    onchangepx: Option<Box<dyn Fn(f64)>>,
116    onchangepct: Option<Box<dyn Fn(Pct)>>,
117    onchangevalue: Option<Box<dyn Fn(f64)>>,
118    onhover: Option<Box<dyn Fn(Pct)>>,
119    held: bool,
120    percent: f64,
121    prev_percent: f64,
122    base_bar_style: BarStyle,
123    accent_bar_style: BarStyle,
124    handle: Circle,
125    base_bar: RoundedRect,
126    accent_bar: RoundedRect,
127    size: taffy::prelude::Size<f32>,
128    style: SliderStyle,
129    range: RangeInclusive<f64>,
130    step: Option<f64>,
131}
132
133impl View for Slider {
134    fn id(&self) -> ViewId {
135        self.id
136    }
137
138    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
139        if let Ok(update) = state.downcast::<SliderUpdate>() {
140            match *update {
141                SliderUpdate::Percent(percent) => self.percent = percent,
142            }
143            self.id.request_layout();
144        }
145    }
146
147    fn event_before_children(
148        &mut self,
149        cx: &mut crate::context::EventCx,
150        event: &crate::event::Event,
151    ) -> EventPropagation {
152        let pos_changed = match event {
153            crate::event::Event::Pointer(PointerEvent::Down(PointerButtonEvent {
154                state, ..
155            })) => {
156                cx.update_active(self.id());
157                self.id.request_layout();
158                self.held = true;
159                self.percent = self.mouse_pos_to_percent(state.logical_point().x);
160                true
161            }
162            crate::event::Event::Pointer(PointerEvent::Up(PointerButtonEvent {
163                state, ..
164            })) => {
165                self.id.request_layout();
166
167                // set the state based on the position of the slider
168                let changed = self.held;
169                if self.held {
170                    self.percent = self.mouse_pos_to_percent(state.logical_point().x);
171                    self.update_restrict_position();
172                }
173                self.held = false;
174                changed
175            }
176            crate::event::Event::Pointer(PointerEvent::Move(pu)) => {
177                self.id.request_layout();
178                if self.held {
179                    self.percent = self.mouse_pos_to_percent(pu.current.logical_point().x);
180                    true
181                } else {
182                    // Call hover callback with the percentage at the current position
183                    if let Some(onhover) = &self.onhover {
184                        let hover_percent = self.mouse_pos_to_percent(pu.current.logical_point().x);
185                        onhover(Pct(hover_percent));
186                    }
187                    false
188                }
189            }
190            crate::event::Event::FocusLost => {
191                self.held = false;
192                false
193            }
194            crate::event::Event::Key(KeyboardEvent {
195                state: KeyState::Down,
196                key,
197                ..
198            }) => {
199                if *key == Key::Named(NamedKey::ArrowLeft) {
200                    self.id.request_layout();
201                    self.percent -= 10.;
202                    true
203                } else if *key == Key::Named(NamedKey::ArrowRight) {
204                    self.id.request_layout();
205                    self.percent += 10.;
206                    true
207                } else {
208                    false
209                }
210            }
211            _ => false,
212        };
213
214        self.update_restrict_position();
215
216        if pos_changed && self.percent != self.prev_percent {
217            if let Some(onchangepx) = &self.onchangepx {
218                onchangepx(self.handle_center());
219            }
220            if let Some(onchangepct) = &self.onchangepct {
221                onchangepct(Pct(self.percent))
222            }
223            if let Some(onchangevalue) = &self.onchangevalue {
224                let value_range = self.range.end() - self.range.start();
225                let mut new_value = self.range.start() + (value_range * (self.percent / 100.0));
226
227                if let Some(step) = self.step {
228                    new_value = (new_value / step).round() * step;
229                }
230
231                onchangevalue(new_value);
232            }
233        }
234
235        EventPropagation::Continue
236    }
237
238    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
239        let style = cx.style();
240        let mut paint = false;
241
242        let base_bar_style = style.clone().apply_class(BarClass);
243        paint |= self.base_bar_style.read_style(cx, &base_bar_style);
244
245        let accent_bar_style = style.apply_class(AccentBarClass);
246        paint |= self.accent_bar_style.read_style(cx, &accent_bar_style);
247        paint |= self.style.read(cx);
248        if paint {
249            cx.window_state.request_paint(self.id);
250        }
251    }
252
253    fn compute_layout(
254        &mut self,
255        _cx: &mut crate::context::ComputeLayoutCx,
256    ) -> Option<peniko::kurbo::Rect> {
257        self.update_restrict_position();
258        let layout = self.id.get_layout().unwrap_or_default();
259
260        self.size = layout.size;
261
262        let circle_radius = self.calculate_handle_radius();
263        let width = self.size.width as f64 - circle_radius * 2.;
264        let center = width * (self.percent / 100.) + circle_radius;
265        let circle_point = Point::new(center, (self.size.height / 2.) as f64);
266        self.handle = crate::kurbo::Circle::new(circle_point, circle_radius);
267
268        let base_bar_height = match self.base_bar_style.height() {
269            PxPctAuto::Px(px) => px,
270            PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
271            PxPctAuto::Auto => self.size.height as f64,
272        };
273        let accent_bar_height = match self.accent_bar_style.height() {
274            PxPctAuto::Px(px) => px,
275            PxPctAuto::Pct(pct) => self.size.height as f64 * (pct / 100.),
276            PxPctAuto::Auto => self.size.height as f64,
277        };
278
279        let base_bar_radii = border_radius(&self.base_bar_style, base_bar_height / 2.);
280        let accent_bar_radii = border_radius(&self.accent_bar_style, accent_bar_height / 2.);
281
282        let mut base_bar_length = self.size.width as f64;
283        if !self.style.edge_align() {
284            base_bar_length -= self.handle.radius * 2.;
285        }
286
287        let base_bar_y_start = self.size.height as f64 / 2. - base_bar_height / 2.;
288        let accent_bar_y_start = self.size.height as f64 / 2. - accent_bar_height / 2.;
289
290        let bar_x_start = if self.style.edge_align() {
291            0.
292        } else {
293            self.handle.radius
294        };
295
296        self.base_bar = peniko::kurbo::Rect::new(
297            bar_x_start,
298            base_bar_y_start,
299            bar_x_start + base_bar_length,
300            base_bar_y_start + base_bar_height,
301        )
302        .to_rounded_rect(base_bar_radii);
303        self.accent_bar = peniko::kurbo::Rect::new(
304            bar_x_start,
305            accent_bar_y_start,
306            self.handle_center(),
307            accent_bar_y_start + accent_bar_height,
308        )
309        .to_rounded_rect(accent_bar_radii);
310
311        self.prev_percent = self.percent;
312
313        None
314    }
315
316    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
317        cx.fill(
318            &self.base_bar,
319            &self
320                .base_bar_style
321                .color()
322                .unwrap_or(palette::css::BLACK.into()),
323            0.,
324        );
325        cx.save();
326        // this clip doesn't currently work because clipping only clips to the bounds of a rectangle, not including border radius.
327        cx.clip(&self.base_bar);
328        cx.fill(
329            &self.accent_bar,
330            &self
331                .accent_bar_style
332                .color()
333                .unwrap_or(palette::css::TRANSPARENT.into()),
334            0.,
335        );
336        cx.restore();
337
338        if let Some(color) = self.style.foreground() {
339            cx.fill(&self.handle, &color, 0.);
340        }
341    }
342}
343impl Slider {
344    /// Create a new reactive slider.
345    ///
346    /// This does **not** automatically hook up any `on_update` logic.
347    /// You will need to manually call [`Slider::on_change_pct`] or [`Slider::on_change_px`] in order to respond to updates from the slider.
348    ///
349    /// You might want to use the simpler constructor [`Slider::new_rw`] which will automatically hook up the `on_update` logic for updating a signal directly.
350    ///
351    /// # Example
352    /// ```rust
353    /// # use floem::prelude::*;
354    /// let percent = RwSignal::new(40.pct());
355    ///
356    /// slider::Slider::new(move || percent.get())
357    ///     .on_change_pct(move |new_percent| percent.set(new_percent))
358    ///     .slider_style(|s| {
359    ///         s.handle_radius(0)
360    ///             .bar_radius(25.pct())
361    ///             .accent_bar_radius(25.pct())
362    ///     })
363    ///     .style(|s| s.width(200));
364    /// ```
365    pub fn new<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Self {
366        let id = ViewId::new();
367        let percent = UpdaterEffect::new(
368            move || {
369                let percent = percent().into();
370                percent.0
371            },
372            move |percent| {
373                id.update_state(SliderUpdate::Percent(percent));
374            },
375        );
376        Slider {
377            id,
378            onchangepx: None,
379            onchangepct: None,
380            onchangevalue: None,
381            onhover: None,
382            held: false,
383            percent,
384            prev_percent: 0.0,
385            handle: Default::default(),
386            base_bar_style: Default::default(),
387            accent_bar_style: Default::default(),
388            base_bar: Default::default(),
389            accent_bar: Default::default(),
390            size: Default::default(),
391            style: Default::default(),
392            range: 0.0..=100.0,
393            step: None,
394        }
395        .class(SliderClass)
396    }
397
398    /// Create a new reactive slider.
399    ///
400    /// This automatically hooks up the `on_update` logic and keeps the signal up to date.
401    ///
402    /// If you need more control over the getting and setting of the value you will want to use [`Slider::new`] which gives you more control but does not automatically keep a signal up to date.
403    ///
404    /// # Example
405    /// ```rust
406    /// # use floem::prelude::*;
407    /// let percent = RwSignal::new(40.pct());
408    ///
409    /// slider::Slider::new_rw(percent)
410    ///     .slider_style(|s| {
411    ///         s.handle_radius(0)
412    ///             .bar_radius(25.pct())
413    ///             .accent_bar_radius(25.pct())
414    ///     })
415    ///     .style(|s| s.width(200));
416    /// ```
417    pub fn new_rw(percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static) -> Self {
418        Self::new(move || percent.get()).on_change_pct(move |pct| percent.set(pct))
419    }
420
421    /// Create a new reactive, ranged slider.
422    ///
423    /// This does **not** automatically hook up any `on_update` logic.
424    /// You will need to manually call [`Slider::on_change_value`] in order to respond to updates from the slider.
425    ///
426    /// # Example
427    /// ```rust
428    /// # use floem::prelude::*;
429    /// let value = RwSignal::new(-25.0);
430    /// let range = -50.0..=100.0;
431    ///
432    /// slider::Slider::new_ranged(move || value.get(), range)
433    ///     .step(5.0)
434    ///     .on_change_value(move |new_value| value.set(new_value))
435    ///     .slider_style(|s| {
436    ///         s.handle_radius(0)
437    ///             .bar_radius(25.pct())
438    ///             .accent_bar_radius(25.pct())
439    ///     })
440    ///     .style(|s| s.width(200));
441    /// ```
442    pub fn new_ranged(value: impl Fn() -> f64 + 'static, range: RangeInclusive<f64>) -> Self {
443        let id = ViewId::new();
444
445        let cloned_range = range.clone();
446
447        let percent = UpdaterEffect::new(
448            move || {
449                let value_range = range.end() - range.start();
450                ((value() - range.start()) / value_range) * 100.0
451            },
452            move |percent| {
453                id.update_state(SliderUpdate::Percent(percent));
454            },
455        );
456        Slider {
457            id,
458            onchangepx: None,
459            onchangepct: None,
460            onchangevalue: None,
461            onhover: None,
462            held: false,
463            percent,
464            prev_percent: 0.0,
465            handle: Default::default(),
466            base_bar_style: Default::default(),
467            accent_bar_style: Default::default(),
468            base_bar: Default::default(),
469            accent_bar: Default::default(),
470            size: Default::default(),
471            style: Default::default(),
472            range: cloned_range,
473            step: None,
474        }
475        .class(SliderClass)
476    }
477
478    fn update_restrict_position(&mut self) {
479        self.percent = self.percent.clamp(0., 100.);
480    }
481
482    fn handle_center(&self) -> f64 {
483        let width = self.size.width as f64 - self.handle.radius * 2.;
484        width * (self.percent / 100.) + self.handle.radius
485    }
486
487    /// Calculate the handle radius based on current size and style
488    fn calculate_handle_radius(&self) -> f64 {
489        match self.style.handle_radius() {
490            PxPct::Px(px) => px,
491            PxPct::Pct(pct) => self.size.width.min(self.size.height) as f64 / 2. * (pct / 100.),
492        }
493    }
494
495    /// Convert mouse x position to percentage, taking handle radius into account
496    fn mouse_pos_to_percent(&self, mouse_x: f64) -> f64 {
497        if self.size.width == 0.0 {
498            return 0.0;
499        }
500
501        let handle_radius = self.calculate_handle_radius();
502
503        // Clamp mouse position to handle center bounds
504        let clamped_x = mouse_x.clamp(handle_radius, self.size.width as f64 - handle_radius);
505
506        // Convert to percentage within the available range
507        let available_width = self.size.width as f64 - handle_radius * 2.;
508        if available_width <= 0.0 {
509            return 0.0;
510        }
511
512        let relative_pos = clamped_x - handle_radius;
513        (relative_pos / available_width * 100.0).clamp(0.0, 100.0)
514    }
515
516    /// Add an event handler to be run when the slider is moved.
517    ///
518    /// Only one callback of pct can be set on this view.
519    /// Calling it again will clear the previously set callback.
520    ///
521    /// You can set [`Slider::on_change_px`], [`Slider::on_change_value`]  and `on_change_pct` callbacks at the same time and both will be called on change.
522    pub fn on_change_pct(mut self, onchangepct: impl Fn(Pct) + 'static) -> Self {
523        self.onchangepct = Some(Box::new(onchangepct));
524        self
525    }
526    /// Add an event handler to be run when the slider is moved.
527    ///
528    /// Only one callback of px can be set on this view.
529    /// Calling it again will clear the previously set callback.
530    ///
531    /// You can set [`Slider::on_change_pct`], [`Slider::on_change_value`]  and `on_change_px` callbacks at the same time and both will be called on change.
532    pub fn on_change_px(mut self, onchangepx: impl Fn(f64) + 'static) -> Self {
533        self.onchangepx = Some(Box::new(onchangepx));
534        self
535    }
536
537    /// Add an event handler to be run when the slider is moved.
538    ///
539    /// This will emit the actual value of the slider according to the current range and step.
540    ///
541    /// Only one callback of value can be set on this view.
542    /// Calling it again will clear the previously set callback.
543    ///
544    /// You can set [`Slider::on_change_pct`], [`Slider::on_change_px`]  and `on_change_value` callbacks at the same time and both will be called on change.
545    pub fn on_change_value(mut self, onchangevalue: impl Fn(f64) + 'static) -> Self {
546        self.onchangevalue = Some(Box::new(onchangevalue));
547        self
548    }
549
550    /// Add an event handler to be run when the mouse hovers over the slider.
551    ///
552    /// The callback receives the percentage value at the current hover position.
553    /// Only one hover callback can be set on this view.
554    /// Calling it again will clear the previously set callback.
555    pub fn on_hover(mut self, onhover: impl Fn(Pct) + 'static) -> Self {
556        self.onhover = Some(Box::new(onhover));
557        self
558    }
559
560    /// Sets the custom style properties of the `Slider`.
561    pub fn slider_style(
562        self,
563        style: impl Fn(SliderCustomStyle) -> SliderCustomStyle + 'static,
564    ) -> Self {
565        self.custom_style(style)
566    }
567
568    /// Sets the step spacing of the `Slider`.
569    pub fn step(mut self, step: f64) -> Self {
570        self.step = Some(step);
571        self
572    }
573}
574
575#[derive(Debug, Default, Clone)]
576pub struct SliderCustomStyle(Style);
577impl From<SliderCustomStyle> for Style {
578    fn from(val: SliderCustomStyle) -> Self {
579        val.0
580    }
581}
582impl From<Style> for SliderCustomStyle {
583    fn from(val: Style) -> Self {
584        Self(val)
585    }
586}
587impl CustomStyle for SliderCustomStyle {
588    type StyleClass = SliderClass;
589}
590
591impl CustomStylable<SliderCustomStyle> for Slider {
592    type DV = Self;
593}
594
595impl SliderCustomStyle {
596    pub fn new() -> Self {
597        Self::default()
598    }
599
600    /// Sets the color of the slider handle.
601    ///
602    /// # Arguments
603    /// * `color` - An optional `Color` that sets the handle's color. If `None` is provided, the handle color is not set.
604    pub fn handle_color(mut self, color: impl Into<Option<Brush>>) -> Self {
605        self = SliderCustomStyle(self.0.set(Foreground, color));
606        self
607    }
608
609    /// Sets the edge alignment of the slider handle.
610    ///
611    /// # Arguments
612    /// * `align` - A boolean value that determines the alignment of the handle. If `true`, the edges of the handle are within the bar at 0% and 100%. If `false`, the bars are shortened and the handle's center appears at the ends of the bar.
613    pub fn edge_align(mut self, align: bool) -> Self {
614        self = SliderCustomStyle(self.0.set(EdgeAlign, align));
615        self
616    }
617
618    /// Sets the radius of the slider handle.
619    ///
620    /// # Arguments
621    /// * `radius` - A `PxPct` value that sets the handle's radius. This can be a pixel value or a percent value relative to the main height of the view.
622    pub fn handle_radius(mut self, radius: impl Into<PxPct>) -> Self {
623        self = SliderCustomStyle(self.0.set(HandleRadius, radius));
624        self
625    }
626
627    /// Sets the color of the slider's bar.
628    ///
629    /// # Arguments
630    /// * `color` - A `StyleValue<Color>` that sets the bar's background color.
631    pub fn bar_color(mut self, color: impl Into<Brush>) -> Self {
632        self = SliderCustomStyle(self.0.class(BarClass, |s| s.background(color)));
633        self
634    }
635
636    /// Sets the border radius of the slider's bar.
637    ///
638    /// # Arguments
639    /// * `radius` - A `PxPct` value that sets the bar's border radius. This can be a pixel value or a percent value relative to the bar's height.
640    pub fn bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
641        self = SliderCustomStyle(self.0.class(BarClass, |s| s.border_radius(radius)));
642        self
643    }
644
645    /// Sets the height of the slider's bar.
646    ///
647    /// # Arguments
648    /// * `height` - A `PxPctAuto` value that sets the bar's height. This can be a pixel value, a percent value relative to the view's height, or `Auto` to use the view's height.
649    pub fn bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
650        self = SliderCustomStyle(self.0.class(BarClass, |s| s.height(height)));
651        self
652    }
653
654    /// Sets the color of the slider's accent bar.
655    ///
656    /// # Arguments
657    /// * `color` - A `StyleValue<Color>` that sets the accent bar's background color.
658    pub fn accent_bar_color(mut self, color: impl Into<Brush>) -> Self {
659        self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.background(color)));
660        self
661    }
662
663    /// Sets the border radius of the slider's accent bar.
664    ///
665    /// # Arguments
666    /// * `radius` - A `PxPct` value that sets the accent bar's border radius. This can be a pixel value or a percent value relative to the accent bar's height.
667    pub fn accent_bar_radius(mut self, radius: impl Into<PxPct>) -> Self {
668        self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.border_radius(radius)));
669        self
670    }
671
672    /// Sets the height of the slider's accent bar.
673    ///
674    /// # Arguments
675    /// * `height` - A `PxPctAuto` value that sets the accent bar's height. This can be a pixel value, a percent value relative to the view's height, or `Auto` to use the view's height.
676    pub fn accent_bar_height(mut self, height: impl Into<PxPctAuto>) -> Self {
677        self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.height(height)));
678        self
679    }
680}
681
682#[cfg(test)]
683mod test {
684
685    use dpi::PhysicalPosition;
686    use ui_events::pointer::{
687        PointerButton, PointerButtonEvent, PointerInfo, PointerState, PointerType, PointerUpdate,
688    };
689
690    use crate::{
691        WindowState,
692        context::{EventCx, UpdateCx},
693        event::Event,
694    };
695
696    use super::*;
697
698    // Test helper to create a minimal WindowState
699    fn create_test_window_state(view_id: ViewId) -> WindowState {
700        WindowState::new(view_id, None)
701    }
702
703    // Test helper to create UpdateCx
704    fn create_test_update_cx(view_id: ViewId) -> UpdateCx<'static> {
705        UpdateCx {
706            window_state: Box::leak(Box::new(create_test_window_state(view_id))),
707        }
708    }
709
710    // Test helper to create EventCx
711    fn create_test_event_cx(view_id: ViewId) -> EventCx<'static> {
712        EventCx {
713            window_state: Box::leak(Box::new(create_test_window_state(view_id))),
714        }
715    }
716
717    // Helper to directly update slider value
718    fn update_slider_value(slider: &mut Slider, value: f64) {
719        let mut cx = create_test_update_cx(slider.id());
720        let state = Box::new(SliderUpdate::Percent(value));
721        slider.update(&mut cx, state);
722    }
723
724    #[test]
725    fn test_slider_initial_value() {
726        let percent = 53.0;
727        let slider = Slider::new(move || percent);
728        assert_eq!(slider.percent, percent);
729    }
730
731    #[test]
732    fn test_slider_bounds() {
733        let mut slider = Slider::new(|| 0.0);
734
735        // Test upper bound
736        update_slider_value(&mut slider, 150.0);
737        slider.update_restrict_position();
738        assert_eq!(slider.percent, 100.0);
739
740        // Test lower bound
741        update_slider_value(&mut slider, -50.0);
742        slider.update_restrict_position();
743        assert_eq!(slider.percent, 0.0);
744    }
745
746    #[test]
747    fn test_slider_pointer_events() {
748        let mut slider = Slider::new(|| 0.0);
749        let mut cx = create_test_event_cx(slider.id());
750
751        // Set initial size for pointer calculations
752        slider.size = taffy::prelude::Size {
753            width: 100.0,
754            height: 20.0,
755        };
756
757        let mouse_x = 75.;
758
759        // Test pointer down at 75%
760        let pointer_down = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
761            state: PointerState {
762                position: dpi::PhysicalPosition::new(mouse_x, 10.0),
763                count: 1,
764                ..Default::default()
765            },
766            button: Some(PointerButton::Primary),
767            pointer: PointerInfo {
768                pointer_id: None,
769                persistent_device_id: None,
770                pointer_type: PointerType::Mouse,
771            },
772        }));
773
774        slider.event_before_children(&mut cx, &pointer_down);
775        slider.update_restrict_position();
776
777        // Calculate expected percentage using the same logic as the slider
778        let handle_radius = slider.calculate_handle_radius();
779        let available_width = slider.size.width as f64 - handle_radius * 2.0;
780        let clamped_x = mouse_x.clamp(handle_radius, slider.size.width as f64 - handle_radius);
781        let relative_pos = clamped_x - handle_radius;
782        let expected_percent = (relative_pos / available_width * 100.0).clamp(0.0, 100.0);
783
784        assert_eq!(slider.percent, expected_percent);
785        assert!(slider.held);
786        assert_eq!(cx.window_state.active, Some(slider.id()));
787    }
788
789    #[test]
790    fn test_slider_drag_state() {
791        let mut slider = Slider::new(|| 50.0);
792        let mut cx = create_test_event_cx(slider.id());
793
794        slider.size = taffy::prelude::Size {
795            width: 100.0,
796            height: 20.0,
797        };
798
799        let move_mouse_x = 75.;
800
801        // Start drag
802        let pointer_down = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
803            state: PointerState {
804                position: PhysicalPosition::new(50.0, 10.0),
805                count: 1,
806                ..Default::default()
807            },
808            button: Some(PointerButton::Primary),
809            pointer: PointerInfo {
810                pointer_id: None,
811                persistent_device_id: None,
812                pointer_type: PointerType::Mouse,
813            },
814        }));
815
816        slider.event_before_children(&mut cx, &pointer_down);
817        assert!(slider.held);
818        assert_eq!(cx.window_state.active, Some(slider.id()));
819
820        // Move while dragging
821        let pointer_move = Event::Pointer(PointerEvent::Move(PointerUpdate {
822            pointer: PointerInfo {
823                pointer_id: None,
824                persistent_device_id: None,
825                pointer_type: PointerType::Mouse,
826            },
827            current: PointerState {
828                position: PhysicalPosition::new(move_mouse_x, 10.0),
829                count: 1,
830                ..Default::default()
831            },
832            coalesced: Vec::new(),
833            predicted: Vec::new(),
834        }));
835        slider.event_before_children(&mut cx, &pointer_move);
836
837        // Calculate expected percentage using the same logic as the slider
838        let handle_radius = slider.calculate_handle_radius();
839        let available_width = slider.size.width as f64 - handle_radius * 2.0;
840        let clamped_x = move_mouse_x.clamp(handle_radius, slider.size.width as f64 - handle_radius);
841        let relative_pos = clamped_x - handle_radius;
842        let expected_percent = (relative_pos / available_width * 100.0).clamp(0.0, 100.0);
843
844        assert_eq!(slider.percent, expected_percent);
845
846        // End drag
847        let pointer_up = Event::Pointer(PointerEvent::Up(PointerButtonEvent {
848            state: PointerState {
849                position: PhysicalPosition::new(75.0, 10.0),
850                count: 1,
851                ..Default::default()
852            },
853            button: Some(PointerButton::Primary),
854            pointer: PointerInfo {
855                pointer_id: None,
856                persistent_device_id: None,
857                pointer_type: PointerType::Mouse,
858            },
859        }));
860
861        slider.event_before_children(&mut cx, &pointer_up);
862        assert!(!slider.held);
863    }
864
865    #[test]
866    fn test_callback_handling() {
867        use std::sync::Arc;
868        use std::sync::atomic::{AtomicBool, Ordering};
869
870        let callback_called = Arc::new(AtomicBool::new(false));
871        let callback_called_clone = callback_called.clone();
872
873        let mut slider = Slider::new(|| 0.0).on_change_pct(move |_| {
874            callback_called_clone.store(true, Ordering::SeqCst);
875        });
876
877        let mut cx = create_test_event_cx(slider.id());
878
879        slider.size = taffy::prelude::Size {
880            width: 100.0,
881            height: 20.0,
882        };
883
884        let pointer_event = Event::Pointer(PointerEvent::Down(PointerButtonEvent {
885            state: PointerState {
886                position: PhysicalPosition::new(60.0, 10.0),
887                count: 1,
888                ..Default::default()
889            },
890            button: Some(PointerButton::Primary),
891            pointer: PointerInfo {
892                pointer_id: None,
893                persistent_device_id: None,
894                pointer_type: PointerType::Mouse,
895            },
896        }));
897
898        slider.event_before_children(&mut cx, &pointer_event);
899        slider.update_restrict_position();
900
901        assert!(callback_called.load(Ordering::SeqCst));
902    }
903
904    // #[test]
905    // FIXME
906    // fn test_handle_positioning_edge_cases() {
907    //     let mut slider = Slider::new(|| 0.0);
908    //     let mut cx = create_test_event_cx(slider.id());
909
910    //     slider.size = taffy::prelude::Size {
911    //         width: 100.0,
912    //         height: 20.0,
913    //     };
914
915    //     let handle_radius = slider.calculate_handle_radius();
916
917    //     // Test mouse at far left (should result in 0%)
918    //     let pointer_left = Event::Pointer(PointerEvent::Down(PointerButtonEvent{
919    //         pos: Point::new(0.0, 10.0),
920    //         button: PointerButton::Mouse(MouseButton::Primary),
921    //         count: 1,
922    //         modifiers: Default::default(),
923    //     });
924
925    //     slider.event_before_children(&mut cx, &pointer_left);
926    //     assert_eq!(slider.percent, 0.0);
927
928    //     // Test mouse at far right (should result in 100%)
929    //     let pointer_right = Event::PointerDown(PointerInputEvent {
930    //         pos: Point::new(100.0, 10.0),
931    //         button: PointerButton::Mouse(MouseButton::Primary),
932    //         count: 1,
933    //         modifiers: Default::default(),
934    //     });
935
936    //     slider.event_before_children(&mut cx, &pointer_right);
937    //     assert_eq!(slider.percent, 100.0);
938
939    //     // Test mouse exactly at handle radius (should result in 0%)
940    //     let pointer_at_radius = Event::PointerDown(PointerInputEvent {
941    //         pos: Point::new(handle_radius, 10.0),
942    //         button: PointerButton::Mouse(MouseButton::Primary),
943    //         count: 1,
944    //         modifiers: Default::default(),
945    //     });
946
947    //     slider.event_before_children(&mut cx, &pointer_at_radius);
948    //     assert_eq!(slider.percent, 0.0);
949
950    //     // Test mouse at width - handle_radius (should result in 100%)
951    //     let pointer_at_end = Event::PointerDown(PointerInputEvent {
952    //         pos: Point::new(slider.size.width as f64 - handle_radius, 10.0),
953    //         button: PointerButton::Mouse(MouseButton::Primary),
954    //         count: 1,
955    //         modifiers: Default::default(),
956    //     });
957
958    //     slider.event_before_children(&mut cx, &pointer_at_end);
959    //     assert_eq!(slider.percent, 100.0);
960    // }
961}