Telar
A modular Rust UI framework with its own template language, reactive signals and a self-contained renderer.
Telar draws every pixel itself — there is no webview and no native widget toolkit underneath. Components are written in .rsx, an indentation-based template language that compiles to plain Rust at build time, so what ships is a single binary with no runtime interpreter.
Early days. Telar is at
0.1.4. The APIs work and are exercised by the apps in this repo, but they will keep moving before1.0.
A component
[logic]
#[derive(Default)]
pub struct Props {
pub icon: &'static str,
pub title: &'static str,
pub body: &'static str,
}
[view]
box fill:surface stroke:border radius:16 width:300 pad:24 gap:10 direction:col
text "{props.icon}" size:32
text "{props.title}" size:18 color:dark
text "{props.body}" size:14 color:muted
[preview "Fast"]
feature_card icon:"⚡" title:"Fast" body:"Software and wgpu renderers with dirty tracking."
A .rsx file has up to four sections: [logic] for verbatim Rust (a pub struct Props declares the component's props), [style] for constants and reusable style classes, [view] for the node tree, and [preview] blocks that the tooling can render in isolation.
Getting started
Add a telar.toml next to your Cargo.toml:
[]
= "auto"
= true
[]
= "my-app"
= 1200
= 800
Declare the app in src/lib.rs — telar::app! wires the theme, the startup hook, the window config and the root component:
app!;
Then:
apps/sandbox in this repo is the reference app and covers most of the surface.
Build profiles
Copy these into your workspace root Cargo.toml — Cargo ignores [profile.*] in a member crate. cargo telar build always implies --release.
[]
= 1
= "line-tables-only"
# Dependencies compile once and are not rebuilt as you edit, and they are not what you are stepping
# through: optimising them is paid for on the first build and buys a renderer and a layout engine that
# run at a usable speed in dev, and dropping their debug info takes 140 MB out of a cdylib that gets
# rewritten on every reload. Your own crates keep theirs, so panics in your code still name a line.
[]
= 3
= false
[]
= 3 # "s" or "z" for a smaller binary instead — matters most on Android
= "fat" # "thin" keeps most of the win for a fraction of the build time
= 1 # better codegen, no parallelism left in that stage
= "symbols" # smaller binary; release backtraces lose function names
debug decides how much the link has to write: false (addresses only), "line-tables-only" (file and line, no debugger) or true (full, debugger-ready). "line-tables-only" is worth keeping for your own crates — a panic still names the line it came from, without carrying what only a debugger reads — and false is worth setting for everything else, which is why the two blocks above differ. Measured on a real app: the rebuild drops ~14 % and the cdylib goes from 154 MB to 15 MB, with panics in the app's own code unchanged. Setting debug = false on [profile.dev] as well takes it to 0.7 MB and saves another 15 ms, which is inside the noise and not worth the panic locations.
Do not set panic = "abort". Telar recovers from two kinds of panic and both need unwinding: a widget handler, effect or render that panics unmounts only that surface and leaves the rest of the application running; and a wgpu validation error or lost device — a transient swapchain mismatch while a compositor resizes a just-opened window is the common case — is caught on the render thread, which drops that one frame and recovers on the next. Under abort both are process death. If binary size is the goal, opt-level = "z" and strip give you more of it with nothing load-bearing attached.
Faster rebuilds
A hot reload is rustc on the crate you edited plus a full relink of the cdylib, and only the first half gets cheaper the smaller your edit is. On apps/sandbox (155 MB cdylib) the rebuild is ~2.0 s, of which ~0.86 s is the link.
The default linker on Linux (GNU ld) works single-threaded; mold and lld parallelise it and take that link to ~0.70 s — about 8 % of the rebuild. Install one (apt install mold, dnf install mold, pacman -S mold, …) and point Cargo at it in <your-project>/.cargo/config.toml:
[]
= ["-C", "link-arg=-fuse-ld=mold"]
Scope it to the host triple rather than to build.rustflags, or an Android build will try to link with the host's linker instead of the NDK's. lld works the same way with -fuse-ld=lld, and needs ld.lld on PATH — the driver's name, not the package's. macOS has shipped a parallel linker of its own since Xcode 15 and needs none of this; MSVC takes no -fuse-ld= at all. cargo telar doctor reports whether one is installed; it never selects or installs anything.
What's in the box
- Reactive signals — a fine-grained graph of signals, memos and effects; no virtual DOM, no diffing.
- Two renderers — a CPU rasterizer on
tiny-skiaand a GPU one onwgpu, both behind the same drawing vocabulary, selected bybackend = "auto" | "hardware" | "software". - Flexbox and grid layout on top of Taffy, with reactive writing direction (LTR/RTL).
- Motion — tweens and springs driven by one frame ticker, with colors interpolated in Oklch.
- Theming — theme tokens plus light/dark mode that can follow the OS.
- Internationalization — translation catalogs baked at build time;
t!validates keys and arguments at compile time. - Navigation — a reactive page stack with animated transitions.
- Hot reload in
cargo telar dev, and an in-app devtools overlay for inspecting the live component tree. - Packaging to native installers per platform, plus Android APKs.
Targets desktop (Linux, macOS, Windows) and Android.
Editor support
The VS Code extension provides syntax highlighting, snippets, diagnostics, completion and component preview, backed by the telar-analyzer language server. The extension bundles a prebuilt server binary, so no extra install step is needed.
Crates
Everything is published under the telar- prefix. Most users only need the telar facade, which re-exports the runtime behind feature flags.
| Crate | Purpose |
|---|---|
telar |
The facade: re-exports the runtime and the app!/t! macros |
cargo-telar |
cargo telar — dev server, previews, packaging |
telar-reactive-core |
Signals, memos, effects, batching |
telar-geometry-core |
Points, rects, transforms, border radii, Oklch color |
telar-layout-core · telar-layout-reactive |
Flexbox/grid engine and its reactive context |
telar-motion-core |
Tweens, springs, the frame ticker |
telar-theme-core |
Theme tokens, light/dark mode |
telar-ui-core · telar-ui-tree · telar-ui-components |
Widget kernel, component tree, widget catalogue |
telar-renderer-core |
Draw commands, culling, dirty tracking |
telar-renderer-software · telar-renderer-hardware |
CPU and wgpu backends |
telar-renderer-text · telar-renderer-assets |
Text shaping and glyph atlas; SVG/PNG/JPEG decoding |
telar-platform-core and telar-platform-{winit,desktop,android,headless} |
Window/event abstraction and its backends |
telar-parser · telar-transpiler · telar-macros |
The .rsx pipeline |
telar-i18n-core · telar-navigate-core · telar-services-core |
i18n runtime, navigation, platform paths and DI |
telar-diagnostics |
Shared tooling (the devtools overlay lives in telar behind dev) |
telar-analyzer lives in this repo but is distributed as a binary through GitHub Releases and the VS Code extension rather than crates.io.
Minimum supported Rust version
Rust 1.95. Bumping it is a minor-version change.
License
Licensed under either of Apache License, Version 2.0 or MIT license at your option.
Unless you explicitly state otherwise, any contribution intentionally submitted for inclusion in this project by you, as defined in the Apache-2.0 license, shall be dual licensed as above, without any additional terms or conditions.