Keyboard shortcuts

Press or to navigate between chapters

Press ? to show this help

Press Esc to hide this help

Canvas & Graphics

Beyond the element tree, Kael gives you direct GPU drawing: an immediate-mode canvas, a vector path builder, gradients, backdrop blur, SVG, and optional Lottie playback. Everything renders through the same per-platform pipeline (Metal / DirectX 11 / Vulkan / browser WebGL2) with device-pixel snapping for crisp output at any DPI.

Visual escape-hatch ladder

When designing a graphics-heavy workflow or giving an AI agent a rendering task, choose the lowest rung that solves the problem:

NeedUse todayNotes
Product UI, dashboards, tool chromestyled div() / kael_uiBest memory and startup profile
Charts, timelines, waveform views, custom controlscanvas(...), paint_quad, paint_path, PathBuilderNative immediate-mode drawing
Game worlds, whiteboards, and large retained 2D surfacesPortableScene2d / portable_scene(...)Same bounded retained commands on native and browser renderers
Icons, diagrams, generated vector assetssvg() / PathBuilderKeep assets inspectable and themeable
Motion graphics and loaderslottie(...) with feature lottieDecodes off the UI path
Frosted or filtered subtreesbackdrop_blur(...) / effect_layer(...)Effect layers are partial CSS-filter coverage, not arbitrary shaders
Run a Kael canvas in a browserkael / kael_ui feature browserSame retained Scene through the WebGL2 renderer
External or hosted browser contentwebview(id, url)Native composition island on desktop; sandboxed iframe island in the wasm backend, with documented cross-origin limits
Golden-image or benchmark evidenceHeadlessRenderer / goldenOff-screen rendering is for tests and measurements
Public custom render target or custom shaderroadmapThe backend renderer is not yet a public arbitrary-shader API

The public graphics_capability_report() API exposes this same truth for readiness checks and agent planning. It reports full cross-backend coverage for styled elements, canvas, the portable retained 2D surface, paths, gradients, SVG, and Lottie; partial coverage for clip shapes, effect layers, and headless rendering; WebView coverage for browser graphics fallback; and roadmap status for public render targets/custom shaders.

Display density and text

Kael lays out in logical pixels and updates the backing scale whenever a native window or browser canvas moves between displays. On macOS, glyph masks use display-independent grayscale antialiasing and baselines are snapped to device pixels. Windows also uses grayscale DirectWrite coverage instead of caching panel-specific ClearType RGB stripes. This avoids stale-resolution text and RGB/BGR subpixel color fringing on scaled, rotated, or differently ordered external panels, while reducing glyph-atlas storage relative to four-channel subpixel masks.

Embed application fonts when typography is part of the product identity. kael_ui::init already registers its bundled Inter and JetBrains Mono faces. Font family, weight, layout scale, and backing resolution remain stable across screens; small rasterization differences between operating-system and browser text engines are still expected.

Canvas

For most custom graphics, use the immediate-mode canvas(size, draw) form. It records native draw commands for the current pass and lets generated code inspect composition before the commands flush into the window:

#![allow(unused)]
fn main() {
use kael::{canvas, point, px, size, stroke, Bounds};

canvas(size(px(320.0), px(180.0)), |draw, _window, _app| {
    draw.reserve_commands(6);
    draw.fill_rect(
        Bounds::new(point(px(0.0), px(0.0)), draw.size()),
        kael::rgb(0x1e1e1e),
    );
    draw.fill_rects([
        (
            Bounds::new(point(px(24.0), px(112.0)), size(px(48.0), px(40.0))),
            kael::rgb(0x3b82f6).into(),
        ),
        (
            Bounds::new(point(px(80.0), px(88.0)), size(px(48.0), px(64.0))),
            kael::rgb(0x60a5fa).into(),
        ),
    ]);
    draw.fill_circles([
        (point(px(232.0), px(64.0)), px(12.0), kael::rgb(0xf59e0b).into()),
        (point(px(268.0), px(64.0)), px(12.0), kael::rgb(0xfbbf24).into()),
    ]);
    draw.stroke_rect(
        Bounds::new(point(px(24.0), px(24.0)), size(px(120.0), px(64.0))),
        stroke(px(2.0), kael::rgb(0xffffff)),
    );

    tracing::info!(summary = draw.to_text(), "canvas draw");
})
}

For stable real-time workloads, call reserve_commands with the expected mixed command count before drawing, and use fill_rects or fill_circles for batches. The batch helpers reserve from the iterator's size hint. Circles are emitted as rounded quads, reusing the renderer's quad fast path instead of tessellating a vector path; this is the preferred route for particle systems, graph nodes, and game sprites that are geometrically circular.

DrawContext::to_text() reports queued command count, path count, quad count, filled/stroked quad counts, text count, image count, saved-state depth, and canvas size without logging text, image data, colors, or drawing coordinates. Use command_count(), path_count(), quad_count(), filled_quad_count(), stroked_quad_count(), text_count(), image_count(), state_stack_depth(), and is_empty() when agents or tests need to verify generated chart, timeline, waveform, canvas editor, or game HUD drawing.

For a scene that persists across frames, use PortableScene2d. It accepts bounded batches of solid or rounded quads, decoded-image sprites, pre-tessellated filled paths, and triangles, with affine transforms, rectangular clips, source-over opacity, typed limit failures, and transactional rollback. The default public ceilings are 100,000 commands/objects, 1,000,000 path vertices, 256 decoded image frames, 64 MiB of decoded image data, and 128 MiB of estimated retained payload. Static path transforms are baked when recorded rather than recomputed on every frame.

#![allow(unused)]
fn main() {
use std::sync::Arc;
use kael::{Bounds, PortableScene2d, PortableSolidQuad, point, portable_scene,
           px, rgb, size};

let mut scene = PortableScene2d::new();
scene.try_reserve_commands(100_000)?;
let quads = (0..100_000).map(|index| {
    let x = (index % 500) as f32 * 3.0;
    let y = (index / 500) as f32 * 3.0;
    PortableSolidQuad::new(
        Bounds::new(point(px(x), px(y)), size(px(2.0), px(2.0))),
        rgb(0x60a5fa),
    )
}).collect::<Vec<_>>();
scene.push_solid_quads(&quads)?;

let surface = portable_scene(size(px(1_500.0), px(600.0)), Arc::new(scene));
Ok::<_, kael::PortableSceneError>(surface)
}

This is the portable game/creative-app escape hatch, not raw GPU access. Custom blend modes, user shaders, compute, depth-tested 3D, and public renderer handles return or report Unsupported and remain explicit roadmap work.

canvas also supports the lower-level two-closure form — a prepaint pass (compute layout/state, returns a value) and a paint pass (draw into the bounds). Inside paint you call window.paint_quad and window.paint_path:

#![allow(unused)]
fn main() {
use kael::{canvas, fill, quad, px, rgb, Bounds, Pixels, Window, App};

canvas(
    move |_bounds: Bounds<Pixels>, _window: &mut Window, _app: &mut App| {
        // prepaint: return any state the paint pass needs
    },
    move |bounds: Bounds<Pixels>, _state, window: &mut Window, _app: &mut App| {
        window.paint_quad(fill(bounds, rgb(0x1e1e1e)));
        // window.paint_path(path, color);
    },
)
.size_full()
}

High-fidelity pointer input and retained scenes

Use on_pointer_event for one drawing path across mouse, touch, and pen. Browser events include stable pointer id/type, primary state, changed and held buttons, pressure, tangential pressure, tilt, twist, contact geometry, cancellation, and up to 256 coalesced samples. Pointer sequences remain routed to the element that received the down event, including independent simultaneous touches. Give a surface a stable .id(...) when it rerenders during a stroke or drag so its capture set persists across frames:

#![allow(unused)]
fn main() {
use kael::{div, InteractiveElement as _, PointerPhase};

div().id("drawing-surface").on_pointer_event(|event, _window, _app| {
    if matches!(event.phase, PointerPhase::Down | PointerPhase::Move) {
        for sample in event.stroke_samples() {
            tracing::trace!(
                pointer = event.pointer_id.get(),
                pressure = sample.pressure,
                tilt_x = sample.tilt_x,
                tilt_y = sample.tilt_y,
                "stroke sample"
            );
        }
    }
})
}

Existing mouse callbacks remain source compatible. Legacy desktop mouse streams are promoted to PointerInputEvent with a stable mouse id. Windows WM_POINTER provides simultaneous touch plus pen pressure, tilt, rotation, contact geometry, cancellation, and bounded chronological history. AppKit provides tablet identity/proximity, pressure, tangential pressure, tilt, rotation, buttons, and timestamps (but no macOS desktop touchscreen or contact ellipse). Wayland wl_touch and X11 XI2.2 provide simultaneous contacts and cancellation; Wayland also reports oriented contact geometry, while tablet pressure/tilt on Linux remains compositor/device-protocol dependent. CapabilityReport exposes these per-platform boundaries. Browser touch and pen expose the full Pointer Events shape.

For large whiteboards and game scenes, SpatialIndex uses a bounded spatial hash instead of scanning every entry. SceneGraph::hit_test and SceneGraph::visible_in_rect reuse a cached index while preserving topmost order. move_node patches only the moved entry's old and new spatial cells; structural changes, visibility edits through get_mut, and hierarchy changes retain the safe lazy full-rebuild fallback. Use spatial_incremental_update_count, spatial_full_rebuild_count, and last_spatial_candidate_count to verify dynamic-scene behavior without inspecting content. Pair culling with TileDamageTracker: invalidate old and new object bounds, repaint the sorted tiles returned by take, and retain every other tile. Pathological regions promote explicitly to TileDamage::Full instead of allocating without bound.

Window::export_frame_png returns real encoded PNG bytes at device-pixel resolution from browser WebGL2, macOS Metal, Windows Direct3D 11, and the Blade renderer used by Linux and optional macOS Blade builds. GPU readback validates dimensions, row pitch, channel order, alpha representation, and a 256 MiB allocation ceiling. It honors checked content protection and returns typed WindowCaptureError variants rather than silently dropping WebView overlays or live surfaces. Platform/compositor chrome and the system cursor are outside the scene. Blade capture renders into a bounded app-owned texture before copying to shared memory, so it does not depend on swapchain copy support; it returns a typed backend error if the selected surface format is not one of the supported 8-bit RGBA/BGRA formats. Capability support remains partial because hosted/live surfaces and operating-system chrome are intentionally outside the retained scene, not because the release gate substitutes a headless renderer.

Vector paths

Build filled or stroked paths with PathBuilder, then hand the result to window.paint_path:

#![allow(unused)]
fn main() {
use kael::{PathBuilder, point, px};

let mut builder = PathBuilder::fill();        // or PathBuilder::stroke(px(2.0))
builder.move_to(point(px(50.0), px(50.0)));
builder.line_to(point(px(130.0), px(50.0)));
builder.curve_to(point(px(130.0), px(130.0)), point(px(160.0), px(90.0))); // quadratic
builder.close();
let path = builder.build()?;
}

Segment methods: move_to, line_to, curve_to (quadratic Bézier), cubic_curve_to, arc_to, and close. Stroked builders also accept dash_array / dash_offset.

Gradients

Gradients are backgrounds you pass to .bg(...):

#![allow(unused)]
fn main() {
use kael::{linear_gradient, linear_color_stop, rgb};

div().bg(linear_gradient(
    45.0,
    linear_color_stop(rgb(0xff0080), 0.0),
    linear_color_stop(rgb(0x7928ca), 1.0),
))
}

Also available: multi_stop_linear_gradient(angle, &[stops]), radial_gradient(cx, cy, radius, &[stops]), and conic_gradient(cx, cy, angle_offset, &[stops]).

Backdrop blur & frosted glass

backdrop_blur blurs whatever is painted behind an element — combine it with a translucent background for a frosted-glass panel:

#![allow(unused)]
fn main() {
use kael::{px, rgba};

div()
    .backdrop_blur(px(20.0))
    .bg(rgba(0xffffff20))
    .rounded_xl()
}

Use cached(child) when a subtree is expensive but only depends on tracked state, deferred(child) when a subtree should keep layout in-tree but paint after ancestors, and effect_layer(child) when a subtree needs native CSS-style content blur or drop shadow. Use LayerStack with LayerOptions when the app needs native in-window modal, fullscreen, or anchored overlay composition instead of a WebView-hosted DOM overlay:

#![allow(unused)]
fn main() {
use kael::{cached, deferred, effect_layer, LayerOptions, px};

let preview = cached(render_preview()).id("preview-cache");
tracing::info!(summary = preview.to_text(), "cached subtree");

let overlay = deferred(effect_layer(render_panel()).content_blur(px(8.))).with_priority(120);
tracing::info!(summary = overlay.to_text(), "deferred overlay");

let modal = LayerOptions::modal();
tracing::info!(summary = modal.to_text(), "native layer options");
}

Inspect Cached::to_text(), Deferred::to_text(), and EffectLayer::to_text() in generated graphics, overlays, previews, and inspectors. Inspect LayerAnchor::to_text(), LayerOptions::to_text(), and LayerStack::to_text() before generated modal/popover/fullscreen layer flows. These helpers report child presence, explicit cache-key presence, draw priority/class, effect combination, blur class, shadow presence, placement, backdrop/dismissal policy, and active layer counts without logging cache ids, child contents, colors, coordinates, margins, blur radii, shadow offsets, shadow colors, or geometry.

SVG

svg() renders a vector asset; text_color fills monochrome SVGs and with_transformation applies rotation/scale:

#![allow(unused)]
fn main() {
use kael::{svg, Transformation, px, rgb, size};

let icon = svg()
    .path("icons/logo.svg")
    .with_transformation(Transformation::scale(size(1.25, 1.25)))
    .size(px(24.0))
    .text_color(rgb(0x2563eb));

tracing::info!(summary = icon.to_text(), "svg");
}

Use Svg::to_text(), Transformation::to_text(), has_path(), path_len_bytes(), has_transformation(), and transformation_key() when generated icon, diagram, or vector-asset UI needs diagnostics. Summaries report path presence/byte length and coarse transform kind without logging SVG paths, asset names, transform coordinates, scale values, or rotation values.

Images and Surfaces

img(source) renders URL, embedded, file-path, cached, decoded, and custom loader image sources with native object-fit behavior:

#![allow(unused)]
fn main() {
use kael::{img, ObjectFit, StyledImage};

let poster = img("https://cdn.example.com/poster.png")
    .object_fit(ObjectFit::Cover)
    .with_fallback(|| fallback_art().into_any_element());

tracing::info!(summary = poster.to_text(), "image");
}

Common application formats—including PNG, JPEG, GIF, WebP, TIFF, BMP, ICO, TGA, HDR, PNM, farbfeld, DDS, and QOI—are enabled without pulling parallel image-processing dependencies into every Kael application. Enable the heavier formats only when the product needs them:

[dependencies]
kael = { version = "0.4", features = ["image-avif", "image-exr"] }

AVIF decoding uses the native libdav1d library and pkg-config discovery. Install both through the platform package manager, or also provide Git, Meson, and Ninja so the binding can build libdav1d from source.

The built-in resource loader rejects empty or larger-than-64-MiB encoded sources, raster or SVG dimensions above 16,384 pixels per axis, more than 256 MiB of decoded frame data, and animations above 10,000 frames. HTTP failures do not retain response bodies or expose resource locations in error messages. Use a custom ImageSource loader that returns a validated RenderImage when a controlled workload intentionally needs a different budget.

Use ImageSource::to_text(), ImageStyle::to_text(), and Img::to_text() for asset-heavy generated UI. The helpers expose source kind, resource identifier byte length, grayscale state, object-fit key, loading/fallback hook presence, and explicit cache binding without logging URLs, file paths, embedded asset names, raw bytes, decoded image IDs, pixel dimensions, or child contents.

For native image caching, scope cache providers around the subtree that owns the asset working set:

#![allow(unused)]
fn main() {
use kael::{image_cache, lru, retain_all};

let cache = lru("gallery-cache", 64);
tracing::info!(summary = cache.to_text(), "image cache policy");

image_cache(cache).child(gallery)
}

Use retain_all(id) for bounded asset sets and lru(id, max_images) for feeds, galleries, maps, and other churning image sets. Inspect RetainAllImageCacheProvider::to_text(), LruImageCacheProvider::to_text(), ImageCacheElement::to_text(), RetainAllImageCache::to_text(), LruImageCache::to_text(), and ImageCacheItem::to_text() when generated UI or agents need cache policy, entry counts, loading/loaded/error counts, capacity, capacity class, and scoped child count without logging resource identifiers, element ids, image ids, image bytes, error details, or asset names.

surface(source) renders platform-native external image buffers, such as CoreVideo pixel buffers on macOS. Use SurfaceSource::to_text() and Surface::to_text() to report source class and object-fit key without logging pixel contents or dimensions.

Lottie

Enable Kael's lottie feature to add the native decoder and renderer:

[dependencies]
kael = { version = "0.4", features = ["lottie"] }

lottie() plays Lottie/dotLottie animations, decoding frames on a background thread so the UI stays responsive:

#![allow(unused)]
fn main() {
use kael::{lottie, LoopMode};

lottie("animations/loader.json")
    .autoplay()
    .loop_forever()            // or .loop_mode(LoopMode::Loop) / .ping_pong()
}

Builders: .autoplay(), .loop_forever(), .loop_mode(LoopMode), .ping_pong(), .object_fit(ObjectFit), .prefetch_frames(n), .with_loading(|| element), .with_fallback(|| element).

See the Astryx showcase's media and visual-effects sections for complete, runnable compositions.