Files
Oxide/docs/windowing.md
T
Homer Simpson 9eead719b0 Import Oxide engine (Stages 0–10) under MIT license
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>
2026-07-05 20:41:02 +02:00

5.4 KiB
Raw Blame History

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:

  1. creates the window and the GPU RenderContext,
  2. calls WindowApp::init once,
  3. then loops: forwards every raw window event to WindowApp::event, calls WindowApp::update once 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 to WindowApp so the engine's oxide_engine::app::App container (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 raw WindowEvent, 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 through ctx.input().
  • update(ctx) — once per frame, before the frame is cleared and presented. ctx.dt is the seconds elapsed since the previous frame (0.0 on 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) or Key::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 15 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.