C+
Packages · View as Markdown

facet

A cross-platform native UI framework. You describe a view declaratively in a @facet { ... } builder block; the block produces a Node tree — pure data, no native types — that a per-platform renderer walks into real widgets. The same description targets every platform: facet itself knows nothing about AppKit, GTK, or any toolkit.

The FACET.1 vocabulary is small and complete: two leaves, label and button, one container, stack, and one modifier, .on_click. The renderer that ships today is facet_appkit, which materializes the tree as NSStackView / NSButton / NSTextField and runs the macOS event loop.

The @facet block

A @facet { ... } block is the shipped contextual builder. Inside it, a bare leaf name resolves against the package (label becomes facet::label), a bare container stack { ... } becomes a nested vertical stack, and a leading-dot line attaches a modifier to the node just built. The block desugars to facet::Builder::new(), one .add(node) per element, and a final .finish() that hands back the accumulated children as a stack Node.

import "facet" as facet;

fn greet(_w: *u8) { return; }

fn view() -> facet::Node {
    return @facet {
        label("Hello from C+")
        button("Greet")
            .on_click(greet)
    };
}

The block's value is an implicit vertical stack of its top-level items, so view() returns a single stack Node holding the label and the button.

Elements and the click handler

  • label(s: str) -> Node — non-interactive text.
  • button(s: str) -> Node — a titled button.
  • stack { ... } — a vertical stack; nest it for sub-layouts.
  • .on_click(cb: fn(*u8)) — attaches a click handler to the preceding node. In keeping with C+ having no closures, the handler is a plain function pointer, not a capturing closure; the *u8 is the sender. Until you set one, a button's handler is a no-op, so click is never a null pointer.

Nesting and modifiers compose as you would expect:

fn save(_w: *u8) { return; }
fn cancel(_w: *u8) { return; }

fn editor() -> facet::Node {
    return @facet {
        label("Untitled")
        stack {
            button("Save").on_click(save)
            button("Cancel").on_click(cancel)
        }
    };
}

Running it

facet produces the description; a renderer presents it. facet_appkit walks a Node into a native view tree and runs the application:

import "facet" as facet;
import "facet_appkit" as facet_appkit;

fn main() -> i32 {
    facet_appkit::run(view());   // opens a window, mounts the tree, blocks
    return 0;
}

render walks the tree by identity (a *facet::Node, no allocation per walk), and run opens the Application + Window, mounts the rendered root, and enters the event loop.

Pure data, zero-copy moves

A Node carries its kind, its text, a click function pointer, and its children. Because it is plain data, you can build, store, and pass a view tree without touching any native API. The builder moves whole nodes rather than copying them: Builder::add takes its argument with take, and both finish and stack use v0.0.25 struct destructuring (let Builder { children } = this;) to move the accumulated Vec out without a copy — the language feature that replaced the deep clone the first spike needed.

Platform notes

FACET.1 is a discovery spike: the description layer is platform-free, but the only renderer shipped today is facet_appkit, so a running UI currently needs macOS. Adding a backend means writing a renderer over the same Node tree — nothing in facet changes. See targets for the platforms C+ builds for.

To drive a facet-built UI from an external agent, see the agent surface. For the builder-block desugaring rules in general, see builder blocks.