zaxis 0.1.0

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 eventHost action after forwarding
ResizedRead actual inner size; update context viewport and call renderer.resize
ScaleFactorChangedRead actual inner size and scale; update viewport and renderer
Occluded(true)Suppress rendering and wait
Occluded(false)Request redraw
CloseRequestedExit the loop
RedrawRequestedRun 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

ResultHost 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 ErrStop 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();
  1. 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");
  2. Give the adapter every window event first, before Context::on_window_event:

    access.process_event(&window, &event);
    let response = context.on_window_event(&event);
  3. Publish after each pass. take_accessibility_update returns None when the tree did not change:

    context.run(|context| show_ui(context));
    if let Some(update) = context.take_accessibility_update() {
        access.update_if_active(|| update);
    }
  4. 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.

Edit on GitHub

On this page