maycoon_widgets/
slider.rs

1use crate::ext::WidgetLayoutExt;
2use maycoon_core::app::info::AppInfo;
3use maycoon_core::app::update::Update;
4use maycoon_core::layout;
5use maycoon_core::layout::{Dimension, LayoutNode, LayoutStyle, LengthPercentageAuto, StyleNode};
6use maycoon_core::state::{State, Val};
7use maycoon_core::vg::kurbo::{Affine, Circle, Point, Rect, RoundedRect, RoundedRectRadii};
8use maycoon_core::vg::peniko::{Brush, Fill};
9use maycoon_core::vg::Scene;
10use maycoon_core::widget::Widget;
11use maycoon_core::window::MouseButton;
12use maycoon_theme::id::WidgetId;
13use maycoon_theme::theme::Theme;
14use nalgebra::Vector2;
15
16/// A slider widget to control a floating point value between `0.0` and `1.0`.
17///
18/// ### Theming
19/// You can style the slider using following properties:
20/// - `color` - The color of the slider bar.
21/// - `color_ball` - The color of the slider ball.
22pub struct Slider<S: State> {
23    layout_style: Val<S, LayoutStyle>,
24    value: Val<S, f32>,
25    on_change: Box<dyn FnMut(&mut S, f32) -> Update>,
26    dragging: bool,
27}
28
29impl<S: State> Slider<S> {
30    /// Create a new Slider widget from a value (should be state bound) and an `on_change` callback.
31    pub fn new(
32        value: impl Into<Val<S, f32>>,
33        on_change: impl FnMut(&mut S, f32) -> Update + 'static,
34    ) -> Self {
35        Self {
36            layout_style: Val::new_val(LayoutStyle {
37                size: Vector2::<Dimension>::new(Dimension::Length(100.0), Dimension::Length(10.0)),
38                margin: layout::Rect::<LengthPercentageAuto> {
39                    left: LengthPercentageAuto::Length(10.0),
40                    right: LengthPercentageAuto::Length(0.0),
41                    top: LengthPercentageAuto::Length(10.0),
42                    bottom: LengthPercentageAuto::Length(10.0),
43                },
44                ..Default::default()
45            }),
46            value: value.into(),
47            on_change: Box::new(on_change),
48            dragging: false,
49        }
50    }
51
52    /// Sets the layout style of the slider and returns itself.
53    pub fn with_value(mut self, value: impl Into<Val<S, f32>>) -> Self {
54        self.value = value.into();
55        self
56    }
57
58    /// Sets the function to be called when the slider is clicked/changed.
59    pub fn with_on_change(
60        mut self,
61        on_change: impl FnMut(&mut S, f32) -> Update + 'static,
62    ) -> Self {
63        self.on_change = Box::new(on_change);
64        self
65    }
66}
67
68impl<S: State> WidgetLayoutExt<S> for Slider<S> {
69    fn set_layout_style(&mut self, layout_style: impl Into<Val<S, LayoutStyle>>) {
70        self.layout_style = layout_style.into();
71    }
72}
73
74impl<S: State> Widget<S> for Slider<S> {
75    fn render(
76        &mut self,
77        scene: &mut Scene,
78        theme: &mut dyn Theme,
79        _: &AppInfo,
80        layout_node: &LayoutNode,
81        state: &S,
82    ) {
83        let value = *self.value.get_ref(state);
84
85        let brush = if let Some(style) = theme.of(self.widget_id()) {
86            Brush::Solid(style.get_color("color").unwrap())
87        } else {
88            Brush::Solid(theme.defaults().interactive().inactive())
89        };
90
91        let ball_brush = if let Some(style) = theme.of(self.widget_id()) {
92            Brush::Solid(style.get_color("color_ball").unwrap())
93        } else {
94            Brush::Solid(theme.defaults().interactive().active())
95        };
96
97        let circle_radius = layout_node.layout.size.height as f64 / 1.15;
98
99        scene.fill(
100            Fill::NonZero,
101            Affine::default(),
102            &brush,
103            None,
104            &RoundedRect::from_rect(
105                Rect::new(
106                    layout_node.layout.location.x as f64,
107                    layout_node.layout.location.y as f64,
108                    (layout_node.layout.location.x + layout_node.layout.size.width) as f64,
109                    (layout_node.layout.location.y + layout_node.layout.size.height) as f64,
110                ),
111                RoundedRectRadii::from_single_radius(20.0),
112            ),
113        );
114
115        scene.fill(
116            Fill::NonZero,
117            Affine::default(),
118            &ball_brush,
119            None,
120            &Circle::new(
121                Point::new(
122                    (layout_node.layout.location.x + layout_node.layout.size.width * value) as f64,
123                    (layout_node.layout.location.y + layout_node.layout.size.height / 2.0) as f64,
124                ),
125                circle_radius,
126            ),
127        );
128    }
129
130    fn layout_style(&mut self, state: &S) -> StyleNode {
131        StyleNode {
132            style: self.layout_style.get_ref(state).clone(),
133            children: Vec::new(),
134        }
135    }
136
137    fn update(&mut self, layout: &LayoutNode, state: &mut S, info: &AppInfo) -> Update {
138        self.value.invalidate();
139        self.layout_style.invalidate();
140
141        let mut update = Update::empty();
142
143        if let Some(cursor) = info.cursor_pos {
144            if cursor.x as f32 >= layout.layout.location.x
145                && cursor.x as f32 <= layout.layout.location.x + layout.layout.size.width
146                && cursor.y as f32 >= layout.layout.location.y
147                && cursor.y as f32 <= layout.layout.location.y + layout.layout.size.height
148            {
149                for (_, btn, el_state) in &info.buttons {
150                    if btn == &MouseButton::Left && el_state.is_pressed() {
151                        self.dragging = el_state.is_pressed();
152                    }
153                }
154
155                if self.dragging {
156                    let new_value =
157                        (cursor.x as f32 - layout.layout.location.x) / layout.layout.size.width;
158
159                    update.insert((self.on_change)(state, new_value));
160                    update.insert(Update::DRAW);
161                }
162            }
163        } else {
164            self.dragging = false;
165        }
166
167        update
168    }
169
170    fn widget_id(&self) -> WidgetId {
171        WidgetId::new("maycoon-widgets", "Slider")
172    }
173}