maycoon_widgets/
slider.rs

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