egui on rails. Documentation: fastframe.dev.
fastframe is the shared foundation of a family of native desktop apps built with Rust and egui: ZapFast, Spotifast, Solco, TonePush, and the Chat with Work local agent. Each piece here was written for one of those apps first and moved here once another app needed it.
- Extract from working apps, never design up front. Code arrives here after it has shipped in an app, not before.
- Shared, then it moves. A piece moves in once at least two apps have it, or once one app has shipped it and the maintainer judges it general enough for the others.
- Small crates, one workspace. Each piece is its own
fastframe-*crate, so an app takes only what it uses. - Good defaults, few knobs. A crate does the right thing with no configuration. Apps keep their own decisions (branding, layout, product choices); fastframe does not make them.
- No telemetry, no hosted services. Nothing here phones home or depends on a server run by us.
- Fix upstream. Bugs in egui, winit and friends are fixed in those projects, not carried here as patches forever.
| Crate | What it does |
|---|---|
fastframe-text |
Follows the desktop's font rendering settings (hinting, antialiasing, sub-pixel positioning, text weight) in egui, and snaps hand-placed text to whole pixels. |
fastframe-fonts |
Bundled Inter (tabular figures) or the platform's own interface face at the app's weights, and installed fonts for every script it lacks, aligned to its baseline. |
fastframe-emoji |
Colour emoji in the platform's own style (Apple, Segoe UI, the desktop's) with a bundled fallback, in every egui text through an egui plugin or inline as selectable placeholders, drawn off the interface thread. |
fastframe-icons |
Embedded SVG icon sets for egui: the icons! macro, a bytes loader that survives reduce_texture_memory, and the Lucide icons the apps share. |
fastframe-theme |
JSON colour palettes, a catalogue loaded off the interface thread, the eight shared palettes, following the Omarchy desktop's theme with filesystem notifications, and revealing a change of colours from the middle of the window outwards. |
fastframe-i18n |
Bundled gettext catalogs: a build-time PO compiler for build.rs, gettext/pgettext/ngettext lookups, system language detection, and the template update script. |
fastframe-log |
Logs to stderr and a per-run file for bug reports, rewrites private targets, and records panics without their payload. |
fastframe-tray |
A tray item with the app's own menu: StatusNotifier on Linux, the notification area on Windows, the menu bar on macOS, with native menus and Dock reopening across window lifecycles. |
fastframe-dock |
An embedded Dock icon for unbundled macOS runs, and unread badges on the macOS Dock, compatible Linux launchers and the Windows taskbar. |
fastframe-titlebar |
The title bar an app draws itself: a header that moves the window and answers a double-click as each platform does, room kept for the buttons (the traffic lights on macOS), and on Windows and Linux caption buttons in the desktop's layout and resizable edges. |
fastframe-shell |
Keeps an app running without a window around eframe::run_native, starts hidden, reopens windows at their remembered size, place and zoom, brings back windows restored off-screen, asks niri to raise the window, and logs updates that freeze it. |
fastframe-audio |
Audio output with the app's own renderer: a stream that gets no callbacks while paused, follows the default device, reopens after failures, and keeps a clock of what has played. |
fastframe-scroll |
Scrolling that feels like the platform's: a 120-point wheel step, and Linux touchpad gestures that keep their speed, glide after the lift, and hold one axis. |
fastframe-instance |
One running copy per user: a crash-safe lock, and a private channel a second launch hands its request over (show the window, open a link, any line the app understands). |
fastframe-now-playing |
The desktop's media controls: what is playing, with artwork, in MPRIS, the Windows media overlay and the macOS Now Playing panel, and the media keys and buttons back. |
fastframe-power |
The device's power, session and network state: energy saver, battery, display, lock, the network's reach and cost, sleep and wake with time to save first, EcoQoS in the background on Windows, and keeping audio out of App Nap on macOS. |
fastframe-file |
Replaces a file without ever leaving it half written: a temporary file beside it, flushed, then moved over it, privately if asked, on a thread of its own so the window never waits for the disk. |
fastframe-desktop |
Who the app is to the desktop and what it says: the desktop entry a Flatpak exports, whether the session is GNOME, a desktop setting read through the portal (no gsettings process), and macOS notification permission guarded by the real application bundle. |
fastframe-notifications |
Native notifications for GUI apps, TUIs and daemons: bounded delivery, click actions, replacement, withdrawal, permissions and platform identity. |
fastframe-update |
Self-update from GitHub releases: detects package-managed installs, verifies signed checksums, installs through a helper with rollback, keeps the handoff format older app versions use, moves renamed installations onto the new name with their launchers, and offers an opt-in pre-release channel. |
Early, extracted piece by piece. APIs will change while the first crates settle; nothing is on crates.io yet.
Depend on a git revision for now:
[dependencies]
fastframe-text = { git = "https://github.com/crmne/fastframe", rev = "<commit>" }Pin a revision rather than a branch, and update it deliberately.
The apps build against forks of egui and winit that carry fixes waiting for
upstream releases. A library cannot pin those: Cargo applies [patch] only in
the root workspace of the app being built, so each app keeps its own
[patch.crates-io] section. fastframe crates depend on the published egui
versions, and they must compile against both the release and the apps' fork.
Every app on fastframe should use the revisions the apps use, so it gets the
same fixes. Copy this into the app's root Cargo.toml:
[patch.crates-io]
ecolor = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
eframe = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
egui = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
egui-wgpu = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
egui-winit = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
egui_extras = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
egui_glow = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
emath = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
epaint = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
epaint_default_fonts = { git = "https://github.com/crmne/egui", rev = "ba6790fe7cf46e58e8d27ce1524cbfdee745e938" }
winit = { git = "https://github.com/crmne/winit", rev = "ed7caa9023f10b397f5b6ec8284a840cbd8a6f65" }Patch every egui crate the app uses from the same revision, so they share one
emath and epaint (add egui_kittest if the app's tests use it), and move
egui and winit together: the egui revision is built against that winit, and
moving one alone does not build. An app that compensated for the zoom shrink
itself (a window_builder that scales the restored size) drops that code when
it moves to egui ba6790fe or later, or the size is scaled twice. Cargo warns
about a patch for a crate the app does not use (egui_extras, say); leave
that line out. With egui ba6790fe or later (emilk/egui#8621), a window or
menu is left out of the accessibility tree on the frame that sizes it, so a
test that reads the tree right after opening one runs one more frame first.
What the forks carry, and where each patch stands upstream. Each patch goes upstream as a pull request and stays in the fork until an egui or winit release includes it; then the fork drops it.
| Fix | Fork | Upstream |
|---|---|---|
| Right-to-left text shaped in its direction | egui 6147de7b, 8e592348 |
emilk/egui#8577, open |
| No busy loop while waiting for a redraw | egui f14640be |
emilk/egui#8398, merged, not yet released |
| No freeze when a Wayland window is hidden: frames paced by the compositor's callbacks | egui 41ff9ddf |
emilk/egui#8631, open |
The same, eframe's side of winit's Occluded |
egui 0b431145 |
emilk/egui#8660, a draft that waits for winit#4710 |
The xdg-shell suspended state reported as Occluded |
winit a51e41b2 |
rust-windowing/winit#4709 and #4710 (0.30), open |
| Paste and file drops together on Wayland: one data device for both (crmne/spotifast#614) | winit 1a8306ad, fb8b24c3; egui 2ab31332 |
rust-windowing/winit#4729 (0.30), open; on winit's main branch, the clipboard pull request #4658 uses the same data device. emilk/egui#8657, a draft that waits for winit#4729 |
| Each emoji one emoji wide (families, skin tones, keycaps, flags) | egui 73e8b6c3 |
not proposed: egui's main branch already draws each of these one emoji wide through the system's colour emoji fonts, and the patch only changes the monochrome fonts an app opts into |
| No runaway resizing on Windows when a window moves between monitors of different scale | winit f4fed12c |
on winit's main branch (39c4009c); the fork carries it for 0.30 |
| macOS Quit through close requests, behind a feature | winit a7b78b27 |
rust-windowing/winit#4692, open |
| A focus request for a widget not drawn yet no longer crashes accessibility on Windows and macOS | egui 4c143c1d |
emilk/egui#8621, merged, not yet released |
| Windows starts on OpenGL drivers older than 3.3: a compatibility-profile fallback | egui 57eb5714 |
emilk/egui#8655, open |
| A window keeps its size across restarts at any interface zoom | egui 56e7ac3c |
emilk/egui#8656, open |
| A closed window leaves the screen on Touch Bar Macs | winit ed7caa90 |
rust-windowing/winit#4728, a draft until confirmed on a Touch Bar Mac |
On stock egui and winit, fastframe still builds and works, without these fixes.
A future forks.toml, with a generator that writes each app's patch section
and a CI check that they agree, will keep those pins in one place. It does not
exist yet; until then, this section has the current revisions, and a fastframe
release that needs newer ones says so in its notes.
cargo fmt --all --check
cargo clippy --locked --all-targets -- -D warnings
cargo clippy --locked --all-targets --all-features -- -D warnings
cargo test --locked --all-targets --all-features
RUSTDOCFLAGS='-D warnings' cargo doc --locked --all-features --no-depsMIGRATION.md lists what each app replaces with each crate.
See AGENTS.md for the rules contributors and coding agents follow.
MIT. See LICENSE.