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::custom_event;
13use crate::event::CustomEvent;
14use crate::unit::FontSizeCx;
15use crate::{
16    Renderer,
17    context::{LayoutChanged, LayoutChangedListener},
18    event::{Event, EventPropagation, FocusEvent},
19    prelude::*,
20    prop, prop_extractor,
21    style::{
22        Background, BorderBottomLeftRadius, BorderBottomRightRadius, BorderTopLeftRadius,
23        BorderTopRightRadius, ContextValue, CustomStylable, CustomStyle, ExprStyle, FontSize,
24        Foreground, Height, LineHeight, Style,
25    },
26    style_class,
27    unit::{Length, LengthAuto, Pct},
28    view::{View, ViewId},
29    views::Decorators,
30};
31
32/// Creates a new [Slider] with a function that returns a percentage value.
33/// See [Slider] for more documentation
34pub fn slider<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Slider {
35    Slider::new(percent)
36}
37
38enum SliderUpdate {
39    Percent(f64),
40}
41
42prop!(pub EdgeAlign: bool {} = false);
43prop!(pub HandleRadius: Length {} = Length::Pct(98.));
44
45prop_extractor! {
46    SliderStyle {
47        foreground: Foreground,
48        handle_radius: HandleRadius,
49        edge_align: EdgeAlign,
50        font_size: FontSize,
51        line_height: LineHeight,
52    }
53}
54style_class!(pub SliderClass);
55style_class!(pub BarClass);
56style_class!(pub AccentBarClass);
57
58prop_extractor! {
59    BarStyle {
60        border_top_left_radius: BorderTopLeftRadius,
61        border_top_right_radius: BorderTopRightRadius,
62        border_bottom_left_radius: BorderBottomLeftRadius,
63        border_bottom_right_radius: BorderBottomRightRadius,
64        color: Background,
65        height: Height
66
67    }
68}
69
70impl BarStyle {
71    fn border_radius(&self) -> crate::style::BorderRadius {
72        crate::style::BorderRadius {
73            top_left: Some(self.border_top_left_radius()),
74            top_right: Some(self.border_top_right_radius()),
75            bottom_left: Some(self.border_bottom_left_radius()),
76            bottom_right: Some(self.border_bottom_right_radius()),
77        }
78    }
79}
80
81impl SliderStyle {
82    fn length_resolve_cx(&self) -> FontSizeCx {
83        let font_size = self.font_size();
84        let line_height = match self.line_height() {
85            crate::text::LineHeightValue::Pt(value) => f64::from(value),
86            crate::text::LineHeightValue::Normal(multiplier) => font_size * f64::from(multiplier),
87        };
88        FontSizeCx::new(font_size, line_height)
89    }
90}
91
92fn border_radius(style: &BarStyle, size: f64, resolve_cx: &FontSizeCx) -> RoundedRectRadii {
93    let border_radius = style.border_radius();
94    RoundedRectRadii {
95        top_left: crate::view::border_radius(
96            border_radius.top_left.unwrap_or(Length::Pt(0.0)),
97            size,
98            resolve_cx,
99        ),
100        top_right: crate::view::border_radius(
101            border_radius.top_right.unwrap_or(Length::Pt(0.0)),
102            size,
103            resolve_cx,
104        ),
105        bottom_left: crate::view::border_radius(
106            border_radius.bottom_left.unwrap_or(Length::Pt(0.0)),
107            size,
108            resolve_cx,
109        ),
110        bottom_right: crate::view::border_radius(
111            border_radius.bottom_right.unwrap_or(Length::Pt(0.0)),
112            size,
113            resolve_cx,
114        ),
115    }
116}
117
118/// State of a slider at a point in time
119#[derive(Debug, Copy, Clone, PartialEq)]
120pub struct SliderState {
121    /// The value in pixels from the start of the slider
122    pub px: f64,
123    /// The value as a percentage (0.0 to 100.0)
124    pub pct: Pct,
125    /// The value mapped to the slider's configured range
126    pub value: f64,
127}
128
129impl SliderState {
130    /// Create a new slider state from a percentage
131    pub fn from_percent(
132        percent: f64,
133        range: &RangeInclusive<f64>,
134        step: Option<f64>,
135        px: f64,
136    ) -> Self {
137        let value_range = range.end() - range.start();
138        let mut value = range.start() + (value_range * (percent / 100.0));
139
140        if let Some(step) = step {
141            value = (value / step).round() * step;
142        }
143
144        Self {
145            px,
146            pct: Pct(percent),
147            value,
148        }
149    }
150}
151
152impl SliderChanged {
153    // we need these functions instead of closures because we need rust to know that the lifetimes of the references are the same
154    fn extract_state(event: &SliderChanged) -> &SliderState {
155        &event.state
156    }
157}
158
159impl SliderHover {
160    // we need these functions instead of closures because we need rust to know that the lifetimes of the references are the same
161    fn extract_state(event: &SliderHover) -> &SliderState {
162        &event.state
163    }
164}
165
166/// Event fired when a slider's value changes
167#[derive(Debug, Clone, Copy, PartialEq)]
168pub struct SliderChanged {
169    /// The new state of the slider
170    pub state: SliderState,
171}
172custom_event!(SliderChanged, SliderState, SliderChanged::extract_state);
173
174/// Event fired that has what the state would be at the current mouse position when hovering over a slider
175#[derive(Debug, Clone, Copy, PartialEq)]
176pub struct SliderHover {
177    /// The state of the slider
178    pub state: SliderState,
179}
180custom_event!(SliderHover, SliderState, SliderHover::extract_state);
181
182/// **A reactive slider.**
183///
184/// You can set the slider to a percent value between 0 and 100.
185///
186/// The slider is composed of four parts: the main view, the background bar, an accent bar, and a handle.
187/// The background bar is separate from the main view because it is shortened when `EdgeAlign` is set to false.
188///
189/// **Responding to events**:
190/// You can respond to slider changes by listening to `SliderChanged` events:
191/// ```rust
192/// # use floem::event::EventPropagation;
193/// # use floem::prelude::*;
194/// # use floem::views::slider::{self, SliderChanged};
195/// slider::Slider::new(|| 40.pct())
196///     .on_event(SliderChanged::listener(), |cx, state| {
197///         println!("Value: {}", state.value);
198///         println!("Percent: {:?}", state.pct);
199///         println!("Pixels: {}", state.px);
200///         EventPropagation::Continue
201///     });
202/// ```
203///
204/// You can also listen to `SliderHover` events to respond when the user hovers over the slider:
205/// ```rust
206/// # use floem::event::EventPropagation;
207/// # use floem::prelude::*;
208/// # use floem::views::slider::{self, SliderHover};
209/// slider::Slider::new(|| 40.pct())
210///     .on_event(SliderHover::listener(), |cx, state| {
211///         println!("Hovering at: {:?}", state.pct);
212///         EventPropagation::Continue
213///     });
214/// ```
215///
216/// These events are only fired on user interaction (mouse events or arrow keys), not on reactive updates.
217///
218/// You can also disable event handling with `Decorators::disabled`. This is useful if you want to use
219/// the slider as a progress bar.
220///
221/// **Styling**:
222/// You can use the `Slider::slider_style` method to get access to a `SliderCustomStyle` which has
223/// convenient functions with documentation for styling all of the properties of the slider.
224///
225/// Styling Example:
226/// ```rust
227/// # use floem::prelude::*;
228/// # use floem::peniko::Brush;
229/// # use floem::style::Foreground;
230/// slider::Slider::new(|| 40.pct())
231///     .slider_style(|s| {
232///         s.edge_align(true)
233///             .handle_radius(50.pct())
234///             .bar_color(palette::css::BLACK)
235///             .bar_radius(100.pct())
236///             .accent_bar_color(palette::css::GREEN)
237///             .accent_bar_radius(100.pct())
238///             .accent_bar_height(100.pct())
239///     });
240///```
241pub struct Slider {
242    id: ViewId,
243    held: bool,
244    state: SliderState,
245    prev_percent: f64,
246    base_bar_style: BarStyle,
247    accent_bar_style: BarStyle,
248    handle: Circle,
249    base_bar: RoundedRect,
250    accent_bar: RoundedRect,
251    style: SliderStyle,
252    range: RangeInclusive<f64>,
253    step: Option<f64>,
254    layout: LayoutChanged,
255}
256
257impl View for Slider {
258    fn id(&self) -> ViewId {
259        self.id
260    }
261
262    fn update(&mut self, _cx: &mut crate::context::UpdateCx, state: Box<dyn std::any::Any>) {
263        if let Ok(update) = state.downcast::<SliderUpdate>() {
264            match *update {
265                SliderUpdate::Percent(percent) => {
266                    self.state = SliderState::from_percent(
267                        percent,
268                        &self.range,
269                        self.step,
270                        self.handle_center_for_percent(percent),
271                    );
272                }
273            }
274            self.update_shapes();
275        }
276    }
277
278    fn event(&mut self, cx: &mut crate::context::EventCx) -> EventPropagation {
279        if let Some(new_layout) = LayoutChangedListener::extract(&cx.event) {
280            self.post_layout(new_layout);
281        }
282
283        let pos_changed = match &cx.event {
284            Event::Pointer(PointerEvent::Down(PointerButtonEvent { state, pointer, .. })) => {
285                if let Some(pointer_id) = pointer.pointer_id {
286                    cx.window_state.set_pointer_capture(pointer_id, self.id);
287                }
288                self.held = true;
289                self.update_state_from_mouse_pos(state.logical_point().x);
290                true
291            }
292            Event::Pointer(PointerEvent::Up(PointerButtonEvent { state, .. })) => {
293                let changed = self.held;
294                if self.held {
295                    self.update_state_from_mouse_pos(state.logical_point().x);
296                    self.clamp_percent();
297                }
298                self.held = false;
299                changed
300            }
301            Event::Pointer(PointerEvent::Move(pu)) => {
302                if self.held {
303                    self.update_state_from_mouse_pos(pu.current.logical_point().x);
304                    true
305                } else {
306                    // Dispatch hover event with state at current position
307                    let hover_state = self.state_from_mouse_pos(pu.current.logical_point().x);
308                    self.id.route_event(
309                        Event::new_custom(SliderHover { state: hover_state }),
310                        crate::event::RouteKind::Directed {
311                            target: self.id.get_element_id(),
312                            phases: crate::context::Phases::TARGET,
313                        },
314                    );
315                    false
316                }
317            }
318            Event::Focus(FocusEvent::Lost) => {
319                self.held = false;
320                false
321            }
322            Event::Key(KeyboardEvent {
323                state: KeyState::Down,
324                key,
325                ..
326            }) => {
327                if *key == Key::Named(NamedKey::ArrowLeft) {
328                    let new_percent = (self.state.pct.0 - 10.).clamp(0., 100.);
329                    self.update_state_from_percent(new_percent);
330                    true
331                } else if *key == Key::Named(NamedKey::ArrowRight) {
332                    let new_percent = (self.state.pct.0 + 10.).clamp(0., 100.);
333                    self.update_state_from_percent(new_percent);
334                    true
335                } else {
336                    false
337                }
338            }
339            _ => false,
340        };
341
342        self.clamp_percent();
343
344        if pos_changed && self.state.pct.0 != self.prev_percent {
345            self.id.route_event(
346                Event::new_custom(SliderChanged { state: self.state }),
347                crate::event::RouteKind::Directed {
348                    target: self.id.get_element_id(),
349                    phases: crate::context::Phases::TARGET,
350                },
351            );
352            self.update_shapes();
353        }
354
355        EventPropagation::Continue
356    }
357
358    fn style_pass(&mut self, cx: &mut crate::context::StyleCx<'_>) {
359        let style = cx.style();
360        let mut paint = false;
361
362        let base_bar_style = style.clone().apply_class(BarClass);
363        paint |= self.base_bar_style.read_style(cx, &base_bar_style);
364
365        let accent_bar_style = style.apply_class(AccentBarClass);
366        paint |= self.accent_bar_style.read_style(cx, &accent_bar_style);
367        paint |= self.style.read(cx);
368        if paint {
369            cx.window_state.request_paint(self.id);
370        }
371    }
372
373    fn paint(&mut self, cx: &mut crate::context::PaintCx) {
374        cx.fill(
375            &self.base_bar,
376            &self
377                .base_bar_style
378                .color()
379                .unwrap_or(palette::css::BLACK.into()),
380            0.,
381        );
382        // Apply temporary clip for accent bar
383        cx.clip(&self.base_bar);
384        cx.fill(
385            &self.accent_bar,
386            &self
387                .accent_bar_style
388                .color()
389                .unwrap_or(palette::css::TRANSPARENT.into()),
390            0.,
391        );
392        cx.clear_clip();
393
394        if let Some(color) = self.style.foreground() {
395            cx.fill(&self.handle, &color, 0.);
396        }
397    }
398}
399
400impl Slider {
401    /// Create a new reactive slider.
402    ///
403    /// Listen to slider changes using the `SliderChanged` event:
404    ///
405    /// # Example
406    /// ```rust
407    /// # use floem::event::EventPropagation;
408    /// # use floem::prelude::*;
409    /// # use floem::views::slider::{self, SliderChanged};
410    /// let percent = RwSignal::new(40.pct());
411    ///
412    /// slider::Slider::new(move || percent.get())
413    ///     .on_event(SliderChanged::listener(), move |cx, event| {
414    ///         percent.set(event.pct);
415    ///         EventPropagation::Continue
416    ///     })
417    ///     .slider_style(|s| {
418    ///         s.handle_radius(0)
419    ///             .bar_radius(25.pct())
420    ///             .accent_bar_radius(25.pct())
421    ///     })
422    ///     .style(|s| s.width(200));
423    /// ```
424    pub fn new<P: Into<Pct>>(percent: impl Fn() -> P + 'static) -> Self {
425        let id = ViewId::new();
426        id.register_listener(LayoutChanged::listener_key());
427        let initial_percent = UpdaterEffect::new(
428            move || {
429                let percent = percent().into();
430                percent.0
431            },
432            move |percent| {
433                id.update_state(SliderUpdate::Percent(percent));
434            },
435        );
436
437        let state = SliderState {
438            px: 0.0,
439            pct: Pct(initial_percent),
440            value: initial_percent,
441        };
442
443        Slider {
444            id,
445            held: false,
446            state,
447            prev_percent: 0.0,
448            handle: Default::default(),
449            base_bar_style: Default::default(),
450            accent_bar_style: Default::default(),
451            base_bar: Default::default(),
452            accent_bar: Default::default(),
453            layout: LayoutChanged {
454                new_box: Default::default(),
455                new_content_box: Default::default(),
456                new_window_origin: Default::default(),
457            },
458            style: Default::default(),
459            range: 0.0..=100.0,
460            step: None,
461        }
462        .class(SliderClass)
463    }
464
465    /// Create a new reactive slider.
466    ///
467    /// This automatically hooks up the event logic and keeps the signal up to date.
468    ///
469    /// 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.
470    ///
471    /// # Example
472    /// ```rust
473    /// # use floem::prelude::*;
474    /// let percent = RwSignal::new(40.pct());
475    ///
476    /// slider::Slider::new_rw(percent)
477    ///     .slider_style(|s| {
478    ///         s.handle_radius(0)
479    ///             .bar_radius(25.pct())
480    ///             .accent_bar_radius(25.pct())
481    ///     })
482    ///     .style(|s| s.width(200));
483    /// ```
484    pub fn new_rw(percent: impl SignalGet<Pct> + SignalUpdate<Pct> + Copy + 'static) -> Self {
485        Self::new(move || percent.get()).on_event(SliderChanged::listener(), move |_cx, state| {
486            percent.set(state.pct);
487            EventPropagation::Continue
488        })
489    }
490
491    /// Create a new reactive, ranged slider.
492    ///
493    /// Listen to value changes using the `SliderChanged` event and read `event.state.value`.
494    ///
495    /// # Example
496    /// ```rust
497    /// # use floem::event::EventPropagation;
498    /// # use floem::prelude::*;
499    /// # use floem::views::slider::{self, SliderChanged};
500    /// let value = RwSignal::new(-25.0);
501    /// let range = -50.0..=100.0;
502    ///
503    /// slider::Slider::new_ranged(move || value.get(), range)
504    ///     .step(5.0)
505    ///     .on_event(SliderChanged::listener(), move |cx, event| {
506    ///         value.set(event.value);
507    ///         EventPropagation::Continue
508    ///     })
509    ///     .slider_style(|s| {
510    ///         s.handle_radius(0)
511    ///             .bar_radius(25.pct())
512    ///             .accent_bar_radius(25.pct())
513    ///     })
514    ///     .style(|s| s.width(200));
515    /// ```
516    pub fn new_ranged(value: impl Fn() -> f64 + 'static, range: RangeInclusive<f64>) -> Self {
517        let id = ViewId::new();
518        id.register_listener(LayoutChanged::listener_key());
519
520        let cloned_range = range.clone();
521
522        let initial_percent = UpdaterEffect::new(
523            move || {
524                let value_range = range.end() - range.start();
525                ((value() - range.start()) / value_range) * 100.0
526            },
527            move |percent| {
528                id.update_state(SliderUpdate::Percent(percent));
529            },
530        );
531
532        let state = SliderState::from_percent(initial_percent, &cloned_range, None, 0.0);
533
534        Slider {
535            id,
536            held: false,
537            state,
538            prev_percent: 0.0,
539            handle: Default::default(),
540            base_bar_style: Default::default(),
541            accent_bar_style: Default::default(),
542            base_bar: Default::default(),
543            accent_bar: Default::default(),
544            layout: LayoutChanged {
545                new_box: Default::default(),
546                new_content_box: Default::default(),
547                new_window_origin: Default::default(),
548            },
549            style: Default::default(),
550            range: cloned_range,
551            step: None,
552        }
553        .class(SliderClass)
554    }
555
556    fn post_layout(&mut self, layout_changed: &LayoutChanged) {
557        self.layout = *layout_changed;
558        self.update_shapes();
559    }
560
561    fn clamp_percent(&mut self) {
562        let clamped = self.state.pct.0.clamp(0., 100.);
563        if clamped != self.state.pct.0 {
564            self.update_state_from_percent(clamped);
565        }
566    }
567
568    fn handle_center(&self) -> f64 {
569        self.handle_center_for_percent(self.state.pct.0)
570    }
571
572    fn handle_center_for_percent(&self, percent: f64) -> f64 {
573        let width = self.layout.new_box.size().width - self.handle.radius * 2.;
574        width * (percent / 100.) + self.handle.radius
575    }
576
577    /// Update the slider state from a mouse position
578    fn update_state_from_mouse_pos(&mut self, mouse_x: f64) {
579        let percent = self.mouse_pos_to_percent(mouse_x);
580        self.update_state_from_percent(percent);
581    }
582
583    /// Update the slider state from a percentage
584    fn update_state_from_percent(&mut self, percent: f64) {
585        self.state = SliderState::from_percent(
586            percent,
587            &self.range,
588            self.step,
589            self.handle_center_for_percent(percent),
590        );
591    }
592
593    /// Create a slider state from a mouse position without updating self
594    fn state_from_mouse_pos(&self, mouse_x: f64) -> SliderState {
595        let percent = self.mouse_pos_to_percent(mouse_x);
596        SliderState::from_percent(
597            percent,
598            &self.range,
599            self.step,
600            self.handle_center_for_percent(percent),
601        )
602    }
603
604    fn update_shapes(&mut self) {
605        self.clamp_percent();
606        let size = self.layout.box_local().size();
607        let resolve_cx = self.style.length_resolve_cx();
608
609        let circle_radius = self.calculate_handle_radius();
610        let width = size.width - circle_radius * 2.;
611        let center = width * (self.state.pct.0 / 100.) + circle_radius;
612        let circle_point = Point::new(center, size.height / 2.);
613        self.handle = crate::kurbo::Circle::new(circle_point, circle_radius);
614
615        let base_bar_height = self
616            .base_bar_style
617            .height()
618            .resolve(size.height, &resolve_cx)
619            .unwrap_or(size.height);
620        let accent_bar_height = self
621            .accent_bar_style
622            .height()
623            .resolve(size.height, &resolve_cx)
624            .unwrap_or(size.height);
625
626        let base_bar_radii = border_radius(&self.base_bar_style, base_bar_height / 2., &resolve_cx);
627        let accent_bar_radii =
628            border_radius(&self.accent_bar_style, accent_bar_height / 2., &resolve_cx);
629
630        let mut base_bar_length = size.width;
631        if !self.style.edge_align() {
632            base_bar_length -= self.handle.radius * 2.;
633        }
634
635        let base_bar_y_start = size.height / 2. - base_bar_height / 2.;
636        let accent_bar_y_start = size.height / 2. - accent_bar_height / 2.;
637
638        let bar_x_start = if self.style.edge_align() {
639            0.
640        } else {
641            self.handle.radius
642        };
643
644        self.base_bar = peniko::kurbo::Rect::new(
645            bar_x_start,
646            base_bar_y_start,
647            bar_x_start + base_bar_length,
648            base_bar_y_start + base_bar_height,
649        )
650        .to_rounded_rect(base_bar_radii);
651        self.accent_bar = peniko::kurbo::Rect::new(
652            bar_x_start,
653            accent_bar_y_start,
654            self.handle_center(),
655            accent_bar_y_start + accent_bar_height,
656        )
657        .to_rounded_rect(accent_bar_radii);
658
659        self.prev_percent = self.state.pct.0;
660        self.id.request_paint();
661    }
662
663    /// Calculate the handle radius based on current size and style
664    fn calculate_handle_radius(&self) -> f64 {
665        let size = self.layout.new_box.size();
666        let basis = size.width.min(size.height) / 2.0;
667        self.style
668            .handle_radius()
669            .resolve(basis, &self.style.length_resolve_cx())
670    }
671
672    /// Convert mouse x position to percentage, taking handle radius into account
673    fn mouse_pos_to_percent(&self, mouse_x: f64) -> f64 {
674        let size = self.layout.new_box.size();
675        if size.width == 0.0 {
676            return 0.0;
677        }
678
679        let handle_radius = self.calculate_handle_radius();
680
681        // Clamp mouse position to handle center bounds
682        let clamped_x = mouse_x.clamp(handle_radius, size.width - handle_radius);
683
684        // Convert to percentage within the available range
685        let available_width = size.width - handle_radius * 2.;
686        if available_width <= 0.0 {
687            return 0.0;
688        }
689
690        let relative_pos = clamped_x - handle_radius;
691        (relative_pos / available_width * 100.0).clamp(0.0, 100.0)
692    }
693
694    /// Sets the custom style properties of the `Slider`.
695    pub fn slider_style(
696        self,
697        style: impl Fn(SliderCustomStyle) -> SliderCustomStyle + 'static,
698    ) -> Self {
699        self.custom_style(style)
700    }
701
702    /// Sets the step spacing of the `Slider`.
703    pub fn step(mut self, step: f64) -> Self {
704        self.step = Some(step);
705        self
706    }
707}
708
709#[derive(Debug, Default, Clone)]
710pub struct SliderCustomStyle(Style);
711impl From<SliderCustomStyle> for Style {
712    fn from(val: SliderCustomStyle) -> Self {
713        val.0
714    }
715}
716impl From<Style> for SliderCustomStyle {
717    fn from(val: Style) -> Self {
718        Self(val)
719    }
720}
721impl CustomStyle for SliderCustomStyle {
722    type StyleClass = SliderClass;
723}
724
725impl CustomStylable<SliderCustomStyle> for Slider {
726    type DV = Self;
727}
728
729impl SliderCustomStyle {
730    pub fn new() -> Self {
731        Self::default()
732    }
733
734    /// Sets the color of the slider handle.
735    ///
736    /// # Arguments
737    /// * `color` - An optional `Brush` that sets the handle's color.
738    pub fn handle_color(mut self, color: impl Into<Brush>) -> Self {
739        let color = color.into();
740        self = SliderCustomStyle(self.0.set(Foreground, Some(color)));
741        self
742    }
743
744    /// Sets the edge alignment of the slider handle.
745    ///
746    /// # Arguments
747    /// * `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.
748    pub fn edge_align(mut self, align: bool) -> Self {
749        self = SliderCustomStyle(self.0.set(EdgeAlign, align));
750        self
751    }
752
753    /// Sets the radius of the slider handle.
754    ///
755    /// # Arguments
756    /// * `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.
757    pub fn handle_radius(mut self, radius: impl Into<Length>) -> Self {
758        self = SliderCustomStyle(self.0.set(HandleRadius, radius));
759        self
760    }
761
762    /// Sets the color of the slider's bar.
763    ///
764    /// # Arguments
765    /// * `color` - An optional `Brush` that sets the bar's background color.
766    pub fn bar_color(mut self, color: impl Into<Brush>) -> Self {
767        let color = color.into();
768        self = SliderCustomStyle(
769            self.0
770                .class(BarClass, move |s| s.set(Background, Some(color).clone())),
771        );
772        self
773    }
774
775    /// Sets the border radius of the slider's bar.
776    ///
777    /// # Arguments
778    /// * `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.
779    pub fn bar_radius(mut self, radius: impl Into<Length>) -> Self {
780        self = SliderCustomStyle(self.0.class(BarClass, |s| s.border_radius(radius)));
781        self
782    }
783
784    /// Sets the height of the slider's bar.
785    ///
786    /// # Arguments
787    /// * `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.
788    pub fn bar_height(mut self, height: impl Into<LengthAuto>) -> Self {
789        self = SliderCustomStyle(self.0.class(BarClass, |s| s.height(height)));
790        self
791    }
792
793    /// Sets the color of the slider's accent bar.
794    ///
795    /// # Arguments
796    /// * `color` - A `Brush` that sets the accent bar's background color.
797    pub fn accent_bar_color(mut self, color: impl Into<Brush>) -> Self {
798        let color = Some(color.into());
799        self = SliderCustomStyle(
800            self.0
801                .class(AccentBarClass, move |s| s.set(Background, color.clone())),
802        );
803        self
804    }
805
806    /// Sets the border radius of the slider's accent bar.
807    ///
808    /// # Arguments
809    /// * `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.
810    pub fn accent_bar_radius(mut self, radius: impl Into<Length>) -> Self {
811        self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.border_radius(radius)));
812        self
813    }
814
815    /// Sets the height of the slider's accent bar.
816    ///
817    /// # Arguments
818    /// * `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.
819    pub fn accent_bar_height(mut self, height: impl Into<LengthAuto>) -> Self {
820        self = SliderCustomStyle(self.0.class(AccentBarClass, |s| s.height(height)));
821        self
822    }
823}
824
825#[derive(Debug, Default, Clone)]
826pub struct SliderCustomExprStyle(Style);
827impl From<SliderCustomExprStyle> for Style {
828    fn from(val: SliderCustomExprStyle) -> Self {
829        val.0
830    }
831}
832impl From<Style> for SliderCustomExprStyle {
833    fn from(val: Style) -> Self {
834        Self(val)
835    }
836}
837impl SliderCustomExprStyle {
838    pub fn new() -> Self {
839        Self::default()
840    }
841
842    pub fn handle_color<T>(mut self, color: ContextValue<T>) -> Self
843    where
844        T: Into<Option<Brush>> + 'static,
845    {
846        self = SliderCustomExprStyle(
847            ExprStyle::from(self.0)
848                .set_context_opt(Foreground, color.map(Into::into))
849                .into(),
850        );
851        self
852    }
853
854    pub fn edge_align<T>(mut self, align: ContextValue<T>) -> Self
855    where
856        T: Into<bool> + 'static,
857    {
858        self = SliderCustomExprStyle(
859            ExprStyle::from(self.0)
860                .set_context(EdgeAlign, align.map(Into::into))
861                .into(),
862        );
863        self
864    }
865
866    pub fn handle_radius<T>(mut self, radius: ContextValue<T>) -> Self
867    where
868        T: Into<Length> + 'static,
869    {
870        self = SliderCustomExprStyle(
871            ExprStyle::from(self.0)
872                .set_context(HandleRadius, radius.map(Into::into))
873                .into(),
874        );
875        self
876    }
877
878    pub fn bar_color<T>(mut self, color: ContextValue<T>) -> Self
879    where
880        T: Into<Option<Brush>> + 'static,
881    {
882        let color = color.map(Into::into);
883        self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
884            ExprStyle::from(s)
885                .set_context_opt(Background, color.clone())
886                .into()
887        }));
888        self
889    }
890
891    pub fn bar_radius<T>(mut self, radius: ContextValue<T>) -> Self
892    where
893        T: Into<Length> + 'static,
894    {
895        self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
896            ExprStyle::from(s).border_radius(radius.clone()).into()
897        }));
898        self
899    }
900
901    pub fn bar_height<T>(mut self, height: ContextValue<T>) -> Self
902    where
903        T: Into<LengthAuto> + 'static,
904    {
905        self = SliderCustomExprStyle(self.0.class(BarClass, move |s| {
906            ExprStyle::from(s).height(height.clone()).into()
907        }));
908        self
909    }
910
911    pub fn accent_bar_color<T>(mut self, color: ContextValue<T>) -> Self
912    where
913        T: Into<Brush> + 'static,
914    {
915        let color = color.map(|color| Some(color.into()));
916        self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
917            ExprStyle::from(s)
918                .set_context_opt(Background, color.clone())
919                .into()
920        }));
921        self
922    }
923
924    pub fn accent_bar_radius<T>(mut self, radius: ContextValue<T>) -> Self
925    where
926        T: Into<Length> + 'static,
927    {
928        self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
929            ExprStyle::from(s).border_radius(radius.clone()).into()
930        }));
931        self
932    }
933
934    pub fn accent_bar_height<T>(mut self, height: ContextValue<T>) -> Self
935    where
936        T: Into<LengthAuto> + 'static,
937    {
938        self = SliderCustomExprStyle(self.0.class(AccentBarClass, move |s| {
939            ExprStyle::from(s).height(height.clone()).into()
940        }));
941        self
942    }
943}