Game Engine Integration
Rinch supports two complementary integration patterns for game engines and custom renderers:
- Embed API — Your game owns the window and GPU. Rinch provides UI as a Vello scene you composite on top.
- RenderSurface — Rinch owns the window. Your game/renderer submits frames (CPU pixels or GPU textures) into a DOM component.
RenderSurface (Recommended)
RenderSurface is a component that embeds external pixel content into rinch’s layout. Your renderer submits frames via a thread-safe SurfaceWriter (CPU pixels) or GpuTextureRegistrar (GPU textures), and rinch composites them during paint. Input events (mouse, keyboard) are routed back to your event handler.
This is the simpler pattern — rinch handles windowing, layout, and event dispatch. You just provide pixels and handle surface-local events.
Quick Start
use rinch::prelude::*;
#[component]
fn app() -> NodeHandle {
let surface = create_render_surface();
// Handle input events from the surface
surface.set_event_handler(|event| {
match event {
SurfaceEvent::MouseDown { x, y, button } => { /* handle click */ },
SurfaceEvent::MouseMove { x, y } => { /* handle hover */ },
SurfaceEvent::MouseWheel { delta_y, .. } => { /* handle zoom */ },
SurfaceEvent::KeyDown(key) => { /* handle keyboard */ },
_ => {}
}
});
// Submit frames from a worker thread
let writer = surface.writer();
std::thread::spawn(move || {
loop {
let pixels = render_frame(); // your renderer
writer.submit_frame(&pixels, width, height);
std::thread::sleep(std::time::Duration::from_millis(16));
}
});
rsx! {
div { style: "display: flex; height: 100%;",
div { style: "width: 200px;",
// Sidebar with rinch UI controls
Button { onclick: || do_something(), "Tool" }
}
RenderSurface { surface: Some(surface), style: "flex: 1;" }
}
}
}
fn main() {
run("My App", 1280, 720, app);
}
CPU Pixel Submission
SurfaceWriter is Send + Sync + Clone — safe to use from any thread:
#![allow(unused)]
fn main() {
let surface = create_render_surface();
let writer = surface.writer();
// From any thread:
let pixels: Vec<u8> = render_rgba(width, height);
writer.submit_frame(&pixels, width, height);
}
Pixels are RGBA8, row-major. The surface redraws automatically after submit_frame().
GPU Texture Compositing
For zero-copy compositing, use GpuTextureRegistrar to provide a wgpu::TextureView directly:
#![allow(unused)]
fn main() {
let surface = create_render_surface();
let registrar = surface.gpu_registrar();
// Get the shared wgpu Device via gpu_handle()
let gpu = gpu_handle().unwrap();
let device = &gpu.device;
let queue = &gpu.queue;
// Create your texture and render to it
let texture = device.create_texture(&wgpu::TextureDescriptor { /* ... */ });
// ... render into texture ...
// Register the texture view for compositing
let view = texture.create_view(&Default::default());
registrar.set_texture_source(view, width, height);
registrar.notify_frame_ready();
}
GpuTextureRegistrar is also Send + Sync + Clone. The texture must be created on the same wgpu::Device (available via gpu_handle()).
Layout Size
Query the surface’s current layout dimensions (set by CSS/Taffy) to match your render resolution:
#![allow(unused)]
fn main() {
let (w, h) = surface.layout_size();
// or from the registrar:
let (w, h) = registrar.layout_size();
}
Surface Events
Events are dispatched to the handler set via set_event_handler(). Coordinates are in logical pixels relative to the surface’s top-left corner.
| Event | Fields | Description |
|---|---|---|
MouseDown | x, y, button | Mouse button pressed |
MouseUp | x, y, button | Mouse button released |
MouseMove | x, y | Mouse moved over surface |
MouseWheel | x, y, delta_x, delta_y | Scroll wheel |
MouseEnter | x, y | Cursor entered surface |
MouseLeave | — | Cursor left surface |
KeyDown | SurfaceKeyData | Key pressed (when focused) |
KeyUp | SurfaceKeyData | Key released (when focused) |
TextInput | String | Text input (when focused) |
FocusGained | — | Surface received keyboard focus |
FocusLost | — | Surface lost keyboard focus |
SurfaceKeyData contains key, code, ctrl, shift, alt, meta.
API Reference: RenderSurface
| Function / Type | Description |
|---|---|
create_render_surface() | Create a new surface handle |
RenderSurfaceHandle | Main handle — set event handler, get writer/registrar |
SurfaceWriter | Thread-safe CPU pixel submission (Send + Sync + Clone) |
GpuTextureRegistrar | Thread-safe GPU texture registration (Send + Sync + Clone) |
RenderSurface | Component — use in RSX with surface: Some(handle) |
SurfaceEvent | Input event enum dispatched to handler |
RenderSurfaceHandle methods:
| Method | Description |
|---|---|
writer() | Get a SurfaceWriter for CPU pixel submission |
gpu_registrar() | Get a GpuTextureRegistrar for GPU texture compositing |
set_event_handler(handler) | Set input event callback (main thread closure) |
layout_size() | Get current (width, height) from CSS layout |
set_texture_source(view, w, h) | Set GPU texture directly (main thread only) |
has_texture_source() | Check if a GPU texture is registered |
id() | Surface ID |
viewport_name() | Internal viewport name |
Embed API
The embed API is for when your game owns the window and wgpu device. Rinch runs headless — you feed it platform events, it produces a Vello scene, and you render/composite it yourself.
Enable with: features = ["desktop"]
Quick Start
use rinch::prelude::*;
use rinch::embed::{RinchContext, RinchContextConfig, RinchOverlayRenderer};
#[component]
fn game_hud() -> NodeHandle {
let health = Signal::new(100);
rsx! {
div { style: "position: absolute; top: 10px; left: 10px;",
Text { size: "lg", color: "white", {|| format!("HP: {}", health.get())} }
}
}
}
fn main() {
// Your game creates the window and wgpu device
let (device, queue, surface, window) = my_engine::init();
// Create rinch UI context
let mut ctx = RinchContext::new(
RinchContextConfig {
width: 1280,
height: 720,
scale_factor: window.scale_factor(),
theme: None,
},
game_hud,
);
// Create overlay renderer from your device
let mut overlay = RinchOverlayRenderer::new(
&device, 1280, 720, wgpu::TextureFormat::Rgba8Unorm,
);
// Game loop
loop {
let events = collect_platform_events(&window);
let actions = ctx.update(&events);
for action in &actions {
match action {
AppAction::SetCursor(cursor) => { /* set cursor */ },
AppAction::Exit => return,
_ => {}
}
}
game.render(&device, &queue);
let ui_texture = overlay.render(&device, &queue, ctx.scene());
composite(&device, &queue, &surface, game_texture, ui_texture);
}
}
RinchContext
RinchContext is the main handle. Create it once, call update() each frame.
Input Routing
For HUD overlays, use wants_mouse and wants_keyboard to decide whether input goes to the UI or the game:
#![allow(unused)]
fn main() {
if ctx.wants_mouse(mouse_x, mouse_y) {
ctx.update(&[mouse_event]); // UI element under cursor
} else {
game.handle_mouse(mouse_x, mouse_y); // game content
}
if ctx.wants_keyboard() {
ctx.update(&[key_event]); // text input focused
} else {
game.handle_key(key); // game shortcuts
}
}
Split Layout (Viewport Hole)
Use GameViewport to mark a transparent region where the game renders:
#![allow(unused)]
fn main() {
use rinch::embed::GameViewport;
#[component]
fn editor_ui() -> NodeHandle {
rsx! {
div { style: "display: flex; flex-direction: column; height: 100%;",
div { class: "toolbar",
Button { onclick: || save(), "Save" }
}
div { style: "display: flex; flex: 1;",
div { style: "width: 200px;", /* side panel */ }
GameViewport { name: "main", style: "flex: 1;" }
}
}
}
}
}
Query the viewport rect to set your game’s render region:
#![allow(unused)]
fn main() {
if let Some(rect) = ctx.viewport_rect("main") {
game.set_viewport(rect.x, rect.y, rect.width, rect.height);
}
}
Resize and Scale Factor
#![allow(unused)]
fn main() {
ctx.resize(new_width, new_height);
overlay.resize(&device, new_width, new_height);
ctx.set_scale_factor(window.scale_factor());
}
Platform Events
Translate your engine’s events to rinch_platform::PlatformEvent:
#![allow(unused)]
fn main() {
use rinch_platform::{PlatformEvent, MouseButton, KeyCode, Modifiers};
PlatformEvent::MouseMove { x: 100.0, y: 200.0 }
PlatformEvent::MouseDown { x: 100.0, y: 200.0, button: MouseButton::Left }
PlatformEvent::MouseUp { x: 100.0, y: 200.0, button: MouseButton::Left }
PlatformEvent::MouseWheel { x: 100.0, y: 200.0, delta_x: 0.0, delta_y: -30.0 }
PlatformEvent::KeyDown {
key: KeyCode::KeyA,
text: Some("a".into()),
modifiers: Modifiers::default(),
}
PlatformEvent::Resized { width: 1920, height: 1080 }
}
API Reference: Embed
RinchContext:
| Method | Description |
|---|---|
new(config, component) | Create and mount a rinch UI |
update(&events) -> Vec<AppAction> | Process events, update layout, return actions |
scene() -> &Scene | Get the Vello scene (lazy rebuild) |
resize(w, h) | Notify of window resize (physical pixels) |
set_scale_factor(scale) | Update DPI scale factor |
viewport_rect(name) -> Option<LayoutRect> | Query a GameViewport’s computed rect |
wants_mouse(x, y) -> bool | True if point hits UI (not viewport hole) |
wants_keyboard() -> bool | True if a text input is focused |
needs_update() -> bool | True if UI needs repaint |
register_font(data) | Register font data for text rendering |
app() / app_mut() | Access the underlying RinchApp |
RinchOverlayRenderer:
| Method | Description |
|---|---|
new(device, w, h, format) | Create from game’s wgpu device |
render(device, queue, scene) -> TextureView | Render scene to texture |
resize(device, w, h) | Resize render target |
texture() | Get the underlying wgpu Texture |
RinchContextConfig:
| Field | Type | Description |
|---|---|---|
width | u32 | Initial viewport width (physical pixels) |
height | u32 | Initial viewport height (physical pixels) |
scale_factor | f64 | Display scale factor |
theme | Option<ThemeProviderProps> | Theme configuration |
LayoutRect:
| Field | Type | Description |
|---|---|---|
x | f32 | Absolute X position (logical pixels) |
y | f32 | Absolute Y position (logical pixels) |
width | f32 | Width (logical pixels) |
height | f32 | Height (logical pixels) |
Which Pattern to Use?
| Scenario | Use |
|---|---|
| Adding UI overlay to an existing game engine | Embed API — game keeps its window/GPU ownership |
| Building a tool with embedded viewports (e.g., level editor, paint app) | RenderSurface — rinch handles the window, you embed content |
| Terminal emulator, video player, or custom canvas inside a rinch app | RenderSurface — component-level integration |
| WASM game with HTML-based UI | Neither — use the browser-native DOM backend |
Examples
examples/game-embed/— Spinning cube with rinch HUD overlay (embed API)examples/render-surface-demo/— Painting app with canvas and navigator (RenderSurface)