- Rust 99.8%
- WGSL 0.2%
- 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 |
||
|---|---|---|
| .forgejo/workflows | ||
| crates | ||
| docs | ||
| tools/lucide-gen | ||
| .gitignore | ||
| Cargo.lock | ||
| Cargo.toml | ||
| CHANGELOG.md | ||
| LICENSE-APACHE | ||
| LICENSE-MIT | ||
| README.md | ||
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'srun).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 aUi, 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: aUirendered offscreen, compared with reference images and saved as PNG.export: charts as PNG images (turns onscreenshotand forma-ui'splot).
forma-winit, on by default:
run: the app runner - windows, a renderer and an event loop for anApp, in one call.clipboard: the OS clipboard - text, pictures and HTML - and, withrun, watching it for every copy in any app.accesskit: screen readers, through AccessKit, inrun.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 assystem-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'smarkdown.plot: forma-ui'splot.planning: forma-ui'splanning.lucide: forma-ui'slucide.bundled-emoji: forma-ui'sbundled-emoji.pdf: forma-ui'spdf.syntax: forma-ui'ssyntax.graph: forma-ui'sgraph.
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::activatecannot 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
rundoes 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-fontsit keeps DejaVu's notice (DejaVu-LICENSE), and withlucideLucide's (itsLICENSE), beside its own.