pushrod_widgets/system_widgets/
toggle_button_widget.rs

1// Pushrod Widgets
2// Toggle 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 sdl2::render::{Canvas, Texture};
17use sdl2::video::Window;
18
19use crate::caches::TextureCache;
20use crate::event::PushrodEvent;
21use crate::event::PushrodEvent::{
22    DrawFrame, MouseButton, WidgetMouseEntered, WidgetMouseExited, WidgetSelected,
23};
24use crate::primitives::draw_base;
25use crate::properties::{
26    WidgetProperties, PROPERTY_BORDER_WIDTH, PROPERTY_ID, PROPERTY_TEXT_JUSTIFICATION,
27    PROPERTY_TOGGLED, TEXT_JUSTIFY_CENTER, TEXT_JUSTIFY_LEFT, TEXT_JUSTIFY_RIGHT,
28};
29use crate::texture_store::TextureStore;
30use crate::widget::Widget;
31use sdl2::pixels::Color;
32use sdl2::rect::Rect;
33
34/// Internal flag indicating if the button is currently accepting mouse button focus.
35const PROPERTY_BUTTON_ACTIVE: u32 = 20000;
36
37/// Internal flag indicating if the mouse is in the bounds of the `Widget`.
38const PROPERTY_BUTTON_IN_BOUNDS: u32 = 20001;
39
40/// Internal flag indicating whether or not this `Widget` originated the `MouseButton` state of `true`.
41const PROPERTY_BUTTON_ORIGINATED: u32 = 20002;
42
43/// Internal flag indicating whether or not this `Widget` has the mouse currently hovered over it, and
44/// the button state is highlighted.
45const PROPERTY_BUTTON_HOVERED: u32 = 20003;
46
47/// Base Widget.
48#[derive(Default)]
49pub struct ToggleButtonWidget {
50    texture_store: TextureStore,
51    properties: WidgetProperties,
52}
53
54/// Auxiliary functions for the `ButtonWidget`.
55impl ToggleButtonWidget {
56    /// Sets the state to hovered, meaning the mouse has entered the bounds of the `Widget`.
57    fn set_hovered(&mut self) {
58        self.properties.set_bool(PROPERTY_BUTTON_HOVERED);
59        self.invalidate();
60    }
61
62    /// Sets the state to unhovered, meaning the mouse has left the bounds of the `Widget`.
63    fn set_unhovered(&mut self) {
64        self.properties.delete(PROPERTY_BUTTON_HOVERED);
65        self.invalidate();
66    }
67
68    /// Swaps the toggled state.
69    fn toggle_selected(&mut self) {
70        if self.properties.get_bool(PROPERTY_TOGGLED) {
71            self.properties.delete(PROPERTY_TOGGLED);
72        } else {
73            self.properties.set_bool(PROPERTY_TOGGLED);
74        }
75    }
76
77    /// Shortcut function to determine if the button is currently in a selected state or not.
78    fn is_selected(&self) -> bool {
79        self.properties.get_bool(PROPERTY_TOGGLED)
80    }
81}
82
83/// Implementation for drawing a `BaseWidget`, with the `Widget` trait objects applied.
84impl Widget for ToggleButtonWidget {
85    widget_default_impl!("ToggleButtonWidget");
86
87    /// This is a `ButtonWidget` that is used as a standard blank `Widget`.
88    ///
89    /// - PROPERTY_MAIN_COLOR: the `Color` of the body of the `Widget`
90    /// - PROPERTY_BORDER_WIDTH: the width of the border to draw
91    /// - PROPERTY_BORDER_COLOR: the `Color` of the border
92    /// - PROPERTY_FONT_COLOR: the color of the font
93    /// - PROPERTY_FONT_NAME: full or relative path to the font file to use to render the text
94    /// - PROPERTY_FONT_SIZE: the size in points of the font
95    /// - PROPERTY_FONT_STYLE: the `FontStyle` to apply to the font
96    /// - PROPERTY_TEXT_JUSTIFICATION: The `TEXT_JUSTIFY_*` constant to use to position the text inside the `Widget`
97    /// - PROPERTY_TEXT: `String` containing the text to display
98    /// - PROPERTY_TOGGLED: boolean indicating whether or not the button is in a selected state
99    ///
100    /// While the button is selected, the color of the background will be `Color::BLACK`, and the
101    /// text will turn `Color::WHITE`.  If the button is _not_ selected, it will revert back to the
102    /// colors that you set for the text color and background colors of the button.
103    fn draw(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) -> Option<&Texture> {
104        // ONLY update the texture if the `BaseWidget` shows that it's been invalidated.
105        if self.invalidated() {
106            let bounds = self.properties.get_bounds();
107            let text_color = if self.properties.get_bool(PROPERTY_BUTTON_HOVERED) {
108                if self.is_selected() {
109                    Some(Color::BLACK)
110                } else {
111                    Some(Color::WHITE)
112                }
113            } else if self.is_selected() {
114                Some(Color::WHITE)
115            } else {
116                None
117            };
118            let (font_texture, width, height) = t.render_text(c, &mut self.properties, text_color);
119            let text_justification = self.properties.get_value(PROPERTY_TEXT_JUSTIFICATION);
120            let border_width = self.properties.get_value(PROPERTY_BORDER_WIDTH);
121            let widget_w = bounds.0;
122            let texture_x: i32 = match text_justification {
123                TEXT_JUSTIFY_LEFT => 0,
124                TEXT_JUSTIFY_CENTER => (widget_w - width) as i32 / 2,
125                TEXT_JUSTIFY_RIGHT => (widget_w - width) as i32,
126                _ => 0,
127            };
128
129            self.texture_store
130                .create_or_resize_texture(c, bounds.0, bounds.1);
131
132            let cloned_properties = self.properties.clone();
133            let back_color = if self.properties.get_bool(PROPERTY_BUTTON_HOVERED) {
134                if self.is_selected() {
135                    Some(Color::WHITE)
136                } else {
137                    Some(Color::BLACK)
138                }
139            } else if self.is_selected() {
140                Some(Color::BLACK)
141            } else {
142                None
143            };
144
145            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
146                draw_base(texture, &cloned_properties, back_color);
147
148                texture
149                    .copy(
150                        &font_texture,
151                        None,
152                        Rect::new(
153                            texture_x + border_width,
154                            ((bounds.1 / 2) - (height / 2) - (border_width / 2) as u32) as i32,
155                            width,
156                            height,
157                        ),
158                    )
159                    .unwrap();
160            })
161            .unwrap();
162        }
163
164        self.texture_store.get_optional_ref()
165    }
166
167    /// Overrides the `handle_event` function, which handles the mouse button, widget entering and
168    /// exiting events.
169    fn handle_event(&mut self, event: PushrodEvent) -> Option<PushrodEvent> {
170        match event {
171            WidgetMouseEntered { .. } => {
172                if self.properties.get_bool(PROPERTY_BUTTON_ACTIVE) {
173                    self.set_hovered();
174                }
175
176                self.properties.set_bool(PROPERTY_BUTTON_IN_BOUNDS);
177            }
178            WidgetMouseExited { .. } => {
179                if self.properties.get_bool(PROPERTY_BUTTON_ACTIVE) {
180                    self.set_unhovered();
181                }
182
183                self.properties.delete(PROPERTY_BUTTON_IN_BOUNDS);
184            }
185            MouseButton {
186                widget_id,
187                button,
188                state,
189            } => {
190                let current_widget_id = self.properties.get_value(PROPERTY_ID);
191
192                if button == 1 {
193                    if state {
194                        self.set_hovered();
195                        self.properties.set_bool(PROPERTY_BUTTON_ACTIVE);
196                        self.properties.set_bool(PROPERTY_BUTTON_ORIGINATED);
197                    } else {
198                        let had_bounds = self.properties.get_bool(PROPERTY_BUTTON_ACTIVE);
199
200                        self.set_unhovered();
201                        self.properties.delete(PROPERTY_BUTTON_ACTIVE);
202
203                        if self.properties.get_bool(PROPERTY_BUTTON_IN_BOUNDS)
204                            && had_bounds
205                            && self.properties.get_bool(PROPERTY_BUTTON_ORIGINATED)
206                            && current_widget_id as u32 == widget_id
207                        {
208                            self.properties.delete(PROPERTY_BUTTON_ORIGINATED);
209
210                            self.toggle_selected();
211
212                            return Some(WidgetSelected {
213                                widget_id: current_widget_id as u32,
214                                state: self.is_selected(),
215                            });
216                        }
217
218                        self.properties.delete(PROPERTY_BUTTON_ORIGINATED);
219                    }
220                }
221            }
222            DrawFrame { .. } => {}
223            _ => eprintln!("ButtonWidget Event: {:?}", event),
224        }
225
226        None
227    }
228}