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

145 lines
5.4 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](render-context.md).
## Overview
The window module wraps [`winit`](https://docs.rs/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`](render-context.md),
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`](modules.md) 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
```rust
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](input.md) 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`](render-context.md) |
| `input()` | The per-frame [`InputState`](input.md) 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](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
```sh
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`.