facet
facet describes user interfaces. It does not draw them: a backend does that. On macOS the backend is facet_appkit; on Linux, facet_gtk. The tree itself is a flex_layout Node. There is no VDOM and no re-render: a handler finds the keyed node that changed and writes it.
A first tree
Bare element names resolve through facet/elements. key is an address: find, the agent surface, and the platform accessibility identifier all use it.
import "facet/facet" as core;
import "facet/elements" as ui;
fn body() -> core::Node {
return @ui {
vstack(key: "body") {
label("hello", key: "greeting")
button("click me", key: "go")
}
};
}
The { after vstack(key: "body") must be on the same line. See Builder blocks.
Component, lifecycle, screen
State lives in a struct. Component supplies the tree. Lifecycle hooks fire for the component (never by it): on_attach after mount, on_detach before teardown. Screen names the window.
import "facet/component" as component;
import "facet/screen" as screen;
import "stdlib/vec" as vec;
struct Counter { clicks: i64 }
impl Counter: component::Component {
fn build(ref this) -> core::Node { return body(); }
}
impl Counter: component::Lifecycle {
fn on_attach(ref this) { return; }
fn on_detach(ref this) { return; }
}
impl Counter: screen::Screen {
fn chrome(this) -> screen::Chrome {
return screen::Chrome::new(title: "hello", width: 360.0f64, height: 200.0f64);
}
fn menu_items(this) -> vec::Vec[screen::MenuItem] {
return vec::new::[screen::MenuItem]();
}
}
fn counter_screen() -> screen::ScreenBox {
return screen::screen_box(Counter { clicks: 0 as i64 });
}
A handler is fn(sender: *u8, ctx: *u8). Pass a bound method (on_click: this.bump) or a free function plus on_click_ctx. The handler finds the one label that changed and writes it. Nothing else in the tree is visited.
Run it
import "facet/runtime" as runtime;
import "stdlib/status" as status;
import "facet_appkit/facet_appkit" as backend;
fn main() -> i32 {
backend::install();
var app: runtime::App = runtime::App::new("hello");
app.screen("counter", counter_screen);
match app.run("counter") {
status::Status::Ok => { return 0; }
_other => { return 1; }
}
}
App is the process tier: named screen routes, an optional menu-bar builder, on_launch / on_quit, and app.run(initial). nav::go / push / pop / quit replace or stack screens. mount::set_content fills a keyed outlet with a component's tree. mount::switch_to parks siblings (Display::None) so view state survives a tab switch.
A target with no backend still compiles: facet's verbs become no-ops that say so once on stderr.
Theme
Colours resolve through roles. Tier 1 is the platform's semantic colours (Color::text(), accent(), window_background(), …). Tier 2 is app-retintable (primary / on_primary, surface, ink(a), success / warning / danger, …). Color::adaptive(light:, dark:) is a light/dark pair resolved at paint time. set_theme re-themes the live app.
import "facet/theme" as theme;
import "facet/vocabulary" as vocab;
theme::set_theme(theme::Theme::new(
primary: vocab::Color::rgba(0.30f64, 0.42f64, 0.85f64, 1.0f64),
));
Color::text() / text_secondary() / text_tertiary() are the label tiers. primary is the brand role.
Services, jobs, resources, agent
after / Cancellable, run_on_main, spawn_ui, run_on_worker, and run_job cover timers, main-thread dispatch, and off-thread work. block_on in a handler blocks the loop. Resources are REST verbs (get / post / put / delete) plus watch over a shared store.
To serve the tree to an external agent: facet/agent::enable() before app.run, then app.agent_mcp(path). For an in-process assistant, facet/agent::in_app(). See Agent surface.
The declared verb list lives in the package's docs/contract.md. A verb absent there does not exist.