Skip to main content

pushrod/widgets/
slider_widget.rs

1// Pushrod Widget Library
2// Slider Widget
3//
4// Licensed under the Apache License, Version 2.0 (the "License");
5// you may not use this file except in compliance with the License.
6// You may obtain a copy of the License at
7//
8// http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS,
12// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13// See the License for the specific language governing permissions and
14// limitations under the License.
15
16use crate::render::callbacks::CallbackRegistry;
17use crate::render::widget::*;
18use crate::render::widget_cache::WidgetContainer;
19use crate::render::widget_config::*;
20use crate::render::{Points, Size, POINT_X, POINT_Y, SIZE_HEIGHT, SIZE_WIDTH};
21
22use sdl2::render::{Canvas, Texture};
23use sdl2::video::Window;
24
25use crate::render::canvas_helper::CanvasHelper;
26use crate::render::layout_cache::LayoutContainer;
27use crate::render::texture_cache::TextureCache;
28use crate::render::texture_store::TextureStore;
29use crate::widgets::slider_widget::SliderOrientation::{SliderHorizontal, SliderVertical};
30use sdl2::pixels::Color;
31use sdl2::rect::{Point, Rect};
32use std::any::Any;
33use std::collections::HashMap;
34
35/// This is the callback type that is used when an `on_value_changed` callback is triggered from this
36/// `Widget`.
37pub type OnValueChangedCallbackType =
38    Option<Box<dyn FnMut(&mut SliderWidget, &[WidgetContainer], &[LayoutContainer], u32)>>;
39
40/// These are the possible slider orientations.
41#[derive(PartialEq, Clone)]
42pub enum SliderOrientation {
43    /// Indicates a horizontally controllable slider.
44    SliderHorizontal,
45
46    /// Indicates a vertically controllable slider.
47    SliderVertical,
48}
49
50/// This is the storage object for the `SliderWidget`.  It stores the config, properties, callback registry.
51pub struct SliderWidget {
52    config: WidgetConfig,
53    system_properties: HashMap<i32, String>,
54    callback_registry: CallbackRegistry,
55    texture_store: TextureStore,
56    min: u32,
57    max: u32,
58    current: u32,
59    orientation: SliderOrientation,
60    in_bounds: bool,
61    active: bool,
62    originated: bool,
63    on_value_changed: OnValueChangedCallbackType,
64}
65
66/// This is the implementation of the `SliderWidget`, a control that draws a bounds line indicator, and a
67/// draggable slider.
68impl SliderWidget {
69    /// Creates a new `SliderWidget` given the `x, y, w, h` coordinates, sets the `min` and `max` values,
70    /// the `current` value, and the `orientation` of the slider as drawn.
71    pub fn new(
72        points: Points,
73        size: Size,
74        min: u32,
75        max: u32,
76        current: u32,
77        orientation: SliderOrientation,
78    ) -> Self {
79        Self {
80            config: WidgetConfig::new(points, size),
81            system_properties: HashMap::new(),
82            callback_registry: CallbackRegistry::new(),
83            texture_store: TextureStore::default(),
84            min,
85            max,
86            current,
87            orientation,
88            in_bounds: false,
89            active: false,
90            originated: false,
91            on_value_changed: None,
92        }
93    }
94
95    /// Assigns the callback closure that will be used when the `Widget` changes value.
96    pub fn on_value_changed<F>(&mut self, callback: F)
97    where
98        F: FnMut(&mut SliderWidget, &[WidgetContainer], &[LayoutContainer], u32) + 'static,
99    {
100        self.on_value_changed = Some(Box::new(callback));
101    }
102
103    /// Internal function that triggers the `on_value_changed` callback.
104    fn call_value_changed_callback(
105        &mut self,
106        widgets: &[WidgetContainer],
107        layouts: &[LayoutContainer],
108    ) {
109        if let Some(mut cb) = self.on_value_changed.take() {
110            cb(self, widgets, layouts, self.current);
111            self.on_value_changed = Some(cb);
112        }
113    }
114}
115
116impl CanvasHelper for SliderWidget {}
117
118/// This is the `Widget` implementation of the `SliderWidget`.
119impl Widget for SliderWidget {
120    /// Draws the `SliderWidget` contents.
121    fn draw(&mut self, c: &mut Canvas<Window>, _t: &mut TextureCache) -> Option<&Texture> {
122        if self.get_config().invalidated() {
123            let bounds = self.get_config().get_size(CONFIG_SIZE);
124
125            self.texture_store
126                .create_or_resize_texture(c, bounds[0] as u32, bounds[1] as u32);
127
128            // Draw base - three lines in the center
129            let half_height = (self.get_config().get_size(CONFIG_SIZE)[SIZE_HEIGHT] / 2) as i32;
130            let half_width = (self.get_config().get_size(CONFIG_SIZE)[SIZE_WIDTH] / 2) as i32;
131            let height = (self.get_config().get_size(CONFIG_SIZE)[SIZE_HEIGHT]) as i32;
132            let width = (self.get_config().get_size(CONFIG_SIZE)[SIZE_WIDTH]) as i32;
133            let base_color = self.get_color(CONFIG_COLOR_BASE);
134            let orientation = self.orientation.clone();
135            let min = self.min;
136            let max = self.max;
137            let current = self.current;
138
139            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
140                texture.set_draw_color(base_color);
141                texture.clear();
142
143                if orientation == SliderHorizontal {
144                    texture.set_draw_color(Color::RGB(192, 192, 192));
145                    texture
146                        .draw_line(
147                            Point::new(10, half_height),
148                            Point::new(width - 10, half_height),
149                        )
150                        .unwrap();
151
152                    texture
153                        .draw_line(
154                            Point::new(10, half_height - 1),
155                            Point::new(width - 10, half_height - 1),
156                        )
157                        .unwrap();
158
159                    texture
160                        .draw_line(
161                            Point::new(10, half_height + 1),
162                            Point::new(width - 10, half_height + 1),
163                        )
164                        .unwrap();
165
166                    // Draw slider at current value
167                    let full_range = max - min;
168                    let slider_center =
169                        ((width as f64 / full_range as f64) * (current - min) as f64) as u32;
170                    let slider_start = if slider_center >= width as u32 - 15 {
171                        width as u32 - 30
172                    } else if slider_center <= 15 {
173                        0
174                    } else {
175                        slider_center - 15
176                    };
177
178                    texture.set_draw_color(base_color);
179                    texture
180                        .fill_rect(Rect::new(slider_start as i32, 0, 30, bounds[SIZE_HEIGHT]))
181                        .unwrap();
182
183                    texture.set_draw_color(Color::RGB(0, 0, 0));
184                    texture
185                        .draw_rect(Rect::new(slider_start as i32, 0, 30, bounds[SIZE_HEIGHT]))
186                        .unwrap();
187                } else if orientation == SliderVertical {
188                    // Draw base - three lines in the center
189
190                    texture.set_draw_color(Color::RGB(192, 192, 192));
191                    texture
192                        .draw_line(
193                            Point::new(half_width, 10),
194                            Point::new(half_width, height - 10),
195                        )
196                        .unwrap();
197
198                    texture
199                        .draw_line(
200                            Point::new(half_width - 1, 10),
201                            Point::new(half_width - 1, height - 10),
202                        )
203                        .unwrap();
204
205                    texture
206                        .draw_line(
207                            Point::new(half_width + 1, 10),
208                            Point::new(half_width + 1, height - 10),
209                        )
210                        .unwrap();
211
212                    // Draw slider at current value
213                    let full_range = max - min;
214                    let slider_center =
215                        ((height as f64 / full_range as f64) * (current - min) as f64) as u32;
216                    let slider_start = if slider_center >= height as u32 - 15 {
217                        height as u32 - 30
218                    } else if slider_center <= 15 {
219                        0
220                    } else {
221                        slider_center - 15
222                    };
223
224                    texture.set_draw_color(base_color);
225                    texture
226                        .fill_rect(Rect::new(0, slider_start as i32, bounds[SIZE_WIDTH], 30))
227                        .unwrap();
228
229                    texture.set_draw_color(Color::RGB(0, 0, 0));
230                    texture
231                        .draw_rect(Rect::new(0, slider_start as i32, bounds[SIZE_WIDTH], 30))
232                        .unwrap();
233                }
234            })
235            .unwrap();
236        }
237
238        self.texture_store.get_optional_ref()
239    }
240
241    /// When a mouse enters the bounds of the `Widget`, this function is triggered.
242    fn mouse_entered(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
243        self.in_bounds = true;
244    }
245
246    /// When a mouse exits the bounds of the `Widget`, this function is triggered.
247    fn mouse_exited(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
248        self.in_bounds = false;
249    }
250
251    /// When a mouse is moved in the bounds of this `Widget`, this function is triggered.
252    fn mouse_moved(
253        &mut self,
254        _widgets: &[WidgetContainer],
255        _layouts: &[LayoutContainer],
256        points: Points,
257    ) {
258        if self.in_bounds && self.active && self.originated {
259            if self.orientation == SliderHorizontal {
260                let width = (self.get_config().get_size(CONFIG_SIZE)[SIZE_WIDTH]) as i32;
261                let position_x =
262                    points[POINT_X] - self.get_config().get_point(CONFIG_ORIGIN)[POINT_X] as i32;
263                let percentage = position_x as f64 / width as f64;
264                let full_range = self.max - self.min;
265                let actual = (percentage * full_range as f64) as u32;
266
267                self.current = self.min + actual;
268
269                self.get_config().set_invalidated(true);
270                self.call_value_changed_callback(_widgets, _layouts);
271            } else if self.orientation == SliderVertical {
272                let height = (self.get_config().get_size(CONFIG_SIZE)[SIZE_HEIGHT]) as i32;
273                let position_y =
274                    points[POINT_Y] - self.get_config().get_point(CONFIG_ORIGIN)[POINT_Y] as i32;
275                let percentage = position_y as f64 / height as f64;
276                let full_range = self.max - self.min;
277                let actual = (percentage * full_range as f64) as u32;
278
279                self.current = self.min + actual;
280
281                self.get_config().set_invalidated(true);
282                self.call_value_changed_callback(_widgets, _layouts);
283            }
284        }
285    }
286
287    /// Handles the scrolling functionality.
288    fn mouse_scrolled(
289        &mut self,
290        _widgets: &[WidgetContainer],
291        _layouts: &[LayoutContainer],
292        points: Points,
293    ) {
294        let mut current_i32 = self.current as i32;
295
296        if self.orientation == SliderHorizontal {
297            current_i32 += points[POINT_X];
298        } else if self.orientation == SliderVertical {
299            current_i32 += -points[POINT_Y];
300        }
301
302        if current_i32 >= self.max as i32 {
303            current_i32 = self.max as i32;
304        } else if current_i32 <= self.min as i32 {
305            current_i32 = self.min as i32;
306        }
307
308        self.current = current_i32 as u32;
309
310        self.get_config().set_invalidated(true);
311        self.call_value_changed_callback(_widgets, _layouts);
312    }
313
314    /// Overrides the `button_clicked` callback to handle toggling.
315    fn button_clicked(
316        &mut self,
317        _widgets: &[WidgetContainer],
318        _layouts: &[LayoutContainer],
319        _button: u8,
320        _clicks: u8,
321        _state: bool,
322    ) {
323        if _button == 1 {
324            if _state {
325                self.active = true;
326                self.originated = true;
327            } else {
328                self.active = false;
329                self.originated = false;
330            }
331
332            self.get_config().set_invalidated(true);
333        }
334
335        self.button_clicked_callback(_widgets, _layouts, _button, _clicks, _state);
336    }
337
338    default_widget_functions!();
339    default_widget_properties!();
340    default_widget_callbacks!();
341}