pushrod/render/
engine.rs

1// Pushrod Rendering Library
2// Extensible Core Library
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::event::Event;
17use sdl2::video::Window;
18use sdl2::Sdl;
19
20use crate::render::layout::Layout;
21use crate::render::layout_cache::LayoutCache;
22use crate::render::widget::{BaseWidget, Widget};
23use crate::render::widget_cache::WidgetCache;
24use crate::render::{make_points_origin, make_size};
25use sdl2::pixels::Color;
26use std::thread::sleep;
27use std::time::{Duration, SystemTime, UNIX_EPOCH};
28
29/// This function is called when when the application requests to quit.  It accepts the currently
30/// running engine, and the return value will indicate whether or not to quit.  Returning a `true`
31/// tells the engine to quit, `false` otherwise.  If this function is _not_ set, the application
32/// will quit when asked.
33pub type OnExitCallbackType = Option<Box<dyn FnMut(&mut Engine) -> bool>>;
34
35/// This is a storage container for the Pushrod event engine.
36pub struct Engine {
37    widget_cache: WidgetCache,
38    layout_cache: LayoutCache,
39    current_widget_id: i32,
40    frame_rate: u8,
41    running: bool,
42    on_exit: OnExitCallbackType,
43}
44
45/// This is the heart of the Pushrod event engine, and is what is used to drive the interaction
46/// between the user and your application.  The suggested method of use for this code is as
47/// follows:
48///
49/// ## Create a new object
50/// Use `Engine::new(w, h, frame_rate)` to instantiate a new object.
51///
52/// ## Set up the base
53/// Call `engine.setup(width, height)`, by passing the width and height of the window that is
54/// being created for the main application.  This is required, as a base widget is added to the
55/// render list.  This `BaseWidget` is considered the parent of the application screen.
56///
57/// ## Add Widgets to the Engine
58/// Call `add_widget(Box::new(widget), "name".to_string())` to add your `Widget` to the managed
59/// display list.
60///
61/// ## Call Run()
62/// Calling `run(sdl, window)` will manage the application screen after all widgets have been added
63/// to the display list.
64///
65/// That's all there is to it.  If you want to see more interactions on how the `Engine` is used in
66/// an application, check out the demo test code, and look at `rust-pushrod-chassis`.
67impl Engine {
68    /// Creates a new `Engine` object.  Sets the engine up with the bounds of the screen, and the desired
69    /// FPS rate, which must be provided at instantiation time.  This is in order to set up the
70    /// `BaseWidget` in the top-level of the `Engine`, so that it knows what area of the screen to
71    /// refresh when required as part of the draw cycle.
72    ///
73    /// **NOTE**: Setting a lower frame_rate will increase the efficiency of your API, however, it
74    /// could lower responsiveness if you have a very active UI.
75    pub fn new(w: u32, h: u32, frame_rate: u8) -> Self {
76        let base_widget = BaseWidget::new(make_points_origin(), make_size(w, h));
77        let mut cache = WidgetCache::new();
78
79        cache.add_widget(Box::new(base_widget), "base".to_string());
80
81        Self {
82            widget_cache: cache,
83            layout_cache: LayoutCache::new(),
84            current_widget_id: 0,
85            frame_rate,
86            running: true,
87            on_exit: None,
88        }
89    }
90
91    /// Adds a `Widget` to the display list.  `Widget`s are rendered in the order in which they were
92    /// created in the display list.
93    pub fn add_widget(&mut self, widget: Box<dyn Widget>, widget_name: String) -> i32 {
94        self.widget_cache.add_widget(widget, widget_name)
95    }
96
97    /// Adds a `Layout` to the `Layout` list.
98    pub fn add_layout(&mut self, layout: Box<dyn Layout>) -> i32 {
99        self.layout_cache.add_layout(layout)
100    }
101
102    /// Sets running flag: `false` shuts down the engine.
103    pub fn set_running(&mut self, state: bool) {
104        self.running = state;
105    }
106
107    /// Assigns the callback closure that will be used the application close/quit is triggered.
108    pub fn on_exit<F>(&mut self, callback: F)
109    where
110        F: FnMut(&mut Engine) -> bool + 'static,
111    {
112        self.on_exit = Some(Box::new(callback));
113    }
114
115    /// Internal function that triggers the `on_exit` callback.
116    fn call_exit_callback(&mut self) -> bool {
117        if let Some(mut cb) = self.on_exit.take() {
118            let return_value = cb(self);
119            self.on_exit = Some(cb);
120
121            return_value
122        } else {
123            eprintln!("No exit callback defined: returning true, application exiting.");
124            true
125        }
126    }
127
128    /// Main application run loop, controls interaction between the user and the application.
129    pub fn run(&mut self, sdl: Sdl, window: Window) {
130        let mut canvas = window
131            .into_canvas()
132            .target_texture()
133            .accelerated()
134            .build()
135            .unwrap();
136
137        canvas.set_draw_color(Color::RGB(255, 255, 255));
138        canvas.clear();
139        canvas.present();
140
141        let mut event_pump = sdl.event_pump().unwrap();
142        let fps_as_ms = (1000.0 / self.frame_rate as f64) as u128;
143
144        'running: loop {
145            let start = SystemTime::now()
146                .duration_since(UNIX_EPOCH)
147                .unwrap()
148                .as_millis();
149
150            for event in event_pump.poll_iter() {
151                match event {
152                    Event::MouseButtonDown {
153                        mouse_btn, clicks, ..
154                    } => {
155                        self.widget_cache.button_clicked(
156                            self.current_widget_id,
157                            mouse_btn as u8,
158                            clicks,
159                            true,
160                            self.layout_cache.get_layout_cache(),
161                        );
162                    }
163
164                    Event::MouseButtonUp {
165                        mouse_btn, clicks, ..
166                    } => {
167                        self.widget_cache.button_clicked(
168                            -1,
169                            mouse_btn as u8,
170                            clicks,
171                            false,
172                            self.layout_cache.get_layout_cache(),
173                        );
174                    }
175
176                    Event::MouseMotion { x, y, .. } => {
177                        let cur_widget_id = self.current_widget_id;
178
179                        self.current_widget_id = self.widget_cache.find_widget(x, y);
180
181                        if cur_widget_id != self.current_widget_id {
182                            self.widget_cache
183                                .mouse_exited(cur_widget_id, self.layout_cache.get_layout_cache());
184                            self.widget_cache.mouse_entered(
185                                self.current_widget_id,
186                                self.layout_cache.get_layout_cache(),
187                            );
188                        }
189
190                        self.widget_cache.mouse_moved(
191                            self.current_widget_id,
192                            vec![x, y],
193                            self.layout_cache.get_layout_cache(),
194                        );
195                    }
196
197                    Event::MouseWheel { x, y, .. } => {
198                        self.widget_cache.mouse_scrolled(
199                            self.current_widget_id,
200                            vec![x, y],
201                            self.layout_cache.get_layout_cache(),
202                        );
203                    }
204
205                    Event::Quit { .. } => {
206                        if self.call_exit_callback() {
207                            break 'running;
208                        }
209                    }
210
211                    remaining_event => {
212                        self.widget_cache.other_event(
213                            self.current_widget_id,
214                            remaining_event,
215                            self.layout_cache.get_layout_cache(),
216                        );
217                    }
218                }
219            }
220
221            self.widget_cache.tick(self.layout_cache.get_layout_cache());
222            self.layout_cache
223                .do_layout(self.widget_cache.borrow_cache());
224            self.widget_cache.draw_loop(&mut canvas);
225
226            canvas.present();
227
228            // This obeys thread sleep time.
229            let now = SystemTime::now()
230                .duration_since(UNIX_EPOCH)
231                .unwrap()
232                .as_millis();
233
234            if now - start < fps_as_ms {
235                let diff = fps_as_ms - (now - start);
236
237                sleep(Duration::from_millis(diff as u64));
238            }
239
240            if !self.running {
241                break 'running;
242            }
243        }
244    }
245}