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