# rust_widgets — Pure Rust GUI Library
<p align="center">
<img src="snapshots/header.jpg" alt="rust_widgets" width="800">
</p>
Cross-platform GUI library in pure Rust. Hardware-adaptive rendering, widget library, touch/gesture support, i18n, and SVG output. Supports desktop, tablet, mobile, embedded, and minimal-profile **mini** targets.
## ✨ Every control is self-drawn
**The library paints 100% of its own controls. It does not create native OS controls — on any platform.**
There is no `CreateWindowExW`/`NSButton`/`gtk_button_new`/`android.widget.Button` anywhere in this crate. Each backend's only job is to hand the renderer a surface to paint into; every button, list, editor, menu and chart below is drawn by the same Rust rasterizer, so a control looks and behaves identically whether it is running on Windows, macOS, Linux, iOS, Android or the web.
```
┌──────────────────────────────────────────┐
│ rust_widgets — paints its own controls │
└──────────────────────────────────────────┘
│ rasterizer output (RGBA / SVG / GPU)
▼
┌──────────────┐ ┌──────────────┐ ┌──────────────┐
│ Windows HWND │ │ macOS NSView │ │ GTK widget │ … one surface per backend
└──────────────┘ └──────────────┘ └──────────────┘
```
### Why this matters
| Appearance | **Identical on every OS** | Differs per OS toolkit and version |
| Widget count | **167 kinds, all platforms** | Only what the OS toolkit offers |
| Dependency weight | **No GUI toolkit linked** | GTK / AppKit / Win32 / Android SDK |
| Headless & embedded | **Runs with no OS at all** (`mini`, SVG) | Impossible |
| Deterministic tests | **Pixel/serialise snapshots** | Needs a real display |
### What each backend *does* own
Self-drawing is not "one backend". A backend still owns the parts that genuinely belong to the operating system, and only those:
- **Surface + event loop** — window creation, the paint callback, resize.
- **Input** — keyboard/mouse/touch translated into a unified `Event`.
- **Platform services** — IME, clipboard, accessibility bridge, file dialogs, DPI scaling.
A backend that cannot supply even a surface (for example a bare framebuffer) still works: it paints into an in-memory buffer instead. See [`docs/ARCHITECTURE.md`](docs/ARCHITECTURE.md).
> **Migrating from 1.x?** Native control creation was removed from all ten backends in 2.0.0. See [`CHANGELOG.md`](CHANGELOG.md) and [`docs/MIGRATION_GUIDE.md`](docs/MIGRATION_GUIDE.md).
All 167 widget kinds are registered in the factory and each publishes its own
property contract; the platform capability matrix
([`docs/plans/platform_capability_matrix.md`](docs/plans/platform_capability_matrix.md))
is generated from source and gated for drift in CI.
[]()
[]()
[]()
[]()
<p align="center">
<a href="README.zh-CN.md">
<img src="https://img.shields.io/badge/%E4%B8%AD%E6%96%87-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue" alt="简体中文">
</a>
</p>
---
## Quick Start
```bash
# Desktop (default)
cargo check
# Mini (reduced std profile, minimal widget set)
cargo check --no-default-features --features mini
# Embedded
cargo check --no-default-features --features embedded
# Tests (lib suite; the CI command is `cargo test --all-features -q`)
cargo test --lib
# Cross-compile checks used by CI (no system libraries required)
cargo check --target wasm32-unknown-unknown --no-default-features --features wasm
cargo check --target x86_64-pc-windows-msvc --no-default-features \
--features "windows desktop-runtime wgpu touch i18n controls-native controls-custom serde serde_json advanced-widgets quality-management"
```
> **Android:** build the JNI test APK with `./tools/build_android_testapp.sh`
> (`ANDROID_SDK_ROOT` defaults to `~/Android/Sdk`; the NDK is taken from
> `$ANDROID_SDK_ROOT/ndk`). See [Build Requirements](#build-requirements).
### Device Profiles
**Pick exactly one.** The device profiles are mutually exclusive: `mini`/`embedded`
compile parts of the crate *out*, so combining one with `desktop` is not a
"lowest common denominator" — it breaks the build.
```bash
# ✅ correct
cargo check # desktop (default)
cargo check --no-default-features --features mini
cargo check --no-default-features --features embedded
# ❌ wrong: desktop stays on, so mobile-profile modules are still compiled
cargo check --features mini
```
| Desktop | `cargo check` | OS surface + event loop | Full widget set | ✅ | ✅ (wgpu enabled by desktop) |
| Tablet | `--no-default-features --features tablet` | OS surface + event loop | Full widget set | ✅ | ✅ (wgpu enabled by tablet) |
| Mobile | `--no-default-features --features mobile` | Mobile API | Full widget set | ✅ | ✅ (wgpu enabled by mobile) |
| Embedded | `--no-default-features --features embedded` | Software | Core widget set | — | — |
| **Mini** | `--no-default-features --features mini` | **reduced std** + alloc | **Core widget set** | — | — |
#### What each profile turns off
The API is the same across profiles; what differs is which capabilities *exist*.
Only profiles that include a platform backend **and** keep `widget::runtime` can
host custom-painted widgets:
| `widget::runtime` (widget registry) | ✅ | — | — |
| Custom-painted widgets (`mount_custom_widget`) | ✅ | — | — |
| `supports_custom_widgets()` | `true` | `false` | `false` |
| Menus / tool bars / status bars | ✅ | ✅ | ✅ |
| Menu shortcuts (displayed) | ✅ | ✅ | ✅ |
| Menu shortcuts (actually fire) | ✅ | ✅ | ✅ |
Where the table shows `—` the capability is **absent, not degraded**: the module
is compiled out, so `supports_custom_widgets()` reports `false` and callers are
expected to refuse the operation rather than mount into a blank window (see
`demo/code_editor`'s startup check).
Menus and shortcuts are deliberately *not* affected: their code carries no
`mini` gate, so a `mini` build is best described as **"no custom-painted widget
surface, but fully working menus"**.
> The `cargo test --all-features` CI command deliberately turns every feature on,
> which includes `desktop` **and** `mini` at once. That combination is the
> regression tripwire for this constraint; see
> [`docs/plans/platform_differences.md`](docs/plans/platform_differences.md) for
the full rationale and the verification matrix.
#### `tablet` / `mobile` need an explicit OS backend
Unlike `desktop`, the `tablet` and `mobile` profiles do **not** pull in an OS
backend by themselves — their only backend entry is `os-auto`, which is currently
an empty feature. Build them with a backend named explicitly:
```bash
# ⚠️ resolves to a stub backend on every OS: no real widgets at all
cargo check --no-default-features --features tablet
# ✅ real backend
cargo check --no-default-features --features "tablet,macos"
```
Two consequences worth knowing before you rely on these profiles:
* Without a backend feature you silently get `macos-fallback-stub` (or the
per-OS equivalent) rather than an error. Check
`rust_widgets::backend_name()` if you are unsure which one you built.
* On macOS, `tablet`/`mobile` select the **objc2 preview** backend, which does
*not* implement custom-painted widget hosting yet. On macOS that currently
requires the `desktop` profile. Query `supports_custom_widgets()` rather than
assuming.
### OS Backends
| Windows (Win32) | `windows` | ✅ |
| macOS (Cocoa/objc2) | `macos` | ✅ |
| iOS (UIKit) | `ios` | ✅ |
| Linux (GTK) | `linux-gtk` | — |
| Linux (Wayland) | `linux-wayland` | — |
| Android (JNI) | `android` | ✅ |
| Web (WASM) | `wasm` | — |
| HarmonyOS | `harmony` | — |
---
## OS Support Matrix
### 1. Platform services per OS
These are the capabilities a backend *reports about the operating system*. Every
one is queried at runtime through `PlatformCapabilities`
(`rust_widgets::PlatformCapabilities`) — read it rather than assume, because a
backend running on an OS it was not compiled for reports `false`.
| **Windows** | `WindowsPlatform` | Desktop | ✅ | ✅ | ✅ | ✅ | ✅ |
| **macOS** | `cocoa` | Desktop | ✅ | ✅ | ✅ | ✅ | ✅ |
| **macOS** (objc2 preview) | `macos-objc2-preview` | Desktop | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Linux / GTK** | GTK backend | Desktop | ✅ | ✅ | ✅ | ✅ | ✅ |
| **Linux / Wayland** | `wayland` | Desktop | ✅ | ✅ | ✅ | ❌ | ✅ |
| **iOS** | `ios-state-backend` | Mobile | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Android** | `android-state-backend` | Mobile | ✅ | ✅ | ✅ | ❌ | ❌ |
| **HarmonyOS** | `harmony-desktop` | Desktop | ✅ | ✅ | ✅ | ❌ | ❌ |
| **Web (WASM)** | `wasm-state-backend` | Embedded | ❌ | ❌ | ❌ | ❌ | ❌ |
| **Portable / no-OS** | `portable` | Embedded | ❌ | ❌ | ❌ | ❌ | ❌ |
**Legend.** *Native menu* means the OS exposes a menu-bar protocol. Wayland has none,
so its backend keeps the menu tree in-process and the host renders it — advertising a
native menu would be false. *Configurable* means the backend exposes OS-level settings
(theme, accent colour, notifier) beyond the capability flags.
> **How to read the `native_menu` column.** A backend that does not override
> `Platform::capabilities` inherits the trait default, which is
> "`true` if the backend reports the `Desktop` family". Wayland, iOS, Android and
> HarmonyOS override it to `false` because they genuinely have no menu protocol;
> Windows, macOS and GTK keep the default. The values above are pinned by a test
> (`published_os_capability_matrix_matches_the_trait_default`), so they cannot drift.
>
> **The control set is *not* in this table, on purpose.** Because every control is
> self-drawn, widget availability does not vary by OS — it varies by **profile**.
> That is the next table.
### 2. Widget availability per profile
What differs across targets is how much of the widget set is **compiled in**, not
what the OS can draw.
| `desktop` | **167 kinds** (full) | ✅ | ✅ | ✅ wgpu | ✅ |
| `tablet` | **167 kinds** (full) | ✅ | ✅ | ✅ wgpu | ✅ |
| `mobile` | **167 kinds** (full) | ✅ | ✅ | ✅ wgpu | ✅ |
| `embedded` | reduced core set | — | — | — software | — |
| `mini` | reduced core set | — | — | — software | — |
A `—` is **absent, not degraded**: the module is compiled out, so
`supports_custom_widgets()` returns `false` and callers are expected to refuse the
operation rather than mount into a blank surface.
The reduced `embedded`/`mini` set is: Window, Button, CheckBox, RadioButton, Label,
LineEdit, ComboBox, SpinBox, ListBox, ProgressBar, Slider, ScrollBar, ScrollArea,
Panel, Frame, GroupBox, TileView, Line, Meter, MiniChart, ImageView, MiniCanvas,
Arc, Spinner, Roller, Dropdown, TextArea, Keyboard, Switch.
### 3. What "support" means per OS
Reading the two tables together:
| Control appearance | ❌ (self-drawn) | ❌ |
| Which controls exist | ❌ | ✅ |
| DPI scaling / IME / a11y | ✅ | ❌ |
| Native menu bar | ✅ | ❌ |
| File/colour/font dialogs | ✅ (host-provided) | ❌ |
| Rendering backend | ❌ | ✅ (GPU vs software) |
So an app that avoids OS-specific APIs is portable by construction: build it once
per profile, and it renders the same everywhere.
---
## Architecture
```
┌────────────────────────────────────────────────────────────┐
│ API Layer — lib.rs + compat.rs (core/alloc bridge) │
├────────────────────────────────────────────────────────────┤
│ Widgets │ Event System │ Layout Engine │
│ (30-80) │ (EventLoop, │ (Box, Grid, Flow, │
│ │ Gesture) │ Stack, Absolute) │
├───────────┴────────────────┴──────────────────────────────┤
│ i18n │ Theme │ Signal System │ Control Backend │
├────────────────────────────────────────────────────────────┤
│ Rendering: SoftwarePaintBackend / SvgPaintBackend / GPU │
├────────────────────────────────────────────────────────────┤
│ Platform: Windows │ macOS │ Linux │ iOS │ Android │ WASM │
└────────────────────────────────────────────────────────────┘
```
---
## Features
### Rust-Native Design
- no_std-ready architecture: all files import shared types via `compat.rs` (`core`/`alloc`) so enabling `#![cfg_attr(feature = "mini", no_std)]` is a tracked step — the `mini` profile currently compiles on std.
- `compat.rs` bridge: `HashMap→BTreeMap`, lightweight-profile lock compatibility, `MiniVec<T,64>`, `MiniString<256>`, `MiniArena` (bumpalo)
- `enum WidgetKind` + `trait Widget` + `trait Draw` + `trait EventHandler` — zero-cost abstractions
- Builder pattern: `Style::new().bg_color(RED).pad_all(8).build()` — compile-time checking
### Rendering Backends
- **SoftwarePaintBackend**: CPU rasterizer (RGBA framebuffer), used by mini/embedded
- **SvgPaintBackend**: SVG pipeline output for testing and documentation
- **GPU (wgpu)**: Hardware-accelerated for desktop/tablet/mobile
### Touch & Gesture
- 11 gesture recognizers: Tap, DoubleTap, LongPress, Swipe, Pan, Fling, TwoFingerTap, TwoFingerSwipe, LongPressDrag, Pinch, Rotate
- Touch-target expansion for small widgets on touch devices
### Layout
- Box, HBox, VBox, Grid, Form, Stack, Flow, Absolute, Anchor, Masonry
- Device-adaptive layout scale, font scale, and minimum touch size
### CSS Styling
- CSS parser + selector engine (`CssParser`, `CssSelector`)
- `Widget::apply_css(css, class)` — per-widget CSS application
- `StyleSheetManager` — global stylesheet registration
- `CssWatcher` — polling-based CSS hot-reload
### Partial Refresh
- `DirtyRegionTracker` with rectangle merging
- `render_dirty_regions()` — clip-based partial redraw via `push_clip/pop_clip`
### Internationalization
- `tr!()` macro for compile-time key-based translation
- en / zh-cn / zh-tw translations (30+ strings per language)
- Context-based and plural variants
- `audit_keys()` for coverage validation
---
## Widget Library
### Desktop/Tablet/Mobile (167 widget kinds)
**Core**: Window, Dialog, MessageBox, FileDialog, ColorDialog, FontDialog, InputDialog, ProgressDialog, PopupWindow, Button, CheckBox, RadioButton, Label, LineEdit, TextEdit, RichEdit, ComboBox, SpinBox, ListBox, ListView, TreeView, ProgressBar, Slider, ScrollBar, ScrollArea, TabWidget, Splitter, GroupBox, MenuBar, Menu, MenuItem, ContextMenu, ToolBar, StatusBar, Canvas, Table, Grid, Chart, ToggleButton
**Date & Time**: Calendar, DateEdit, TimeEdit, DateTimeEdit, DatePicker, TimePicker, DateTimePicker, CupertinoDatePicker, DateRangePicker, MobileDatePicker
**Containers**: CollapsiblePane, DockWidget, MdiArea, StackedWidget, ToolBox, TabBar, NavigationStack, PagerPageView, Carousel, BottomSheet, ModalBottomSheet
**Mobile**: BottomNavigationBar, NavigationDrawer, AppBar, SafeArea, PullToRefresh, RefreshControl, SearchBar, CupertinoSwitch, CupertinoSlider, CupertinoNavigationBar, CupertinoSegmentedControl, AdaptiveScaffold
**Input**: CommandLink, FontComboBox, KeySequenceEdit, MaskedEdit, AutoCompleteEdit, MultiSelectComboBox, EditableComboBox, RangeSlider, FloatingLabel, TagInput, InplaceEditor, SearchBox, ShortcutEditor
**Display**: LCDNumber, Dial, ProgressCircle, Rating, Icon, Sparkline, Tooltip, Badge, Chip, Avatar, SkeletonLoader, EmptyState
**Charts**: LineChart, BarChart, PieChart, Sparkline
**Web**: WebView, WebEngineView, WebEnginePage, WebEngineSettings, WebEngineDownloadItem, WebEngineCookieStore, WebEngineWebChannel, WebEngineFindTextResult, WebEngineNotification, WebEngineScriptDialog, WebEngineContextMenuRequest
**Menus**: PieMenu, RibbonBar, MenuButton, DropdownMenu, Popover, SegmentedButton
**Special**: FreeformShape, QRCode, ColorHistory, ColorWell, MasonryLayout, Stepper, Divider, SwipeToDismiss, Toolbox, PropertiesPanel, PropertyGrid, WizardDialog, Wizard, AnimatedImage, HeroAnimation, BezierCurveEditor, LottieWidget, RiveWidget, VideoPlayer, ImageGallery, AudioVisualizer, CameraPreview, BarcodeScanner, Breakcrumb, CodeEditor, ColorPicker, CommandEntry, CommandPalette, DiffViewer, MapView, MediaPlayer, NotificationCenter, Snackbar, SplitButton, TerminalView, ToastStack
### Mini / Embedded (reduced core widget set)
Window, Button, CheckBox, RadioButton, Label, LineEdit, ComboBox, SpinBox, ListBox, ProgressBar, Slider, ScrollBar, ScrollArea, Panel, Frame, GroupBox, TileView, Line, Meter, MiniChart, ImageView, MiniCanvas, Arc, Spinner, Roller, Dropdown, TextArea, Keyboard, Switch
---
## Widget Properties
Every control publishes its own property contract, so you can read, write and
**enumerate** a control's state without knowing its concrete type. The same code
works for a button, a chart and a code editor, on every platform.
```rust
use rust_widgets::core::Rect;
use rust_widgets::widget::{
widget_property_get, widget_property_names, widget_property_set, WidgetFactory,
};
use rust_widgets::CapabilityValue;
let factory = WidgetFactory::new_with_defaults();
let mut button = factory.create("button", Rect::new(10, 10, 100, 30), "OK").unwrap();
// Read and write by name
factory.write_property(button.as_mut(), "text", CapabilityValue::String("Save".into())).unwrap();
let text = factory.read_property(button.as_ref(), "text").unwrap();
assert_eq!(text, CapabilityValue::String("Save".into()));
// Or enumerate the whole contract — the API for a property editor or a serialiser.
// `enabled`, `visible`, `tooltip` and `geometry` appear here for every control.
for name in widget_property_names(button.as_ref()).unwrap() {
println!("{name} = {:?}", widget_property_get(button.as_ref(), name).unwrap());
}
```
Because the list comes from the control itself, it cannot go stale — and a test
fails by name if a control advertises a property it will not answer.
### Error semantics
| `UnknownProperty` | This control has **no property by that name** — a caller bug. |
| `ReadOnlyProperty` | The property **exists** but is not writable (e.g. `geometry`, `row_count`). Render a disabled field. |
| `TypeMismatch` | Wrong value type, or a value out of range. |
| `UnsupportedOnWidget` | The control has no contract at all. Should not occur in 2.0.0. |
> Reading by **id** (`rust_widgets::widget::read_widget_property_by_id`) resolves
> through the widget runtime, so the control must be registered first; use the id
> `runtime::register` returns. See [`docs/MIGRATION_GUIDE.md`](docs/MIGRATION_GUIDE.md).
---
## C ABI & Language Bindings
```bash
cargo build --release
clang -Iexamples examples/c_abi_poll_demo.c -Ltarget/release -lrust_widgets -o target/release/c_abi_poll_demo
python examples/python/demo_basic.py
```
| C | ✅ |
| C++ | ✅ |
| Python | ✅ |
| Java (JNI) | ✅ |
---
## Core Modules
| `core` | Point, Rect, Size, Color, Font, ObjectId | All profiles |
| `widget` | Widget implementations | All profiles |
| `event` | Event types, EventLoop, GestureEngine | All profiles |
| `compat` | core/alloc bridge, MiniVec, MiniString, MiniArena | All profiles |
| `render` | SoftwarePaintBackend, SvgPaintBackend, GPU (wgpu) | All profiles |
| `layout` | Box, Grid, Flow, Stack, Absolute, Anchor, Masonry | All profiles |
| `signal` | GenericSignal, Signal1, ConnectionScope | All profiles |
| `style` | WidgetStyle, CSS parser, animations, theme states | All profiles |
| `object` | Object/class-name system | All profiles |
| `platform` | Windows, macOS, Linux, iOS, Android, WASM, Harmony | Desktop+ |
| `gesture` | 11 gesture recognizers | Desktop+ (touch) |
| `i18n` | `tr!()` macro, I18nManager, en/zh-cn/zh-tw | Desktop+ |
| `theme` | Theme manager, dark/light mode | Desktop+ |
| `gpu` | GPU adapter detection, buffer pools | Desktop+ |
| `chart` | Line, Bar, Pie, Scatter, Area charts | Desktop+ |
| `web` | WebEngine, WebView, JS engine | Desktop+ |
| `pdf` | PDF document creation | Desktop+ |
| `print` | Print support | Desktop+ |
| `performance` | Profiler, frame rate monitor | Desktop+ |
| `memory` | ObjectPool, ArenaAllocator, BufferPool | Desktop+ |
---
## Build Requirements
| Desktop | 1.87+ | wgpu, GTK/Wayland (Linux), objc2 (macOS) |
| Mini | 1.87+ | heapless, hashbrown, bumpalo (no_std-ready; profile compiles on std) |
| Embedded | 1.87+ | None (software-only) |
### Image codecs and cross-compilation
AVIF support uses the **pure-Rust** `avif` codec (ravif), not `avif-native`, so
building `mobile`/`tablet`/`desktop` for a foreign target does **not** require a
`dav1d` sysroot or cross-configured `pkg-config`. Earlier releases pulled in
`dav1d-sys`, which failed to cross-compile for Android/iOS/wasm unless a
pkg-config sysroot was set up by hand.
The trade-off is decode speed: the pure-Rust codec is slower than the C `dav1d`
backend, and it adds ~15 build-time crates (`rav1e` et al.).
---
## Performance
| Binary size | ~5MB | < 100KB |
| RAM (typical) | < 100MB | < 32KB |
| Frame rate | 60 FPS | 30 FPS |
| Widget creation | < 1ms | < 0.1ms |
---
## License
MIT License — see [LICENSE](LICENSE).
## Support
- Issues: [GitHub Issues](https://github.com/mikewolfli/rust-widgets/issues)
- Documentation: [docs/](docs/) directory