Custom host
Connect Context and Renderer to a winit ApplicationHandler without the built-in runner.
Use a custom host when you already own the event loop, need runtime presentation
switching, or construct the context with custom fonts. When the application also owns
the GPU device, the swapchain and the frame (a game engine, an editor), draw into its
pass or texture instead: see Embedding in a wgpu host. To draw inside a process
you did not write, over the frames it presents, see Overlay in another process. The built-in runner
also accepts a font family through RunOptions::with_font_family. The complete runnable
reference is examples/integration.rs.
Native resources
Create the native window from winit's resumed callback. Store it in an Arc and
initialize a renderer for that window:
use std::sync::Arc;
use zaxis::{Context, PresentationMode, Renderer};
use zaxis::winit::{event_loop::ActiveEventLoop, window::Window};
fn initialize(event_loop: &ActiveEventLoop)
-> Result<(Arc<Window>, Context, Renderer), Box<dyn std::error::Error>>
{
let window = Arc::new(event_loop.create_window(Window::default_attributes())?);
let renderer = pollster::block_on(Renderer::new_with_presentation_mode(
Arc::clone(&window), PresentationMode::Vsync,
))?;
let mut context = Context::new();
context.set_viewport(window.inner_size(), window.scale_factor());
window.request_redraw();
Ok((window, context, renderer))
}To use your own fonts, build the context with Context::with_fonts(FontFamily::new(regular) .with_weight(FontWeight::BOLD, bold)) instead of Context::new(); Context::with_font(FontArc)
still takes a single regular face. See Replace the font.
This host uses pollster = "1.0.1" as a direct dependency. One Context is intended
for one native viewport. Keep model and context outside disposable native resources
if they must survive suspension.
Forward events
For matching native WindowId, forward events to Context::on_window_event before
your host-specific handling. Request native redraw when EventResponse::repaint is
true. Use consumed to decide whether other application handlers should process input.
consumed is decided inside on_input / on_window_event, before the next pass, from the key
claims, focus groups and hit regions the last finished pass published: a key a focused widget
claimed with Ui::keys, an arrow of a focus group and an action's stroke are consumed right
there, with no UI pass to wait for, so a host can withhold the key from the application beneath
at once (the browser key policy and z-hook do the same with the figure they get back).
Run the first pass before forwarding keys; until then nothing is claimed and the key is
returned unconsumed. Losing window focus (Focus(false)) releases every held key and drops
queued events and key owners, so a release that never arrives leaves nothing stuck. Contexts
are independent: separate windows keep separate claims, groups and queues. Counters of all
of this are in Context::input_stats().
After events and UI passes, apply the native cursor hint:
window.set_cursor(context.cursor_icon()). It uses the same clipped hit regions
as pointer input, including column resizing, window resize grips and text fields.
The built-in desktop runner updates it automatically and keeps resize cursors
active through pointer capture outside the control.
| Native event | Host action after forwarding |
|---|---|
Resized | Read actual inner size; update context viewport and call renderer.resize |
ScaleFactorChanged | Read actual inner size and scale; update viewport and renderer |
Occluded(true) | Suppress rendering and wait |
Occluded(false) | Request redraw |
CloseRequested | Exit the loop |
RedrawRequested | Run the UI and render, unless occluded or zero-sized |
On resize/DPI changes, refresh from the window itself:
let size = window.inner_size();
context.set_viewport(size, window.scale_factor());
renderer.resize(size)?;The event bridge updates DPI using its previous physical viewport estimate. Reading the native window afterwards establishes the actual viewport and surface size.
Build and present
Do not skip Context::run on an OS redraw because needs_repaint() is false.
The flag schedules future work; it does not mean the surface can skip presentation.
context.run(|context| {
zaxis::Window::new("Tools").show(context, |ui| {
ui.label("Ready");
});
});
let status = renderer.render(context.draw_data(), context.style().background)?;Context::run currently always returns true. It builds a complete UI pass, updates
the geometry cache, clears transient input after the callback, and exposes the frame
through draw_data(). Repaint requests made during the callback survive it.
Renderer outcomes
| Result | Host action |
|---|---|
Ok(Presented) | Current frame is presented |
Ok(Retry) | Record a retry deadline, e.g. now + 16 ms |
Ok(Dormant) | Wait for resize, unocclusion, or another relevant native event |
Err(DeviceLost(_)) | Recreate Renderer from the native Arc, preserve its presentation mode, request redraw |
Other Err | Stop or report the unrecoverable error |
The renderer recovers lost/outdated surfaces itself and then returns Retry. Device
loss requires a new renderer. Context and application state can be kept intact.
Sleep and retry backoff
Use the following scheduling rule from about_to_wait. Pass your current retry
deadline; clear it before attempting a redraw and replace it if that attempt returns
Retry.
use zaxis::Instant;
use zaxis::Context;
use zaxis::winit::event_loop::ControlFlow;
fn schedule(context: &Context, retry_at: Option<Instant>) -> (bool, ControlFlow) {
let now = Instant::now();
let redraw = retry_at.map_or(context.needs_repaint(), |time| time <= now);
let deadline = retry_at.or(context.next_repaint()).filter(|time| *time > now);
(redraw, deadline.map_or(ControlFlow::Wait, ControlFlow::WaitUntil))
}If redraw is true, call window.request_redraw(). Apply the returned control flow
to the active event loop. A pending surface retry takes precedence over immediate UI
repaint requests, preventing a busy retry loop. While the native viewport is occluded
or has zero width/height, use Wait and suppress rendering.
Suspend and resume
Drop renderer and native window on suspend where platform surface lifetime requires
it. Clear input with context.on_window_event(&WindowEvent::Focused(false)). On resume,
recreate native resources, update the retained context's viewport, and request redraw.
The integration example drops its entire State on suspend, including its context
and counters. For retained application state, use the ownership split in the
desktop runner, or retain model/context separately in your host.
Accessibility
A custom host connects the Context to assistive technology with an
accesskit_winit::Adapter, re-exported as zaxis::accesskit_winit (feature
accesskit, on by default; the integration example requires it). The adapter reports
through the event loop, so the loop carries its event type:
use zaxis::accesskit_winit::{Adapter, Event as AccessEvent, WindowEvent as AccessRequest};
let event_loop = EventLoop::<AccessEvent>::with_user_event().build()?;
let proxy = event_loop.create_proxy();-
Create the adapter before the window is shown. Create the window hidden, create the adapter, then show the window. Keep the adapter next to the window and drop it before the window.
let window = Arc::new(event_loop.create_window( Window::default_attributes().with_visible(false), )?); let access = Adapter::with_event_loop_proxy(event_loop, &window, proxy); window.set_visible(true); context.set_accessibility_title("My application"); -
Give the adapter every window event first, before
Context::on_window_event:access.process_event(&window, &event); let response = context.on_window_event(&event); -
Publish after each pass.
take_accessibility_updatereturnsNonewhen the tree did not change:context.run(|context| show_ui(context)); if let Some(update) = context.take_accessibility_update() { access.update_if_active(|| update); } -
Handle the three adapter events in
ApplicationHandler::user_event:let redraw = match event.window_event { AccessRequest::InitialTreeRequested => { context.set_accessibility_active(false); context.set_accessibility_active(true); true } AccessRequest::ActionRequested(request) => { context.on_accessibility_action(&request).repaint } AccessRequest::AccessibilityDeactivated => { context.set_accessibility_active(false); false } }; if redraw { window.request_redraw(); }
Nothing is collected until InitialTreeRequested; the frame after it publishes the
whole tree, and later frames only what changed. Turning collection off and on again on
that event makes the next update a complete tree, whatever was sent before. A request
takes effect on the next pass, like input. An update that is not taken is not lost:
the next one is then complete again. With several windows, route the event by
event.window_id.
AccessKit types appear in the public API only here: take_accessibility_update,
on_accessibility_action and the re-exports zaxis::accesskit and
zaxis::accesskit_winit. On Linux and the BSDs the adapter does nothing without the
accesskit_unix feature. See Accessibility.
Native IME
After building each frame with Context::run, call context.sync_ime(&window)
to enable IME for the focused TextEdit and position the composition/candidate window.
Forward WindowEvent::Ime events through on_window_event as with other input.
The built-in runner and integration example already perform this synchronization.