pushrod/widgets/
checkbox_widget.rs

1// Pushrod Widget Library
2// Checkbox 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::{
21    make_points, make_size, Points, Size, POINT_X, POINT_Y, SIZE_HEIGHT, SIZE_WIDTH,
22};
23
24use sdl2::render::{Canvas, Texture};
25use sdl2::video::Window;
26
27use crate::render::canvas_helper::CanvasHelper;
28use crate::render::layout_cache::LayoutContainer;
29use crate::render::texture_cache::TextureCache;
30use crate::render::texture_store::TextureStore;
31use crate::render::widget_config::CompassPosition::Center;
32use crate::widgets::image_widget::ImageWidget;
33use crate::widgets::text_widget::{TextJustify, TextWidget};
34use sdl2::pixels::Color;
35use sdl2::rect::Rect;
36use std::any::Any;
37use std::collections::HashMap;
38
39/// This is the callback type that is used when an `on_toggle` callback is triggered from this
40/// `Widget`.
41pub type OnToggleCallbackType =
42    Option<Box<dyn FnMut(&mut CheckboxWidget, &[WidgetContainer], &[LayoutContainer], bool)>>;
43
44/// This is the storage object for the `CheckboxWidget`.  It stores the config, properties, callback registry.
45pub struct CheckboxWidget {
46    config: WidgetConfig,
47    system_properties: HashMap<i32, String>,
48    callback_registry: CallbackRegistry,
49    texture_store: TextureStore,
50    text_widget: TextWidget,
51    unchecked_widget: ImageWidget,
52    checked_widget: ImageWidget,
53    active: bool,
54    selected: bool,
55    in_bounds: bool,
56    originated: bool,
57    on_toggle: OnToggleCallbackType,
58}
59
60/// This is the implementation of the `CheckboxWidget` that draws a button on the screen that can be
61/// toggled on or off.
62impl CheckboxWidget {
63    /// Creates a new `CheckboxWidget` given the `x, y, w, h` coordinates, the `text` to display
64    /// inside the button, `font_size` of the font to display, and the initial `selected` state: `true`
65    /// being checked, `false` otherwise.
66    pub fn new(points: Points, size: Size, text: String, font_size: i32, selected: bool) -> Self {
67        let mut text_widget = TextWidget::new(
68            String::from("assets/OpenSans-Regular.ttf"),
69            sdl2::ttf::FontStyle::NORMAL,
70            font_size,
71            TextJustify::Left,
72            text,
73            make_points(
74                points[POINT_X] + size[SIZE_HEIGHT] as i32 + 6,
75                points[POINT_Y] + 2,
76            ),
77            make_size(
78                size[SIZE_WIDTH] - size[SIZE_HEIGHT] - 10,
79                size[SIZE_HEIGHT] - 4,
80            ),
81        );
82
83        let mut config = WidgetConfig::new(points.clone(), size.clone());
84        let mut unchecked_widget = ImageWidget::new(
85            String::from("assets/checkbox_unselected.png"),
86            make_points(points[POINT_X] + 2, points[POINT_Y] + 2),
87            make_size(size[SIZE_HEIGHT] - 4, size[SIZE_HEIGHT] - 4),
88            true,
89        );
90        let mut checked_widget = ImageWidget::new(
91            String::from("assets/checkbox_selected.png"),
92            make_points(points[POINT_X] + 2, points[POINT_Y] + 2),
93            make_size(size[SIZE_HEIGHT] - 4, size[SIZE_HEIGHT] - 4),
94            true,
95        );
96
97        text_widget.set_color(CONFIG_COLOR_TEXT, Color::RGB(0, 0, 0));
98        unchecked_widget.set_compass(CONFIG_IMAGE_POSITION, Center);
99        checked_widget.set_compass(CONFIG_IMAGE_POSITION, Center);
100
101        config.set_toggle(CONFIG_SELECTED_STATE, selected);
102
103        Self {
104            config,
105            system_properties: HashMap::new(),
106            callback_registry: CallbackRegistry::new(),
107            texture_store: TextureStore::default(),
108            text_widget,
109            unchecked_widget,
110            checked_widget,
111            active: false,
112            selected,
113            in_bounds: false,
114            originated: false,
115            on_toggle: None,
116        }
117    }
118
119    /// Assigns the callback closure that will be used when the `Widget` toggles state.
120    pub fn on_toggle<F>(&mut self, callback: F)
121    where
122        F: FnMut(&mut CheckboxWidget, &[WidgetContainer], &[LayoutContainer], bool) + 'static,
123    {
124        self.on_toggle = Some(Box::new(callback));
125    }
126
127    /// Internal function that triggers the `on_toggle` callback.
128    fn call_toggle_callback(&mut self, widgets: &[WidgetContainer], layouts: &[LayoutContainer]) {
129        if let Some(mut cb) = self.on_toggle.take() {
130            cb(self, widgets, layouts, self.selected);
131            self.on_toggle = Some(cb);
132        }
133    }
134}
135
136impl CanvasHelper for CheckboxWidget {}
137
138/// This is the `Widget` implementation of the `CheckboxWidget`.
139impl Widget for CheckboxWidget {
140    /// Draws the `CheckboxWidget` contents.
141    fn draw(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) -> Option<&Texture> {
142        if self.get_config().invalidated() {
143            let bounds = self.get_config().get_size(CONFIG_SIZE);
144            let base_color = self.get_color(CONFIG_COLOR_BASE);
145            let border_color = self.get_config().get_color(CONFIG_COLOR_BORDER);
146
147            self.texture_store
148                .create_or_resize_texture(c, bounds[0] as u32, bounds[1] as u32);
149
150            // Paint the base widget first.  Forcing a draw() call here will ignore invalidation.
151            // Invalidation is controlled by the top level widget (this box).
152            let checkbox_widget_texture = if self.active {
153                if self.in_bounds {
154                    if self.selected {
155                        self.unchecked_widget.draw(c, t).unwrap()
156                    } else {
157                        self.checked_widget.draw(c, t).unwrap()
158                    }
159                } else if self.selected {
160                    self.checked_widget.draw(c, t).unwrap()
161                } else {
162                    self.unchecked_widget.draw(c, t).unwrap()
163                }
164            } else if self.selected {
165                self.checked_widget.draw(c, t).unwrap()
166            } else {
167                self.unchecked_widget.draw(c, t).unwrap()
168            };
169
170            let text_widget_texture = self.text_widget.draw(c, t).unwrap();
171
172            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
173                texture.set_draw_color(base_color);
174                texture.clear();
175
176                texture
177                    .copy(
178                        text_widget_texture,
179                        None,
180                        Rect::new(
181                            2 + bounds[1] as i32 + 6,
182                            0,
183                            bounds[0] - bounds[1],
184                            bounds[1] - 4,
185                        ),
186                    )
187                    .unwrap();
188
189                texture
190                    .copy(
191                        checkbox_widget_texture,
192                        None,
193                        Rect::new(2, 2, bounds[1] - 4, bounds[1] - 4),
194                    )
195                    .unwrap();
196
197                texture.set_draw_color(border_color);
198                texture
199                    .draw_rect(Rect::new(0, 0, bounds[0], bounds[1]))
200                    .unwrap();
201            })
202            .unwrap();
203        }
204
205        self.texture_store.get_optional_ref()
206    }
207
208    /// When a mouse enters the bounds of the `Widget`, this function is triggered.
209    fn mouse_entered(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
210        self.in_bounds = true;
211        self.mouse_entered_callback(_widgets, _layouts);
212        self.get_config().set_invalidated(true);
213    }
214
215    /// When a mouse exits the bounds of the `Widget`, this function is triggered.
216    fn mouse_exited(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
217        self.in_bounds = false;
218        self.mouse_exited_callback(_widgets, _layouts);
219        self.get_config().set_invalidated(true);
220    }
221
222    /// Overrides the `button_clicked` callback to handle toggling.
223    fn button_clicked(
224        &mut self,
225        _widgets: &[WidgetContainer],
226        _layouts: &[LayoutContainer],
227        _button: u8,
228        _clicks: u8,
229        _state: bool,
230    ) {
231        if _button == 1 {
232            if _state {
233                self.active = true;
234                self.originated = true;
235            } else {
236                self.active = false;
237
238                if self.in_bounds && self.originated {
239                    self.selected = !self.selected;
240                    self.set_toggle(CONFIG_SELECTED_STATE, self.selected);
241                    self.call_toggle_callback(_widgets, _layouts);
242                }
243
244                self.originated = false;
245            }
246
247            self.get_config().set_invalidated(true);
248        }
249
250        self.button_clicked_callback(_widgets, _layouts, _button, _clicks, _state);
251    }
252
253    default_widget_functions!();
254    default_widget_properties!();
255    default_widget_callbacks!();
256}