rust_widgets 2.7.0

Pure Rust cross-platform native GUI library with hardware-adaptive rendering, 180 widgets, touch/gesture support, i18n, and SVG-pipeline-accurate output
docs.rs failed to build rust_widgets-2.7.0
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: rust_widgets-0.5.19

rust_widgets — Pure Rust GUI Library

A cross-platform GUI library written in pure Rust. It paints every control itself — there is no CreateWindowExW, NSButton, gtk_button_new or android.widget.Button anywhere in the crate — and it can render to a window, to a PNG, or to SVG. Desktop, tablet, mobile, embedded and a minimal mini profile are all supported.

Why self-drawn controls are worth the effort:

Self-drawn (this library) Native controls
Appearance Identical on every OS Differs per toolkit and version
Control count 180 kinds everywhere Only what the toolkit offers
Dependencies No GUI toolkit linked GTK / AppKit / Win32 / Android SDK
Headless / embedded Runs with no OS at all (mini, SVG) Impossible
Tests Pixel and SVG snapshots Needs a real display

A backend still owns the parts that genuinely belong to the operating system — window creation and the event loop, input translation, and platform services (IME, clipboard, file dialogs, DPI). If a backend cannot even supply a surface (a bare framebuffer, for instance), the library paints into an in-memory buffer instead. See docs/ARCHITECTURE.md.

Install

[dependencies]
rust_widgets = "2.7.0"

Pick exactly one device profile. They are mutually exclusive — mini and embedded compile parts of the crate out, so combining one with desktop is not a lowest common denominator, it is a broken build:

rust_widgets = { version = "2.7.0", features = ["desktop"] }                       # default
rust_widgets = { version = "2.7.0", default-features = false, features = ["tablet"] }
rust_widgets = { version = "2.7.0", default-features = false, features = ["mobile"] }
rust_widgets = { version = "2.7.0", default-features = false, features = ["embedded"] }
rust_widgets = { version = "2.7.0", default-features = false, features = ["mini"] }

cargo check --features embedded is wrong: desktop is a default feature, so that command enables two mutually exclusive profiles at once. Always pass --no-default-features when selecting any profile other than desktop.

Hello, control

use rust_widgets::core::{Color, Rect};
use rust_widgets::style::WidgetStyle;
use rust_widgets::widget::base_widgets::button::Button;
use rust_widgets::widget::Widget;   // brings `set_style` into scope

fn main() {
    // A control is a value placed in a `Rect`. Rendering it to SVG is the shortest path
    // to seeing it; a backend only changes where the pixels go, not how the control draws.
    let mut button = Button::new("Click me".to_string(), Rect::new(0, 0, 160, 36));

    // Style is a plain struct of optional values, so an unset field inherits instead of
    // overwriting what the theme resolved for this control.
    let style = WidgetStyle {
        background_color: Some(Color::rgb(33, 150, 243)),
        text_color: Some(Color::WHITE),
        border_radius: Some(6),
        ..WidgetStyle::default()
    };
    button.set_style(style);

    println!("{}", rust_widgets::widget::svg::render_to_svg(&mut button));
}

Window creation, the event loop, layout, theming, i18n and the C ABI are each covered in their own chapter — start at cookbook/en/src/README.md.

Rendering

Backend Use Notes
Software rasteriser Default everywhere Anti-aliased, no GPU required, runs headless
SVG Snapshots, printing, vector output Byte-reproducible; this is what snapshots/svg/ holds
wgpu GPU acceleration Explicit fallback 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 contract identically — the SVG emitter writes dominant-baseline="text-before-edge" — so a label measured against one backend lands in the same place in the other. That property is what makes snapshots/svg/ a usable review artifact rather than a second, divergent renderer.

Sizing a control

A control is given a rectangle by whatever placed it, and that rectangle is the area it may occupy — not how big it should draw. Those are two questions, and answering the second one is what stops a switch from being painted as a 240x120 stadium:

implicit_size = max(floor, content + padding)

The max is the load-bearing part: the floor is a minimum tappable area, so a button labelled with 5 px of text is still 64x40 rather than 64x18. rust_widgets::widget::ControlMetrics exposes that formula and the geometry helpers built on it, and rust_widgets::widget::metrics::dimensions holds every control dimension (track sizes, thumb radii, field heights, paddings) so one fact has one definition:

helper the question it answers
implicit_size / content_box how big should I be? / where may my content go?
center_in / centered_disc / centered_square centre fixed chrome, clamped never expanded
centered_band / full_width_band full width, my height, vertically centred
top_band / bottom_band / band_inset pin a bar to an edge and let content follow
focus_ring_rect / focus_ring_color the keyboard ring, inset so it never overlaps a neighbour

Laying out a control

A layout asks each child how big it wants to be and acts on the answer, rather than being told in advance. Each control states its wish as a floor, a preferred value and a ceiling, because "how small may I squeeze you" and "how large may I stretch you" are questions a single size cannot answer:

use rust_widgets::layout::{AxisHints, Hints, LayoutParams, ChildInfo, Layout};

// 120 px wide minimum, would like 200, never past 400; free to stretch horizontally.
let hints = Hints { width: AxisHints::new(120, 200, 400), height: AxisHints::fixed(32) };
let children = [ChildInfo::new(widget_id, hints).with_params(LayoutParams::filled())];

let mut out = Vec::new();
layout.arrange(rect, &children, &mut |id, child_rect| out.push((id, child_rect)));

fill is separate from the size on purpose: a slider and a button can share a preferred size while only one of them should be stretched across a form. AxisHints::new normalises min <= pref <= max on construction, so an unnormalised hint cannot be represented. Every existing layout keeps working unchanged — Layout::arrange defaults to forwarding to Layout::update.

An example is committed at examples/readme_check.rs, so these snippets are compiled on every build rather than drifting from the API.

Control behaviour

Three contracts that a control's shape alone does not communicate:

  • clicked requires the release to land inside the control. A drag that starts on a button and ends off it emits canceled instead, and only the primary button activates.
  • pressed is a continuous quantity. Dragging off a held button clears it and dragging back restores it, so the control reflects where the pointer actually is.
  • A focus ring is drawn when the user is on the keyboard, not merely when the control has focus. Event::FocusGained carries a FocusReason, and FocusReason::draws_focus_ring() is false for a pointer press — a ring under the cursor reads as a stuck highlight.

Verifying a change

cargo test --no-default-features --features desktop            # full suite
cargo clippy --no-default-features --features desktop --all-targets -- -D warnings
cargo run  --no-default-features --features desktop --example export_control_svgs
bash tools/run_all_gates.sh                                    # every gate, PASS/FAIL table

The 376 SVGs under snapshots/svg/ are one file per control per appearance (dark and light). 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 shows up as a diff in review, and a control whose two files are identical is visibly theme-blind.

control.md is the same set laid out for reading: every control's dark and light snapshot, grouped into 17 families by the module that implements it. It is generated by tools/generate_control_index.py from the registry and the source tree, so it is a view of the snapshots rather than a second copy of them — a control added to the registry appears in the gallery without anyone editing it. The same gate that regenerates the snapshots also regenerates this page and refuses a stale copy, so the two cannot drift.

tools/run_all_gates.sh runs everything in tools/check_*.sh and prints a per-gate table with timings. Every gate is expected to be able to fail; the ones that matter most were verified by reverse injection — deliberately reintroducing the defect and confirming the gate goes red.

What "180 controls" covers

The widget library spans 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 and 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 that Material has no equivalent for: candlestick, volume, depth, indicator, radar, heatmap, gauge, sparkline.

Profiles that strip the widget set down (mini, embedded) keep the 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 support has host tests under tools/run_wayland_compositor_tests.sh.
  • iOS / Android — full profile; the JNI test APK is built with tools/build_android_testapp.sh.
  • Web (wasm32) — WebGL/WebGPU surface; cfg(target_arch = "wasm32") describes the sandbox, not an OS, and the OS-specific knowledge stays inside src/platform/.
  • Embedded / mini — software rasteriser only, no OS services.

Anything that cannot genuinely be done yet reports false from a runtime capability query rather than pretending. supports_custom_widgets(), supports_web_engine() and has_real_engine() are honest answers, not compile-time stubs.

Text coverage

The default build draws Latin/ASCII only. The crate ships no font data, 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, any other script a fallback glyph (a hollow box, "tofu")

The label still lays out and the control still renders — only the glyph is wrong. A line is also ordered for painting by the Unicode bidirectional algorithm, so an Arabic or Hebrew run draws right to left. Ordering is not glyph shape: 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 — and, for complex scripts, a shaping step. It is asked for by name, because a mini or embedded profile must not carry glyphs it never draws:

cargo build --features "desktop,fonts-cjk-bitmap"

fonts-cjk-bitmap adds a generated 16x16 CJK face (Han, kana, CJK punctuation and fullwidth forms, ~85 KB read on demand). Latin rendering stays byte-identical when it is on: a face is appended to the fallback stack and can only answer for characters the base face has no glyph for.

feature data what it adds
fonts-cjk-bitmap 84 996 bytes, generated a 16x16 CJK bitmap face — Chinese, Japanese, Korean text, no shaping needed
fonts-cjk 361 704 bytes, OFL subset the same script as outlines, so it is antialiased and scales to any px size
fonts-vector-latin 35 896 bytes, OFL subset real advances and kerning, from a face named by Font::family
fonts-complex 70 576 bytes, OFL subset Arabic joining, so بيت shapes to the word rather than three isolated letters
fonts-emoji-color 1 602 492 bytes, OFL subset colour emoji — 317 codepoints including the 26 regional indicators

fonts-cjk-bitmap and fonts-cjk are two answers to one script, and the difference is size against quality: an outline subset at the bitmap's own coverage would weigh 581 KB, roughly 7x, so the bitmap is what a mini/embedded build uses and the vector face is for a desktop host that wants Chinese antialiased at any size. Enabling both makes the bitmap win for a character it covers — it is the cheaper face, and the stack keeps the cheap source in front.

None of these is enabled by any profile, and --all-features is the only way to get all of them at once. The vector and colour subsets are lazy-loaded from the binary's read-only section and are recorded, with their upstream digests, in NOTICE.

The boundary is asserted from both sides (render::pipeline::pixel_ops::text_coverage_tests and render::text::glyph_source's tests) — a non-Latin character must resolve to the fallback glyph on a default build, and enabling the CJK data must move that boundary by exactly the face it adds — so the documentation cannot silently overstate what is drawn.

Language bindings

The C ABI lives in src/bindings/ and exposes every control through 130 rw_* functions with a capability-based property and event model. C, C++, Python and Java (JNI) bindings are exercised in CI; the generated header is checked for drift by tools/check_abi.sh.

See 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/reports/ Audit and quality reports
docs/log/ Per-round engineering logs

Requirements

Rust 1.87+. No system GUI libraries are needed for the default build; Linux additionally uses Wayland/X11 for the surface, and the image codecs are pure Rust so cross-compiling to Android, iOS or wasm needs no pkg-config sysroot.

License

MIT — see LICENSE.

Support

build version tests license