notan_app/
builder.rs

1#![allow(clippy::type_complexity)]
2
3use crate::assets::{AssetLoader, Assets};
4use crate::config::*;
5use crate::graphics::Graphics;
6use crate::handlers::{
7    AppCallback, AppHandler, DrawCallback, DrawHandler, EventCallback, EventHandler,
8    ExtensionHandler, InitCallback, InitHandler, PluginHandler, SetupCallback,
9};
10use crate::parsers::*;
11use crate::plugins::*;
12use crate::{App, Backend, BackendSystem, FrameState, GfxExtension, GfxRenderer};
13use indexmap::IndexMap;
14#[cfg(feature = "audio")]
15use notan_audio::Audio;
16use notan_core::events::{Event, EventIterator};
17use notan_core::mouse::MouseButton;
18use notan_input::internals::{
19    clear_keyboard, clear_mouse, process_keyboard_events, process_mouse_events,
20    process_touch_events,
21};
22
23pub use crate::handlers::SetupHandler;
24
25/// Configurations used at build time
26pub trait BuildConfig<S, B>
27where
28    B: Backend,
29{
30    /// Applies the configuration on the builder
31    fn apply(&self, builder: AppBuilder<S, B>) -> AppBuilder<S, B>;
32
33    /// This config will be applied before the app is initiated not when is set
34    fn late_evaluation(&self) -> bool {
35        false
36    }
37}
38
39/// The builder is charge of create and configure the application
40pub struct AppBuilder<S, B> {
41    setup_callback: SetupCallback<S>,
42    backend: B,
43
44    plugins: Plugins,
45    assets: Assets,
46
47    init_callback: Option<InitCallback<S>>,
48    update_callback: Option<AppCallback<S>>,
49    draw_callback: Option<DrawCallback<S>>,
50    event_callback: Option<EventCallback<S>>,
51
52    plugin_callbacks: Vec<Box<dyn FnOnce(&mut App, &mut Assets, &mut Graphics, &mut Plugins)>>,
53    extension_callbacks: Vec<Box<dyn FnOnce(&mut App, &mut Assets, &mut Graphics, &mut Plugins)>>,
54
55    late_config: Option<IndexMap<std::any::TypeId, Box<dyn BuildConfig<S, B>>>>,
56
57    use_touch_as_mouse: bool,
58
59    pub(crate) window: WindowConfig,
60}
61
62impl<S, B> AppBuilder<S, B>
63where
64    S: 'static,
65    B: BackendSystem + 'static,
66{
67    /// Creates a new instance of the builder
68    pub fn new<H, Params>(setup: H, backend: B) -> Self
69    where
70        H: SetupHandler<S, Params>,
71    {
72        let builder = AppBuilder {
73            backend,
74            plugins: Default::default(),
75            assets: Assets::new(),
76            setup_callback: setup.callback(),
77            init_callback: None,
78            update_callback: None,
79            draw_callback: None,
80            event_callback: None,
81            plugin_callbacks: vec![],
82            extension_callbacks: vec![],
83            window: Default::default(),
84            late_config: Some(Default::default()),
85            use_touch_as_mouse: true,
86        };
87
88        builder.default_loaders()
89    }
90
91    #[allow(unreachable_code)]
92    fn default_loaders(self) -> Self {
93        #[cfg(feature = "audio")]
94        {
95            self.add_loader(create_texture_parser())
96                .add_loader(create_audio_parser())
97        }
98
99        #[cfg(not(feature = "audio"))]
100        {
101            self.add_loader(create_texture_parser())
102        }
103    }
104
105    /// Converts touch events as mouse events
106    pub fn touch_as_mouse(mut self, enabled: bool) -> Self {
107        self.use_touch_as_mouse = enabled;
108        self
109    }
110
111    /// Applies a configuration
112    pub fn add_config<C>(mut self, config: C) -> Self
113    where
114        C: BuildConfig<S, B> + 'static,
115    {
116        if config.late_evaluation() {
117            if let Some(late_config) = &mut self.late_config {
118                let typ = std::any::TypeId::of::<C>();
119                late_config.insert(typ, Box::new(config));
120            }
121
122            self
123        } else {
124            config.apply(self)
125        }
126    }
127
128    /// Sets a callback used before the application loop starts running
129    pub fn initialize<H, Params>(mut self, handler: H) -> Self
130    where
131        H: InitHandler<S, Params>,
132    {
133        self.init_callback = Some(handler.callback());
134        self
135    }
136
137    /// Sets a callback used on each frame
138    pub fn update<H, Params>(mut self, handler: H) -> Self
139    where
140        H: AppHandler<S, Params>,
141    {
142        self.update_callback = Some(handler.callback());
143        self
144    }
145
146    /// Sets a callback executed after each update to draw
147    pub fn draw<H, Params>(mut self, handler: H) -> Self
148    where
149        H: DrawHandler<S, Params>,
150    {
151        self.draw_callback = Some(handler.callback());
152        self
153    }
154
155    /// Sets a callback to be used on each event
156    pub fn event<H, Params>(mut self, handler: H) -> Self
157    where
158        H: EventHandler<S, Params>,
159    {
160        self.event_callback = Some(handler.callback());
161        self
162    }
163
164    /// Sets a plugin that can alter or control the app
165    pub fn add_plugin<P: Plugin + 'static>(mut self, mut plugin: P) -> Self {
166        plugin.build(&mut self);
167        self.plugins.add(plugin);
168        self
169    }
170
171    /// Adds a plugin using parameters from the app
172    pub fn add_plugin_with<P, H, Params>(mut self, handler: H) -> Self
173    where
174        P: Plugin + 'static,
175        H: PluginHandler<P, Params> + 'static,
176    {
177        let cb =
178            move |app: &mut App, assets: &mut Assets, gfx: &mut Graphics, plugins: &mut Plugins| {
179                let p = handler.callback().exec(app, assets, gfx, plugins);
180                plugins.add(p);
181            };
182        self.plugin_callbacks.push(Box::new(cb));
183        self
184    }
185
186    /// Adds an extension using parameters from the app
187    pub fn add_graphic_ext<R, E, H, Params>(mut self, handler: H) -> Self
188    where
189        R: GfxRenderer,
190        E: GfxExtension<R> + 'static,
191        H: ExtensionHandler<R, E, Params> + 'static,
192    {
193        let cb =
194            move |app: &mut App, assets: &mut Assets, gfx: &mut Graphics, plugins: &mut Plugins| {
195                let e = handler.callback().exec(app, assets, gfx, plugins);
196                gfx.add_extension(e);
197            };
198        self.extension_callbacks.push(Box::new(cb));
199        self
200    }
201
202    /// Adds a new [AssetLoader]
203    pub fn add_loader(mut self, loader: AssetLoader) -> Self {
204        self.assets.add_loader(loader);
205        self
206    }
207
208    /// Creates and run the application
209    pub fn build(self) -> Result<(), String> {
210        let mut builder = self;
211        if let Some(late_config) = builder.late_config.take() {
212            for (_, config) in late_config {
213                builder = config.apply(builder);
214            }
215        }
216
217        let AppBuilder {
218            mut backend,
219            setup_callback,
220            mut plugins,
221            mut assets,
222
223            init_callback,
224            update_callback,
225            draw_callback,
226            event_callback,
227            mut plugin_callbacks,
228            mut extension_callbacks,
229            window,
230            use_touch_as_mouse,
231            ..
232        } = builder;
233
234        let initialize = backend.initialize(window)?;
235
236        let mut graphics = Graphics::new(backend.get_graphics_backend())?;
237
238        #[cfg(feature = "audio")]
239        let audio = Audio::new(backend.get_audio_backend())?;
240        #[cfg(feature = "audio")]
241        let mut app = App::new(Box::new(backend), audio);
242
243        #[cfg(not(feature = "audio"))]
244        let mut app = App::new(Box::new(backend));
245
246        app.window().set_touch_as_mouse(use_touch_as_mouse);
247
248        let (width, height) = app.window().size();
249        let win_dpi = app.window().dpi();
250        graphics.set_size(width, height);
251        graphics.set_dpi(win_dpi);
252
253        // add graphics extensions
254        extension_callbacks.reverse();
255        while let Some(cb) = extension_callbacks.pop() {
256            cb(&mut app, &mut assets, &mut graphics, &mut plugins);
257        }
258
259        // add plugins
260        plugin_callbacks.reverse();
261        while let Some(cb) = plugin_callbacks.pop() {
262            cb(&mut app, &mut assets, &mut graphics, &mut plugins);
263        }
264
265        // create the state
266        let mut state = setup_callback.exec(&mut app, &mut assets, &mut graphics, &mut plugins);
267
268        // init callback from plugins
269        let _ = plugins.init(&mut app, &mut assets, &mut graphics).map(|flow| match flow {
270            AppFlow::Next => Ok(()),
271            _ => Err(format!(
272                "Aborted application loop because a plugin returns on the init method AppFlow::{flow:?} instead of AppFlow::Next",
273            )),
274        })?;
275
276        // app init life event
277        if let Some(cb) = init_callback {
278            cb.exec(&mut app, &mut assets, &mut plugins, &mut state);
279        }
280
281        let mut current_touch_id: Option<u64> = None;
282
283        let mut first_loop = true;
284        if let Err(e) = initialize(app, state, move |app, mut state| {
285            // update system delta time and fps here
286            app.system_timer.update();
287
288            let win_size = app.window().size();
289            if graphics.size() != win_size {
290                let (width, height) = win_size;
291                graphics.set_size(width, height);
292            }
293
294            let win_dpi = app.window().dpi();
295            if (graphics.dpi() - win_dpi).abs() > f64::EPSILON {
296                graphics.set_dpi(win_dpi);
297            }
298
299            // Manage pre frame events
300            if let AppFlow::SkipFrame = plugins.pre_frame(app, &mut assets, &mut graphics)? {
301                return Ok(FrameState::Skip);
302            }
303
304            // update delta time and fps here
305            app.timer.update();
306
307            assets.tick((app, &mut graphics, &mut plugins, &mut state))?;
308
309            let delta = app.timer.delta_f32();
310
311            let use_touch_as_mouse = app.window().touch_as_mouse();
312
313            // Manage each event
314            let mut events = app.backend.events_iter();
315            while let Some(evt) = events.next() {
316                if use_touch_as_mouse {
317                    touch_as_mouse(&mut current_touch_id, &mut events, &evt);
318                }
319
320                process_keyboard_events(&mut app.keyboard, &evt, delta);
321                process_mouse_events(&mut app.mouse, &evt, delta);
322                process_touch_events(&mut app.touch, &evt, delta);
323
324                match plugins.event(app, &mut assets, &evt)? {
325                    AppFlow::Skip => {}
326                    AppFlow::Next => {
327                        if let Some(cb) = &event_callback {
328                            cb.exec(app, &mut assets, &mut plugins, state, evt);
329                        }
330                    }
331                    AppFlow::SkipFrame => return Ok(FrameState::Skip),
332                }
333            }
334
335            // Manage update callback
336            match plugins.update(app, &mut assets)? {
337                AppFlow::Skip => {}
338                AppFlow::Next => {
339                    if let Some(cb) = &update_callback {
340                        cb.exec(app, &mut assets, &mut plugins, state);
341                    }
342                }
343                AppFlow::SkipFrame => return Ok(FrameState::Skip),
344            }
345
346            // Manage draw callback
347            match plugins.draw(app, &mut assets, &mut graphics)? {
348                AppFlow::Skip => {}
349                AppFlow::Next => {
350                    if let Some(cb) = &draw_callback {
351                        cb.exec(app, &mut assets, &mut graphics, &mut plugins, state);
352                    }
353                }
354                AppFlow::SkipFrame => return Ok(FrameState::Skip),
355            }
356
357            // call next frame in lazy mode if user is pressing mouse or keyboard
358            if app.window().lazy_loop() {
359                let mouse_down = !app.mouse.down.is_empty();
360                let key_down = !app.keyboard.down.is_empty();
361                if mouse_down || key_down {
362                    app.window().request_frame();
363                }
364            }
365
366            clear_mouse(&mut app.mouse);
367            clear_keyboard(&mut app.keyboard);
368
369            // Manage post frame event
370            let _ = plugins.post_frame(app, &mut assets, &mut graphics)?;
371
372            // Clean possible dropped resources on the backend
373            graphics.clean();
374            #[cfg(feature = "audio")]
375            app.audio.clean();
376
377            // dispatch Event::Exit before close the app
378            if app.closed {
379                let evt = Event::Exit;
380                let _ = plugins.event(app, &mut assets, &evt)?;
381                if let Some(cb) = &event_callback {
382                    cb.exec(app, &mut assets, &mut plugins, state, evt);
383                }
384            }
385
386            // Using lazy loop we need to draw 2 frames at the beginning to avoid
387            // a blank window when the buffer is swapped
388            if !app.closed && app.window().lazy_loop() && first_loop {
389                first_loop = false;
390                app.window().request_frame();
391            }
392
393            Ok(FrameState::End)
394        }) {
395            log::error!("{}", e);
396        }
397
398        Ok(())
399    }
400}
401
402#[inline]
403fn touch_as_mouse(current_touch_id: &mut Option<u64>, events: &mut EventIterator, evt: &Event) {
404    match evt {
405        Event::TouchStart { id, x, y } => {
406            if current_touch_id.is_none() || current_touch_id.unwrap() == *id {
407                *current_touch_id = Some(*id);
408                events.push_front(Event::MouseDown {
409                    button: MouseButton::Left,
410                    x: *x as _,
411                    y: *y as _,
412                });
413            }
414        }
415        Event::TouchMove { id, x, y } => {
416            if let Some(last_id) = current_touch_id {
417                if last_id == id {
418                    events.push_front(Event::MouseMove {
419                        x: *x as _,
420                        y: *y as _,
421                    });
422                }
423            }
424        }
425        Event::TouchEnd { id, x, y } => {
426            if let Some(last_id) = current_touch_id {
427                if last_id == id {
428                    *current_touch_id = None;
429                    events.push_front(Event::MouseUp {
430                        button: MouseButton::Left,
431                        x: *x as _,
432                        y: *y as _,
433                    });
434                }
435            }
436        }
437        Event::TouchCancel { id, x, y } => {
438            if let Some(last_id) = current_touch_id {
439                if last_id == id {
440                    *current_touch_id = None;
441                    events.push_front(Event::MouseUp {
442                        button: MouseButton::Left,
443                        x: *x as _,
444                        y: *y as _,
445                    });
446                }
447            }
448        }
449        _ => {}
450    }
451}