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