Skip to content
frus

frus

A cross-platform UI framework written entirely in Rust.

One codebase → desktop, Android, and the web. GPU-rendered. Built from components. No DSL, no codegen, no bespoke CLI — just cargo.

CI License Rust Status

Quick start · Gallery · Architecture · Status · Contributing · Français


Four screens of the demo application, entered and left through frus's spring route transitions

The sample application moving between four of its screens. Every pixel — the layout, the type, the charts, the springs — is drawn by frus on the GPU.


What is frus?

frus is a greenfield attempt at a UI framework designed in Rust from day one: the entire framework — renderer, layout, widgets, gestures, theming, animation, accessibility — is Rust. There is no embedded VM, no second language for app logic, and no platform channel between your code and the pixels.

The parts that must be native (window creation, IME, screen readers, the Android activity) live behind one thin crate, frus-shell. Everything above it is portable.

use frus::{button, column, row, text, Align, BuildContext, FrusApp, Variant, Widget};

// A widget is what it builds. One that has something to remember keeps it with a hook.
fn counter(cx: &BuildContext) -> Box<dyn Widget> {
    let count = cx.use_state(|| 0);
    let (up, down) = (count.clone(), count.clone());
    Box::new(column![
        text(format!("{}", count.get())).size(48.0),
        row![
            button("+", move || up.update(|n| *n += 1)).variant(Variant::Filled),
            button("−", move || down.update(|n| *n -= 1)).variant(Variant::Outlined),
        ].gap(20.0),
    ].gap(16.0).align(Align::Center))
}

// One declaration wires up desktop, Android and web entry points.
frus::main!(FrusApp::from_fn(counter));

That is a complete, runnable application. cargo run on desktop, cargo apk run on Android, wasm-bindgen for the browser — the source does not change.

Components, state, controllers, routes

A widget with nothing to remember is a StatelessWidget; one with something to keep is a StatefulWidget whose State outlives the rebuilds — with init_state, did_update_widget, dispose and a set_state you can call from any handler. A plain function can keep state too, with use_state, use_ref, use_memo and use_effect.

use frus::{button, column, BuildContext, GoRoute, GoRouter, FrusApp, TextField, Widget};

fn sign_in(cx: &BuildContext) -> Box<dyn Widget> {
    let email = cx.use_text_controller("");          // the field's text, held outside the field
    let router = cx.router();
    Box::new(column![
        TextField::new("").label("Email").controller(&email),
        button("Continue", move || router.go(format!("/welcome/{}", email.text()))),
    ])
}

let router = GoRouter::new(vec![
    GoRoute::new("/", |_, _| frus::component(sign_in)),
    GoRoute::new("/welcome/:name", |_, state| {
        frus::text(format!("Hello, {}", state.param("name").unwrap_or("you")))
    }),
]);
frus::main!(FrusApp::router(router));

The router keeps a stack of pages, slides between them, answers the back gesture, follows redirects, and keeps a covered page's state until it is popped.

Why another UI framework?

One language, top to bottom App logic, widgets, layout, and the renderer are all Rust. No FFI boundary in the hot path, no serialization across a bridge.
Components you can test without a window A component is a value that says what it looks like, and its state is plain Rust beside it. Made, updated, disposed: the whole lifecycle runs in tests with no GPU and no window — as do more than 1,900 of this repo's tests.
GPU-native rendering wgpu targets Vulkan, Metal, DX12, and WebGPU from one backend. Vector paths are tessellated with lyon; text is shaped by cosmic-text.
Everything is overridable Widgets ship themed defaults, never hardcoded ones. If a widget paints it, you can restyle it or swap the slot.
cargo-native No frus doctor, no custom package manager, no generated build directory. cargo build, cargo test, cargo apk run.

What it looks like

All of this is one application — crates/frus-demo — and one source tree.

The task list: app bar, alerts, a segmented control, a text field, checkboxes, drag-and-drop targets and a floating action button A chart dashboard: a line chart with a clickable legend above a grouped bar chart
Widgets, gestures, theming — the list, with drag-and-drop reordering and swipe-to-dismiss. Charts — line, area, grouped and stacked bars, with a legend that filters the series.
A Kanban board of three columns of cards A data table with a search field, sortable headers, row checkboxes and pagination
Drag-and-drop — cards move between columns, and the rest reflows live under the finger. Data tables — sorting, selection, pagination, and an inline-editable variant.
The same application running on an Android phone

The same code on a phone. Android is a first-class target, not a port: a native activity, Vulkan, a real IME with composition and swipe typing, system insets, and the lifecycle. This is a photograph of a device, not a rendering — it is the one picture here that would be worth nothing otherwise.

The settings screen in the light theme

The theme is not a coat of paint. Light and dark are generated from a seed colour, and every widget takes its colours from the theme rather than from a constant — so an application can restyle the whole library, or one widget, without forking it.

Every picture above except the phone is rendered, through the same pipeline a window uses: cargo run -p frus-demo --features shots --bin shots -- docs/media. They are regenerated after a change rather than slowly going stale.

Quick start

Prerequisites: Rust 1.88 or newer — the minimum supported version, checked in CI — and a GPU with Vulkan, Metal, or DX12 drivers.

git clone https://github.com/KalybosPro/frus
cd frus

cargo run -p frus-hello        # the counter above
cargo run -p frus-demo         # a larger todo/kanban app
cargo run -p frus-transforms   # animation and transform showcase
cargo test --workspace         # ~2,180 tests; the rendering ones need a GPU

Start your own app

The repo ships a cargo generate template that produces a project wired for desktop and Android:

cargo install cargo-generate                          # once
cargo generate --path templates/app --name my-app
cd my-app && cargo run

The template asks for the path to your frus checkout — frus is not on crates.io yet, so dependencies resolve through path. See docs/getting-started.md.

Android

cargo install cargo-apk        # once
cargo apk run -p frus-demo --lib   # build, install, launch

Requires the Android SDK + NDK with ANDROID_HOME / ANDROID_NDK_ROOT set and a device visible to adb devices.

Web

rustup target add wasm32-unknown-unknown
cargo install wasm-bindgen-cli

cargo build -p frus-hello --target wasm32-unknown-unknown --profile web-release
wasm-bindgen --target web --no-typescript \
  --out-dir crates/frus-hello/web/pkg \
  target/wasm32-unknown-unknown/web-release/frus_hello.wasm

cd crates/frus-hello/web && python3 -m http.server 8080

Needs a WebGPU-capable browser (Chrome/Edge 113+) on a secure context. Details in crates/frus-hello/web/README.md.

Architecture

Four layers. Dependencies only ever point downward, and only frus-shell knows what platform it is on.

┌──────────────────────────────────────────────────────────────┐
│  Application     what you write                              │
│  frus (facade) · frus-hello · frus-demo · frus-transforms    │
├──────────────────────────────────────────────────────────────┤
│  Shell           platform layer                              │
│  frus-shell — Application, Command, Subscription,            │
│               lifecycle, IME, a11y, net, main!               │
├──────────────────────────────────────────────────────────────┤
│  Widgets         UI & interaction                            │
│  frus-widgets — Ui/scene, widget tree, gestures, theme       │
├──────────────────────────────────────────────────────────────┤
│  Foundations     render & measure                            │
│  frus-core · frus-layout · frus-text · frus-gpu ·            │
│  frus-image · frus-l10n                                      │
└──────────────────────────────────────────────────────────────┘
Crate Role
frus Facade — the single dependency an app needs. Re-exports shell + widgets + main!.
frus-core Geometry, color (incl. HCT), paths, decorations, text styles, animation, scene graph, semantics.
frus-layout Flexbox layout over taffy.
frus-text Shaping and measurement via cosmic-text.
frus-gpu wgpu device, 2D painter, path tessellation, glyph atlas, compositor, offscreen rendering.
frus-image PNG/JPEG decoding to ImageData.
frus-l10n i18n via Fluent bundles + locale negotiation.
frus-widgets The widget library and interaction model (~150 modules).
frus-shell Window, event loop, lifecycle, Command/Subscription, IME, AccessKit, fetch.
frus-test Headless rendering, snapshots, golden-image comparison.
frus-hello The canonical minimal app. Source of the cargo generate template.
frus-demo Larger sample app exercising most widgets.
frus-fetch-example End-to-end network example: RemoteData, loading/error/data states.
frus-transforms Animated showcase of transforms, aspect ratio, fractional sizing.

Read ARCHITECTURE.md before your first non-trivial change — it explains where a given kind of code belongs and why.

Project status

Pre-alpha. The core is real and exercised by three sample apps, but the API is not stable and nothing is published to crates.io yet.

Platform State Notes
Desktop (Windows / Linux / macOS) Working winit + wgpu, clipboard, screen-reader a11y via AccessKit, dev live-reload
Android Working Native activity, Vulkan, real IME (composition & swipe), insets, system bars, the system clipboard, lifecycle — validated on device
Web (wasm + WebGPU) Functional Rendering, input, animation, subscriptions, async effects & fetch. Clipboard, a11y and live-reload are not wired up
iOS / macOS native Not started The shell layer is isolated, so adding a target is a contained job

What works today: flex/grid/wrap layout, 1D & 2D scrolling with fill-then-scroll, text input with IME, drag-and-drop reordering with live reflow, data tables, editable grids, charts, date/time pickers, dropdowns, trees, toasts, modals, drawers, navigation with spring transitions and back-gesture, an overridable theme, RTL and i18n, spring, implicit and explicit animations, a wheel picker, lifecycle, effects and subscriptions, async HTTP with typed JSON, and golden-image testing (169 reference images).

Known gaps — these are the best places to help:

  • Publishing to crates.io (everything is path-based today).
  • Web clipboard, accessibility, and live-reload.
  • iOS and native macOS shells.
  • Text rendering edge cases, and broader golden coverage.
  • A searchable documentation site built from the design notes.

See ROADMAP.md for the full picture.

Where to start

The project is early enough that a single pull request can shape a subsystem. These are real, open, and written up with where to look and how to know you are done:

🟢 Turn on missing_docs, crate by crate Start with the small crates. One crate is a whole PR.
🟡 Publish to crates.io The single biggest thing between the project and anyone trying it.
🟡 The overscroll stretch effect Current Android stretches the content instead of glowing. A render-target effect, and where to start reading is written down.
🟡 Clipboard and accessibility on the web Both exist on desktop; the web drops them on the floor.
🔴 An iOS shell The architecture bets this is a contained job. Nobody has tested the bet.

🟢 good first issue · 🟡 help wanted · 🔴 design first — all open issues

Not sure where you fit? Open an issue and say what you enjoy working on. English or French, both fine.

Contributing

Start with CONTRIBUTING.md. The short version:

cargo test --workspace          # must be green
cargo clippy --workspace --all-targets
cargo fmt --all

Every change ships with tests; every non-trivial change ships with a design note. Discussion happens in issues and discussions — English or French, both fine.

By participating you agree to the Code of Conduct.

Documentation

License

Licensed under either of

at your option.

Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in the work by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.

About

A cross-platform UI framework written entirely in Rust.

Resources

Code of conduct

Contributing

Security policy

Stars

13 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages