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.
rust_widgets — Pure Rust GUI Library
A cross-platform GUI library written in pure Rust that paints every control itself. There is no
NSButton, no gtk_button_new, no android.widget.Button — a test greps the source for those and
fails if one appears. The only CreateWindowExW in the crate is the one that creates the window.
It renders to a live window, to a PNG, or to SVG, and it runs where a GUI toolkit cannot: mini and
embedded need no OS at all.
| Self-drawn (this library) | Native controls | |
|---|---|---|
| Appearance | Identical on every OS | Differs per toolkit and per toolkit version |
| Controls | 181 kinds everywhere | Only the ones the toolkit ships |
| Dependencies | No GUI toolkit linked | GTK / AppKit / Win32 / Android SDK |
| Headless, embedded | Runs with no OS (mini, SVG) |
Not possible |
| Tests | Byte-reproducible SVG and pixel snapshots | Needs a real display |
The backend keeps only what genuinely belongs to the operating system: window creation, the event
loop, input translation, and services such as IME, clipboard, file dialogs and DPI. If a backend
cannot even offer a surface, the library paints into an in-memory buffer and carries on. See
docs/ARCHITECTURE.md.
Install
[]
= "2.8.7"
Choose exactly one device profile. They are mutually exclusive: mini and embedded compile
parts of the crate out, so pairing one with desktop is not a lowest common denominator — it is a
build that cannot work.
= { = "2.8.7", = ["desktop"] } # default
= { = "2.8.7", = false, = ["tablet"] }
= { = "2.8.7", = false, = ["mobile"] }
= { = "2.8.7", = false, = ["embedded"] }
= { = "2.8.7", = false, = ["mini"] }
cargo check --features embeddedis wrong, and quietly so:desktopis a default feature, so that command switches on two mutually exclusive profiles at once. Pass--no-default-featureswhenever you select anything other thandesktop.
Hello, control
use ;
use WidgetStyle;
use Button;
use Widget; // brings `set_style` into scope
Window creation, the event loop, layout, theming, i18n and the C ABI each have their own chapter —
start at cookbook/en/src/README.md.
Rendering
| Backend | Use | Notes |
|---|---|---|
| Software rasteriser | Default everywhere | Anti-aliased, no GPU, runs headless |
| SVG | Snapshots, printing, vector output | Byte-reproducible; this is what snapshots/svg/ holds |
| wgpu | GPU acceleration | Explicit ladder: Vulkan/Metal/DX12/WebGPU → GL → CPU |
The renderer's text origin is the glyph box's top-left, not a baseline. Both backends honour that
one contract — the SVG emitter writes dominant-baseline="text-before-edge" — so a label measured
against one lands in the same place in the other. That is the whole reason snapshots/svg/ is usable
as review material instead of being a second renderer that quietly disagrees.
Sizing, layout, behaviour
A control is handed a rectangle by whatever placed it, and that rectangle is the area it may occupy — not how big it should draw. Keeping those two questions apart is what stops a switch from being painted as a 240x120 stadium:
implicit_size = max(floor, content + padding)
The max is load-bearing: the floor is a minimum tappable area, so a button labelled with 5 px of
text is still 64x40, not 64x18. widget::ControlMetrics exposes that formula and the helpers built on
it; widget::metrics::dimensions holds every control dimension, so one fact has one definition.
Layout asks each child how big it wants to be rather than being told in advance. A child answers
with a floor, a preferred value and a ceiling, because "how small may I be squeezed" and "how large
may I be stretched" cannot both be expressed by one number. fill is separate from the size on
purpose: a slider and a button may share a preferred size while only one should stretch across a form.
AxisHints::new normalises min <= pref <= max on construction, so an unnormalised hint cannot exist.
Behaviour is three contracts a control's shape does not communicate:
clickedrequires the release to land inside the control. A drag that starts on a button and ends off it emitscanceled, and only the primary button activates.pressedis continuous. Dragging off a held button clears it; dragging back restores it.- A focus ring means the user is on the keyboard, not merely that the control has focus.
FocusReason::draws_focus_ring()isfalsefor a pointer press — a ring under the cursor reads as a stuck highlight.
Theming, animation, disabled states
Colours resolve through one ladder — the control's explicit style, then the theme's resolved style for
that control, then a literal as the last resort — so an untouched control still follows an appearance
switch while a caller's deliberate colour always wins. WidgetState re-resolves that ladder on
hover/pressed/disabled, which is how a theme author's "button:hover" key reaches the paint code.
Animation is one stored-target PropertyDriver (a value that travels between 0.0 and 1.0, priced
by a MotionSlot tempo token) surfaced through Widget::tick / Widget::is_animating, so the frame
loop discovers a control owes frames without every control registering itself somewhere.
set_animating is deliberately not a thing you toggle: is_animating() is derived, so the control and
the frame loop cannot disagree about whether anything is moving.
Disabled is two moves, named separately because using one where the other belongs produces the opposite of the intended state:
| recedes by stepping toward | measured | |
|---|---|---|
disabled_ink_on — text, icons |
the surface's own contrast colour | 4.57–5.91:1 |
disabled_surface_near — fills, panels |
the page behind them | always less prominent than enabled |
Both weights come from dimensions::DISABLED_VEIL_ALPHA (0.55), the smallest value that clears the
4.5:1 body-text floor on both appearances. A half-transparent mid-grey has no direction: it darkens
over a light surface and lightens over a dark one, so "disabled" would read as more prominent on
whichever appearance was already hardest to read.
Text coverage
The default build draws Latin/ASCII only. No font data ships with the crate, so glyphs come from a
fixed 8x8 bitmap face covering U+0000–U+007F.
| Input | What the default build draws |
|---|---|
A, z, 7, ! |
the real glyph |
| CJK, Cyrillic, Arabic, emoji, anything else | a fallback glyph — a hollow box |
The label still lays out and the control still renders; only the glyph is wrong. Lines are ordered for painting by the Unicode bidirectional algorithm, so an Arabic run draws right to left — but ordering is not shape, and script-specific shaping (Arabic joining, Indic reordering) is not applied at all.
This library therefore does not implement Unicode text rendering and is not described as
multilingual. Coverage beyond Latin/ASCII is a separate opt-in axis that needs font data, asked for by
name, because a mini build must not carry glyphs it will never draw:
| feature | data | what it adds |
|---|---|---|
fonts-cjk-bitmap |
84 996 bytes, generated | 16x16 CJK bitmap face — Chinese, Japanese, Korean, no shaping needed |
fonts-cjk |
361 704 bytes, OFL subset | the same script as outlines: antialiased, any size |
fonts-vector-latin |
35 896 bytes, OFL subset | real advances and kerning |
fonts-complex |
70 576 bytes, OFL subset | Arabic joining, so بيت is a word and not three letters |
fonts-emoji-color |
1 602 492 bytes, OFL subset | colour emoji, 317 codepoints including the 26 regional indicators |
None is enabled by any profile. The vector and colour subsets are lazy-loaded from the binary's
read-only section and recorded, with upstream digests, in NOTICE.
Icons
Icon ships 69 IconName tokens. With icons (on by default) each token draws the real
Material Symbols outline; with the feature off it draws generated fallback geometry derived from
the same outlines, so the two paths cannot describe different shapes for one icon.
cargo build # the real outlines (default)
cargo build --no-default-features --features desktop # the derived fallback shapes
IconName::data() answers IconData, not Option, so adding a token without geometry is a compile
error rather than a blank spot on the screen. The token set is declared once in tools/icon_tokens.txt
and tools/gen_icon_names.py generates the type from it: adding an icon is a two-line edit plus a
generator run, not the six-file chore it used to be.
A host can register its own icons at runtime; the built-in set is a closed vocabulary, and
register_icon is the open extension point beside it:
use register_icon;
// SVG path data on the 960-unit design grid, negative y upward (the Material Symbols convention).
assert!;
icon.set_icon;
register_icon_on_grid takes a grid argument for a source on another scale (24 units, as Lucide and
Tabler use). The icon data is Apache-2.0 (Google LLC); attribution and the licence gate are in
NOTICE and tools/check_icon_licences.sh.
The feature is deliberately not in any device profile: mini and embedded are sized, and a
payload the caller did not ask for is wrong there. Ask for it — --features mini,icons — if you want it.
Verifying a change
for; do # the rest of the matrix
done
for; do # build-only, every profile
done
Do not reach for --all-features instead: desktop and mini are mutually exclusive and mini
switches the crate to no_std, so it is not a substitute for the matrix. The gate runner can report
host-limited checks as skipped; native GTK, Apple and Android tests run in platform-specific CI jobs.
The SVGs under snapshots/svg/ are one file per control per appearance. They are
committed and regenerated, and tools/check_svg_snapshots.sh fails byte-for-byte if a control's
drawing changed without them being updated — so a wrong-looking control appears as a reviewable diff,
and a control whose two files are identical is visibly theme-blind. control.md is the
same set laid out for reading, generated from the registry so a new control shows up without anyone
editing the gallery.
A snapshot is not always the right evidence: it shows the resting state, so a defect on a hover,
disabled or animating path is invisible to it. Those are pinned by pixel probes
(tests/disabled_text_contrast.rs, tests/disabled_surface_probe.rs, tests/m3_animation_probe.rs)
that assert the promised quantity — a contrast ratio, a painted height, a segment count — rather than a
proxy like a byte length.
Every gate is expected to be able to fail; the ones that matter were verified by reverse injection, reintroducing the defect and confirming the gate turns red. A gate that cannot fail is a decoration, and this repository does not collect those.
What the 181 controls cover
Text and inputs (button, toggle, entry fields, masked/OTP/date/time editors, search, tag input, keyboard, rich text, markdown, code editor, terminal); selection and display (lists, tables, virtualised grids with frozen columns, trees, chips, badges, ratings, progress, skeleton loaders); containers and chrome (tabs, splitters, dock panels, MDI, toolbars, menus, status bar, ribbon, toolbox); dialogs and overlays (message box, file/font/colour pickers, wizard, popover, tooltip, banner, toast, snackbar, bottom sheet); navigation; media; and a chart family Material has no equivalent for — candlestick, volume, depth, indicator, radar, heatmap, gauge, sparkline.
mini and embedded keep a reduced core. The exact set per profile is generated and gated, and listed
in docs/plans/platform_capability_matrix.md.
Platform notes
- Windows · macOS · Linux — full profile, real surface and event loop.
- Linux/Wayland — composition tests live in
tools/run_wayland_compositor_tests.sh. - iOS · Android — full profile; the JNI test APK builds with
tools/build_android_testapp.sh. - Web (wasm32) — WebGL/WebGPU surface.
cfg(target_arch = "wasm32")describes a sandbox, not an OS, and all OS knowledge stays insidesrc/platform/. - Embedded · mini — software rasteriser only, no OS services.
Anything that genuinely cannot be done yet says so: supports_custom_widgets(),
supports_web_engine() and has_real_engine() are runtime answers, not compile-time stubs pretending
to be features.
Language bindings
The C ABI in src/bindings/ exposes every control through 146 rw_* functions with a
capability-based property and event model. C, C++, Python, Java (JNI) and Node bindings are exercised
in CI, and the generated header is checked for drift by tools/check_abi.sh. Start with
cookbook/en/src/chapters/language-bindings.md.
Documentation
| Document | Contents |
|---|---|
cookbook/ |
The handbook — English, 简体中文, 繁體中文 |
docs/ARCHITECTURE.md |
Module layout and layering rules |
docs/MIGRATION_GUIDE.md |
Migrating from 1.x (native controls were removed in 2.0) |
CHANGELOG.md |
Release notes, with the evidence for each fix |
docs/plans/ |
Design plans and the engineering principles behind them |
docs/log/ |
Per-round engineering logs |
docs/reports/ |
Audit and quality reports |
Requirements
Rust 1.88+. The default build needs no system GUI libraries; on Linux the surface uses
Wayland/X11. Image codecs are pure Rust, so cross-compiling to Android, iOS or wasm needs no
pkg-config sysroot.
License
MIT — see LICENSE.
Support
Issues: GitHub Issues