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}