Enterprise-grade native UI components for Rust desktop applications.
Pure Rust. GPUI native. Element Plus-inspired APIs. No Tauri, no WebView, no browser runtime.
- What is Liora?
- What you can build
- Requirements
- Choose the right dependency setup
- GPUI dependency and local patch policy
- Quick start: create a Liora app
- Application initialization
- Window startup, system theme, and icons
- Using the Liora modules
- Component examples
- Advanced usage
- Internationalization and locales
- Component catalog
- Native packaging
- Troubleshooting
- Quality gates
- Technical differentiators
- Documentation maintenance rule
- License
Liora is a native Rust + GPUI component SDK for building polished desktop applications. It provides a one-stop liora facade crate plus focused modules for core runtime setup, theme tokens, components, icons, tray integration, package metadata, and GitHub Release update flows.
Liora is intentionally not a web application shell:
- no Tauri runtime;
- no WebView, HTML/CSS/DOM, or browser application shell;
- no web chart runtime or frontend bundler;
- Gallery and Docs are real native GPUI applications that use the same public SDK surface as downstream apps.
Use Liora when you want a Rust desktop app with:
| Need | Liora answer |
|---|---|
| Native desktop UI | GPUI windows, GPUI element trees, native text/layout/paint paths. |
| Enterprise component coverage | Element Plus-inspired components across layout, forms, overlays, navigation, data display, charts, and advanced inputs. |
| One-line app initialization | liora::init_liora(cx) initializes core state, component services, and key bindings. |
| Light/Dark/System theming | ThemeMode, semantic tokens, runtime switching, and system appearance tracking. |
| System tray apps | liora-tray wraps tray-icon and muda with stable app commands. |
| Native release artifacts | liora-packager + xtask package validate and generate package plans for Linux, macOS, and Windows. |
| Updater integration | liora-updater checks GitHub Releases, selects assets, verifies SHA-256, and returns explicit install plans. |
| Item | Requirement |
|---|---|
| Rust | rustc 1.95+, Rust edition 2024. |
| UI backend | Official Zed GPUI git dependency pinned to Liora's verified revision. |
| Linux native deps | GTK3, Wayland/X11, xkbcommon, fontconfig/freetype, Vulkan, ALSA, pkg-config; see scripts/install-fedora-deps.sh for a Fedora-oriented baseline. |
| macOS | Apple Silicon is covered by the release workflow; install Xcode Command Line Tools. |
| Windows | MSVC toolchain; GPUI's Windows backend provides the application manifest through windows-manifest. |
Most applications should depend on the facade crate:
[dependencies]
liora = "0.2"Use focused crates only when you deliberately want a narrower surface:
[dependencies]
liora-components = "0.2"
liora-core = "0.2"
liora-theme = "0.2"
liora-icons = "0.2"
liora-icons-lucide = "0.2"
liora-icons-antd = "0.2"
liora-icons-ionic = "0.2"
liora-icons-tabler = "0.2"
liora-icons-carbon = "0.2"
liora-icons-material = "0.2"
liora-tray = "0.2"
liora-updater = "0.2"
liora-packager = "0.2"The facade re-exports stable module names:
use liora::{components, core, icons, icons_lucide, icons_antd, icons_tabler, theme, tray};
use liora::prelude::*;
#[cfg(feature = "updater")]
use liora::updater;
#[cfg(feature = "packager")]
use liora::packager;If you do not need packaging or updater helpers in your app dependency graph, turn off facade defaults and re-enable only what you need:
[dependencies]
liora = { version = "0.2", default-features = false }
# Or keep only updater helpers:
liora = { version = "0.2", default-features = false, features = ["updater"] }Liora uses official Zed GPUI only from the official Zed upstream repository. Do not use renamed or community forks such as open-gpui.
Why the extra GPUI setup exists:
lioracrates are published on crates.io.- Current Liora development targets a newer official Zed GPUI git revision than the old registry
gpui 0.2.2fallback. - Cargo does not let a crates.io package force a git-only transitive dependency on every downstream app.
- Therefore published Liora crates use Cargo's multiple-location dependency form: registry fallback for publication, official Zed git rev for local development.
- Final applications must add a root-level
[patch.crates-io]entry so every transitivegpuidependency resolves to the official Zed commit.
Use this application manifest pattern:
[package]
name = "acme-notes"
version = "0.2.0"
edition = "2024"
publish = false
[dependencies]
liora = "0.2"
# Add gpui manually when your crate mentions gpui types directly:
# - gpui::App / Window / Context / Render / RenderOnce
# - gpui::div(), px(), size(), Entity<T>
# - function signatures such as fn render(..., cx: &mut gpui::Context<Self>)
gpui = { version = "0.2.2", default-features = false }
# Add gpui_platform manually in final binary crates that create native windows
# with gpui_platform::application().run(...).
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
gpui = { version = "0.2.2", default-features = false, features = ["wayland", "x11", "font-kit"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false, features = ["wayland", "x11", "font-kit"] }
[target.'cfg(target_os = "macos")'.dependencies]
gpui = { version = "0.2.2", default-features = false, features = ["font-kit"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false, features = ["font-kit"] }
[target.'cfg(target_os = "windows")'.dependencies]
gpui = { version = "0.2.2", default-features = false }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false }
[patch.crates-io]
gpui = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718" }When do you manually add gpui?
// You need a direct gpui dependency because this file names gpui types and macros.
use gpui::{App, Context, IntoElement, Render, Window, div, px};
use liora::components::{Button, Title};
struct RootView;
impl Render for RootView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div()
.p(px(24.0))
.child(Title::new("Acme Notes").h2())
.child(Button::new("Create note").primary())
}
}
fn needs_gpui_app_type(_cx: &mut App) {}When can you avoid direct gpui usage? Very small helper crates that only build data models, theme values, update requests, or packaging metadata can depend on focused Liora crates without opening windows or naming gpui types.
The repository keeps third_party/zed only as non-published upstream-source reference material for prior Linux startup-window patch work and PR comparison. Current development should use the official zed-industries/zed git dependency above. If a temporary local patch is needed for app-only verification, keep it outside publishable SDK manifests and document the boundary.
cargo new acme-notes
cd acme-notesPaste the manifest from GPUI dependency and local patch policy, or start with this compact Linux/macOS/Windows manifest:
[package]
name = "acme-notes"
version = "0.2.0"
edition = "2024"
publish = false
[dependencies]
liora = "0.2"
gpui = { version = "0.2.2", default-features = false }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false }
[target.'cfg(any(target_os = "linux", target_os = "freebsd"))'.dependencies]
gpui = { version = "0.2.2", default-features = false, features = ["wayland", "x11", "font-kit"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false, features = ["wayland", "x11", "font-kit"] }
[target.'cfg(target_os = "macos")'.dependencies]
gpui = { version = "0.2.2", default-features = false, features = ["font-kit"] }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false, features = ["font-kit"] }
[target.'cfg(target_os = "windows")'.dependencies]
gpui = { version = "0.2.2", default-features = false }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false }
[patch.crates-io]
gpui = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718" }use gpui::{App, AppContext, Context, IntoElement, Render, Window, WindowOptions, div, px};
use liora::components::{Button, Card, Space, Tag, Text, Title};
use liora::init_liora;
struct RootView;
impl Render for RootView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
div().p(px(24.0)).child(
Card::new(
Space::new()
.vertical()
.child(Title::new("Acme Notes").h2())
.child(Text::new("A native Rust desktop app powered by GPUI and Liora."))
.child(
Space::new()
.child(Button::new("New note").primary())
.child(Button::new("Import"))
.child(Tag::new("Pure Rust").success()),
),
)
.title("Welcome")
.width_lg(),
)
}
}
fn main() {
gpui_platform::application().run(|cx: &mut App| {
// One call initializes theme/config state, overlay/message services,
// and key bindings for interactive Liora controls.
init_liora(cx);
let _ = cx.open_window(
WindowOptions {
titlebar: Some(gpui::TitlebarOptions {
title: Some("Acme Notes".into()),
..Default::default()
}),
..Default::default()
},
|_, cx| cx.new(|_| RootView),
);
});
}cargo runInside this repository:
cargo run -p liora-gallery
cargo run -p liora-docsliora-gallery is the component showcase and app-shell reference. liora-docs is the native documentation app and Markdown renderer.
Use the facade entry points for normal app binaries:
use gpui::App;
use liora::{FontConfig, FontWeight, Options};
use liora::{ThemeMode, init_liora, init_liora_with_mode, init_liora_with_options};
fn init_default(cx: &mut App) {
// Recommended default: follow the operating system theme.
init_liora(cx);
}
fn init_dark(cx: &mut App) {
// Explicit startup mode.
init_liora_with_mode(cx, ThemeMode::Dark);
}
fn init_with_system_font_names(cx: &mut App) {
// No font files are loaded here. GPUI resolves these names from the OS.
let fonts = FontConfig::system()
.with_ui_families(["Segoe UI", "MiSans", "Arial"])
.with_ui_weight(FontWeight::MEDIUM)
.with_code_families(["JetBrains Mono", "SF Mono", "Monospace"]);
init_liora_with_options(cx, Options::system().with_fonts(fonts));
}If you depend on focused crates instead of the facade, use the matching component initializer:
use gpui::App;
use liora_components::{ThemeMode, init_liora, init_liora_with_mode};
fn init_components_only(cx: &mut App) {
init_liora(cx);
init_liora_with_mode(cx, ThemeMode::System);
}Important distinction:
// High-level app setup: core theme + portals + MessageManager + component key bindings.
liora::init_liora(cx);
liora_components::init_liora(cx);
// Lower-level core setup only: use when building a custom component crate or replacing services yourself.
liora_core::init_liora_with_mode(cx, liora_core::ThemeMode::System);A production app should create the window hidden, attach system theme tracking before creating the root view, then activate the window after open_window returns. This avoids first-frame theme flicker and mirrors the pattern used by the native Gallery and Docs apps.
On Windows release builds, use the same subsystem setting as Zed so launching the GUI .exe does not create a blank console window:
#![cfg_attr(not(debug_assertions), windows_subsystem = "windows")]WindowFrameMode::Custom follows Zed's GPUI compatibility model: Windows/macOS hide the native titlebar through TitlebarOptions::appears_transparent when the window is created; Linux/FreeBSD use WindowDecorations::Client. That means Windows/macOS frame-mode changes need a window reopen, while Linux can request decorations live through request_window_frame_mode.
use gpui::{App, AppContext, Context, Render, Window, WindowOptions, px, size};
use liora::components::{Title, apply_window_frame_mode, WindowFrameMode};
use liora::{attach_system_theme_observer, init_liora, startup_maximized_window_bounds};
struct RootView;
impl Render for RootView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
Title::new("Maximized Liora window").h2()
}
}
fn main() {
gpui_platform::application().run(|cx: &mut App| {
init_liora(cx);
let options = apply_window_frame_mode(
WindowOptions {
show: false,
app_id: Some("acme-notes".into()),
window_bounds: Some(startup_maximized_window_bounds(
cx,
size(px(1440.0), px(900.0)),
)),
titlebar: Some(gpui::TitlebarOptions {
title: Some("Acme Notes".into()),
..Default::default()
}),
..Default::default()
},
WindowFrameMode::System,
);
if let Ok(handle) = cx.open_window(options, |window, cx| {
attach_system_theme_observer(window, cx);
cx.new(|_| RootView)
}) {
let any_handle: gpui::AnyWindowHandle = handle.into();
let _ = any_handle.update(cx, |_, window, _| window.activate_window());
}
});
}On Linux/Wayland, taskbar icons are resolved by desktop identity (app_id + .desktop + icon theme), not by setting a window icon directly. Liora exposes helpers used by Gallery/Docs; downstream apps can use the same pattern with app-owned icon assets:
use liora::core::{
LinuxDesktopIdentity, LinuxDesktopPngIcon, ensure_linux_desktop_identity,
linux_desktop_entry, linux_desktop_png_icon_path,
};
fn register_linux_identity() {
let icon_name = "acme-notes";
let desktop_entry = linux_desktop_entry(
icon_name,
"Acme Notes",
"Native notes app built with Liora",
icon_name,
);
let _ = ensure_linux_desktop_identity(LinuxDesktopIdentity {
app_id: icon_name,
desktop_entry: &desktop_entry,
png_icons: &[LinuxDesktopPngIcon {
size: 512,
bytes: include_bytes!("../assets/acme-notes-512.png"),
}],
});
let _icon_path = linux_desktop_png_icon_path(icon_name, 512);
}The recommended app dependency. It re-exports:
use liora::{init_liora, init_liora_with_mode, init_liora_with_options};
use liora::{FontConfig, Options, ThemeMode};
use liora::{components, core, icons, icons_lucide, icons_antd, icons_tabler, theme, tray};Core runtime, theme config, window helpers, Linux desktop identity, popper/portal state, unique IDs, and theme switching:
use liora::core::{apply_theme_mode, sync_system_theme, ThemeMode};
fn set_dark(window: &mut gpui::Window, cx: &mut gpui::App) {
apply_theme_mode(window, cx, ThemeMode::Dark);
}
fn follow_system_again(window: &mut gpui::Window, cx: &mut gpui::App) {
apply_theme_mode(window, cx, ThemeMode::System);
sync_system_theme(window, cx);
}Semantic tokens and shared component enums:
use liora::theme::{ButtonSize, ButtonVariant, Theme};
let light = Theme::light();
let dark = Theme::dark();
let primary_variant = ButtonVariant::Primary;
let large = ButtonSize::Large;
let surface = light.neutral.card;Reusable native controls. Most stateless components can be built inline:
use liora::components::{Button, Progress, Space, Tag, Text, Title};
let header = Space::new()
.vertical()
.child(Title::new("Deployments").h3())
.child(Text::new("Production rollout status"))
.child(Progress::new(72.0).primary().show_text(true))
.child(Tag::new("Healthy").success());Stateful controls should live in gpui::Entity<T> fields so focus, selection, popup state, and text values survive renders:
use gpui::{AppContext, Context, Entity, Render, Window};
use liora::components::{Input, Switch};
struct SettingsView {
search: Entity<Input>,
notifications: Entity<Switch>,
}
impl SettingsView {
fn new(cx: &mut Context<Self>) -> Self {
Self {
search: cx.new(|cx| Input::new("", cx).placeholder("Search settings")),
notifications: cx.new(|cx| Switch::new(true, cx)),
}
}
}
impl Render for SettingsView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
gpui::div()
.child(self.search.clone())
.child(self.notifications.clone())
}
}liora-icons contains the native GPUI Icon primitive and asset loader. Bundled icon-library crates follow the same API shape as liora-icons-lucide: each crate exposes an IconName enum, IconName::all(), IconName::file(), IconName::svg_path(), and implements liora_icons::IntoIconPath plus gpui::IntoElement. IconName resolves to a virtual liora-icon://... asset path so release builds can ship only the SVG files copied by liora-icons-optimizer instead of embedding every icon in the binary.
Available bundled libraries:
| Crate | Facade module | Naming rule | Example |
|---|---|---|---|
liora-icons-lucide |
liora::icons_lucide |
upstream kebab-case to PascalCase | IconName::Settings |
liora-icons-antd |
liora::icons_antd |
name + AntD style suffix | IconName::SaveOutlined, IconName::SaveFilled, IconName::SaveTwotone |
liora-icons-ionic |
liora::icons_ionic |
base, Outline, or Sharp suffix |
IconName::Add, IconName::AddOutline, IconName::AddSharp |
liora-icons-tabler |
liora::icons_tabler |
outline base name, filled uses Filled suffix |
IconName::Home, IconName::HomeFilled |
liora-icons-carbon |
liora::icons_carbon |
Carbon name flattened to PascalCase; one preferred size per icon | IconName::Save, IconName::CheckmarkFilled |
liora-icons-material |
liora::icons_material |
Material 24px style suffixes | IconName::Search, IconName::SearchOutlined, IconName::SearchRound |
The Docs app has a dedicated Icon Libraries overview plus an Icon library navigation group. Each bundled library has its own page (Lucide Icons, Ant Design Icons, Ionicons, Tabler Icons, Carbon Icons, Material Icons) rendered as a virtualized responsive Grid; clicking any square icon item copies the fully-qualified IconName path.
Icon primitives plus bundled icon names:
use liora::core::Config;
use liora::icons::Icon;
use liora::icons_lucide::IconName;
use liora::components::Button;
let save = Button::new("Save").primary().icon_start(IconName::Save);
let icon = Icon::new(IconName::Settings).size_units(18.0);
// Other bundled libraries use the same Icon API.
let antd_save = Icon::new(liora::icons_antd::IconName::SaveOutlined);
let tabler_home = Icon::new(liora::icons_tabler::IconName::HomeFilled);Icon bundle auto optimization keeps application code unchanged while reducing packaged SVG resources. Add the optimizer as a build dependency and call the builder from the host application's existing Cargo build script:
[build-dependencies]
liora-icons-optimizer = "0.2"fn main() {
liora_icons_optimizer::Optimizer::new()
.bundle_auto()
.run();
// keep existing build.rs logic here.
}The optimizer scans the current app and reachable Liora dependency sources, rebuilds target/liora/icons/apps/<app>/assets/liora-icons, and writes target/liora/icons/reports/<app>.md. Host code still uses IconName directly; the generated bundle is a packaging resource, not an application API. Packaging tools automatically collect that directory, so application developers should not copy generated SVGs by hand or run extra packaging commands.
Runtime loading is also automatic. IconAssetSource searches installer resources, portable resources, generated dev bundles, and the typed icon crate's dev= fallback path. If a virtual icon still cannot be found, Liora renders a visible placeholder icon instead of a silent blank. Set LIORA_ICON_DEBUG=1 only while debugging to print the candidate path chain and the final hit/miss decision.
The optimizer only handles Liora's bundled typed icon libraries (liora-icons-lucide, liora-icons-antd, and the other liora-icons-* packs). Caller-owned SVGs remain regular application assets and are not copied, rewritten, deleted, or renamed by liora-icons-optimizer. Use the normal asset strategy for business icons:
use liora::icons::{Icon, inline_svg_asset_path};
// External or packaged app asset. Ensure your app/packager ships this file.
let brand = Icon::new("assets/icons/brand-mark.svg").size_lg();
let file_icon = Icon::new("file:///opt/acme/icons/status.svg");
// Tiny static SVG payload embedded in code.
let inline = Icon::new(inline_svg_asset_path(
r#"<svg viewBox="0 0 24 24"><path d="M4 12h16"/></svg>"#,
));If you want custom SVGs to be mounted outside the executable for installer builds, keep them under your app's normal assets/ tree and let the packager copy that tree. If you want a first-class custom typed icon pack with optimizer support, create a dedicated liora-icons-yourpack-style crate that exposes an IconName enum and a known SVG directory; do not mix business assets into assets/liora-icons, which is reserved for generated bundled-library resources.
Avoid IconName::all() in normal production app code: it intentionally asks the optimizer to bundle a whole icon pack. It is appropriate for icon-browser pages like Liora Docs, but ordinary apps should reference concrete IconName::Search / IconName::Settings variants so the bundle stays small.
When using raw gpui_platform::application(), install the Liora icon asset source if your app uses bundled SVG payloads:
fn main() {
gpui_platform::application()
.with_assets(liora_icons::IconAssetSource)
.run(|cx| {
liora::init_liora(cx);
// open windows...
});
}System tray facade. liora-tray only provides the generic tray primitives; application behavior such as menu labels, residency toggles, dynamic icon choices, and quit policy must live in the host app, not in the SDK:
use liora::tray::{
Tray, TrayCommand, TrayConfig, TrayMenuItemSpec, icon_from_png_bytes,
};
fn install_tray() -> liora::tray::Result<()> {
let icon = icon_from_png_bytes(include_bytes!("../assets/tray-default.png"))?;
let config = TrayConfig::new("acme-notes")
.tooltip("Acme Notes")
.icon(icon)
.menu(vec![
TrayMenuItemSpec::action("Show", TrayCommand::Show),
TrayMenuItemSpec::check("Start at login", TrayCommand::Custom("login".into()), false),
TrayMenuItemSpec::separator(),
TrayMenuItemSpec::submenu(
"Status",
vec![
TrayMenuItemSpec::action("Online", TrayCommand::SetIcon("online".into())),
TrayMenuItemSpec::action("Busy", TrayCommand::SetIcon("busy".into())),
],
),
TrayMenuItemSpec::separator(),
TrayMenuItemSpec::action("Quit", TrayCommand::Quit),
]);
let tray = Tray::install(config)?;
// In your app event loop, map platform menu events with:
// if let Some(command) = tray.command_for_event(&event) { ... }
// On Linux/FreeBSD, periodically call liora::tray::pump_platform_events().
drop(tray);
Ok(())
}Reusable GitHub Release update flow for your own app:
use liora::updater::{AssetKind, AssetSelector, Platform, UpdateRequest, Updater};
fn check_for_update() -> Result<(), liora::updater::UpdaterError> {
let platform = Platform::current().expect("supported desktop platform");
let request = UpdateRequest::new(
"acme-notes",
"v0.3.0",
platform,
std::env::temp_dir().join("acme-notes-updates"),
)
.selector(
AssetSelector::for_platform(platform)
.matching_prefix("acme-notes")
.kind_priority([AssetKind::Installer, AssetKind::RawExecutable]),
);
if let Some(update) = Updater::new("acme", "acme-notes")
.with_checksum_asset_name("SHA256SUMS.txt")
.prepare_update(&request)?
{
println!("new version: {}", update.release.tag);
println!("asset: {}", update.asset.name);
println!("install plan: {:?}", update.install_plan);
// Run installation only after a visible user action.
}
Ok(())
}Reusable packaging metadata and validation helpers. Most applications will copy the repository's xtask pattern, but the library is publishable for custom release tools:
use liora::packager::{AppMetadata, validate_app_packaging_layout};
fn validate_release_inputs() {
let app = AppMetadata::new(
"acme-notes",
"com.acme.Notes",
"Acme Notes",
"acme-notes",
"acme-notes",
"Utility",
"Native notes application.",
"acme-notes",
)
.with_license("MIT")
.with_homepage("https://acme.example/notes")
.with_authors(["Acme Team"])
.with_publisher("Acme")
.with_copyright("Copyright © Acme");
let report = validate_app_packaging_layout(std::env::current_dir().unwrap(), [&app]);
if !report.is_ok() {
for error in report.errors {
eprintln!("{error}");
}
}
}use gpui::{div, px};
use liora::components::{Button, Card, Flex, Space, Statistic, Tag, Text, Title};
let dashboard = Flex::new()
.gap(px(16.0))
.child(
Card::new(
Space::new()
.vertical()
.child(Title::new("Revenue").h3())
.child(Statistic::new("MRR", "$42,800"))
.child(Tag::new("+12.4%").success()),
)
.width_lg(),
)
.child(
Card::new(
div()
.child(Text::new("Ship a native desktop dashboard without a WebView."))
.child(Button::new("Open report").primary()),
)
.title("Summary")
.hoverable(),
);Grid is for icon walls, card decks, settings tiles, and other two-dimensional layouts. Use fit_item(...) when item size should stay stable and the number of columns should adapt; use fit_columns(n) when the column count should stay fixed and items should scale. GridItem is square by default.
use liora::components::{Grid, GridItem, Space, Text};
use liora::icons::Icon;
use liora::icons_lucide::IconName;
let icon_wall = Grid::new()
.fit_item_md()
.gap_md()
.child(GridItem::new(
Space::new()
.vertical()
.align_center()
.gap_sm()
.child(Icon::new(IconName::Settings).size_lg())
.child(Text::new("Settings")),
));
let fixed_columns = Grid::new()
.fit_columns(4)
.gap_md()
.child(GridItem::new(Text::new("Scales with column width")));use liora::components::{
Button, Progress, Space, Tag, toast_error, toast_success,
};
let actions = Space::new()
.child(Button::new("Save").primary().on_click(|_, _window, cx| {
toast_success("Saved", cx);
}))
.child(Button::new("Delete").danger().on_click(|_, _window, cx| {
toast_error("Deletion failed in this demo", cx);
}))
.child(Tag::new("Draft").warning())
.child(Progress::new(48.0).show_text(true));use gpui::{AppContext, Context, Entity, Render, Window};
use liora::components::{Button, Checkbox, Form, FormItem, Input, Space};
struct LoginForm {
email: Entity<Input>,
remember: Entity<Checkbox>,
}
impl LoginForm {
fn new(cx: &mut Context<Self>) -> Self {
Self {
email: cx.new(|cx| Input::new("", cx).placeholder("name@example.com").clearable(true)),
remember: cx.new(|cx| Checkbox::new(true, cx)),
}
}
}
impl Render for LoginForm {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
Form::new()
.child(FormItem::new().label("Email").child(self.email.clone()))
.child(FormItem::new().label("Remember me").child(self.remember.clone()))
.child(Space::new().child(Button::new("Sign in").primary()))
}
}Mention is also a stateful input-style component. Store it as an Entity<Mention> and provide machine-readable item values. When a user clicks a suggestion or presses Enter, Liora replaces the active trigger query with trigger + item.value + trailing space, then calls on_select.
use gpui::{Context, Entity, Render, Window};
use liora::components::{Card, Mention, MentionItem, Space, Text, toast_success};
struct AssigneeField {
people: Entity<Mention>,
issue: Entity<Mention>,
}
impl AssigneeField {
fn new(cx: &mut Context<Self>) -> Self {
Self {
people: cx.new(|cx| {
Mention::new(
vec![
MentionItem::new("alice", "Alice Chen").description("Design systems"),
MentionItem::new("bob", "Bob Smith").description("Release engineering"),
],
cx,
)
.placeholder("Type @ to mention a teammate")
.on_select(|item, _window, cx| {
toast_success(format!("Selected @{}", item.value), cx);
})
}),
issue: cx.new(|cx| {
Mention::new(vec![MentionItem::new("128", "#128 Improve chart hover")], cx)
.trigger('#')
.placeholder("Type # to reference an issue")
}),
}
}
}
impl Render for AssigneeField {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
Space::new()
.vertical()
.child(Text::new("Mention selection writes back to the input value."))
.child(Card::new(self.people.clone()))
.child(Card::new(self.issue.clone()))
}
}For example, typing hello @al and selecting the item whose value is alice produces hello @alice . With trigger('#'), typing fix #1 and selecting value 128 produces fix #128 .
use liora::components::NavigationMenu;
use liora::icons_lucide::IconName;
let menu = NavigationMenu::new()
.id("main-nav")
.item("dashboard", "Dashboard", Some(IconName::LayoutDashboard))
.submenu("settings", "Settings", Some(IconName::Settings), |menu| {
menu.item("profile", "Profile", None)
.item("security", "Security", None)
})
.on_select(|id, _window, _cx| {
eprintln!("selected menu item: {id}");
});Use Shell for most application windows. It is the high-level Liora app-frame component that owns the common regions: optional custom TitleBar, header, left sidebar, right sidebar / inspector, scrollable main content, footer, and overlays. Use TitleBar and Sidebar directly when you are building a lower-level composition, but prefer Shell when you want a single fluent entry point for highly customizable app layout.
Stateful controls such as NavigationMenu still belong in the parent view as Entity<T> fields. The example below uses only Liora SDK components for layout; application entrypoints may still use GPUI runtime types such as Context, Entity, Render, and Window.
use gpui::{AppContext, Context, Entity, Render, Window};
use liora::components::{
Button, Card, NavigationMenu, NavigationMenuMode, Shell, ShellOverlayPosition, Sidebar, Space, Text, Title, TitleBar, WindowFrameMode,
};
use liora::core::Config;
use liora::icons::Icon;
use liora::icons_lucide::IconName;
struct AppShell {
menu: Entity<NavigationMenu>,
}
impl AppShell {
fn new(cx: &mut Context<Self>) -> Self {
Self {
menu: cx.new(|_| {
NavigationMenu::new()
.id("main-nav")
.mode(NavigationMenuMode::Vertical)
.default_active("dashboard")
.item("dashboard", "Dashboard", Some(IconName::LayoutDashboard))
.item("settings", "Settings", Some(IconName::Settings))
}),
}
}
}
impl Render for AppShell {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl gpui::IntoElement {
let theme = cx.global::<Config>().theme.clone();
Shell::new(
Card::new(
Space::new()
.vertical()
.gap_sm()
.child(Title::new("Dashboard").h3())
.child(Text::new("Main content goes here.")),
)
.no_shadow(),
)
.id("acme-shell")
.mode(WindowFrameMode::Custom)
.titlebar(
TitleBar::new()
.title("Acme Notes")
.subtitle("Native GPUI app")
.height_units(52.0)
.padding_x_units(18.0)
.gap_units(10.0)
.actions_gap_units(6.0)
.window_controls(true)
.action(Button::new("New").small()),
)
.sidebar(
Sidebar::new()
.id("app-sidebar")
.brand("Acme Workspace")
.brand_subtitle("Native GPUI")
.logo(Icon::new(IconName::Sparkles).size_units(20.0))
.expanded_width_units(280.0)
.header_padding_units(14.0)
.content_padding_units(8.0)
.footer_padding_units(12.0)
.gap_units(8.0)
.rounded_units(16.0)
.scrollable()
.child(self.menu.clone())
.footer(Text::new("v1.0").sm()),
)
.footer(Text::new("Ready").xs())
.footer_height_units(40.0)
.header_background(theme.neutral.card)
.footer_background(theme.neutral.card)
.body_background(theme.neutral.body)
.main_background(theme.neutral.card)
.main_rounded_units(18.0)
.overlay(Text::new("Saved").xs())
.overlay_position(ShellOverlayPosition::TopRight)
.overlay_inset_units(16.0)
.main_scroll()
.main_padding_units(24.0)
}
}use gpui::rgb;
use liora::components::{
AreaChart, BarChart, ChartPoint, ChartSeries, HeatBar, HeatBarItem, LineChart, PieChart, Sparkline,
};
let revenue = ChartSeries::new("Revenue", [
ChartPoint::new("Mon", 12.0),
ChartPoint::new("Tue", 18.0),
ChartPoint::new("Wed", 16.0),
ChartPoint::new("Thu", 24.0),
ChartPoint::new("Fri", 32.0),
]);
let costs = ChartSeries::new("Costs", [
ChartPoint::new("Mon", 8.0),
ChartPoint::new("Tue", 9.0),
ChartPoint::new("Wed", 11.0),
ChartPoint::new("Thu", 13.0),
ChartPoint::new("Fri", 15.0),
]);
let line = LineChart::new([revenue.clone(), costs.clone()])
.show_grid(true)
.show_axis(true)
.show_legend(true)
.show_tooltip(true);
let area = AreaChart::new([revenue.clone()]).show_tooltip(true);
let bars = BarChart::new([revenue.clone(), costs.clone()]).grouped();
let pie = PieChart::new([revenue.clone(), costs.clone()]).show_percentage_labels(true);
let spark = Sparkline::new([3.0, 4.0, 8.0, 6.0, 12.0]).show_last_point(true);
let heat = HeatBar::new([
HeatBarItem::new("Low", 18, rgb(0x22, 0xc5, 0x5e).into()),
HeatBarItem::new("Medium", 42, rgb(0xf5, 0x9e, 0x0b).into()),
HeatBarItem::new("High", 9, rgb(0xef, 0x44, 0x44).into()),
]);use gpui::{AppContext, Context, Entity, Render, Window};
use liora::components::{CodeBlock, CodeDiagnostic, CodeEditor};
let snippet = CodeBlock::new("cargo run -p liora-gallery")
.language("bash")
.copyable(true);
struct EditorView {
editor: Entity<CodeEditor>,
}
impl EditorView {
fn new(cx: &mut Context<Self>) -> Self {
Self {
editor: cx.new(|cx| {
CodeEditor::new("fn main() { println!(\"hello\"); }", cx)
.language("rust")
.diagnostics(vec![CodeDiagnostic::info(1, 1, "Example diagnostic")])
}),
}
}
}
impl Render for EditorView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
self.editor.clone()
}
}use liora::components::{Button, Image, Preview, QrCode, Space, Upload};
let utilities = Space::new()
.vertical()
.child(QrCode::new("https://github.com/yhyzgn/liora").show_text(true))
.child(Image::new("file:///tmp/screenshot.png").width(gpui::px(240.0)))
.child(Preview::new("file:///tmp/screenshot.png").child(Button::new("Preview image")))
.child(Upload::new().width_lg());use gpui::{Context, IntoElement};
use liora::components::{TableColumn, TreeNode, VirtualizedTable, VirtualizedTree};
fn build_table(cx: &mut Context<MyView>) -> gpui::Entity<VirtualizedTable> {
let rows = vec![
("Liora".to_string(), "Ready".to_string()),
("GPUI".to_string(), "Native".to_string()),
];
cx.new(|_| {
VirtualizedTable::new(
vec![TableColumn::new("name", "Name"), TableColumn::new("status", "Status")],
rows.len(),
move |row, key, _window, _cx| {
let value = match key.as_ref() {
"name" => rows[row].0.clone(),
"status" => rows[row].1.clone(),
_ => String::new(),
};
liora::components::Text::new(value).into_any_element()
},
)
})
}
fn build_tree(cx: &mut Context<MyView>) -> gpui::Entity<VirtualizedTree> {
cx.new(|cx| {
VirtualizedTree::new(
vec![TreeNode::new("root", "Workspace").child(TreeNode::new("src", "src"))],
cx,
)
.show_checkbox(true)
})
}
struct MyView;use liora::components::{Segmented, SegmentedOption};
use liora::core::{ThemeMode, apply_theme_mode};
fn theme_switcher(current: ThemeMode) -> Segmented {
Segmented::new(vec![
SegmentedOption::new("System", "system"),
SegmentedOption::new("Light", "light"),
SegmentedOption::new("Dark", "dark"),
])
.value(current.value())
.on_change(|value, window, cx| {
if let Some(mode) = ThemeMode::from_value(value.as_ref()) {
apply_theme_mode(window, cx, mode);
}
})
}Liora separates font resource loading from font family / weight selection:
- If the family is already installed on the user's system, do not load any file. Set the ordered fallback family list with
FontConfig. - If the app ships private fonts, register bytes first with
load_app_fonts,load_fonts_from_dir,load_font_assets,load_embedded_fonts, or the low-levelload_custom_fontscompatibility helper. - Then choose the ordered UI/code fallback lists and optional default weights with
Options::with_fonts(...)at startup orset_font_config(...)at runtime.with_ui_families(["MiSans", ...])selects a family; usewith_ui_weight(FontWeight::MEDIUM)when the desired face should render at Medium weight.
Supported file extensions are ttf, otf, ttc, otc, woff, and woff2, but actual parsing is delegated to the official GPUI backend for each platform. Prefer ttf/otf/ttc/otc for native desktop apps. On Linux/WGPU, the current GPUI fontdb path can ignore WOFF/WOFF2 bytes without returning an error, so use FontLoadOptions::require_family(...) and check FontLoadReport::missing_required_families whenever a specific family must be active.
use liora::{FontConfig, FontWeight, Options, init_liora_with_options, set_font_config};
fn init_with_system_fonts(cx: &mut gpui::App) {
init_liora_with_options(
cx,
Options::system().with_fonts(
FontConfig::system()
.with_ui_families(["Segoe UI", "MiSans", "Arial"]) // Ordered fallback list.
.with_ui_weight(FontWeight::MEDIUM)
.with_code_families(["JetBrains Mono", "SF Mono", "Monospace"]),
),
);
}
fn switch_to_system_ui_and_monospace_code(cx: &mut gpui::App) {
set_font_config(
cx,
FontConfig::system()
.with_ui_families(["MiSans", "Segoe UI", "Arial"])
.with_ui_weight(FontWeight::MEDIUM)
.with_code_families(["JetBrains Mono", "SF Mono", "Monospace"]),
);
}use std::borrow::Cow;
use liora::{
FontConfig, FontLoadMode, FontLoadOptions, FontWeight, Options,
init_liora_with_options, load_app_fonts,
};
fn init_with_embedded_font(cx: &mut gpui::App) {
let report = load_app_fonts(
cx,
FontLoadOptions::new(FontLoadMode::Embedded).embedded(
"Inter-Regular.ttf",
Cow::Borrowed(include_bytes!("../assets/fonts/Inter-Regular.ttf").as_slice()),
),
);
if !report.failures.is_empty() || !report.required_families_available() {
eprintln!("font load failures: {report:?}");
}
init_liora_with_options(
cx,
Options::system().with_fonts(
FontConfig::system()
.with_ui_families(["Inter", "Segoe UI", "Arial"])
.with_ui_weight(FontWeight::MEDIUM),
),
);
}This is the recommended pattern when full font families are large. Keep a small regular face embedded for raw executables, and ship the complete family under assets/fonts in installers or portable archives.
use std::{borrow::Cow, path::PathBuf};
use liora::{
FontConfig, FontLoadMode, FontLoadOptions, FontWeight, Options,
init_liora_with_options, load_app_fonts,
};
fn font_dirs(app_binary: &str) -> Vec<PathBuf> {
let mut dirs = vec![PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/fonts")];
if let Ok(exe) = std::env::current_exe() {
if let Some(exe_dir) = exe.parent() {
dirs.push(exe_dir.join("assets/fonts")); // Windows/install root or portable root.
dirs.push(exe_dir.join("..").join("Resources").join("assets/fonts")); // macOS .app.
}
}
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
dirs.push(PathBuf::from("/usr/lib").join(app_binary).join("assets/fonts"));
dirs
}
fn init_with_external_then_embedded(cx: &mut gpui::App) {
let mut options = FontLoadOptions::new(FontLoadMode::ExternalThenEmbedded).embedded(
"MiSans-Medium.ttf",
Cow::Borrowed(include_bytes!("../assets/fonts/MiSans/MiSans-Medium.ttf").as_slice()),
)
.require_family("MiSans");
for dir in font_dirs("my-gpui-app") {
options = options.external_dir(dir);
}
let report = load_app_fonts(cx, options);
if !report.failures.is_empty() || !report.required_families_available() {
eprintln!("font load failures: {report:?}");
}
// Mixed source example: UI uses the shipped MiSans family, code uses a system family.
init_liora_with_options(
cx,
Options::system().with_fonts(
FontConfig::system()
.with_ui_families(["MiSans", "Segoe UI", "Arial"])
.with_ui_weight(FontWeight::MEDIUM)
.with_code_families(["JetBrains Mono", "SF Mono", "Monospace"]),
),
);
}use liora::{load_font_assets, load_font_files, load_fonts_from_dir};
fn register_more_fonts(cx: &mut gpui::App) {
let asset_report = load_font_assets(cx, ["fonts/Brand-Regular.otf".into()]);
let dir_report = load_fonts_from_dir(cx, "assets/fonts");
let file_report = load_font_files(cx, [std::path::PathBuf::from("/opt/my-app/fonts/BrandCode.ttf")]);
for report in [asset_report, dir_report, file_report] {
if !report.failures.is_empty() {
eprintln!("font load failures: {report:?}");
}
}
}For Liora's own apps, Gallery and Docs keep the full MiSans TTF family under each app's assets/fonts/MiSans/ and set the app UI default to FontWeight::MEDIUM through FontConfig::with_ui_weight(...). Release packaging is split into two explicit font variants: without-fonts is the default smaller asset and does not bundle app font files; with-fonts bundles external assets/fonts for installers/portable archives and builds raw executables with the app embedded-fonts feature for a small fallback face.
Menu is the shared command descriptor. It can be registered with GPUI's official platform menu API, rendered as an in-window fallback menu bar, or reused by a command palette. These are intentionally separate layers:
| Goal / environment | Use | Notes |
|---|---|---|
| Register OS/platform menu semantics | Menu::register(cx, menus) |
Delegates to GPUI App::set_menus. On macOS the menu usually appears in the global screen menu bar. On Linux/Wayland/KDE/GNOME and Windows, visibility is platform/backend dependent and it is not inserted into your GPUI element tree. |
| Always show a menu inside the app window | MenuBar::new(menus) |
MenuBar is a Liora visual component. Put it in a Container header, Shell region, or custom TitleBar. |
| System frame, native platform behavior is enough | Menu::register(...) only |
Good for macOS-native behavior; some Linux/Windows environments may not show a window menu. |
| System frame, but the menu must be visible in the window | Menu::register(...) plus a header MenuBar |
This is what Gallery does: platform registration remains active, while the header fallback is stable across environments. |
| Custom frame / client-side decorations | Menu::register(...) plus a visible MenuBar in your chrome/header |
A custom titlebar does not cause GPUI to inject a menu into your element tree. |
| Docs/settings/preview only | MenuBar or a single Menu with .perform_builtin_actions(false) |
Prevents demos from quitting the app, opening URLs, or writing the clipboard. |
use gpui::App;
use liora::components::{Menu, MenuBar, MenuItem};
fn app_menus() -> [Menu; 2] {
[
Menu::new("File")
.item(MenuItem::open_file())
.item(MenuItem::open_folder())
.item(MenuItem::separator())
.item(MenuItem::quit()),
Menu::new("Edit")
.item(MenuItem::undo())
.item(MenuItem::redo())
.item(MenuItem::separator())
.item(MenuItem::copy())
.item(MenuItem::paste()),
]
}
fn register_platform_menu(cx: &mut App) {
Menu::register(cx, app_menus());
}
fn in_window_menu_bar() -> MenuBar {
MenuBar::new(app_menus()).perform_builtin_actions(false)
}Render the in-window menu bar by placing MenuBar in your root layout and rendering the popover portal used by its dropdowns:
use gpui::{App, IntoElement, ParentElement, Styled, Window, div, px};
use liora::components::{AppWindowFrame, Container, MenuBar};
fn render_root(window: &mut Window, cx: &mut App) -> impl IntoElement {
let menu_bar: MenuBar = in_window_menu_bar();
// Required for MenuBar dropdowns and every Liora popover-based component.
liora::core::render_active_popover_in_window(window, cx);
AppWindowFrame::new(
"My App",
Container::new()
.header(div().w_full().child(menu_bar))
.header_height(px(40.0))
.child("Window body"),
)
}Menu::register(...) only calls GPUI's official App::set_menus; the visible in-window row comes from MenuBar::new(...).
Most apps only need liora::init_liora(cx). If you build a custom root shell that manually manages overlay layers, keep portal rendering near the window root:
use liora::core::{
render_active_drawer_in_window, render_active_modal_in_window, render_active_popover_in_window,
};
fn render_overlays(window: &mut gpui::Window, cx: &mut gpui::App) {
render_active_popover_in_window(window, cx);
render_active_modal_in_window(window, cx);
render_active_drawer_in_window(window, cx);
}Plain Popover content gets default 16 px padding so simple text/card bubbles are not cramped. If your popup body is already a complete surface (for example a menu, command palette, or custom confirmation panel), remove the shared padding and let that body own its spacing:
use liora::components::{Button, Popover};
let popup = Popover::new(Button::new("Actions"))
.flush_content()
.content(|_window, _cx| {
// Your menu/panel root controls min width, padding, scrolling, and item spacing.
liora::components::Space::new()
.padding_md()
.child(Button::new("Archive"))
.child(Button::new("Delete").danger())
});
let roomy_popup = Popover::new(Button::new("Details"))
.content_padding(gpui::px(20.0))
.content(|_window, _cx| "Padded plain content");Dropdown, DropdownButton, Menu submenus, and Popconfirm already use this flush mode internally, so their menus and confirmation panels keep consistent dimensions across placements.
Do not put product data models in liora-components. Store app state in your GPUI view/entity and pass only display values/callbacks to components:
use gpui::{Context, IntoElement, Render, Window};
use liora::components::{Empty, Table, TableColumn, TableRow, Tag, Text};
struct OrdersView {
rows: Vec<(String, String)>,
}
impl Render for OrdersView {
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl gpui::IntoElement {
if self.rows.is_empty() {
Empty::new().description("No orders yet").into_any_element()
} else {
let rows = self.rows.iter().map(|(id, status)| {
TableRow::new()
.cell("id", Text::new(id.clone()))
.cell("status", Tag::new(status.clone()).success())
});
Table::new(vec![TableColumn::new("id", "Order"), TableColumn::new("status", "Status")])
.rows(rows)
.into_any_element()
}
}
}Liora's locale system is deliberately low-coupling: language resources live in external TOML files, Rust code uses typed keys such as locales::empty::description, and the runtime translator can be replaced by the application.
Use this feature when you need:
assets/locales/<locale>.tomlfiles outside Rust source code;- generated
locales::section::keyconstants instead of hardcoded strings like"empty.description"; - runtime language switching with
apply_locale(window, cx, locale); - a fallback locale plus Liora's small built-in fallback resources;
- an escape hatch for a custom translation backend through
Translator.
Create one TOML file per locale. Nested TOML tables become dot-separated keys internally, while Rust call sites use generated modules.
# assets/locales/en-US.toml
[common]
ok = "OK"
cancel = "Cancel"
[empty]
description = "No data"
[docs]
subtitle = "Native documentation"# assets/locales/zh-CN.toml
[common]
ok = "确定"
cancel = "取消"
[empty]
description = "暂无数据"
[docs]
subtitle = "原生文档"Keep the same key set in every locale file whenever possible. Missing keys fall back to the configured fallback locale, then Liora's built-in core resources, then the key path itself.
Add the build dependency and a tiny build.rs to your application crate:
// build.rs
#[path = "../../crates/liora-core/src/locales_codegen.rs"]
mod locales_codegen;
fn main() {
locales_codegen::generate_locales_from_package("liora_core::Locales");
}Add toml build-dependency in Cargo.toml:
[build-dependencies]
toml.workspace = trueThen include the generated module from your app:
pub mod locales {
include!(concat!(env!("OUT_DIR"), "/locales_keys.rs"));
}By default the generator scans the current package's ./assets/locales directory and emits constants like:
locales::common::ok
locales::empty::description
locales::docs::subtitleIf your resources live elsewhere, configure paths in Cargo.toml. Relative paths are resolved from the package root.
[package.metadata.liora.locales]
paths = ["assets/locales", "../shared/locales"]use liora::{Options, init_liora_with_options};
fn setup(cx: &mut gpui::App) {
let options = Options::system()
.with_locale("en-US")
.with_fallback_locale("zh-CN")
.try_with_locales_dir("assets/locales")
.unwrap_or_else(|_| Options::system().with_locale("en-US"));
init_liora_with_options(cx, options);
}Gallery and Docs use env!("CARGO_MANIFEST_DIR") to build an absolute path for packaged applications:
fn app_locales_dir() -> std::path::PathBuf {
std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("assets/locales")
}For Liora text-oriented component builders, pass the typed key directly. The component resolves it during render, so runtime language switching updates the UI after a window refresh.
use liora::components::{Button, Empty, Paragraph, Text, Title};
fn content() -> impl gpui::IntoElement {
gpui::div()
.child(Title::new(locales::docs::subtitle).h2())
.child(Text::new(locales::empty::description))
.child(Paragraph::with_text(locales::common::loading))
.child(Button::new(locales::common::ok).primary())
.child(Empty::new().description(locales::empty::description))
}When you need an immediate SharedString for APIs that do not accept LocalizedText yet, call tr(cx, key) explicitly:
use liora::{locales, tr};
fn window_title(cx: &gpui::App) -> gpui::SharedString {
tr(cx, locales::common::loading)
}use liora::apply_locale;
fn switch_to_chinese(window: &mut gpui::Window, cx: &mut gpui::App) {
if let Err(error) = apply_locale(window, cx, "zh-CN") {
eprintln!("failed to switch locale: {error}");
}
}If the target locale is not already loaded, use switch_locale_from_dir(window, cx, locale, dir) to load it from disk and switch in one operation.
Applications with an existing localization stack can implement Translator and inject it with Options::with_translator(...), set_translator(...), or set_shared_translator(...). Liora components depend only on typed Locales keys; the backend still receives the string path so your app can map it to a database, remote service, ICU/Fluent layer, or any other system.
use gpui::SharedString;
use liora::{LocaleId, Translator};
struct AppTranslator;
impl Translator for AppTranslator {
fn translate(&self, locale: &LocaleId, key: &str) -> Option<SharedString> {
Some(format!("{}:{}", locale.as_str(), key).into())
}
}- Do not hardcode translation paths at call sites; prefer
locales::section::key. - Use direct keys for component builders, and
tr(cx, key)only when an immediate string is required. - Keep language files external to Rust source code.
- Keep all app shell text in TOML; Docs markdown page bodies can remain single-language unless your product requires translated markdown content.
- Re-run
cargo checkafter editing TOML so build scripts regenerate the typed keys.
| Category | Components |
|---|---|
| Basic and layout | Button, ButtonGroup, Icon, Link, Text, Title, Paragraph, SelectableTextGroup, Space, Grid, GridItem, Divider, Row, Col, Container, Shell, Sidebar, TitleBar, Flex, Scrollbar, ScrollableMask, Splitter, DockLayout, Affix, Backtop |
| Form controls | Input, InputNumber, Textarea, Checkbox, CheckboxGroup, Radio, RadioGroup, Switch, Select, Slider, Form, FormItem, Rate, DatePicker, TimePicker, DateTimePicker, Upload, Cascader, Transfer, ColorPicker, Autocomplete, InputTag, Mention, TreeSelect, SearchableList, OtpInput |
| Feedback and overlays | Alert, Tooltip, Popover, Popconfirm, Dialog, Drawer, Message, Notification, MessageBox, Loading, Dropdown, DropdownButton, Preview, Tour, HoverCard, FocusTrap |
| Navigation | NavigationMenu, Tabs, Breadcrumb, Steps, PageHeader, Anchor, Accordion |
| Data display | Table, List, VirtualizedTable, VirtualizedTree, VirtualizedList, Progress, Skeleton, Empty, Result, Descriptions, Timeline, Tree, Pagination, Statistic, Segmented, Tag, Avatar, Badge, Calendar, Carousel, Image, Watermark, Kbd, GroupBox, StatusBar, SettingsPage, SettingsGroup, SettingsItem |
| Charts and metrics | LineChart, AreaChart, BarChart, PieChart, RingChart, Sparkline, SignalMeter, HeatBar, SegmentRatioBar, CandlestickChart |
| Editing and utility | CodeBlock, CodeEditor, QrCode, Timer, Label, Operation, Clipboard, draggable list helpers |
| App shell and platform | Shell, AppWindowFrame, TitleBar, Sidebar, WindowFrameMode, StatusBar, DockLayout, Menu / MenuBar, liora-tray, Linux desktop identity helpers, package metadata helpers, updater helpers |
Liora avoids duplicate controls for the same job:
- Use
Drawerfor both full drawers and lightweight sheet-style panels.Drawer::sheet()provides the compact defaults that would otherwise become a separate Sheet control. - Use
Selectfor fixed options and searchable selection.Select::searchable(...),.multiple(), item groups, disabled items, and footer slots cover Combobox-style workflows. - Use
Switchfor settings booleans andSegmented/ button-style selections for toolbar or view-mode choices; the former standalone Toggle control was removed to avoid API duplication. - Use
Textfor both inline text and lightweight app documents.Text::document(...),TextBlock, andText::markdown(...)cover TextView-style About/Help/Release notes content.Text,Title, andParagraphare mouse-selectable by default; call.selectable(false)only for decorative labels or non-copyable chrome. UseSelectableTextGroupwhen selection must continue across multipleTextandParagraphblocks, such as release notes, help pages, and documentation articles.
Repository-owned packaging readiness is implemented through the published liora-packager library plus the repository-local xtask command wrapper:
cargo run -p xtask -- package validate
cargo run -p xtask -- package release-readiness
cargo run -p xtask -- package build --all-apps --font-variant without-fonts
cargo run -p xtask -- package build --all-apps --font-variant with-fonts
cargo run -p xtask -- package ci --app gallery --format platform-defaults --skip-build --font-variant without-fonts
cargo run -p xtask -- package ci --app gallery --format platform-defaults --skip-build --font-variant with-fonts
cargo run -p xtask -- package smoke --app gallery --format platform-defaults --font-variant without-fonts
cargo run -p xtask -- package install-smoke --app gallery --format platform-defaults --dry-run --font-variant without-fontsFont variants:
| Variant | Raw executable | Installers / portable archives | Use when |
|---|---|---|---|
without-fonts |
Normal release build, no app font bytes embedded | Does not include app assets/fonts |
Default/smaller download; uses system fonts or user-provided external font paths |
with-fonts |
Builds app with --features embedded-fonts |
Includes app assets/fonts as external resources |
You want bundled MiSans resources/fallback for Gallery/Docs typography |
--font-variant defaults to without-fonts. CI publishes both variants and names release assets with -without-fonts or -with-fonts suffixes.
Supported release artifacts include:
| Platform | Raw apps | Gallery installers/packages |
|---|---|---|
| Linux x64 | liora-docs, liora-gallery |
AppImage, .deb, .rpm, portable .tar.gz |
| macOS arm64 | liora-docs, liora-gallery |
.dmg |
| Windows x64 | liora-docs.exe, liora-gallery.exe |
NSIS setup .exe, MSI |
Packaging rules:
- keep apps pure Rust + GPUI native;
- keep app icons and tray/status icons in app-owned asset folders;
- use
liora-packager/xtaskfor package metadata instead of adding a web runtime; - Windows app build scripts should embed icon/file metadata only; GPUI's Windows backend already provides the application manifest.
Your application is missing the root patch:
[patch.crates-io]
gpui = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718" }Verify with:
cargo tree -i gpui
cargo tree -p gpuiAdd direct dependencies when your final binary names GPUI types or starts the app runtime:
[dependencies]
gpui = { version = "0.2.2", default-features = false }
gpui_platform = { git = "https://github.com/zed-industries/zed", rev = "2c346f60a76fe3f0367ef924927f50a6efdf5718", default-features = false }Install native build dependencies. On Fedora-like systems, inspect and adapt:
scripts/install-fedora-deps.shOn Debian/Ubuntu-like systems, install the equivalent libgtk-3-dev, Wayland/X11/xkbcommon, fontconfig/freetype, Vulkan, ALSA, and pkg-config packages.
On Wayland, register a desktop identity and set WindowOptions.app_id to the same name. The compositor resolves taskbar icons from .desktop metadata and the icon theme.
WindowOptions {
app_id: Some("acme-notes".into()),
..Default::default()
}Create windows with show: false, call attach_system_theme_observer(window, cx) at the beginning of the open_window callback, and activate the window after open_window returns.
Store stateful controls in gpui::Entity<T> fields rather than constructing them inside every render pass.
struct ViewState {
input: gpui::Entity<liora::components::Input>,
}Use liora::init_liora(cx) or liora_components::init_liora(cx). If you intentionally use only liora_core, initialize component services yourself before calling toast helpers.
Do not embed your own Windows Common Controls manifest in app build.rs when using GPUI's Windows backend. Embed icons and file metadata only; gpui_platform enables GPUI's windows-manifest feature for Windows.
Only upload distributable raw binaries, installers/packages, and SHA256SUMS.txt. Keep generated notes/config files in the release body or CI artifacts, not as end-user release assets.
apps/liora-docs/content/pages/quick_start.mdfor adoption setup.apps/liora-docs/content/pages/theme_system.mdfor startup theme/window behavior.apps/liora-docs/content/pages/packaging_workflow.mdfor release packaging.apps/liora-gallery/src/demos/for component-by-component usage.
Before publishing or submitting changes, run:
cargo fmt --all --check
cargo check --workspace --all-targets
cargo test --workspace
cargo check -p liora-docs --bin check_snippets
cargo doc --workspace --no-deps
cargo run -p xtask -- package validate
cargo run -p xtask -- package release-readiness
cargo run -p xtask -- package ci --app gallery --format platform-defaults --dry-run --skip-build
cargo run -p xtask -- package install-smoke --app gallery --format platform-defaults --dry-runFor release builds:
cargo build --workspace --release
cargo run --release -p xtask -- package validate
cargo run --release -p xtask -- package release-readiness- Native first: all components render through GPUI element trees, native text, native input, and native paint paths.
- Application-ready defaults: theme, overlay, message, keyboard, and selection behavior work from one setup call.
- Composable over prescriptive: components expose builder-style APIs; product data and screen composition stay in applications.
- Token-driven visuals: light/dark/system themes use semantic tokens for surfaces, text, borders, masks, and interaction states.
- Performance-aware data UI: charts and virtualized views include downsampling, hit testing, cache limits, and visible-area rendering patterns.
liora::init_liora(cx) is the recommended application entry point when using the facade crate. It initializes Liora core/theme state, global component services, and key bindings for interactive controls.
Use liora::init_liora_with_mode(cx, ThemeMode::Light | ThemeMode::Dark | ThemeMode::System) when the product needs to choose an explicit startup theme mode. Runtime theme switches use apply_theme_mode(window, cx, mode) from liora_core or the facade's core module.
Typography defaults are system-native: Liora does not load branded fonts by default and does not map the whole UI to Zed-specific font aliases. Custom fonts are opt-in via ordered FontConfig fallback lists, Options, load_app_fonts, load_fonts_from_dir, load_font_assets, load_embedded_fonts, the low-level load_custom_fonts, and set_font_config.
Stateful controls such as Input, Switch, Select, TreeSelect, CodeEditor, and virtualized views should live in gpui::Entity<T> fields so focus, open state, selections, scroll state, and text values survive re-rendering.
Liora is more than a component catalog:
- One-dependency adoption: the crates.io
liorafacade re-exports the maintained public SDK modules so app manifests stay compact while focused utility crates remain independently usable. - One-call application setup:
init_liora(cx)centralizes core configuration, component services, and keyboard bindings so applications do not repeat per-widget setup. - Native Markdown documentation: Markdown stays as authored content, while the running Docs app renders it into Liora/GPUI nodes and verifies external Rust snippets.
- Native charts without a browser layer: chart primitives use Rust data structures, GPUI paint paths, hit testing, and downsampling instead of a WebView chart runtime.
- Application-shell coverage: tray residency, toasts, theme switching, searchable component navigation, and real layout patterns are exercised in native apps.
- Packaging-aware from the workspace: installer information, manifests, checksums, backend configs, and dry-run install plans are validated alongside code.
Every future code change must ask: does README need to change?
Update README.md and README.zh-CN.md in the same change when you modify:
- public crate names, features, or dependency instructions;
- GPUI revision, patch strategy, or platform feature flags;
- initialization APIs, theme behavior, fonts, icons, window startup, or tray behavior;
- component names, major component APIs, examples, or app-shell patterns;
- packaging, updater, release assets, CI commands, MSRV, or troubleshooting guidance.
If README does not need a change, state that explicitly in the final change summary.
Read CONTRIBUTING.md before opening a pull request. Important boundaries:
- keep Liora pure Rust + GPUI native;
- do not introduce Tauri, WebView, HTML/CSS/DOM, browser runtime, or web chart shells;
- do not put product data models or page-only helpers into
liora-components; - keep Gallery, Docs, snippets, tests, and both READMEs in sync with public behavior.
Liora currently uses LicenseRef-Liora; see LICENSE.md. Do not assume an OSS license until the project maintainer replaces that policy with explicit OSS or commercial terms.