Rust GUI Framework
  • Rust 99.8%
  • WGSL 0.2%
Find a file
ps 93681a0aea
All checks were successful
ci / check (push) Successful in 57m59s
ci / cross (push) Successful in 9m1s
Draft the 0.4 plan
- Collect what 0.3 and 0.3.1 moved to 0.4 into docs/plan-0.4.md
- Defer macOS and Windows; the screen reader pass is Orca only
- Do drops, raising and the print parent with forma's own code beside winit
- Wait for winit 0.31 only for the upgrade itself
2026-10-06 11:48:04 +02:00
.forgejo/workflows Fix drags across new data, reduced motion stalls and Escape in the graph 2026-10-05 22:19:28 +02:00
crates Release 0.3.1 2026-10-06 00:40:27 +02:00
docs Draft the 0.4 plan 2026-10-06 11:48:04 +02:00
tools/lucide-gen Ship Lucide's license and check the icons against the release 2026-10-04 17:08:11 +02:00
.gitignore Import aurora as forma 2026-09-30 13:06:17 +02:00
Cargo.lock Release 0.3.1 2026-10-06 00:40:27 +02:00
Cargo.toml Release 0.3.1 2026-10-06 00:40:27 +02:00
CHANGELOG.md Finish the 0.3.1 changelog and retire its change notes 2026-10-06 00:40:27 +02:00
LICENSE-APACHE Import aurora as forma 2026-09-30 13:06:17 +02:00
LICENSE-MIT Import aurora as forma 2026-09-30 13:06:17 +02:00
README.md Mention watching the clipboard in the READMEs 2026-10-05 23:12:49 +02:00

forma

A retained GUI framework for Rust: an arena widget tree laid out with taffy, text by cosmic-text, input in its own vocabulary, and a high-level draw list as output. The core crate knows nothing about windows or GPUs; separate crates render and connect it to a window.

Status: 0.3 - usable, and still changing: minor versions may break the API until 1.0 (see the changelog). The code started as the GUI framework of a game engine editor and became a general-purpose library - see the plan.

New to forma? The guide walks through building an app, from a first window to your own widgets.

Crates

Crate Package What it is
crates/forma forma-ui (imported as forma) Core: widgets, style, layout (right to left too), text, input (touch too), events, themes, draw list, composite widgets (tabs, list popup, find bar, color picker), forms, dates, editable table and tree models; Markdown, charts, planning views, PDF, syntax highlighting and the Lucide icons as features
crates/forma-wgpu forma-wgpu Renders draw lists with wgpu into your own render pass (Renderer) or a window (SurfaceRenderer): glyph atlas with color emoji, rounded rects, lines, meshes, shadows, clipping, images (large ones in tiles), several windows through one renderer; screenshots and charts as PNG (features)
crates/forma-winit forma-winit The app runner and the platform: run, a whole app in one call, with several windows or none; the OS clipboard, screen readers (AccessKit), file dialogs, native menus, notifications, a tray icon, global hotkeys, printing, recent documents, the system's settings and one instance at a time; and input translation, cursor icons and IME placement for an event loop of your own

Using it

[dependencies]
forma = { package = "forma-ui", version = "0.3" }
forma-winit = "0.3"

forma's layout and drawing are slow unoptimized; give debug builds [profile.dev] opt-level = 1 and [profile.dev.package."*"] opt-level = 3. It needs Rust 1.90 or newer and a GPU wgpu supports (Vulkan, Metal, DirectX 12 or OpenGL). The repository's examples need Rust 1.92, for their profiler (puffin 0.20 and puffin_http 0.17), so cargo +1.90 builds the libraries but not --all-targets.

Quick look

use forma::{Actions, Event, Style, Ui, icons};
use forma_winit::{App, Frame, Settings, WindowKey};

#[derive(Clone)]
enum Msg { Add }

#[derive(Default)]
struct Counter { count: u32, actions: Actions<Msg> }

impl App for Counter {
    fn build(&mut self, _window: WindowKey, ui: &mut Ui) {
        self.actions.prune(ui);
        let root = ui.root_panel(Style::new().fill().column().padding(16.0).gap(8.0));
        ui.label(root, format!("Count: {}", self.count), Style::new());
        let add = ui.button(root, "", Style::new());
        ui.icon(add, icons::PLUS, Style::new());
        self.actions.on_click(add, Msg::Add);
    }

    fn update(&mut self, frame: &mut Frame, events: Vec<Event>) {
        // Any message rebuilds the UI before the frame is drawn.
        for Msg::Add in frame.messages(&self.actions, &events) {
            self.count += 1;
        }
    }
}

forma_winit::run(Settings::new("Counter"), Counter::default())?;

When the app's model changes, the same build function runs again as ui.rebuild(|ui| build(ui, &model)): widgets are matched by key or position and keep their state (focus, caret, a drag, scroll), and only what the build output changed is applied. ui.key(id).memo(parent, version, ..) skips a subtree whose data did not change. Actions turns events into the app's own messages, Frame::offer hands them to components, and panel_with / row_with / column_with build nested panels. Icons come from SVG (Icon::from_svg, or the built-in forma::icons). remove, move_widget and set_style edit the tree directly; without forma-winit, ui.layout and ui.draw_list hand a backend such as forma_wgpu::Renderer what to draw.

Ui::handle_input says whether forma used each event, so everything it ignores (shortcuts, game input) goes to the app. Keys are resolved to editing commands by a rebindable Keymap; copy and paste go through a Clipboard. Composite widgets (list, find, picker, tabs) are Components: build them with the rest of the UI and offer them the drained events. Ready-made ones live in forma::widgets: Menu (also as a context menu), MenuBar, Select, ComboBox, Dialog, RadioGroup, NumberField, RangeSlider, Collapsible, Toasts, progress_bar, busy_bar and spinner, and for large data VirtualList, TreeView (reorderable by dragging) and Table, each with optional multiple selection; DockArea arranges panels in tab groups the user splits, resizes and drags about. A checkbox becomes a toggle with Style::switch, and any widget gets a tooltip with Style::tooltip. Apps add their own leaf widgets with Ui::custom and the CustomWidget trait (see the canvas example), and give text areas a language with EditProfile.

Every control is reachable with Tab and operable from the keyboard, text fields have undo/redo, and popups get Escape and outside presses first (Event::DismissRequested; Style::modal for dialogs).

The UI says when it needs drawing (Ui::needs_redraw, Ui::next_wakeup), so an app can sleep between changes; the caret blinks and Style::transition animates colors and offsets on their own, on the UI's Clock (a manual one in tests). Hit testing and painting skip what is out of view.

Screen readers and other assistive technology see the UI through AccessKit (the accesskit feature of forma-ui, on by default in forma-winit's run): every widget has a role, a name and its state, text fields expose their caret and selection, and Style::access describes anything more. forma::testing::Harness drives a Ui headlessly for tests. The examples show the full loop with winit.

Examples

cargo run -p forma-winit --example gallery
cargo run -p forma-winit --example todo
cargo run -p forma-wgpu --example controls

gallery shows every ready-made widget in a dock; todo is a small whole app through forma_winit::run. background feeds the UI from a worker thread that wakes the app, and fluent is an app in English and German, translated with Fluent. screenshot renders every widget in both bundled themes to image files, without a window (cargo run -p forma-wgpu --example screenshot -- target/screenshots 2). Also boxes, inspector, canvas (a custom widget) and embedded (forma drawn into an app's own render pass). FORMA_PROFILE=1 on the inspector example starts a puffin profiler server.

Cargo features

forma-ui, on by default:

  • bundled-fonts: DejaVu Sans and Sans Mono built in, so text is the same on every machine.

forma-ui, opt in:

  • accesskit: the UI as an AccessKit tree for screen readers (on in forma-winit's run).
  • serde: themes, colors, key bindings and layout state saved and read.
  • jiff: the date and time types to and from jiff's, and today's date in the system's time zone.
  • markdown: Markdown shown as widgets, and edited (pulldown-cmark).
  • plot: charts - lines, bars, areas, pies, heatmaps, histograms, box plots, sparklines - and their SVG export (lyon).
  • planning: a kanban board, calendar views and a timeline with a Gantt chart.
  • lucide: the Lucide icon set; an app carries only the icons it uses.
  • bundled-emoji: Twemoji's color emoji built in (about 1.5 MB).
  • pdf: PDF files of a Ui, a draw list or a chart (krilla).
  • syntax: syntax highlighting for text areas (syntect, in pure Rust).
  • graph: a force-directed graph of nodes and links, panned, zoomed and read node by node by screen readers.

forma-wgpu, opt in (nothing is on by default):

  • screenshot: a Ui rendered offscreen, compared with reference images and saved as PNG.
  • export: charts as PNG images (turns on screenshot and forma-ui's plot).

forma-winit, on by default:

  • run: the app runner - windows, a renderer and an event loop for an App, in one call.
  • clipboard: the OS clipboard - text, pictures and HTML - and, with run, watching it for every copy in any app.
  • accesskit: screen readers, through AccessKit, in run.
  • system-settings: light or dark, reduced motion, high contrast and the accent color as the system says, on Linux too.
  • native-menu: the macOS and Windows menu bar.

forma-winit, opt in:

  • system-theme: the same as system-settings, under its 0.2 name.
  • dialogs: the system's file dialogs.
  • notifications: the system's notifications, with actions and answers.
  • tray: an icon in the system tray, with a tooltip and a menu.
  • hotkeys: global hotkeys, which reach the app while another app has the focus.
  • print: printing a PDF through the system.
  • recent-documents: the system's recent documents.
  • single-instance: one instance at a time; a second start hands its arguments to the first.
  • settings-file: the app's settings as one serde struct in a JSON file.
  • open: links in the browser, files with their default app.
  • serde: window and layout state saved and read.
  • markdown: forma-ui's markdown.
  • plot: forma-ui's plot.
  • planning: forma-ui's planning.
  • lucide: forma-ui's lucide.
  • bundled-emoji: forma-ui's bundled-emoji.
  • pdf: forma-ui's pdf.
  • syntax: forma-ui's syntax.
  • graph: forma-ui's graph.

Known limitations

  • Dock panels cannot float in windows of their own.
  • On Linux, menus are drawn in the window (MenuBar); the native menu bar is macOS and Windows only.
  • On Wayland, files cannot be dropped onto a window (winit has no drag and drop there), and window positions are neither reported nor restored.
  • On Wayland, Context::activate cannot raise a window that is open: winit 0.30 has no way to activate it with the token the compositor's focus stealing prevention asks for. A window opened with the token takes the focus. A touch cannot move or resize a window there either.
  • On GNOME the system tray needs the AppIndicator extension; without it there is no tray, and an app can find that out before it relies on one.
  • On GNOME under Wayland (and other compositors without the data-control protocol) the clipboard holds text only: copying or pasting pictures and HTML fails there.
  • Watching the clipboard (Context::watch_clipboard) works on X11 and over Wayland's data-control protocol (KDE Plasma, wlroots desktops): not on GNOME under Wayland, macOS or Windows, where it says so.
  • Notifications on macOS and Windows have no actions, and cannot be replaced or withdrawn.
  • Global hotkeys on Wayland go through the desktop portal's GlobalShortcuts (KDE Plasma, GNOME from 48): the desktop asks the user, who may choose other keys. A desktop without it has none.
  • Printing opens the PDF in the system's PDF viewer on macOS and Windows, and on Linux without the desktop portal's print dialog.
  • Touch: no long press for a context menu and no flicks that keep scrolling; trackpad pinches and turns reach forma on macOS only.
  • A right-to-left layout does not mirror tab dragging, a text area's gutter and lines that do not wrap, or what custom widgets draw. The calendar's week and day views and the timeline run left to right, as their dates do.
  • Markdown is edited as source with a preview, not as formatted text.
  • No web target yet: forma-ui and forma-wgpu compile for wasm32, but run does not work in a browser.
  • The platform services cannot be tested headless (an app tests its side of notifications, the tray, hotkeys and printing with their Memory* backends); they are checked by hand before a release.
  • No screen reader pass yet: the accessibility tree is tested through AccessKit's own consumer, not with Orca, NVDA or VoiceOver.
  • macOS and Windows are compile-checked only: forma builds for both, and CI checks that it does, but it has been tested by hand on Linux only. The native menu, file dialogs, notifications, the tray, hotkeys, printing, recent documents, single instance, saved window positions and the system settings have only been run on Linux.

Development

cargo test --workspace
cargo test -p forma-wgpu -- --ignored   # GPU tests; needs an adapter (lavapipe works)
cargo clippy --workspace --all-targets -- -D warnings
cargo fmt --all --check
cargo bench -p forma-ui                 # criterion benchmarks of the hot paths

Design decisions are recorded in docs/adr. How a release is made: docs/releasing.md.

License

Licensed under either of Apache License, Version 2.0 or MIT license at your option.

forma-ui also carries fonts and icons under licenses of their own, which its license field names too:

  • The DejaVu fonts (bundled-fonts, on by default): the Bitstream Vera and Arev fonts licenses, the DejaVu changes in the public domain - see assets/fonts/README.md and DejaVu-LICENSE.
  • Twemoji (bundled-emoji): the emoji graphics under CC-BY 4.0, in Mozilla's font build under the Apache License 2.0 - see Twemoji.Mozilla-LICENSE.md.
  • Lucide (lucide): ISC, and MIT for the icons it took from Feather - see assets/lucide/LICENSE.

An app built with these features ships them, so it does what their licenses ask where it lists its licenses (an About box, its notices):

  • With bundled-emoji, it credits Twemoji - "Emoji graphics: Twemoji, Copyright Twitter, Inc and other contributors, CC-BY 4.0" - and keeps the font's license file.
  • With bundled-fonts it keeps DejaVu's notice (DejaVu-LICENSE), and with lucide Lucide's (its LICENSE), beside its own.