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.
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
*_tohelpers, 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 *;
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.
[]
= "0.1.0-preview.1"
[]
= "thin"
= "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.
[]
= "https://github.com/DrawnUi/DrawnUi.Rust/releases/download/v0.1.0-preview.1/skia-binaries-{key}.tar.gz"
[]
= "em++"
= [
"-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",
]
[]
= ["-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):
Per platform:
- Windows, Linux, macOS:
cargo run --release. Assets go next to the exe (the template'sbuild.rs),icon.icois 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=-Awarningsavoids it. - Browser:
rustup target add wasm32-unknown-emscripten, emsdk 6 with its tools on PATH, then the template'sweb.ps1/web.sh; servedist/over http. - Android: cargo-ndk, NDK 26;
cargo ndk ... rustc --lib --release --crate-type cdylibmakes the activity's library (android_mainin the template'ssrc/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 ownCargo.toml. - iOS: the steps of
dev/ios(an.appbundle, 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: unreachablein 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.rsis the only file that names the Skia GPU engine.drawnui/web/drawnui_host.jsthe 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.mdwhat is ported from the .NET engine, what is skipped, what differs.