Skip to main content

pushrod/widgets/
list_widget.rs

1// Pushrod Widget Library
2// List 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
21use sdl2::render::{Canvas, Texture, TextureQuery};
22use sdl2::video::Window;
23
24use crate::render::canvas_helper::CanvasHelper;
25use crate::render::layout_cache::LayoutContainer;
26use crate::render::texture_cache::TextureCache;
27use crate::render::texture_store::TextureStore;
28use crate::render::{Points, Size, POINT_Y, SIZE_WIDTH};
29use sdl2::pixels::Color;
30use sdl2::rect::Rect;
31use std::any::Any;
32use std::collections::HashMap;
33use std::path::Path;
34
35/// This is the callback type that is used when an `on_selected` callback is triggered from this
36/// `Widget`.
37pub type OnSelectedCallbackType =
38    Option<Box<dyn FnMut(&mut ListWidget, &[WidgetContainer], &[LayoutContainer], i32)>>;
39
40/// This is the storage object for the `ListWidget`.  It stores the config, properties, callback registry.
41pub struct ListWidget {
42    config: WidgetConfig,
43    system_properties: HashMap<i32, String>,
44    callback_registry: CallbackRegistry,
45    texture_store: TextureStore,
46    list_items: Vec<String>,
47    highlighted_item: i32,
48    selected_item: i32,
49    in_bounds: bool,
50    on_selected: OnSelectedCallbackType,
51}
52
53/// This is the implementation of the `ListWidget`, a control that displays a list of items that can be
54/// selected.
55impl ListWidget {
56    /// Creates a new `ListWidget` given the `x, y, w, h` coordinates.
57    pub fn new(points: Points, size: Size) -> Self {
58        Self {
59            config: WidgetConfig::new(points, size),
60            system_properties: HashMap::new(),
61            callback_registry: CallbackRegistry::new(),
62            texture_store: TextureStore::default(),
63            list_items: vec![],
64            highlighted_item: -1,
65            selected_item: -1,
66            in_bounds: false,
67            on_selected: None,
68        }
69    }
70
71    /// Adds a text item to the `ListWidget`.
72    pub fn add_item(&mut self, item: String) -> usize {
73        let item_size = self.list_items.len() + 1;
74
75        self.list_items.push(item);
76
77        item_size
78    }
79
80    /// Assigns the callback closure that will be used when the `Widget` changes value, based on a selected
81    /// item.
82    pub fn on_selected<F>(&mut self, callback: F)
83    where
84        F: FnMut(&mut ListWidget, &[WidgetContainer], &[LayoutContainer], i32) + 'static,
85    {
86        self.on_selected = Some(Box::new(callback));
87    }
88
89    /// Internal function that triggers the `on_selected` callback.  The selected item ID indicates the value
90    /// in the `ListWidget` that has been selected.  If the value is set to `-1`, it means the list items
91    /// have been de-selected.
92    fn call_selected_callback(&mut self, widgets: &[WidgetContainer], layouts: &[LayoutContainer]) {
93        if let Some(mut cb) = self.on_selected.take() {
94            cb(self, widgets, layouts, self.selected_item);
95            self.on_selected = Some(cb);
96        }
97    }
98}
99
100impl CanvasHelper for ListWidget {}
101
102/// This is the `Widget` implementation of the `ListWidget`.
103impl Widget for ListWidget {
104    /// Draws the `ListWidget` contents.
105    fn draw(&mut self, c: &mut Canvas<Window>, t: &mut TextureCache) -> Option<&Texture> {
106        if self.get_config().invalidated() {
107            let bounds = self.get_config().get_size(CONFIG_SIZE);
108
109            self.texture_store
110                .create_or_resize_texture(c, bounds[0] as u32, bounds[1] as u32);
111
112            let base_color = self.get_color(CONFIG_COLOR_BASE);
113            let hover_color = self.get_color(CONFIG_COLOR_HOVER);
114            let border_color = self.get_config().get_color(CONFIG_COLOR_BORDER);
115            let list_size = self.list_items.len();
116            let highlighted_item = self.highlighted_item;
117            let selected_item = self.selected_item;
118            let list_items = self.list_items.clone();
119
120            let ttf_context = t.get_ttf_context();
121            let texture_creator = c.texture_creator();
122            let mut font = ttf_context
123                .load_font(Path::new(&String::from("assets/OpenSans-Regular.ttf")), 16)
124                .unwrap();
125
126            font.set_style(sdl2::ttf::FontStyle::NORMAL);
127
128            c.with_texture_canvas(self.texture_store.get_mut_ref(), |texture| {
129                texture.set_draw_color(base_color);
130                texture.clear();
131
132                let list_height: u32 = 30;
133
134                for i in 0..list_size {
135                    let mut text_color = Color::RGB(0, 0, 0);
136                    let mut color = if highlighted_item == i as i32 {
137                        hover_color
138                    } else {
139                        Color::RGB(255, 255, 255)
140                    };
141
142                    if selected_item == i as i32 {
143                        color = Color::RGB(0, 0, 0);
144                        text_color = Color::RGB(255, 255, 255);
145                    }
146
147                    texture.set_draw_color(color);
148                    texture
149                        .fill_rect(Rect::new(
150                            0,
151                            (list_height * i as u32) as i32,
152                            bounds[SIZE_WIDTH],
153                            30,
154                        ))
155                        .unwrap();
156
157                    let surface = font
158                        .render(&list_items[i].clone())
159                        .blended_wrapped(text_color, bounds[SIZE_WIDTH])
160                        .map_err(|e| e.to_string())
161                        .unwrap();
162                    let font_texture = texture_creator
163                        .create_texture_from_surface(&surface)
164                        .map_err(|e| e.to_string())
165                        .unwrap();
166
167                    let TextureQuery { width, height, .. } = font_texture.query();
168                    let texture_y = (list_height * i as u32) as i32 + 3;
169                    let texture_x = 10;
170
171                    texture
172                        .copy(
173                            &font_texture,
174                            None,
175                            Rect::new(texture_x, texture_y, width, height),
176                        )
177                        .unwrap();
178                }
179
180                texture.set_draw_color(border_color);
181                texture
182                    .draw_rect(Rect::new(0, 0, bounds[0], bounds[1]))
183                    .unwrap();
184            })
185            .unwrap();
186        }
187
188        self.texture_store.get_optional_ref()
189    }
190
191    /// When a mouse enters the bounds of the `Widget`, this function is triggered.
192    fn mouse_entered(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
193        self.in_bounds = true;
194    }
195
196    /// When a mouse exits the bounds of the `Widget`, this function is triggered.
197    fn mouse_exited(&mut self, _widgets: &[WidgetContainer], _layouts: &[LayoutContainer]) {
198        self.in_bounds = false;
199        self.highlighted_item = -1;
200        self.get_config().set_invalidated(true);
201    }
202
203    /// When a mouse is moved in the bounds of this `Widget`, this function is triggered.
204    fn mouse_moved(
205        &mut self,
206        _widgets: &[WidgetContainer],
207        _layouts: &[LayoutContainer],
208        points: Points,
209    ) {
210        if self.in_bounds {
211            let position_y =
212                points[POINT_Y] - self.get_config().get_point(CONFIG_ORIGIN)[POINT_Y] as i32;
213            let previous_highlighted_item = self.highlighted_item;
214
215            self.highlighted_item = position_y / 30;
216
217            if self.highlighted_item >= self.list_items.len() as i32 {
218                self.highlighted_item = -1;
219            }
220
221            if self.highlighted_item != previous_highlighted_item {
222                self.get_config().set_invalidated(true);
223            }
224        }
225    }
226
227    /// Overrides the `button_clicked` callback to handle toggling.
228    fn button_clicked(
229        &mut self,
230        _widgets: &[WidgetContainer],
231        _layouts: &[LayoutContainer],
232        button: u8,
233        _clicks: u8,
234        state: bool,
235    ) {
236        if button == 1 && state {
237            self.selected_item = self.highlighted_item;
238            self.get_config().set_invalidated(true);
239
240            self.call_selected_callback(_widgets, _layouts);
241        }
242    }
243
244    default_widget_functions!();
245    default_widget_properties!();
246    default_widget_callbacks!();
247}