Skip to main content

nightshade_api/
app.rs

1//! The caller-owned main loop: [`open`] a window, then [`frame`] it yourself.
2
3use crate::runner::ApiState;
4use nightshade::prelude::*;
5use nightshade::run::pump::{PumpShell, pump_frame, pump_shell_new, pump_shell_ready};
6
7/// Window settings for [`open_with`]. Plain data with sensible defaults.
8pub struct Window {
9    pub title: String,
10    pub size: Option<(u32, u32)>,
11}
12
13impl Default for Window {
14    fn default() -> Self {
15        Self {
16            title: "nightshade".to_string(),
17            size: None,
18        }
19    }
20}
21
22/// A running engine you drive one [`frame`] at a time.
23///
24/// `world` is the real engine world, yours to read and mutate between frames.
25/// `frame_limit` stops the loop after that many frames, and is seeded from
26/// the `NIGHTSHADE_API_FRAMES` environment variable so examples double as
27/// smoke tests.
28pub struct App {
29    pub world: World,
30    pub frame_limit: Option<u32>,
31    frames_rendered: u32,
32    shell: PumpShell,
33}
34
35/// Opens a window with the default settings and returns once the renderer is
36/// ready and the standard scene defaults (sky, sun, grid, orbit camera) are in
37/// place.
38pub fn open() -> App {
39    open_with(Window::default())
40}
41
42/// Opens a window with the given settings. See [`open`].
43pub fn open_with(window: Window) -> App {
44    let state = ApiState::<()> {
45        setup: None,
46        update: None,
47        data: None,
48        clears_draw_pools: false,
49        frame_limit: None,
50        frames_rendered: 0,
51    };
52    let mut shell =
53        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
54    shell.context.world.resources.window.title = window.title;
55
56    let mut pumps_without_window = 0;
57    while !pump_shell_ready(&shell) {
58        if !pump_frame(&mut shell) {
59            break;
60        }
61        let window_exists = shell.context.world.resources.window.handle.is_some();
62        if window_exists && shell.context.initialized && shell.context.renderer.is_none() {
63            panic!("failed to create the renderer, see the log for the error");
64        }
65        if !window_exists {
66            pumps_without_window += 1;
67            if pumps_without_window > 10000 {
68                panic!("failed to create the window, see the log for the error");
69            }
70        }
71    }
72
73    if let Some((width, height)) = window.size
74        && let Some(handle) = shell.context.world.resources.window.handle.clone()
75        && let Some(size) = handle.request_inner_size(winit::dpi::PhysicalSize::new(width, height))
76        && size.width > 0
77        && size.height > 0
78    {
79        shell.context.world.resources.window.cached_viewport_size = Some((size.width, size.height));
80        if let Some(renderer) = shell.context.renderer.as_mut()
81            && let Err(error) = renderer.resize_surface(size.width, size.height)
82        {
83            tracing::error!("Failed to resize surface: {error}");
84        }
85    }
86
87    let mut world = World::default();
88    std::mem::swap(&mut world, &mut shell.context.world);
89
90    schedule_remove(
91        &mut world.resources.schedules.frame,
92        system_names::RESET_MOUSE,
93    );
94    schedule_remove(
95        &mut world.resources.schedules.frame,
96        system_names::RESET_KEYBOARD,
97    );
98    schedule_remove(
99        &mut world.resources.schedules.frame,
100        system_names::RESET_TOUCH,
101    );
102
103    let frame_limit = crate::runner::frame_limit_from_environment();
104
105    App {
106        world,
107        frame_limit,
108        frames_rendered: 0,
109        shell,
110    }
111}
112
113/// Pumps events and renders one frame. Returns false when the window closes,
114/// escape exits, or the frame limit is reached.
115///
116/// Everything drawn with the `draw_` functions since the previous call is
117/// visible for this frame and cleared afterward. Edge triggered input
118/// ([`key_pressed`](crate::prelude::key_pressed),
119/// [`mouse_clicked`](crate::prelude::mouse_clicked), the mouse deltas)
120/// reflects this frame's events and stays readable until the next call. The
121/// per frame input reset that normally runs inside the engine's frame
122/// schedule runs here instead, before pumping, so those flags survive the
123/// gap where your code runs.
124pub fn frame(app: &mut App) -> bool {
125    if let Some(limit) = app.frame_limit
126        && app.frames_rendered >= limit
127    {
128        return false;
129    }
130
131    nightshade::ecs::input::systems::reset_mouse_system(&mut app.world);
132    nightshade::ecs::input::systems::reset_keyboard_system(&mut app.world);
133    nightshade::ecs::input::systems::reset_touch_system(&mut app.world);
134
135    std::mem::swap(&mut app.world, &mut app.shell.context.world);
136    let alive = pump_frame(&mut app.shell);
137    std::mem::swap(&mut app.world, &mut app.shell.context.world);
138
139    crate::draw::clear_draw_pools(&mut app.world);
140    app.frames_rendered += 1;
141
142    alive
143}