pushrod/widgets/
image_button_widget.rs

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