pushrod/widgets/
push_button_widget.rs

1// Pushrod Widget Library
2// Push Button 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::{
20    WidgetConfig, CONFIG_BORDER_WIDTH, CONFIG_COLOR_BASE, CONFIG_COLOR_BORDER, CONFIG_COLOR_TEXT,
21    CONFIG_SIZE,
22};
23use crate::render::{
24    make_points, make_size, Points, Size, POINT_X, POINT_Y, SIZE_HEIGHT, SIZE_WIDTH,
25};
26
27use sdl2::render::{Canvas, Texture};
28use sdl2::video::Window;
29
30use crate::render::layout_cache::LayoutContainer;
31use crate::render::texture_cache::TextureCache;
32use crate::render::texture_store::TextureStore;
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_click` callback is triggered from this
40/// `Widget`.
41pub type OnClickCallbackType =
42    Option<Box<dyn FnMut(&mut PushButtonWidget, &[WidgetContainer], &[LayoutContainer])>>;
43
44/// This is the storage object for the `PushButtonWidget`.  It stores the config, properties, callback registry.
45pub struct PushButtonWidget {
46    config: WidgetConfig,
47    system_properties: HashMap<i32, String>,
48    callback_registry: CallbackRegistry,
49    texture_store: TextureStore,
50    base_widget: BaseWidget,
51    text_widget: TextWidget,
52    active: bool,
53    in_bounds: bool,
54    originated: bool,
55    on_click: OnClickCallbackType,
56}
57
58/// This is the `PushButtonWidget` implementation, which displays a block of text inside a clickable
59/// box.  Clicking the box will cause an `on_click` callback to be triggered, which will call a block
60/// of text, if the callback has been configured.
61impl PushButtonWidget {
62    /// Creates a new `PushButtonWidget`, given `x, y, w, h` coordinates, some `text` to display,
63    /// and the `font_size` to use.
64    pub fn new(points: Points, size: Size, text: String, font_size: i32) -> Self {
65        let mut base_widget = BaseWidget::new(points.clone(), size.clone());
66        let mut text_widget = TextWidget::new(
67            String::from("assets/OpenSans-Regular.ttf"),
68            sdl2::ttf::FontStyle::NORMAL,
69            font_size,
70            TextJustify::Center,
71            text,
72            make_points(points[POINT_X] + 2, points[POINT_Y] + 2),
73            make_size(size[SIZE_WIDTH] - 4, size[SIZE_HEIGHT] - 4),
74        );
75
76        base_widget.set_color(CONFIG_COLOR_BASE, Color::RGB(255, 255, 255));
77        base_widget.set_color(CONFIG_COLOR_BORDER, Color::RGB(0, 0, 0));
78        base_widget.set_numeric(CONFIG_BORDER_WIDTH, 2);
79
80        text_widget.set_color(CONFIG_COLOR_TEXT, Color::RGB(0, 0, 0));
81
82        Self {
83            config: WidgetConfig::new(points, size),
84            system_properties: HashMap::new(),
85            callback_registry: CallbackRegistry::new(),
86            texture_store: TextureStore::default(),
87            base_widget,
88            text_widget,
89            active: false,
90            in_bounds: false,
91            originated: false,
92            on_click: None,
93        }
94    }
95
96    fn draw_hovered(&mut self) {
97        self.base_widget
98            .set_color(CONFIG_COLOR_BASE, Color::RGB(0, 0, 0));
99        self.text_widget
100            .set_color(CONFIG_COLOR_TEXT, Color::RGB(255, 255, 255));
101        self.text_widget
102            .set_color(CONFIG_COLOR_BASE, Color::RGB(0, 0, 0));
103        self.get_config().set_invalidated(true);
104    }
105
106    fn draw_unhovered(&mut self) {
107        self.base_widget
108            .set_color(CONFIG_COLOR_BASE, Color::RGB(255, 255, 255));
109        self.text_widget
110            .set_color(CONFIG_COLOR_TEXT, Color::RGB(0, 0, 0));
111        self.text_widget
112            .set_color(CONFIG_COLOR_BASE, Color::RGB(255, 255, 255));
113        self.get_config().set_invalidated(true);
114    }
115
116    /// Assigns the callback closure that will be used when a button click is triggered.
117    pub fn on_click<F>(&mut self, callback: F)
118    where
119        F: FnMut(&mut PushButtonWidget, &[WidgetContainer], &[LayoutContainer]) + 'static,
120    {
121        self.on_click = Some(Box::new(callback));
122    }
123
124    /// Internal function that triggers the `on_click` callback.
125    fn call_click_callback(&mut self, widgets: &[WidgetContainer], layouts: &[LayoutContainer]) {
126        if let Some(mut cb) = self.on_click.take() {
127            cb(self, widgets, layouts);
128            self.on_click = Some(cb);
129        }
130    }
131}
132
133/// This is the `Widget` implementation of the `PushButtonWidget`.
134impl Widget for PushButtonWidget {
135    fn draw(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) -> Option<&Texture> {
136        if self.get_config().invalidated() {
137            let bounds = self.get_config().get_size(CONFIG_SIZE);
138            let base_color = self.get_color(CONFIG_COLOR_BASE);
139
140            self.texture_store
141                .create_or_resize_texture(c, bounds[0] as u32, bounds[1] as u32);
142
143            // Paint the base widget first.  Forcing a draw() call here will ignore invalidation.
144            // Invalidation is controlled by the top level widget (this box).
145            let base_widget_texture = self.base_widget.draw(c, t).unwrap();
146            let text_widget_texture = self.text_widget.draw(c, t).unwrap();
147
148            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
149                texture.set_draw_color(base_color);
150                texture.clear();
151
152                texture
153                    .copy(
154                        base_widget_texture,
155                        None,
156                        Rect::new(0, 0, bounds[0], bounds[1]),
157                    )
158                    .unwrap();
159
160                texture
161                    .copy(
162                        text_widget_texture,
163                        None,
164                        Rect::new(2, 2, bounds[0] - 4, bounds[1] - 4),
165                    )
166                    .unwrap();
167            })
168            .unwrap();
169        }
170
171        self.texture_store.get_optional_ref()
172    }
173
174    /// When a mouse enters the bounds of the `Widget`, this function is triggered.  This function
175    /// implementation is **optional**.
176    fn mouse_entered(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
177        if self.active {
178            self.draw_hovered();
179        }
180
181        self.in_bounds = true;
182        self.mouse_entered_callback(_widgets, _layouts);
183    }
184
185    /// When a mouse exits the bounds of the `Widget`, this function is triggered.  This function
186    /// implementation is **optional**.
187    fn mouse_exited(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
188        if self.active {
189            self.draw_unhovered();
190        }
191
192        self.in_bounds = false;
193        self.mouse_exited_callback(_widgets, _layouts);
194    }
195
196    /// When a mouse button is clicked within (or outside of) the bounds of the `Widget`, this
197    /// function is called.  If a mouse button is clicked, and the mouse leaves the bounds of the
198    /// `Widget`, the mouse release event will still be triggered for the last `Widget` which
199    /// received the mouse down state.  This prevents `Widget`s from becoming confused.  This
200    /// behavior is tracked by the main loop, not by the `Widget` code.  Therefore, when a mouse
201    /// button is released outside of the bounds of _this_ `Widget`, you must adjust your state
202    /// accordingly, if you pay attention to the `button_clicked` function.  This function
203    /// implementation is **optional**.
204    fn button_clicked(
205        &mut self,
206        _widgets: &[WidgetContainer],
207        _layouts: &[LayoutContainer],
208        _button: u8,
209        _clicks: u8,
210        _state: bool,
211    ) {
212        if _button == 1 {
213            if _state {
214                self.draw_hovered();
215                self.active = true;
216                self.originated = true;
217            } else {
218                let had_bounds = self.active;
219
220                self.draw_unhovered();
221                self.active = false;
222
223                if self.in_bounds && had_bounds && self.originated {
224                    // Callback here
225                    self.call_click_callback(_widgets, _layouts);
226                }
227
228                self.originated = false;
229            }
230        }
231
232        self.button_clicked_callback(_widgets, _layouts, _button, _clicks, _state);
233    }
234
235    default_widget_functions!();
236    default_widget_properties!();
237    default_widget_callbacks!();
238}