C+
Packages · View as Markdown

facet_gtk

facet's GTK 4 backend — the Linux/BSD counterpart of facet_appkit and facet_uikit.

facet owns the description tree and all layout (the shared flex_layout engine). This package answers the contract's verbs with GTK and records in MANIFEST.md what it has not answered yet.

facet_gtk   the registration surface: install(), and nothing else
views       the Renderer's five verbs + intrinsic size
controls    one GTK body per node kind, and the CSS each one composes
input       the READ half — gestures, keys, control actions, sender readers
paint       the shared band — background / radius / opacity / enabled / tooltip
geometry    computed frames -> GtkFixed placements, and a scroll's extent
scheduler   schedule + the Scheduler service, over the GLib main loop
window      the GtkWindow host, the run loop, and the menus
chrome      the toolbar, the window buttons, and the drag region
swipe       swipe-to-reveal, assembled — GTK has no such row
anim        the two things that move on their own: a progress tween, a GIF
recycler    list, collection and tree over GtkListView — rows built as they scroll in
dialogs     alert / choose / prompt as facet trees, and the file chooser
drawing     the canvas replay, against cairo and Pango
web         a WebKitGTK view, opened at runtime rather than linked

Status is a measurement

$ python3 tools/parity.py
  appkit    338 / 360    93%
  uikit     335 / 360    93%
  gtk       353 / 360    98%   <-- this package

  appkit     68 / 68    100%
  uikit      67 / 68     98%
  gtk        68 / 68    100%   <-- this package

Two numbers, because props are WRITES and handlers are READS — a backend at 100% on props can still be a control that is clicked and calls nothing. Both are measured now, and both are gated: --check carries a floor per axis, because the two fail differently. A missing prop is a control that ignores you; a missing handler is a control that never answers.

The second number reaching 68/68 does not mean every handler is EXACT — two are narrowed by facet's own contract rather than by GTK, and MANIFEST.md §3 names both. What it means is that no declared handler is silently dead.

Read MANIFEST.md before trusting any adjective — including that one. tools/parity.py --check fails if the number drops, so a refactor that quietly loses a verb is caught by the gate rather than by someone noticing a control stopped working.

Two ways to check it

cd vendor/facet_gtk && ../../target/release/cpc test    # 212 tests, no display needed
cd examples/facet_gtk_probe && ../../target/release/cpc run
cd examples/facet_gallery   && ../../target/release/cpc run

# ...and the gallery can walk ITSELF — every demo, one a tick:
FACET_GALLERY_WALK=1 ./target/debug/facet_gallery     # 35 screens, warnings visible
FACET_GTK_SCROLL=1   ./target/debug/facet_gallery     # every scroll move, and who made it
FACET_GTK_SELECT=1   ./target/debug/facet_gallery     # a nav select, bracketed: at click, after
                                                      # the handler, and again 500ms later
FACET_GTK_FRAMES=1   ./target/debug/facet_gallery     # the computed frame tree
FACET_GTK_SYNC=1     ./target/debug/facet_gallery     # what a frame costs, in its four parts —
                                                      # mount, solve, place, menu. A debt row is a
                                                      # guess until it is a number; MANIFEST §2's
                                                      # `relayout_all` row was retired by this one

The suite pins everything the backend DECIDES — the mappings, the CSS it composes, the bits it claims, the encoders — and makes no widget, because a suite that needs a login session is a suite that stops running. The probe is the other half: whether the switch reported its change, whether the slider fired while it was being dragged, whether the field kept its caret. An agent has no hands, so that half is an app you run.

The gallery is the third: it is the same application macOS runs, unchanged, and it reaches this backend through facet_runtime/runtime_linux.cplus. If it builds and comes up, the whole stack — contract, backend, facade, app — is wired end to end.

Seeing what was computed

FACET_GTK_FRAMES=1 ./target/debug/facet_gallery 2>&1 | head -40

One line per node: its key, its kind, the frame flex computed, and whether it has a widget. This is the package's one diagnostic and it exists because a backend under an external layout engine fails in a way a screenshot cannot explain — every control in the right place and none of them visible. w=0 on a kind that has a body is that bug; view=- on a backed kind is the other one. Both have already happened here, and both were found in a second by this.

It runs

That is new. The previous generation of this package could only ever be type-checked — it targeted a superseded 34-entry per-kind Renderer, imported facet::Renderer from a module that no longer exports it, and the GTK stack did not link on Linux at all. All three are fixed; this one is built and run.

import "facet_gtk/facet_gtk" as backend;

fn main() -> i32 {
    backend::install();
    var root: facet::Node = ui();
    let _ok: bool = backend::open_window(#addr_of(root), "facet · GTK 4", 520.0f64, 340.0f64);
    var lp: glib::MainLoop = glib::MainLoop::new(glib::main_context_default(), false);
    lp.run();
    return 0;
}

The tree ui() builds is portable: it is the same description facet_appkit would mount on a Mac.

The two rules worth knowing before editing

One code path for create and apply. create configures a new widget with paint::all_bits(), which is the same call apply makes with the node's real dirty word. A prop honoured on update is therefore honoured on create, and the two cannot drift. Every write is gated on its bit.

Ownership. A fresh GtkWidget carries a floating reference. create sinks it (facet holds the widget in the node) and view_release drops it. Without the sink an unparented widget leaks; without the release, a removed one does.