Skip to main content

pushrod/widgets/
tab_bar_widget.rs

1// Pushrod Widget Library
2// Tab Bar 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, SIZE_HEIGHT, SIZE_WIDTH};
21
22use sdl2::render::{Canvas, Texture, TextureQuery};
23use sdl2::video::Window;
24
25use crate::render::layout_cache::LayoutContainer;
26use crate::render::texture_cache::TextureCache;
27use crate::render::texture_store::TextureStore;
28use sdl2::pixels::Color;
29use sdl2::rect::{Point, Rect};
30use std::any::Any;
31use std::collections::HashMap;
32use std::path::Path;
33
34/// This is the callback type that is used when an `on_tab_selected` callback is triggered from this
35/// `Widget`.  Returns a number indicating the tab that was selected, starting from `0`.
36pub type OnTabSelectedCallbackType =
37    Option<Box<dyn FnMut(&mut TabBarWidget, &[WidgetContainer], &[LayoutContainer], u16)>>;
38
39/// This is the storage object for the `TabBarWidget`.  It stores the config, properties, callback registry, tab
40/// items, and other storage values for internal rendering.
41pub struct TabBarWidget {
42    config: WidgetConfig,
43    system_properties: HashMap<i32, String>,
44    callback_registry: CallbackRegistry,
45    texture_store: TextureStore,
46    tab_items: Vec<String>,
47    tab_widths: Vec<u32>,
48    on_tab_selected: OnTabSelectedCallbackType,
49    selected_item: i16,
50    hovered_item: i16,
51    in_bounds: bool,
52    calculated: bool,
53}
54
55/// This is the implementation of the `TabBarWidget`, which displays a series of tabs specified by the
56/// tab items in the constructor.  Tab bar items are automatically sized and rendered depending on the
57/// number of items specified.
58impl TabBarWidget {
59    /// Creates a new `TabBarWidget`, given the `x, y, w, h` coordinates, and the tab items to be shown
60    /// in the tab bar area.
61    pub fn new(points: Points, size: Size, tab_items: Vec<String>) -> Self {
62        Self {
63            config: WidgetConfig::new(points, size),
64            system_properties: HashMap::new(),
65            callback_registry: CallbackRegistry::new(),
66            texture_store: TextureStore::default(),
67            on_tab_selected: None,
68            tab_items,
69            tab_widths: vec![0],
70            selected_item: -1,
71            hovered_item: -1,
72            in_bounds: false,
73            calculated: false,
74        }
75    }
76
77    /// Assigns the callback closure that will be used when a tab is selected.
78    pub fn on_tab_selected<F>(&mut self, callback: F)
79    where
80        F: FnMut(&mut TabBarWidget, &[WidgetContainer], &[LayoutContainer], u16) + 'static,
81    {
82        self.on_tab_selected = Some(Box::new(callback));
83    }
84
85    /// Internal function that triggers the `on_tab_selected` callback.
86    fn call_tab_selected_callback(
87        &mut self,
88        widgets: &[WidgetContainer],
89        layouts: &[LayoutContainer],
90        tab: u16,
91    ) {
92        if let Some(mut cb) = self.on_tab_selected.take() {
93            cb(self, widgets, layouts, tab);
94            self.on_tab_selected = Some(cb);
95        }
96    }
97
98    /// Adjusts the widgets being displayed on screen.  Internal function.
99    fn adjust_widgets(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) {
100        let ttf_context = t.get_ttf_context();
101        let texture_creator = c.texture_creator();
102        let num_tabs = self.tab_items.len();
103        let mut font = ttf_context
104            .load_font(Path::new(&String::from("assets/OpenSans-Regular.ttf")), 10)
105            .unwrap();
106        let mut tab_widths = Vec::new();
107        let bounds = self.get_config().get_size(CONFIG_SIZE);
108
109        font.set_style(sdl2::ttf::FontStyle::NORMAL);
110
111        for i in 0..num_tabs {
112            let surface = font
113                .render(&self.tab_items[i])
114                .blended_wrapped(Color::RGB(0, 0, 0), bounds[SIZE_WIDTH])
115                .map_err(|e| e.to_string())
116                .unwrap();
117            let font_texture = texture_creator
118                .create_texture_from_surface(&surface)
119                .map_err(|e| e.to_string())
120                .unwrap();
121
122            let TextureQuery { width, .. } = font_texture.query();
123
124            tab_widths.push((width + 20) as u32);
125        }
126
127        self.tab_widths = tab_widths;
128        self.calculated = true;
129    }
130
131    /// Determines the tab item that matches the X coordinates of the mouse within the bounds of
132    /// the `Widget`.
133    fn find_hovered_item(&self, x: i32) -> i16 {
134        let mut selected_item = -1;
135        let mut start_x: i32 = 20;
136
137        for i in 0..self.tab_widths.len() {
138            if x >= start_x && x <= (start_x + self.tab_widths[i] as i32 + 30) {
139                selected_item = i as i16;
140                break;
141            }
142
143            start_x += self.tab_widths[i] as i32 + 31;
144        }
145
146        selected_item
147    }
148}
149
150/// This is the `Widget` implementation of the `TabBarWidget`.
151impl Widget for TabBarWidget {
152    fn draw(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) -> Option<&Texture> {
153        if !self.calculated {
154            self.adjust_widgets(c, t);
155        }
156
157        if self.get_config().invalidated() {
158            let bounds = self.get_config().get_size(CONFIG_SIZE);
159            let base_color = self.get_color(CONFIG_COLOR_BASE);
160
161            self.texture_store
162                .create_or_resize_texture(c, bounds[0] as u32, bounds[1] as u32);
163
164            let tab_widths = self.tab_widths.clone();
165            let tab_items = self.tab_items.clone();
166            let selected_tab = self.selected_item;
167            let hovered_tab = self.hovered_item;
168
169            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
170                texture.set_draw_color(base_color);
171                texture.clear();
172
173                let mut start_x: u32 = 20;
174
175                for i in 0..tab_widths.len() {
176                    let mut font_color = Color::RGB(0, 0, 0);
177
178                    if selected_tab == i as i16 {
179                        texture.set_draw_color(Color::RGB(128, 128, 128));
180                        font_color = Color::RGB(255, 255, 255);
181                    } else if hovered_tab == i as i16 {
182                        texture.set_draw_color(Color::RGB(192, 192, 192));
183                    } else {
184                        texture.set_draw_color(Color::RGB(224, 224, 224));
185                    }
186
187                    texture
188                        .fill_rect(Rect::new(
189                            start_x as i32,
190                            0,
191                            tab_widths[i] + 30,
192                            bounds[SIZE_HEIGHT],
193                        ))
194                        .unwrap();
195
196                    let (font_texture, font_width, font_height) = t.render_text(
197                        texture,
198                        String::from("assets/OpenSans-Regular.ttf"),
199                        14,
200                        sdl2::ttf::FontStyle::NORMAL,
201                        tab_items[i].clone(),
202                        font_color,
203                        bounds[SIZE_WIDTH],
204                    );
205
206                    texture
207                        .copy(
208                            &font_texture,
209                            None,
210                            Rect::new(
211                                start_x as i32 + 10,
212                                (bounds[SIZE_HEIGHT] / 2 - 10) as i32,
213                                font_width,
214                                font_height,
215                            ),
216                        )
217                        .unwrap();
218
219                    start_x += tab_widths[i] + 30 + 1;
220                }
221
222                texture.set_draw_color(Color::RGB(0, 0, 0));
223                texture
224                    .draw_line(
225                        Point::new(0, bounds[SIZE_HEIGHT] as i32 - 1),
226                        Point::new(bounds[SIZE_WIDTH] as i32, bounds[SIZE_HEIGHT] as i32 - 1),
227                    )
228                    .unwrap();
229            })
230            .unwrap();
231        }
232
233        self.texture_store.get_optional_ref()
234    }
235
236    /// When a mouse enters the bounds of the `Widget`, this function is triggered.  Overridden by
237    /// this `Widget`.
238    fn mouse_entered(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
239        self.in_bounds = true;
240    }
241
242    /// When a mouse exits the bounds of the `Widget`, this function is triggered.  Overidden by
243    /// this `Widget`.
244    fn mouse_exited(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
245        self.in_bounds = false;
246        self.hovered_item = -1;
247        self.set_invalidated(true);
248    }
249
250    /// Overrides the `mouse_moved` function, used to determine the position of the tab bar that is
251    /// currently under the mouse coordinates.
252    fn mouse_moved(
253        &mut self,
254        _widgets: &[WidgetContainer],
255        _layouts: &[LayoutContainer],
256        points: Points,
257    ) {
258        if self.calculated {
259            let origin = self.get_config().get_point(CONFIG_ORIGIN);
260            let true_x = points[POINT_X] - origin[POINT_X];
261            let previous_hovered_item = self.hovered_item;
262            let hovered_item = self.find_hovered_item(true_x);
263
264            self.hovered_item = hovered_item;
265
266            if previous_hovered_item != hovered_item {
267                self.set_invalidated(true);
268            }
269        }
270    }
271
272    /// Overrides the `button_clicked` function, used to determine when a mouse clicks inside the bounds
273    /// of a tab, triggering the `on_tab_selected` callback where appropriate.
274    fn button_clicked(
275        &mut self,
276        _widgets: &[WidgetContainer],
277        _layouts: &[LayoutContainer],
278        button: u8,
279        _clicks: u8,
280        state: bool,
281    ) {
282        if button == 1 && self.in_bounds && self.calculated && state && self.hovered_item != -1 {
283            self.selected_item = self.hovered_item;
284            self.set_invalidated(true);
285
286            if self.selected_item > -1 {
287                self.call_tab_selected_callback(_widgets, _layouts, self.selected_item as u16);
288            }
289        }
290    }
291
292    default_widget_functions!();
293    default_widget_properties!();
294    default_widget_callbacks!();
295}