drawnui 0.1.0-preview.1

DrawnUI for Rust: a Skia-drawn UI engine, the same controls and contract as DrawnUI for .NET and React, on the desktop, mobile and the web.
docs.rs failed to build drawnui-0.1.0-preview.1
Please check the build logs for more information.
See Builds for ideas on how to fix a failed build, or Metadata for how to configure docs.rs builds.
If you believe this is docs.rs' fault, open an issue.
Visit the last successful build: drawnui-0.1.0-preview.6

DrawnUI for Rust

A Skia-drawn UI engine for Rust: layout, gestures, caching and controls you can subclass and paint. Same family as DrawnUI for .NET and DrawnUI for React. The crate is drawnui.

Status (2026-10-04)

Platform Drawing State
Windows Skia Ganesh on OpenGL (winit + glutin) done
Browser (wasm32-unknown-emscripten) Skia Ganesh on WebGL2 done, live demo at https://hellorust.drawnui.net
macOS Skia Ganesh on Metal (a CAMetalLayer, paced by the display) done, accepted on an M1 (dev/mac/RESULT-2.md)
Linux Skia Ganesh on OpenGL, the host of Windows (winit X11 / Wayland + glutin) built and run in WSL 2 (Ubuntu 22.04, WSLg): all tests pass; HelloRust on X11 (wheel, clicks, typing, clipboard, window icon, AT-SPI tree and actions, Orca reading the catalog and pages: solved-linux.md 13), on a headless Weston for Wayland. Where the driver ignores vsync (WSLg's software OpenGL) the host spaces frames one refresh apart itself; mouse moves that arrive in bursts (WSLg) fling the same distance as on Windows. In WSLg motion is not fully smooth: WSLg forwards frames to Windows on its own schedule, and its GPU path fails on this machine (OpenGL on the CPU). WSLg's own Wayland compositor crashes on the window: run on X11 there (solved-linux.md 5). A real Linux desktop (GPU, GNOME / KDE) not checked yet (dev/linux.sh)
iOS Skia Ganesh on Metal, as macOS (a CAMetalLayer on the UIKit view) simulator done (iPhone 17 Pro, iOS 26.5, checked by hand: touch, flings, safe area, rotation, background); a real iPhone next
Android Skia Ganesh on Vulkan by default (ash), OpenGL ES as the fallback and an option (glutin over EGL); winit GameActivity, APK built with Gradle emulator (API 36, 37, x86_64, 16 KB pages) and a phone (Blackview BV8800, Android 11, Mali-G57: Vulkan 1.1): touch, safe area, Back with the shell, background and back, a relaunched activity, assets from the APK, the soft keyboard (Gboard English and Pinyin), TalkBack (emulator and the phone: explore by touch, swipes, double tap); frames paced by Choreographer (91.6 FPS on the phone's 90 Hz panel); Vulkan vs GL on the phone in solved-android.md 24; Skia built for arm64, armv7, x86_64, x86, the APK carries arm64, armv7 and x86_64 (x86: a skia-bindings layout mismatch, solved-android.md 12) (dev/android.ps1)

Tested on Android (2026-10-04)

A phone: Blackview BV8800 (Android 11, Mali-G57 MC2 with Vulkan 1.1, 1080x2408 at scale 3, a 90 Hz panel), HelloRust release and debug builds:

Area What was checked How
Drawing Vulkan (the default) and OpenGL ES give the same pages; Vulkan vs OpenGL ES numbers below adb shell setprop debug.drawnui.gpu gl|vulkan, the host's frame stats in logcat
Frame pacing 91.6 FPS steady, every frame on a vsync (10.91 ms; the panel's real rate, from SurfaceFlinger's present times), animations stepped by the vsync time Choreographer frame callbacks; dumpsys SurfaceFlinger --latency
Safe area Content inside the status and navigation bars by default; Ui::mobile_fullscreen(true) draws under them, SkiaShell keeps its bars and pages out screenshots, both modes
Activity relaunch A configuration change Android relaunches the activity for (font scale) keeps the app drawing, with the new insets adb shell settings put system font_scale 1.15, then back
Soft keyboard Gboard into SkiaEditor: English typing and Backspace; Chinese Pinyin composing, committing 你好 and deleting it (DrawnUi.Net #231: the same result as MAUI) Gboard's on-screen keys tapped, the Pinyin keyboard added for the test and removed
System fonts Text the app's fonts lack (Chinese) drawn from Android's system fonts in the editor and rich labels the Pinyin text above
TalkBack Explore by touch reads cards and buttons, swipes move between them in reading order, double tap opens a card TalkBack on (settings put secure enabled_accessibility_services ...), checked by hand, settings restored
Back The system Back goes back in the shell, at the root the app goes to the background the Back key

Emulators (x86_64, 16 KB pages): API 36 with gesture and 3-button navigation (each switch relaunches the activity: the app keeps drawing, the insets follow), API 37 (TalkBack driven through the emulator's virtual touchscreen, adb emu event send; the soft keyboard; Back; background and back).

What the engine has (details, and what differs from DrawnUI for .NET and React, in PARITY.md):

  • Control tree with fluent builders, properties that invalidate what they change, measure / arrange / paint, render transforms, opacity, clipping, z-index, effects margin.
  • Every cache type of DrawnUI: Operations, OperationsFull, Image, GPU, ImageDoubleBuffered (bitmaps made on background threads on the desktop), ImageComposite, ImageCompositeGPU. A lost GPU context is recreated and what lived on it is drawn again.
  • Layouts: Absolute, Column, Row, Wrap, Grid, SkiaDecoratedGrid, RescalingLayout; templated lists with recycling (ItemsSource + ItemTemplate, RecyclingTemplate), virtualisation.
  • Controls: SkiaShape, SkiaLabel, SkiaRichLabel, TextSpan, SkiaButton, SkiaEditor (with IME), SkiaScroll and SkiaScrollBar, refresh, SkiaCarousel, SkiaShaderCarousel, SkiaDrawer, SkiaShell (pages, tabs, popups, modals, toasts), SkiaToggle, SkiaSwitch, SkiaCheckbox, SkiaRadioButton, SkiaSlider, SkiaProgress, SkiaBackdrop.
  • Media: SkiaImage (decoded off the frame thread, preload), SkiaImageTiles, SkiaSvg, SkiaGif, SkiaLottie, SkiaSprite and SkiaSpriteSet, SkSL shader effects and gl-transitions, SkMesh.
  • Input: tap, pan, fling, long press, hover, context menu, mouse wheel and touchpad (axis, glide), keyboard and focus, accessibility: an ARIA overlay in the browser, screen readers on the desktop through AccessKit (UI Automation, NSAccessibility, AT-SPI) with Tab and arrow navigation, a focus ring, drawn selectable text, slider and progress values a screen reader reads and adjusts, scrolls a screen reader pages (ACCESSIBILITY.md).
  • Animators: value, range, spring, ping-pong, pendulum, ripple, the *_to helpers, timers.
  • Tests: 699 headless tests (cargo test -p drawnui -p hellorust), many of them ports of the .NET engine's tests with the same numbers.

examples/hellorust is the DrawnUI Hello app: the same 20 pages as the React and .NET versions, on the desktop and in the browser from one source.

use drawnui::prelude::*;

#[derive(Default)]
struct App { count: i32 }

fn build(_app: &mut App) -> Build<SkiaLayout> {
    SkiaLayout::column().spacing(16).padding(24).children((
        SkiaLabel::new("").font_size(24).text_color(Color::WHITE)
            .observe(|me, app: &App| me.set_text(format!("Count {}", app.count))),
        SkiaButton::new("Tap me").on_tapped(|_me, app: &mut App, _cx| app.count += 1),
    ))
}

fn main() {
    drawnui::run("Counter", || Box::new(Ui::new(App::default(), build).font("Default", "assets/OpenSans-Regular.ttf")));
}

Crates

Crate What it is Version
drawnui the engine: controls, layouts, caches, gestures, animations, accessibility, the desktop, mobile and browser hosts 0.1.0-preview.1
drawnui-skia-safe Skia for Rust: rust-skia's skia-safe 0.153.3 plus SkMesh; drawnui re-exports it as drawnui::skia 0.153.3
drawnui-skia-bindings the native Skia and its C bindings under drawnui-skia-safe; downloads prebuilt Skia 0.153.3

An app depends on drawnui only; the two Skia crates come with it. Skia's API is drawnui::skia (the same as skia_safe, SkMesh included): never add skia-safe or skia-bindings to an app, a program can link one Skia only.

Getting started

A new app: copy templates/app (an empty app with an icon, assets, a web page and build scripts for the desktop, the browser and Android; it takes drawnui from crates.io), rename myapp, build. Or by hand, in a new project (cargo new myapp):

Cargo.toml: drawnui from crates.io. Skia comes with it: drawnui-skia-safe (rust-skia's skia-safe 0.153.3 plus SkMesh), used as drawnui::skia. Do not add skia-safe yourself: an app can link only one Skia. No [patch] is needed for the desktop and the browser.

[dependencies]
drawnui = "0.1.0-preview.1"

[profile.release]
lto = "thin"
panic = "abort"

.cargo/config.toml: Skia prebuilt from the drawnui release (skia-bindings fills in {key}: the commit the crate was packaged from, the target, the features) and the browser's link flags. Do not set FORCE_SKIA_BINARIES_DOWNLOAD: inside a crate it makes skia-bindings compile Skia from source. Without a prebuilt archive for your target and features (32-bit x86 Android, Linux / Windows arm64, Intel Macs, other features) Skia is compiled from source once: LLVM, Python and Ninja.

[env]
SKIA_BINARIES_URL = "https://github.com/DrawnUi/DrawnUi.Rust/releases/download/v0.1.0-preview.1/skia-binaries-{key}.tar.gz"

[target.wasm32-unknown-emscripten]
linker = "em++"
rustflags = [
    "-C", "link-arg=-sMAX_WEBGL_VERSION=2",
    "-C", "link-arg=-sMODULARIZE=1",
    "-C", "link-arg=-sEXPORT_NAME=createDrawnUi",
    "-C", "link-arg=-sEXPORTED_RUNTIME_METHODS=GL,HEAPU8,UTF8ToString",
    "-C", "link-arg=-sALLOW_MEMORY_GROWTH=1",
    # 1 MB stack and the overflow check: keep both (see "The wasm stack" below).
    "-C", "link-arg=-sSTACK_SIZE=1048576",
    "-C", "link-arg=-sSTACK_OVERFLOW_CHECK=1",
    "-C", "link-arg=-sERROR_ON_UNDEFINED_SYMBOLS=0",
]

[target.'cfg(target_os = "android")']
rustflags = ["-C", "link-arg=-Wl,-z,max-page-size=16384"]

The web page loads the build and drawnui_host.js, which must come from the same drawnui version (the template's web.ps1 / web.sh copy it from the dependency):

<canvas id="canvas" style="width:100vw;height:100dvh;display:block;touch-action:none"></canvas>
<script src="myapp.js"></script>
<script src="drawnui_host.js"></script>
<script>DrawnUi.start({ canvas: document.getElementById("canvas"), create: createDrawnUi });</script>

Per platform:

  • Windows, Linux, macOS: cargo run --release. Assets go next to the exe (the template's build.rs), icon.ico is the window and taskbar icon. Linux needs the X11 / Wayland and GL development packages. On Linux, rustc 1.94.1 can crash while printing the warnings of some dependency versions (a compiler bug); RUSTFLAGS=-Awarnings avoids it.
  • Browser: rustup target add wasm32-unknown-emscripten, emsdk 6 with its tools on PATH, then the template's web.ps1 / web.sh; serve dist/ over http.
  • Android: cargo-ndk, NDK 26; cargo ndk ... rustc --lib --release --crate-type cdylib makes the activity's library (android_main in the template's src/lib.rs), packed by Gradle with a GameActivity. Until two fixes are upstream, add the patched winit and android-activity (dev/winit-android, dev/android-activity-null-text) with [patch.crates-io] in the app's own Cargo.toml.
  • iOS: the steps of dev/ios (an .app bundle, signing for a device).
  • SkMesh (drawnui::skia::Mesh, Canvas::draw_mesh): in drawnui-skia-safe, so every app has it.

Release notes and the toolchain of each release: CHANGELOG.md. API docs: https://docs.rs/drawnui

Skills for AI agents

skills/drawnui-rust/SKILL.md teaches an agent to build apps with this crate (the browser build every app must copy, the wasm stack and its overflow check, the web host page, Windows build, app icon, assets, cache types). Save it under ~/.claude/skills/drawnui-rust/SKILL.md or fetch it from the demo site: https://hellorust.drawnui.net/skills/drawnui-rust/SKILL.md — llms.txt / llms-full.txt at the site root point to it. Pair it with the DrawnUI framework skill from https://drawnui.net/llms.txt.

dev\sync-skills.ps1 copies the maintainer's local skill into skills\, refusing anything that looks internal; the web build of an example with a web\llms.txt serves skills\ and writes llms-full.txt.

Build

Desktop (Windows, macOS):

cargo test -p drawnui -p hellorust                 # headless tests, no window
cargo run --release -p hellorust                   # the demo; the build copies its assets next to it
cd examples\bench; cargo run --release -- 20000 g0   # G0 workload: 20000 draw calls, nothing else
cd examples\bench; cargo run --release -- 2000 mesh  # 2000 animated SkMesh waves

skia-safe comes from a sibling checkout of rust-skia (../rust-skia, branch skmesh): upstream rust-skia plus the SkMesh patches in dev/rust-skia-skmesh until they are upstream. Skia is compiled from source once per machine and target, then exported as a rust-skia binaries archive that every later build unpacks (dev\skia-binaries.ps1 on Windows, -Web for the browser; the macOS steps are in dev/mac/TASK-1-opengl.md and TASK-2-metal.md, Metal adds the skia-safe metal feature there). LLVM (Windows), Xcode command line tools (macOS), Python and Ninja must be installed. Keep target dirs on short paths on Windows: Skia's build fails on long ones.

Browser:

dev\build.ps1 -Example hellorust -WebOnly   # or bench, images, hello

The browser build needs the wasm32-unknown-emscripten Rust target and emsdk 6 (EMSDK set, default C:\Dev\Tools\emsdk). Its Skia is compiled from source (once, about 6 minutes): the prebuilt wasm Skia from rust-skia cannot be linked with Rust 1.93 or newer (rust-skia issue 1287). Output lands in target\web\<example>, every file stamped with its content hash; serve that folder over http and open index.html (bench: ?n=20000&g0=1 for the G0 workload, ?n=2000&mesh=1 for the SkMesh waves, canvaskit.html?n=20000 for the G0 workload drawn through CanvasKit from JavaScript). Every benchmark shows its FPS on screen.

iOS simulator (on a Mac, Xcode installed, rustup target add aarch64-apple-ios-sim):

dev/ios/run-sim.sh            # builds HelloRust.app, boots a simulator, installs and launches it
dev/ios/run-sim.sh --release

--example dungeon builds another example. dev/ios/run-device.sh does the same on a connected iPhone (it signs the app). Its Skia is one more source build (target aarch64-apple-ios-sim, or aarch64-apple-ios for a phone, feature metal), exported as an archive like the others.

The start has no flash of another color: the launch screen takes the app's launch_color from dev/ios/apps.sh, and the view behind the canvas takes the app's Ui::background.

Every platform problem we hit and its fix, short, for anyone porting a Rust + Skia library: solved-win.md, solved-wasm.md, solved-mac.md, solved-ios.md, solved-android.md.

The wasm stack (read before changing the web link flags)

The browser build runs on a 1 MB main-thread stack (-sSTACK_SIZE=1048576 in .cargo/config.toml), with emscripten's stack cookie check (-sSTACK_OVERFLOW_CHECK=1). Keep both.

Emscripten's default stack is 64 KB. Layout and paint walk the control tree recursively, so a deep page needs more: HelloRust's Shapes, Layouts and Shaders pages needed about 90 KB. A wasm stack overflow is not caught by default: the stack runs into the memory below it and corrupts it silently. What that looked like (2026-10-01, desktop and headless tests all green):

  • memory allocation of N bytes failed, memory access out of bounds, Aborted(), RuntimeError: unreachable in the browser console, often inside asset delivery or a font load;
  • errors that move: a page that works alone fails after another page was opened, because the memory was already damaged;
  • nothing on the desktop, whose main thread has 1 MB.

Diagnosis: link once with -sSTACK_OVERFLOW_CHECK=2 (checks every stack pointer move; slow, for diagnosis only). An overflow then aborts at once with stack overflow (Attempt to set SP to ..., with stack limits [...]). If a page ever needs more than 1 MB, raise STACK_SIZE, or make the deep walk iterative; never remove the check. A change of these flags rebuilds the wasm Skia (about 6 minutes).

Benchmarks (phase 0, 2026-09-30, macOS, Android)

Same draw list, N rounded rectangles per frame, 1609x1163 canvas, Chrome on Windows, WebGL2:

Draw calls Rust + Skia in one wasm module CanvasKit 0.42 driven from JS
2,000 59.9 FPS, 2.8 ms CPU 60.0 FPS, 3.7 ms CPU
5,000 60.0 FPS, 5.1 ms CPU 59.8 FPS, 7.4 ms CPU
20,000 59.7 FPS, 12.5 ms CPU 47.8 FPS, 20.6 ms CPU

Windows desktop (Ganesh GL): 20,000 draw calls at 60 FPS, 10 to 12 ms CPU, frame interval p99 25 to 33 ms.

macOS (2026-10-02, MacBook Air M1, 60 Hz Retina, Ganesh on Metal): 20,000 draw calls at a steady 60 FPS, 10.3 ms CPU, frame interval p99 17.3 ms; 2,000 SkMesh waves 6.1 ms CPU (dev/mac/RESULT-2.md).

SkMesh waves (a triangle strip each, vertex program with a time uniform, one draw call each), Windows desktop, Ganesh GL, 1250x875: 200 waves 60 FPS at 1.1 ms CPU; 2,000 waves 60 FPS at 6.4 ms CPU; 20,000 waves are CPU-bound (about 3.2 us per mesh) and drop below 60.

Payload of the bench (it links only the Skia code it uses):

Build raw brotli
gl 3.93 MB 1.15 MB
gl + text layout (ICU, HarfBuzz) + SVG 7.39 MB 2.27 MB
CanvasKit 0.42 wasm, for scale 7.32 MB 2.28 MB

Android: Vulkan vs OpenGL ES (2026-10-04)

A phone, Blackview BV8800: Android 11, Mali-G57 MC2 (Vulkan 1.1), 1080x2408, a 90 Hz panel (one frame = 10.91 ms). The same release APK, the API switched with adb shell setprop debug.drawnui.gpu gl|vulkan; the host's frame stats from logcat (p50 of every 300-frame window). The bench scenes ran 4 times each, Vulkan and OpenGL ES alternating, 15 s per run.

In short: on heavy scenes Vulkan draws 21 to 35 % more frames; where both keep up with the 90 Hz panel, Vulkan needs 26 to 59 % less CPU per frame (about half on HelloRust's pages).

Scene Vulkan OpenGL ES Vulkan better by
20,000 draw calls (bench 20000,g0) 40.2 FPS, 24.8 ms a frame 33.3 FPS, 30.0 ms +21 % FPS
2,000 SkMesh waves (bench 2000,mesh) 57.2 FPS, 17.5 ms 42.4 FPS, 23.6 ms +35 % FPS
2,000 animated shapes, the whole bench scene 91.6 FPS, CPU 5.4 ms 91.6 FPS, CPU 7.3 ms -26 % CPU
HelloRust: Recycled cells, fling series 91.6 FPS, CPU 1.45 ms 91.6 FPS, CPU 2.71 ms -46 % CPU
HelloRust: Uneven cells, fling series 91.6 FPS, CPU 0.94 ms 91.6 FPS, CPU 2.32 ms -59 % CPU
HelloRust: Lottie & GIF 91.6 FPS, CPU 1.94 ms 91.6 FPS, CPU 3.67 ms -47 % CPU
HelloRust: Shaders (transitions, plasma, ripples) 91.6 FPS, CPU 1.60 ms 91.6 FPS, CPU 2.75 ms -42 % CPU

HelloRust rows: one run per API, optimized with debug assertions (the FPS label build), the average of its 300-frame windows. CPU = the frame's work on the app thread (recording and submitting the GPU commands). The heavy bench scenes are bound by it, which is where Vulkan's thinner driver shows. Vulkan is the default on Android; OpenGL ES stays as the fallback and an option (Ui::gpu_backend). Details: solved-android.md 23 to 25.

Layout

  • drawnui/ the engine crate. src/gpu.rs is the only file that names the Skia GPU engine.
  • drawnui/web/drawnui_host.js the browser host.
  • examples/hello/ a counter app; examples/hellorust/ the demo catalog; examples/bench/ the benchmark scene; examples/images/ a feed of photos. One source for both targets.
  • spikes/fluent/ the compile spike that fixed the builder and handler shapes.
  • dev/ build scripts.
  • PARITY.md what is ported from the .NET engine, what is skipped, what differs.