Skip to content

Guide

Windows and async work #

knots.App owns the windows, the event loop, the renderer, and a queue for async work. This page describes its API.

App configuration #

var app = try knots.App.init(io, gpa, .{
    .window = .{
        .width = 1280,
        .height = 720,
        .title = "My app",
        .resizable = true,
        .min_size = .{ .width = 480, .height = 320 },
        .canvas_selector = "#canvas", // Browser builds only.
    },
    .renderer = .{ .present_mode = .fifo, .clear_color = .{ 0, 0, 0, 1 } },
    .ui = .{ .theme = ui.Theme.dark },
    .depth_buffer = false,
    .accessibility = true,
});
Field Description
window Size, title, resizable, min_size, max_size, and the browser canvas_selector.
renderer present_mode (default .fifo, which waits for vertical sync) and clear_color.
ui theme, fonts, state_ttls, and scroll_line_size.
depth_buffer Adds a depth buffer, for GPUCanvas code that needs one.
arena_reset_mode How the frame arena resets. Default: keep the capacity.
max_completions_recv The maximum async completions to run in one frame. Default: 64.
accessibility Turns the platform accessibility adapter on or off. Default: on.

io runs async work and the window backend. allocator holds the state that stays for the life of the app. Keep the App at one address until start returns, because windows keep pointers to it.

The View #

Each frame callback gets a *knots.View and a *ui.Frame:

fn frame(view: *knots.View, context: *ui.Frame) !void { ... }
  • view.app is the *App. Use it for the calls on this page.
  • view.id is the Viewport.Id of the window. .main is the first window.
  • view.renderer is a snapshot of the renderer: its configuration, the present modes that the surface supports, and the last reconfigure error.

To get your own state, put App in a field of your struct and use @fieldParentPtr. The tutorial shows this.

Multiple windows #

Open more native windows after start is called. Each window has its own frame callback, UI state, and renderer. All windows share one GPU device.

const inspector = try view.app.openWindow(view.id, .{
    .window = .{ .width = 520, .height = 320, .title = "Inspector" },
}, inspectorFrame);

// Later:
try view.app.closeWindow(inspector);
  • The first argument is the window to copy the renderer and UI configuration from. Set renderer or ui in the options to use a different configuration.
  • Closing the main window closes all windows and stops the app.
  • The browser has one canvas, so openWindow returns error.UnsupportedPlatform.

Async work #

A frame callback must return quickly. For slow work, such as file access or network requests, use dispatch. Knots runs the function concurrently with io. When it completes, Knots calls your completion callback on the main thread, inside a frame of the window that you name.

try view.app.dispatch(view.id, loadFile, .{ io, path }, onLoaded);

fn loadFile(io: std.Io, path: []const u8) anyerror![]u8 {
    // Runs concurrently. Do not touch UI state here.
    ...
}

fn onLoaded(view: *knots.View, frame: *ui.Frame, result: anyerror![]u8) !void {
    const self: *Self = @alignCast(@fieldParentPtr("app", view.app));
    self.contents = try result;
    frame.requestRedraw();
}
  • The type of the third argument of the callback is the return type of the function.
  • If the window closes before the work completes, Knots discards the result.
  • A browser build without worker threads returns error.ConcurrencyUnavailable. Handle this error.
  • app.concurrencyInFlight() returns the number of tasks that have not completed.

Wake the app from other code #

App sleeps when there are no events. If a different thread or a callback changes your data, call requestFrame:

try app.requestFrame(.main);

In a frame callback, use frame.requestRedraw() instead.

Change the renderer #

reconfigureRenderer changes the present mode or clear color. The change applies at the next frame. If the surface does not support the present mode, view.renderer.reconfigure_error holds error.UnsupportedPresentMode.

var config = view.renderer.config;
config.present_mode = .mailbox;
try view.app.reconfigureRenderer(view.id, config);

Screenshots #

requestReadback copies the next rendered frame of a window to memory. takeReadback returns the pixels when the copy is complete. The caller then owns the memory.

try app.requestReadback(.main, gpa);
// In a later frame:
if (try app.takeReadback(.main)) |taken| {
    var readback = taken;
    defer readback.deinit();
    // readback.width, .height, .format, .bytes_per_row, and .bytes hold the image.
    try savePixels(readback);
}

Developer tools #

knots.debug.DevTools is an overlay that shows frame time, window size, async tasks, and renderer settings. Render it at the end of your frame callback. The playground example shows how to connect it.