Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Game Engine Integration

Rinch supports two complementary integration patterns for game engines and custom renderers:

  1. Embed API — Your game owns the window and GPU. Rinch provides UI as a Vello scene you composite on top.
  2. RenderSurface — Rinch owns the window. Your game/renderer submits frames (CPU pixels or GPU textures) into a DOM component.

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.

EventFieldsDescription
MouseDownx, y, buttonMouse button pressed
MouseUpx, y, buttonMouse button released
MouseMovex, yMouse moved over surface
MouseWheelx, y, delta_x, delta_yScroll wheel
MouseEnterx, yCursor entered surface
MouseLeaveCursor left surface
KeyDownSurfaceKeyDataKey pressed (when focused)
KeyUpSurfaceKeyDataKey released (when focused)
TextInputStringText input (when focused)
FocusGainedSurface received keyboard focus
FocusLostSurface lost keyboard focus

SurfaceKeyData contains key, code, ctrl, shift, alt, meta.

API Reference: RenderSurface

Function / TypeDescription
create_render_surface()Create a new surface handle
RenderSurfaceHandleMain handle — set event handler, get writer/registrar
SurfaceWriterThread-safe CPU pixel submission (Send + Sync + Clone)
GpuTextureRegistrarThread-safe GPU texture registration (Send + Sync + Clone)
RenderSurfaceComponent — use in RSX with surface: Some(handle)
SurfaceEventInput event enum dispatched to handler

RenderSurfaceHandle methods:

MethodDescription
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:

MethodDescription
new(config, component)Create and mount a rinch UI
update(&events) -> Vec<AppAction>Process events, update layout, return actions
scene() -> &SceneGet 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) -> boolTrue if point hits UI (not viewport hole)
wants_keyboard() -> boolTrue if a text input is focused
needs_update() -> boolTrue if UI needs repaint
register_font(data)Register font data for text rendering
app() / app_mut()Access the underlying RinchApp

RinchOverlayRenderer:

MethodDescription
new(device, w, h, format)Create from game’s wgpu device
render(device, queue, scene) -> TextureViewRender scene to texture
resize(device, w, h)Resize render target
texture()Get the underlying wgpu Texture

RinchContextConfig:

FieldTypeDescription
widthu32Initial viewport width (physical pixels)
heightu32Initial viewport height (physical pixels)
scale_factorf64Display scale factor
themeOption<ThemeProviderProps>Theme configuration

LayoutRect:

FieldTypeDescription
xf32Absolute X position (logical pixels)
yf32Absolute Y position (logical pixels)
widthf32Width (logical pixels)
heightf32Height (logical pixels)

Which Pattern to Use?

ScenarioUse
Adding UI overlay to an existing game engineEmbed 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 appRenderSurface — component-level integration
WASM game with HTML-based UINeither — 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)