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    shell.context.world.resources.window.initial_size = window.size;
56
57    let mut pumps_without_window = 0;
58    while !pump_shell_ready(&shell) {
59        if !pump_frame(&mut shell) {
60            break;
61        }
62        let window_exists = shell.context.world.resources.window.handle.is_some();
63        if window_exists && shell.context.initialized && shell.context.renderer.is_none() {
64            panic!("failed to create the renderer, see the log for the error");
65        }
66        if !window_exists {
67            pumps_without_window += 1;
68            if pumps_without_window > 10000 {
69                panic!("failed to create the window, see the log for the error");
70            }
71        }
72    }
73
74    let mut world = World::default();
75    std::mem::swap(&mut world, &mut shell.context.world);
76
77    schedule_remove(
78        &mut world.resources.schedules.frame,
79        system_names::RESET_MOUSE,
80    );
81    schedule_remove(
82        &mut world.resources.schedules.frame,
83        system_names::RESET_KEYBOARD,
84    );
85    schedule_remove(
86        &mut world.resources.schedules.frame,
87        system_names::RESET_TOUCH,
88    );
89
90    let frame_limit = crate::runner::frame_limit_from_environment();
91
92    App {
93        world,
94        frame_limit,
95        frames_rendered: 0,
96        shell,
97    }
98}
99
100/// Builds a scene and saves it as a png, with no visible window. The window
101/// and renderer come up hidden, `setup` runs against the same defaults
102/// [`open`] applies, the scene settles for half a second of frames so
103/// streamed textures and lighting captures land, then the frame is captured
104/// to `path`.
105///
106/// ```ignore
107/// render_image(1920, 1080, "scene.png", |world| {
108///     spawn_floor(world, 10.0);
109///     let gem = spawn_torus(world, vec3(0.0, 1.5, 0.0));
110///     set_emissive(world, gem, [0.3, 0.8, 1.0], 8.0);
111/// });
112/// ```
113pub fn render_image(
114    width: u32,
115    height: u32,
116    path: impl Into<std::path::PathBuf>,
117    setup: impl FnOnce(&mut World),
118) {
119    let state = ApiState::<()> {
120        setup: None,
121        update: None,
122        data: None,
123        clears_draw_pools: false,
124        frame_limit: None,
125        frames_rendered: 0,
126    };
127    let mut shell =
128        pump_shell_new(Box::new(state)).expect("failed to create the engine event loop");
129    shell.context.world.resources.window.start_hidden = true;
130    shell.context.world.resources.window.initial_size = Some((width, height));
131
132    while !pump_shell_ready(&shell) {
133        if !pump_frame(&mut shell) {
134            return;
135        }
136    }
137
138    setup(&mut shell.context.world);
139
140    for _ in 0..30 {
141        tick_hidden(&mut shell);
142    }
143    crate::environment::screenshot(&mut shell.context.world, path.into());
144    for _ in 0..10 {
145        tick_hidden(&mut shell);
146    }
147}
148
149fn tick_hidden(shell: &mut PumpShell) {
150    let context = &mut shell.context;
151    let Some(renderer) = context.renderer.as_mut() else {
152        return;
153    };
154    if let Some(next_state) = tick_offscreen(&mut context.world, context.state.as_mut(), renderer) {
155        context.state = next_state;
156    }
157}
158
159/// Pumps events and renders one frame. Returns false when the window closes,
160/// escape exits, or the frame limit is reached.
161///
162/// Everything drawn with the `draw_` functions since the previous call is
163/// visible for this frame and cleared afterward. Edge triggered input
164/// ([`key_pressed`](crate::prelude::key_pressed),
165/// [`mouse_clicked`](crate::prelude::mouse_clicked), the mouse deltas)
166/// reflects this frame's events and stays readable until the next call. The
167/// per frame input reset that normally runs inside the engine's frame
168/// schedule runs here instead, before pumping, so those flags survive the
169/// gap where your code runs.
170pub fn frame(app: &mut App) -> bool {
171    if let Some(limit) = app.frame_limit
172        && app.frames_rendered >= limit
173    {
174        return false;
175    }
176
177    nightshade::ecs::input::systems::reset_mouse_system(&mut app.world);
178    nightshade::ecs::input::systems::reset_keyboard_system(&mut app.world);
179    nightshade::ecs::input::systems::reset_touch_system(&mut app.world);
180
181    std::mem::swap(&mut app.world, &mut app.shell.context.world);
182    let alive = pump_frame(&mut app.shell);
183    std::mem::swap(&mut app.world, &mut app.shell.context.world);
184
185    crate::draw::clear_draw_pools(&mut app.world);
186    app.frames_rendered += 1;
187
188    alive
189}