Full project snapshot migrated to new Gitea remote without history: engine, editor, physics, script, examples, tests, docs, and assets. Relicensed from GPLv3 to MIT and updated repo URLs. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
5.4 KiB
Windowing & the Application Loop
Stage 2 reference for oxide_engine::window — opening a window, running the
event loop, and receiving raw input. For what happens inside a frame (GPU
setup, clearing, resize handling) see render-context.md.
Overview
The window module wraps winit so applications never
talk to the event loop directly. You implement the WindowApp trait, hand it
to run() together with a WindowConfig, and the engine:
- creates the window and the GPU
RenderContext, - calls
WindowApp::initonce, - then loops: forwards every raw window event to
WindowApp::event, callsWindowApp::updateonce per frame, and clears + presents the surface.
The loop runs in Poll mode (continuous rendering, as a game expects), not
event-driven Wait mode (as a desktop utility would use).
Stage 6 rename. This trait was originally
App. Stage 6 renamed it toWindowAppso the engine'soxide_engine::app::Appcontainer (scene, assets, scheduled systems) could live in the prelude unambiguously. The two cover different roles: this trait is the per-frame window/event handler; the container is engine state your handler typically wraps around.
Minimal application
use oxide_engine::prelude::*;
use oxide_engine::window::event::{Key, NamedKey, ElementState, WindowEvent};
#[derive(Default)]
struct MyApp;
impl WindowApp for MyApp {
fn init(&mut self, ctx: &mut AppCtx<'_>) {
ctx.set_clear_color(Color::rgb(0.39, 0.58, 0.93));
}
fn event(&mut self, ctx: &mut AppCtx<'_>, event: &WindowEvent) {
if let WindowEvent::KeyboardInput { event: key, .. } = event {
if key.state == ElementState::Pressed
&& key.logical_key == Key::Named(NamedKey::Escape)
{
ctx.request_exit();
}
}
}
fn update(&mut self, ctx: &mut AppCtx<'_>) {
let _seconds_since_last_frame = ctx.dt;
}
}
fn main() -> anyhow::Result<()> {
run(WindowConfig::default(), MyApp)
}
run() blocks the calling thread until the app exits — an OS requirement (the
event loop must own the main thread), not an engine choice.
WindowConfig
Initial window settings. All fields are plain data:
| Field | Default | Meaning |
|---|---|---|
title |
"Oxide" |
Window title |
width, height |
1280 × 720 | Initial inner size, logical pixels |
resizable |
true |
Whether the user can resize |
clear_color |
Color::BLACK |
Initial per-frame clear color |
The WindowApp trait
Three callbacks, all optional (empty default bodies):
init(ctx)— once, after the window and GPU exist, before the first frame. Set the title, clear color, load resources.event(ctx, event)— for every rawWindowEvent, including ones the engine also reacts to (close request, resize), so apps can observe everything. Stage 2 exposes events untranslated; the Stage-7 input system layers per-key edge detection and remappable named actions on top, surfaced throughctx.input().update(ctx)— once per frame, before the frame is cleared and presented.ctx.dtis the seconds elapsed since the previous frame (0.0on the first).
Per frame the order is: pending event calls → update → render.
AppCtx
Every callback receives &mut AppCtx, the engine state an app may touch:
| Member | Purpose |
|---|---|
dt |
Frame delta time in seconds (field) |
set_clear_color(color) / clear_color() |
Per-frame clear color; changes apply on the next frame |
size() |
Current surface size in physical pixels |
set_title(title) |
Change the window title |
request_exit() |
Leave the event loop after the current callback |
render() |
Direct access to the RenderContext |
input() |
The per-frame InputState snapshot |
Raw event types
oxide_engine::window::event re-exports the winit event vocabulary
(WindowEvent, KeyEvent, MouseButton, ElementState, KeyCode,
PhysicalKey, Key, NamedKey, ModifiersState, …) so applications don't
need their own winit dependency. The whole crates are also available as
oxide_engine::winit and oxide_engine::wgpu for anything not curated.
Two keyboard representations matter:
KeyEvent::physical_key(PhysicalKey::Code(KeyCode::KeyW)) — the physical key position, layout-independent. Use for game-style controls.KeyEvent::logical_key(Key::Named(NamedKey::Escape)orKey::Character(…)) — what the key means under the user's layout. Use for shortcuts and text.
Engine-handled events
The runner reacts to these before forwarding them:
| Event | Engine behavior |
|---|---|
CloseRequested |
Exits the loop (apps can't veto it in Stage 2) |
Resized |
Reconfigures the surface (see render-context.md) |
RedrawRequested |
Computes dt, calls update, renders the frame |
Errors during window/GPU creation or rendering are returned from run();
winit callbacks can't propagate Result, so the runner stashes the first
error and exits the loop.
Trying it
cargo run -p oxide-examples --bin hello_window
Keys 1–5 switch clear-color presets, Space cycles, Esc quits; average
FPS is logged once per second. The editor (cargo run -p oxide-editor) uses
the same infrastructure and quits with Ctrl+Q.