A declarative UI framework in Swift, rendering through Vulkan.
The current application-facing API is documented in docs/api.md. Coding agents: start from AGENTS.md (architecture, layout, build, conventions).
Views are described the way SwiftUI describes them — a body returning nested
value types — but the whole stack underneath is here: layout via Yoga, text via
HarfBuzz and FreeType, and a single Vulkan pipeline that draws everything.
struct Counter: View {
@State private var count = 0
var body: some View {
VStack(padding: 8) {
Text("count: \(count)", color: .accent)
Text("[ increment ]", onClick: { count += 1 })
}
}
}On a clean Debian or Arch machine the whole stack — packages, wlroots 0.19, NPRPC, compositor, Swift clients — is:
./scripts/bootstrap.sh --yes # or --release
./scripts/check-env.sh # what the build can seeThat is documented in docs/install.md (Docker images and the QEMU install-test VM live there too). The short form if the packages are already on the machine:
swift build # Swift + C++ canvas engine (SwiftPM compiles both)
swift run HelloWorld # demo
swift run LavaSpotify # music player (see docs/lavaspotify.md)
swift run LavaTerm # terminal emulator (click the grid, type)
swift test # headless tests, no GPU neededSystem packages: Vulkan, GLFW, FreeType, HarfBuzz (and on Linux for global menus: GLib + libdbusmenu-glib). No Meson/Ninja — SwiftPM builds the C++ engine.
Linux only today.
LavaUI is a normal SwiftPM product of this repo. SwiftPM also builds the nested
canvas package (C++ engine + shader resources) from the same checkout — you
only declare a dependency on this repository.
./scripts/install-deps.sh --yes # Debian/Ubuntu or Arch; see packaging/deps/The compositor and NPRPC need more than the engine (wlroots 0.19, Boost,
dbusmenu, rsvg, …). install-deps.sh is the list. A windowed
LavaUI app that will never talk to the compositor can get by with Vulkan,
GLFW, FreeType, HarfBuzz and libdbusmenu-glib alone.
You also need a working Vulkan ICD (e.g. vulkan-radeon, nvidia-utils,
vulkan-intel) and a Swift 6.3 toolchain (scripts/install-swift.sh).
mkdir MyApp && cd MyApp
swift package init --type executableEdit Package.swift:
// swift-tools-version: 6.0
import PackageDescription
let package = Package(
name: "MyApp",
platforms: [.macOS(.v13)], // ignored on Linux; keeps the manifest valid
products: [
.executable(name: "MyApp", targets: ["MyApp"]),
],
dependencies: [
// Prefer a tag once you pin releases:
// .package(url: "https://github.com/nikitapn/LavaUI.git", from: "0.1.0"),
.package(url: "https://github.com/nikitapn/LavaUI.git", branch: "main"),
],
targets: [
.executableTarget(
name: "MyApp",
dependencies: [
// Package identity = last path component of the URL ("LavaUI").
.product(name: "LavaUI", package: "LavaUI"),
],
swiftSettings: [
// Required: LavaUI talks to the C++ engine via C++ interop.
.interoperabilityMode(.Cxx),
]
),
],
// Match the engine (std::expected / C++23).
cxxLanguageStandard: .gnucxx2b
)Local clone instead of GitHub:
.package(path: "../LavaUI"), // folder name becomes the package id
// then: .product(name: "LavaUI", package: "LavaUI")Replace the generated source with something like
Sources/MyApp/MyApp.swift:
import Foundation
import LavaUI
@main
struct MyApp {
static func main() {
guard let editor = LavaApp.open(title: "My App") else {
exit(1)
}
LavaApp.run(editor: editor) {
VStack(padding: 12) {
Text("hello from LavaUI", color: .accent)
}
}
}
}swift build
swift run MyAppSwiftPM will fetch this repo (including canvas/), compile Yoga + the Vulkan
engine, pack checked-in SPIR-V and default fonts, and link system libraries via
pkg-config. You do not need Meson, Ninja, or a prebuilt libcanvas.
Framework fonts and engine shaders ship with LavaUI / canvas. Your images belong on your executable:
// Package.swift — on the MyApp target:
resources: [
.process("Resources"),
],
// Load at runtime:
let icon = ImageStore.loadAsset(
named: "icon.png",
bundle: .module,
into: editor
)Put files under Sources/MyApp/Resources/.
The repo has a second half: a Wayland compositor under compositor/ —
C++23 on wlroots 0.19 — that runs a desktop of LavaUI apps, and everything else
Linux runs alongside them through xdg-shell and Xwayland.
compositor/scripts/dev-run # nested in the Wayland session you are in
compositor/scripts/dev-run -H # headless, software rendering, no window
compositor/scripts/dev-run -r # release build, compositor and shell alike
compositor/scripts/dev-run -- env LAVA_CLIENT=1 ./.build/debug/LavaTerm
compositor/scripts/start-lava-compositor setup # a real session, on a real GPUThe compositor's own README (compositor/README.md).
The view tree is retained; the draw list is immediate.
A View is a struct rebuilt whenever something changes. Behind it sits a
persistent node tree that owns identity, @State storage, Yoga nodes, cached
text measurements, and observation subscriptions. Rebuilding a view does not
rebuild that tree — it reconciles against it.
Identity is structural: the tree's shape is encoded in its types, so
TupleView<Text, Button> reconciles positionally with no keys and no diffing.
Only EitherView (an if) and ForEach (keyed) need real reconciliation.
Each frame that something changed:
body recompute (only nodes whose observed state changed)
→ Yoga layout (only dirty subtrees)
→ draw list emission (a flat POD buffer)
→ one Vulkan pipeline, in index order
The loop is frame-driven, not event-driven. A state change sets a dirty
flag; nothing walks the graph synchronously. The loop blocks in
glfwWaitEvents until input arrives, so an idle window costs nothing.
Everything draws through one ordered batch stream. Rectangles, rounded
rectangles, circles, stroked segments and glyphs share the quad pipeline:
shapes use a rounded-box signed distance field and glyphs sample an R8 atlas.
Large connected polylines switch to a dedicated LINE_STRIP pipeline and then
switch back without leaving the stream. Paint order remains emission order — a
caret can cover its own glyphs and a popup can cover a chart. Batches break on
scissor, texture, or pipeline changes.
Swift owns everything above the pixels. Layout, hit testing, text shaping, and input routing are Swift. C++ receives a draw command buffer and knows nothing about widgets. The rule for what stays in C++: retain what is expensive to build and keyed by content (the glyph atlas, Vulkan objects); re-emit everything keyed by position or structure.
| Target | Contains | Depends on |
|---|---|---|
LavaText |
Editing logic: cursors, selection, undo, word/line navigation, soft wrap, syntax rules, search | nothing |
LavaMenu |
Application menu IR + declarative DSL (MenuBar / MenuItem); no drawing |
nothing |
LavaUI |
Views, Yoga layout, draw list, fonts, input, theming | LavaText, LavaMenu, CxxCanvas, CYoga |
HelloWorld |
Demo app (DemoExample) and an FBD diagram editor |
LavaUI, FBDModel |
LavaSwitcher |
3D Ctrl+Tab / Mod+Tab app switcher (live window posters) | LavaUI, LavaClient |
LavaSpotify / SpotifyApp |
LavaSpotify UI + Connect control of spotifyd | LavaUI, SpotifyCore |
SpotifyCore |
Spotify Web API, OAuth, cover download (no Vulkan) | nothing |
LavaTerm / LavaTermApp |
Terminal emulator (PTY + ANSI + Canvas grid) | LavaUI, LavaTermCore |
LavaTermCore |
VT grid + ANSI parser (headless, unit-tested) | nothing |
canvas/ (package) |
C++ engine (CxxCanvas) + Yoga (CYoga), built by SwiftPM |
system Vulkan/GLFW/FreeType/HarfBuzz |
compositor/ |
Wayland compositor and control-plane servant — C++23, built by meson rather than SwiftPM | wlroots 0.19, canvas/, NPRPC |
LavaTaskbar LavaDock LavaLauncher LavaSettings LavaDebug |
The desktop shell — panel, dock, launcher, settings, GPU inspector. Ordinary LavaUI clients, with no privileges the demo does not have | LavaUI, LavaClient |
LavaText and LavaMenu having no dependencies at all is deliberate:
editing logic and menu IR are where fiddly correctness lives, and keeping them
out of reach of Vulkan and C++ interop means they are tested headlessly. That
is enforced by the build graph rather than by discipline.