diff options
| -rw-r--r-- | assets/ext-window-field/field.rcss | 77 | ||||
| -rw-r--r-- | assets/ext-window-field/field.rml | 21 | ||||
| -rw-r--r-- | meson.build | 1 | ||||
| -rw-r--r-- | packages/ext-window-field/include/unbox/ext-window-field/ext_window_field.hpp | 36 | ||||
| -rw-r--r-- | packages/ext-window-field/meson.build | 102 | ||||
| -rw-r--r-- | packages/ext-window-field/src/extension.cpp | 378 | ||||
| -rw-r--r-- | packages/ext-window-field/src/probe.hpp | 64 | ||||
| -rw-r--r-- | packages/ext-window-field/tests/test_glue.cpp | 365 | ||||
| -rw-r--r-- | packages/ext-window-field/tests/test_window_field.cpp | 20 | ||||
| -rw-r--r-- | packages/host-bin/meson.build | 2 | ||||
| -rw-r--r-- | packages/host-bin/src/main.cpp | 21 |
11 files changed, 1085 insertions, 2 deletions
diff --git a/assets/ext-window-field/field.rcss b/assets/ext-window-field/field.rcss new file mode 100644 index 0000000..da84358 --- /dev/null +++ b/assets/ext-window-field/field.rcss @@ -0,0 +1,77 @@ +/* The WINDOW FIELD layout (GLOSSARY: "window field"). RML compositing Wave 3. + LAYOUT + ANIMATION live ENTIRELY here (the user's contract decision): the C++ + glue only feeds the bound "wins" list + the per-row `focused` flag; this RCSS + decides every window's on-screen box and animates changes. Real tiling / + stage-manager is a LATER refinement on this same field + list — Wave 3 keeps + it SIMPLE but clearly working. + + THE LAYOUT (simple, N-window-safe with no per-index data): the field is a + flex row that wraps; every window is a flex tile sharing the space. The + FOCUSED window grows to dominate the field (a large flex-basis) and is + highlighted + lifted; the rest stay a row of smaller equal tiles. Because it + is pure flex keyed only on the `.focused` class, it works for any window + count and needs no geometry from C++. A `transition` on flex/transform/border + animates focus + map/unmap changes (RmlUi tweens the class flip). */ + +/* The field fills its whole surface (sized to the output by the glue). It is a + wrapping flex row with a little gutter; transparent so an unpainted gap shows + the scene below (per-pixel alpha). */ +body.field { + width: 100%; + height: 100%; + display: flex; + flex-direction: row; + flex-wrap: wrap; + align-items: stretch; + justify-content: center; + padding: 6dp; + background-color: transparent; +} + +/* A window tile. Default (unfocused): an equal, smaller share of the row. The + transition animates focus changes (flex-basis grow/shrink, the lift transform, + the highlight border) AND the layout reflow when a window maps/unmaps. RmlUi + 6.2 has no bare `linear` keyword, so the carrier ease is cubic-in-out. */ +div.win { + display: block; + position: relative; + flex: 1 1 30%; + min-width: 20%; + height: 46%; + margin: 6dp; + background-color: #1c1c20ff; + border-radius: 8dp; + border-width: 2dp; + border-color: #00000000; + overflow: hidden; + transform: scale(0.96); + transform-origin: 50% 50%; + transition: flex-basis 0.22s cubic-in-out, + width 0.22s cubic-in-out, + height 0.22s cubic-in-out, + transform 0.22s cubic-in-out, + border-color 0.22s cubic-in-out; +} + +/* The FOCUSED window DOMINATES the field: a large flex-basis so it takes the + bulk of the row (the rest wrap below as the smaller tile row), full height, a + bright highlight border, and a subtle lift (scale 1.0) — the "raise". This is + the only state C++ drives (the `focused` bool); everything else is geometry + RCSS owns. */ +div.win.focused { + flex: 1 1 100%; + height: 70%; + border-color: #4da3ffff; + transform: scale(1.0); +} + +/* The live window texture fills its tile 1:1 (the <img src=live_uri> of the + surface element). The tile's rounded overflow:hidden clips it to the card. */ +div.win img.live { + display: block; + position: absolute; + left: 0; + top: 0; + width: 100%; + height: 100%; +} diff --git a/assets/ext-window-field/field.rml b/assets/ext-window-field/field.rml new file mode 100644 index 0000000..15f031e --- /dev/null +++ b/assets/ext-window-field/field.rml @@ -0,0 +1,21 @@ +<rml> +<head> +<link type="text/rcss" href="field.rcss"/> +</head> +<!-- The WINDOW FIELD document (GLOSSARY: "window field"). data-model "ui". The + C++ glue (ext-window-field) drives ONLY the data: the bound "wins" list (one + row per mapped toplevel, each a live SURFACE ELEMENT) + the per-row `focused` + flag. LAYOUT + ANIMATION are RCSS (field.rcss) — the user's contract + decision: C++ never computes a window's on-screen rectangle. + + Each row renders one <img src="{{ w.live_uri }}"> = the window's live + texture (UiSubstrate::create_surface_element -> SurfaceElement::source_uri()). + The kernel manages the toplevel's whole subsurface/popup tree behind that + single <img>; the document addresses only the root. data-class-focused keys + the RCSS highlight/raise off the focused window. --> +<body data-model="ui" class="field"> +<div data-for="w : wins" class="win" data-class-focused="w.focused"> +<img class="live" src="{{ w.live_uri }}"/> +</div> +</body> +</rml> diff --git a/meson.build b/meson.build index 17668f5..102d8ab 100644 --- a/meson.build +++ b/meson.build @@ -62,4 +62,5 @@ subdir('packages/ext-xdg-shell') subdir('packages/ext-layer-shell') subdir('packages/ext-stage-dock') subdir('packages/ext-keybindings') +subdir('packages/ext-window-field') subdir('packages/host-bin') diff --git a/packages/ext-window-field/include/unbox/ext-window-field/ext_window_field.hpp b/packages/ext-window-field/include/unbox/ext-window-field/ext_window_field.hpp new file mode 100644 index 0000000..732a7c2 --- /dev/null +++ b/packages/ext-window-field/include/unbox/ext-window-field/ext_window_field.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include <unbox/kernel/extension.hpp> + +#include <memory> + +// ext-window-field — RML compositing's window manager (GLOSSARY: "window +// field"). The CORE extension that composites application toplevels as RCSS +// SURFACE ELEMENTS inside ONE ui surface, rather than as wlr_scene nodes. It: +// - subscribes to ext-xdg-shell's toplevel map/unmap/focus (typed Service), +// - turns each toplevel's root wl_surface (Toplevel::wl_surface()) into a +// kernel SurfaceElement (UiSubstrate::create_surface_element), +// - lists the windows into its own ui surface's RML document (a data-bound +// list of live source_uri()s) and lets RCSS lay them out + animate, +// - drives focus policy (click-to-focus -> SurfaceElement::focus_keyboard() + +// Toplevel::focus()), and takes each toplevel OUT of the wlr_scene composite +// (Toplevel::hide()) while it owns the window field. +// +// Layout and animation live entirely in RCSS (the user's contract decision); +// this extension only DRIVES the document (which windows exist + their data) and +// focus — it never computes on-screen geometry imperatively. +// +// This header is the unit's WHOLE cross-extension contract: a factory only +// (a leaf — exports no hooks/services yet; a future tiling/stage policy is RCSS +// + bound data on the same field). host-bin installs it ONLY when RML +// compositing is enabled. Manifest: { id "window-field", tier core, +// depends_on "xdg-shell" }. Single wl_event_loop thread throughout. + +namespace unbox::ext_window_field { + +// Construct the extension. Cheap and side-effect free (per the Extension +// contract); ALL wiring happens in activate(). Ownership transfers to the +// caller (host-bin installs it into the Server). +[[nodiscard]] auto create() -> std::unique_ptr<kernel::Extension>; + +} // namespace unbox::ext_window_field diff --git a/packages/ext-window-field/meson.build b/packages/ext-window-field/meson.build new file mode 100644 index 0000000..1b3766d --- /dev/null +++ b/packages/ext-window-field/meson.build @@ -0,0 +1,102 @@ +# ext-window-field — RML compositing Phase 2, Wave 3 (GLOSSARY: "window field"). +# The core extension that composites application toplevels as RCSS SURFACE +# ELEMENTS inside ONE ui surface (the "window field"), instead of as wlr_scene +# nodes: it subscribes to ext-xdg-shell's toplevel lifecycle, turns each +# toplevel's root wl_surface into a kernel SurfaceElement, lists them into its +# RML document, and lets RCSS lay them out + animate. Layout/animation are RCSS +# (the user's contract decision); this glue only DRIVES the document + focus. +# +# Installed by host-bin ONLY when RML compositing is enabled (the flag) — so the +# classic wlr_scene path stays the default until this is proven on the real seat. +# Tier: core; depends_on "xdg-shell". + +ext_window_field_inc = include_directories('include') + +# Glue library: needs the kernel ABI (ui substrate + SurfaceElement) and +# ext-xdg-shell's public contract (Toplevel::wl_surface(), the Service events). +ext_window_field_lib = static_library( + 'unbox-ext-window-field', + 'src/extension.cpp', + include_directories: ext_window_field_inc, + dependencies: [kernel_dep, ext_xdg_shell_dep], +) + +# What host-bin links against (the factory). kernel_dep rides through for the +# Extension ABI the factory returns; ext-xdg-shell stays a build-time dep of OUR +# lib only. +ext_window_field_dep = declare_dependency( + link_with: ext_window_field_lib, + include_directories: ext_window_field_inc, + dependencies: [kernel_dep], +) + +# Tests, asymmetric (AGENTS.md: strict cores, lenient shell). This unit is PURE +# GLUE — its only policy (window layout) lives in RCSS, not C++, so there is no +# doctest-able decision core; the SMOKE test (factory/manifest) + a lenient +# HEADLESS GLUE test (below) are the whole suite. + +# SMOKE test: the factory yields the window-field extension with the right +# manifest. Cheap, no kernel/wlroots running. +ext_window_field_smoke_test = executable( + 'ext-window-field-smoke-tests', + 'tests/test_window_field.cpp', + include_directories: [ext_window_field_inc, include_directories('src')], + dependencies: [ext_window_field_dep, doctest_dep], +) +test( + 'ext-window-field-smoke', + ext_window_field_smoke_test, + suite: 'ext-window-field', +) + +# HEADLESS GLUE test: install ext-xdg-shell + ext-window-field on the wlr +# headless backend, map REAL in-process client toplevels, then drive the +# map/unmap/focus PIPELINE and assert the bound window MODEL via the private test +# probe (src/probe.hpp) + that Toplevel::hide() was driven. Lenient ext-tier +# glue: the substrate is null on pixman (no GL), so the field UiSurface + +# SurfaceElements are null — this exercises the model + hide(), not the visual +# (same posture as ext-stage-dock's test_glue.cpp). Needs CLIENT-side xdg-shell +# bindings generated from the canonical xdg-shell.xml exactly as ext-stage-dock's +# test_glue.cpp does (header + private-code-as-header, #included once by the +# single C++ TU). `src` on the include path so the test reaches the private probe +# factory (own src/ only). +wf_wayland_client_dep = dependency('wayland-client') +wf_wayland_protocols_dir = dependency('wayland-protocols').get_variable('pkgdatadir') +wf_xdg_shell_xml = wf_wayland_protocols_dir / 'stable' / 'xdg-shell' / 'xdg-shell.xml' + +wf_xdg_shell_client_header = custom_target( + 'wf-xdg-shell-client-header', + input: wf_xdg_shell_xml, + output: 'xdg-shell-client-protocol.h', + command: [wayland_scanner, 'client-header', '@INPUT@', '@OUTPUT@'], +) +# private-code emitted as a .h so the C++ TU #includes it exactly once. +wf_xdg_shell_client_code = custom_target( + 'wf-xdg-shell-client-code-impl', + input: wf_xdg_shell_xml, + output: 'xdg-shell-client-protocol-code.h', + command: [wayland_scanner, 'private-code', '@INPUT@', '@OUTPUT@'], +) + +ext_window_field_glue_test = executable( + 'ext-window-field-glue-tests', + 'tests/test_glue.cpp', + wf_xdg_shell_client_header, + wf_xdg_shell_client_code, + include_directories: [ext_window_field_inc, include_directories('src')], + dependencies: [ext_window_field_dep, ext_xdg_shell_dep, wf_wayland_client_dep, doctest_dep], +) +test( + 'ext-window-field-glue', + ext_window_field_glue_test, + suite: 'ext-window-field', + # Real socket handshake + cooperative event-loop pump; generous timeout so a + # slow CI box does not flake (the test fails fast on its own logic). + timeout: 60, +) + +# Aggregate alias: `ninja -C build ext-window-field-tests`. +alias_target('ext-window-field-tests', + ext_window_field_smoke_test, + ext_window_field_glue_test, +) diff --git a/packages/ext-window-field/src/extension.cpp b/packages/ext-window-field/src/extension.cpp new file mode 100644 index 0000000..c1d297e --- /dev/null +++ b/packages/ext-window-field/src/extension.cpp @@ -0,0 +1,378 @@ +#include <unbox/ext-window-field/ext_window_field.hpp> + +#include "probe.hpp" + +#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp> +#include <unbox/kernel/host.hpp> +#include <unbox/kernel/ui.hpp> +#include <unbox/kernel/wlr.hpp> + +#include <algorithm> +#include <cstddef> +#include <memory> +#include <stdexcept> +#include <string> +#include <vector> + +// ext-window-field glue (RML compositing Phase 2, Wave 3 — GLOSSARY: "window +// field"). This is the WHOLE unit: there is no pure decision core to doctest — +// the "what windows exist + their per-row data" decision is trivial bookkeeping +// over a vector, and the LAYOUT (the only real policy) lives entirely in RCSS +// (assets/ext-window-field/field.{rml,rcss}) per the user's contract decision. +// So this glue only DRIVES the document: it pushes the live window list + the +// focused flag and lets RCSS lay them out and animate. +// +// The pipeline: +// on_toplevel_mapped -> create_surface_element(wl_surface) (owns the live +// import), Toplevel::hide() (take it OUT of wlr_scene +// — the surface element is now the only compositor of +// those pixels), append a Window row, dirty("wins"). +// on_toplevel_unmapped -> drop the Window (frees its SurfaceElement), dirty. +// on_toplevel_focused -> mark that window focused, dirty (RCSS raises/ +// highlights the focused row). +// +// Keyboard focus itself is ext-xdg-shell::Toplevel::focus()'s job (the seat +// keyboard enter); pointer/touch input-back to the client is automatic in the +// kernel (a pick on a surface element routes to its client). So this extension +// only tracks the focused flag for the RCSS highlight — it does NOT wire seat +// calls. Click-to-focus a BACKGROUND window is a known gap (the kernel does not +// yet notify the wm that a surface element was pressed); see the report's +// change-request. For Wave 3, focus follows map + on_toplevel_focused (Alt+Tab +// via ext-keybindings produces the latter). +// +// Everything runs on the single wl_event_loop thread. Every resource is a RAII +// member of WindowField; teardown is reverse-declaration destruction (no manual +// teardown lists — extension-agent.md). The Subscriptions release FIRST +// (declared last), then the field UiSurface (its bindings capture `this` and +// read windows_, so it is destroyed BEFORE windows_), then the windows_ vector +// (each Window's SurfaceElement frees its import) — all before the Host borrow +// goes away. + +namespace unbox::ext_window_field { +namespace { + +using kernel::Host; +using Toplevel = ext_xdg_shell::Toplevel; + +// One window in the field. The Toplevel* is a BORROW valid map..unmapped (per +// the ext-xdg-shell contract), so we may key our own tracking on it and deref +// it within that window; we drop the Window the instant on_toplevel_unmapped +// fires for it (never deref after). `element` owns the live import + the +// frame-callback duty; it is null on a no-GL backend (headless pixman) — the +// model is still tracked there (lenient-glue degrade). title/app_id are copied +// at map time (the Toplevel's title()/app_id() views are call-only). +struct Window { + Toplevel* tl = nullptr; // borrow, live until unmapped + std::unique_ptr<kernel::SurfaceElement> element; // owns the live import (null = no GL) + std::string title; // copied at map time + std::string app_id; // copied at map time +}; + +// The window field document lives in EXTERNAL ASSET FILES (loaded via +// UiSurfaceSpec::rml_path so layout changes need no recompile + dev hot-reload): +// assets/ext-window-field/field.rml — the RML STRUCTURE: data-model "ui", +// data-for="w : wins" emitting one .win per window with the live <img +// src="{{ w.live_uri }}">, data-class-focused="w.focused" for the RCSS +// highlight/raise. It links field.rcss via <link type="text/rcss" ...>. +// assets/ext-window-field/field.rcss — ALL the layout: the focused window +// fills the field, the rest are a row of smaller tiles along the bottom; a +// `transition` animates focus/layout changes. +// The C++ binding setup (bind_list*/bind_list_string/bool in create_field_surface) +// is re-applied by the substrate across hot-reloads. + +class WindowFieldExtension final : public kernel::Extension, public TestProbe { +public: + [[nodiscard]] auto manifest() const -> const kernel::Manifest& override { return manifest_; } + + // ---- TestProbe (src/probe.hpp; glue-test only) ---- + [[nodiscard]] auto activated() const -> bool override { return activated_; } + [[nodiscard]] auto window_count() const -> std::size_t override { return windows_.size(); } + [[nodiscard]] auto live_uri(std::size_t i) const -> std::string override { + if (i >= windows_.size() || windows_[i].element == nullptr) { + return std::string{}; + } + return windows_[i].element->source_uri(); + } + [[nodiscard]] auto has_surface_element(std::size_t i) const -> bool override { + return i < windows_.size() && windows_[i].element != nullptr; + } + [[nodiscard]] auto focused_index() const -> std::ptrdiff_t override { + return index_of(focused_); + } + [[nodiscard]] auto hidden_count() const -> std::size_t override { return hidden_count_; } + + void activate(Host& host) override { + host_ = &host; + + // Fatal: a missing ext-xdg-shell Service. The field composites that + // extension's toplevels as surface elements — meaningless without it. + // depends_on "xdg-shell" guarantees it activated first, so absence is a + // broken core session (extension.hpp: activation failure is fatal). + shell_ = host.service<ext_xdg_shell::Service>(); + if (shell_ == nullptr) { + throw std::runtime_error( + "ext-window-field: ext-xdg-shell Service unavailable (depends_on " + "\"xdg-shell\" not satisfied)"); + } + + // Create the window field surface up front (sized to the primary output + // if one exists yet). Null on a no-GL backend (headless pixman) — we + // degrade gracefully: the model is still tracked, hide() still runs, + // there is just no composited surface. + create_field_surface(); + + // Re-size the field to a freshly-added output (Wave 3 tracks the primary + // output only — multi-output is a follow-up; see the report). If the + // field was created before any output existed (size 0), this gives it + // its first real size. + output_added_ = host.subscribe( + host.on_output_added(), [this](const kernel::OutputEvent&) { size_to_primary_output(); }); + + // Window lifecycle via the ext-xdg-shell Service. The Toplevel* borrow + // is valid mapped..unmapped, so we key windows_ on it and only deref a + // live one; we drop the Window on unmapped. + mapped_ = host.subscribe( + shell_->on_toplevel_mapped(), + [this](const ext_xdg_shell::ToplevelEvent& e) { on_mapped(e.toplevel); }); + focused_sub_ = host.subscribe( + shell_->on_toplevel_focused(), + [this](const ext_xdg_shell::ToplevelEvent& e) { on_focused(e.toplevel); }); + unmapped_ = host.subscribe( + shell_->on_toplevel_unmapped(), + [this](const ext_xdg_shell::ToplevelEvent& e) { on_unmapped(e.toplevel); }); + + activated_ = true; + } + +private: + // ---- window lifecycle --------------------------------------------------- + + // A toplevel mapped: composite it as a surface element + take it out of + // wlr_scene. ext-xdg-shell focuses a freshly-mapped window, so it becomes + // the focused window of the field (on_toplevel_focused may also fire — both + // converge on the same value). + void on_mapped(Toplevel* tl) { + if (tl == nullptr) { + return; + } + Window w; + w.tl = tl; + w.title = std::string(tl->title()); // copy: title() is call-only + w.app_id = std::string(tl->app_id()); // copy: app_id() is call-only + + // Turn the toplevel's ROOT wl_surface into a live surface element. The + // wl_surface is a borrow valid until unmapped (we drop the element then). + // create_surface_element returns null on a no-GL backend / failed import + // (graceful degrade); the model is still tracked. wl_surface() returns + // null only for an already-destroyed toplevel (never for a live mapped + // one) — guard anyway. + if (host_->ui().available()) { + if (wlr_surface* surface = tl->wl_surface(); surface != nullptr) { + w.element = host_->ui().create_surface_element(surface); + } + } + + // Take the toplevel OUT of wlr_scene: the surface element is now the ONLY + // compositor of those pixels (the substrate drives the client's frame + // callbacks). hide() is NOT unmap — the client stays mapped, its Toplevel* + // borrow stays valid, and no on_toplevel_unmapped fires. + tl->hide(); + ++hidden_count_; + + windows_.push_back(std::move(w)); + focused_ = tl; // map-focus: a freshly mapped window is focused + dirty_wins(); + } + + // A toplevel unmapping: drop its Window (frees the SurfaceElement — we must + // NOT sample a surface element after its wl_surface is gone; UB per ui.hpp) + // and clear focus tracking if it was the focused one. The Toplevel* borrow + // is dead after this call — never deref it again. + void on_unmapped(Toplevel* tl) { + std::erase_if(windows_, [tl](const Window& w) { return w.tl == tl; }); + if (focused_ == tl) { + focused_ = nullptr; + } + dirty_wins(); + } + + // Keyboard focus moved to `tl` (map-focus, click/tap-to-focus on a surface + // element the kernel routed, or programmatic Toplevel::focus() — e.g. + // ext-keybindings' Alt+Tab). We only update the focused flag the RCSS keys + // its highlight/raise off; the seat keyboard enter itself is ext-xdg-shell's + // job, already done by the time this fires. + void on_focused(Toplevel* tl) { + focused_ = tl; + dirty_wins(); + } + + // ---- field surface ------------------------------------------------------ + + // Create the window field UiSurface at SceneLayer::normal (the toplevel + // band) sized to the primary output, and register all data bindings BEFORE + // the first frame. Null surface (no-GL backend) is fine — we skip the + // bindings and the model is still tracked. + void create_field_surface() { + wlr_box box = primary_output_box(); + + kernel::UiSurfaceSpec spec; + // External asset (RELATIVE to the asset root the orchestrator wires) so + // the field document is editable without recompiling + dev hot-reloads. + spec.rml_path = "ext-window-field/field.rml"; + spec.model = "ui"; + spec.x = box.x; + spec.y = box.y; + // The substrate rejects non-positive geometry; on a backend with no + // output yet box is 0x0, so clamp to >= 1 (size_to_primary_output() + // resizes to the real output on on_output_added). + spec.width = std::max(1, box.width); + spec.height = std::max(1, box.height); + spec.layer = kernel::SceneLayer::normal; // the application toplevel band + spec.visible = true; + + field_surface_ = host_->ui().create_surface(spec); + if (field_surface_ == nullptr) { + return; // no GL path: degrade gracefully (model only) + } + + // List bindings. All registered BEFORE the first frame, capturing only + // `this` (whose members outlive the surface, which is destroyed before + // them in reverse declaration order). The list is named "wins"; the RML + // iterates data-for="w : wins". + field_surface_->bind_list( + "wins", [this]() -> std::size_t { return windows_.size(); }); + // live_uri: the SurfaceElement's <img src> (its live texture URI). Empty + // when the element is null (no-GL) — the RML still renders the row (RCSS + // shows the placeholder background); the substrate ignores an empty src. + field_surface_->bind_list_string( + "wins", "live_uri", [this](std::size_t i) -> std::string { + if (i >= windows_.size() || windows_[i].element == nullptr) { + return std::string{}; + } + return windows_[i].element->source_uri(); + }); + // focused: the RCSS highlight/raise key (data-class-focused="w.focused"). + field_surface_->bind_list_bool( + "wins", "focused", [this](std::size_t i) -> bool { + return i < windows_.size() && windows_[i].tl == focused_; + }); + // title / app_id: optional per-row strings (a future label/RCSS hook). + field_surface_->bind_list_string( + "wins", "title", [this](std::size_t i) -> std::string { + return i < windows_.size() ? windows_[i].title : std::string{}; + }); + field_surface_->bind_list_string( + "wins", "app_id", [this](std::size_t i) -> std::string { + return i < windows_.size() ? windows_[i].app_id : std::string{}; + }); + } + + // Re-read the bound list (count + every visible row field) and re-render the + // field on the next frame. No-op when the surface is null (no-GL backend) — + // the model is still tracked for the probe. + void dirty_wins() { + if (field_surface_ != nullptr) { + field_surface_->dirty("wins"); + } + } + + // Resize/reposition the field to the primary output's box. Called on + // on_output_added. No-op when the surface is null or the box is empty. + void size_to_primary_output() { + if (field_surface_ == nullptr) { + return; + } + wlr_box box = primary_output_box(); + if (box.width <= 0 || box.height <= 0) { + return; + } + field_surface_->set_position(box.x, box.y); + field_surface_->set_size(box.width, box.height); + } + + // The primary (first) output's box in layout coords, or an empty box (0x0) + // when no output exists yet. Wave 3 sizes the single field to the primary + // output; multi-output (one field per output) is a follow-up. + [[nodiscard]] auto primary_output_box() const -> wlr_box { + wlr_box box{}; + wlr_output_layout* ol = host_->output_layout(); + if (ol == nullptr) { + return box; + } + wlr_output_layout_output* lo = nullptr; + wl_list_for_each(lo, &ol->outputs, link) { + wlr_box b{}; + wlr_output_layout_get_box(ol, lo->output, &b); + if (!wlr_box_empty(&b)) { + box = b; + } + break; // primary output only (Wave 3) + } + return box; + } + + // ---- helpers ------------------------------------------------------------ + + // The list index of toplevel `tl`, or -1 if it is not tracked / is null. + [[nodiscard]] auto index_of(Toplevel* tl) const -> std::ptrdiff_t { + if (tl == nullptr) { + return -1; + } + for (std::size_t i = 0; i < windows_.size(); ++i) { + if (windows_[i].tl == tl) { + return static_cast<std::ptrdiff_t>(i); + } + } + return -1; + } + + const kernel::Manifest manifest_{ + .id = "window-field", + .tier = kernel::Tier::core, + .depends_on = {"xdg-shell"}, + }; + + Host* host_ = nullptr; + ext_xdg_shell::Service* shell_ = nullptr; // borrow; fetched in activate() + bool activated_ = false; // TestProbe; set at end of activate() + std::size_t hidden_count_ = 0; // # windows taken out of wlr_scene + + // The currently focused window (a borrow valid until its unmapped). Drives + // the per-row `focused` bool the RCSS highlights/raises. Cleared when its + // window unmaps. + Toplevel* focused_ = nullptr; + + // The window model. Declared BEFORE field_surface_ so the surface (whose + // bindings read windows_) is destroyed FIRST — windows_ (and its + // SurfaceElements) stay valid through the surface's teardown, then drop, all + // before host_'s borrow ends. Each Window owns a SurfaceElement (frees its + // import on erase/destruction). + std::vector<Window> windows_; + + // The window field ui surface. Destroyed before windows_ (declared after it) + // so any binding getter invoked during its teardown still sees a live + // windows_; destroyed before host_'s borrow ends (it is a member). Null on a + // no-GL backend. + std::unique_ptr<kernel::UiSurface> field_surface_; + + // RAII subscriptions — declared LAST so they release FIRST at teardown, + // before the field surface + model their callbacks touch (listener-lifetime). + kernel::Subscription output_added_; + kernel::Subscription mapped_; + kernel::Subscription focused_sub_; + kernel::Subscription unmapped_; +}; + +} // namespace + +auto create() -> std::unique_ptr<kernel::Extension> { + return std::make_unique<WindowFieldExtension>(); +} + +auto make_extension_with_probe() -> ExtensionWithProbe { + auto ext = std::make_unique<WindowFieldExtension>(); + TestProbe* probe = ext.get(); + return ExtensionWithProbe{.extension = std::move(ext), .probe = probe}; +} + +} // namespace unbox::ext_window_field diff --git a/packages/ext-window-field/src/probe.hpp b/packages/ext-window-field/src/probe.hpp new file mode 100644 index 0000000..da2c953 --- /dev/null +++ b/packages/ext-window-field/src/probe.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include <unbox/kernel/extension.hpp> + +#include <cstddef> +#include <memory> +#include <string> + +// Test-only probe surface (PRIVATE — src/, never part of the contract). The +// headless glue test needs to read the window-field MODEL (the bound window +// list) without a GL substrate: on the headless pixman backend the ui surface + +// the SurfaceElements are null (no GL path), so there is nothing visual to +// assert — only the list/model and that Toplevel::hide() was driven. This probe +// exposes exactly that. The public create() hides the concrete extension behind +// kernel::Extension; make_extension_with_probe() hands back the same Extension +// plus a borrowed probe the test polls. +// +// Glue/shell test convenience only; never a contract claim. All calls are on +// the single event-loop thread, like the extension itself. + +namespace unbox::ext_window_field { + +// A non-owning view onto the live extension for tests. Valid as long as the +// returned unique_ptr (and thus the extension) is alive. +class TestProbe { +public: + virtual ~TestProbe() = default; + + // True once activate() completed (Service fetched, hooks wired, the window + // field ui surface creation attempted). + [[nodiscard]] virtual auto activated() const -> bool = 0; + + // The number of windows currently tracked in the window field (the bound + // "wins" list size). + [[nodiscard]] virtual auto window_count() const -> std::size_t = 0; + + // The live_uri the list binds for window `i` (SurfaceElement::source_uri(), + // or "" when the surface element is null — no GL path on the headless + // pixman backend). Empty string for an out-of-range index. + [[nodiscard]] virtual auto live_uri(std::size_t i) const -> std::string = 0; + + // Whether window `i` carries a non-null SurfaceElement (true only when the + // substrate has a GL path). False on the headless pixman backend. + [[nodiscard]] virtual auto has_surface_element(std::size_t i) const -> bool = 0; + + // The index of the currently focused window in the list, or -1 if none / + // the focused window is not tracked. The RCSS keys its highlight/raise off + // the per-row `focused` bool this drives. + [[nodiscard]] virtual auto focused_index() const -> std::ptrdiff_t = 0; + + // How many tracked windows have been hidden out of wlr_scene (had + // Toplevel::hide() driven on map). One per mapped window the field owns. + [[nodiscard]] virtual auto hidden_count() const -> std::size_t = 0; +}; + +struct ExtensionWithProbe { + std::unique_ptr<unbox::kernel::Extension> extension; // install() this + TestProbe* probe = nullptr; // borrow into the above +}; + +// Same extension as create(), but also yields a probe borrow. +[[nodiscard]] auto make_extension_with_probe() -> ExtensionWithProbe; + +} // namespace unbox::ext_window_field diff --git a/packages/ext-window-field/tests/test_glue.cpp b/packages/ext-window-field/tests/test_glue.cpp new file mode 100644 index 0000000..d49d26f --- /dev/null +++ b/packages/ext-window-field/tests/test_glue.cpp @@ -0,0 +1,365 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include <doctest/doctest.h> + +#include "probe.hpp" // PRIVATE: a unit may read its own src/ (drives the model) + +#include <unbox/ext-xdg-shell/ext_xdg_shell.hpp> +#include <unbox/kernel/host.hpp> +#include <unbox/kernel/server.hpp> +#include <unbox/kernel/wlr.hpp> + +#include <wayland-client.h> + +// CLIENT-side xdg-shell bindings, generated from the canonical xdg-shell.xml the +// same way ext-stage-dock's test_glue.cpp does (header + private-code-as-header, +// #included once by this single C++ TU). Lets in-process real clients map +// xdg_toplevels so we drive the window-field map/unmap/focus PIPELINE end to end +// on the wlr headless backend. +#include "xdg-shell-client-protocol.h" +#include "xdg-shell-client-protocol-code.h" + +#include <cstdint> +#include <cstdlib> +#include <cstring> +#include <memory> +#include <poll.h> +#include <unistd.h> + +// ext-window-field GLUE test — lenient, headless (AGENTS.md: strict cores, +// lenient shell). This unit has NO pure decision core (layout is RCSS, not C++), +// so the whole verification is this integration pipeline: +// install ext-xdg-shell + ext-window-field, activate (fetch the Service — the +// only fatal path), map a REAL toplevel, and assert (via the private probe) +// that it became a TRACKED window (count 1, hidden out of wlr_scene), mapping +// a SECOND grows the list to 2, unmapping removes it, and on_toplevel_focused +// flips the tracked focused index. +// +// On the headless pixman backend the ui substrate has no GL path, so the field +// UiSurface + the SurfaceElements are null — exactly the degrade the brief calls +// for: the MODEL/list + Toplevel::hide() still work; there is no live texture to +// assert (live_uri is empty, has_surface_element() false). A separate OBSERVER +// extension (mirroring ext-xdg-shell/tests/test_minimize.cpp) captures the mapped +// Toplevel borrows so the test can drive on_toplevel_focused via the Service. + +namespace { + +using unbox::ext_window_field::ExtensionWithProbe; +using unbox::ext_window_field::TestProbe; +using unbox::ext_xdg_shell::Service; +using unbox::ext_xdg_shell::Toplevel; +using unbox::ext_xdg_shell::ToplevelEvent; + +// ---- observer extension: captures the live Toplevel borrows (test-only) ----- +// +// Depends on xdg-shell (so its Service is registered first). Holds the Service +// (to call Toplevel::focus(), which produces on_toplevel_focused) and the most +// recently mapped Toplevel*, plus the FIRST mapped one — both borrows valid +// mapped..unmapped, so the test can re-focus the first window after the second +// maps and assert the field's focused index follows. +class Observer final : public unbox::kernel::Extension { +public: + [[nodiscard]] auto manifest() const -> const unbox::kernel::Manifest& override { + return manifest_; + } + + void activate(unbox::kernel::Host& host) override { + svc_ = host.service<Service>(); + REQUIRE(svc_ != nullptr); + sub_mapped_ = host.subscribe( + svc_->on_toplevel_mapped(), [this](const ToplevelEvent& e) { + last_mapped_ = e.toplevel; + if (first_mapped_ == nullptr) { + first_mapped_ = e.toplevel; + } + }); + sub_unmapped_ = host.subscribe( + svc_->on_toplevel_unmapped(), [this](const ToplevelEvent& e) { + if (e.toplevel == last_mapped_) { + last_mapped_ = nullptr; + } + if (e.toplevel == first_mapped_) { + first_mapped_ = nullptr; + } + }); + } + + [[nodiscard]] auto service() -> Service* { return svc_; } + [[nodiscard]] auto last_mapped() -> Toplevel* { return last_mapped_; } + [[nodiscard]] auto first_mapped() -> Toplevel* { return first_mapped_; } + +private: + unbox::kernel::Manifest manifest_{"test-observer", unbox::kernel::Tier::standard, + {"xdg-shell"}}; + Service* svc_ = nullptr; + Toplevel* last_mapped_ = nullptr; + Toplevel* first_mapped_ = nullptr; + unbox::kernel::Subscription sub_mapped_; + unbox::kernel::Subscription sub_unmapped_; +}; + +// ---- minimal xdg-shell client (mirrors ext-stage-dock/tests/test_glue.cpp) --- + +struct Client { + wl_display* display = nullptr; + wl_registry* registry = nullptr; + wl_compositor* compositor = nullptr; + wl_shm* shm = nullptr; + xdg_wm_base* wm_base = nullptr; +}; + +// One mapped toplevel's client objects (the field tracks multiple windows, so we +// map several from the same connection). +struct Window { + wl_surface* surface = nullptr; + xdg_surface* xsurface = nullptr; + xdg_toplevel* toplevel = nullptr; + wl_buffer* buffer = nullptr; + bool configured = false; +}; + +void wm_base_ping(void*, xdg_wm_base* wm, std::uint32_t serial) { + xdg_wm_base_pong(wm, serial); +} +const xdg_wm_base_listener kWmBaseListener{wm_base_ping}; + +void registry_global(void* data, wl_registry* reg, std::uint32_t name, + const char* iface, std::uint32_t version) { + auto* c = static_cast<Client*>(data); + if (std::strcmp(iface, wl_compositor_interface.name) == 0) { + c->compositor = static_cast<wl_compositor*>( + wl_registry_bind(reg, name, &wl_compositor_interface, 4)); + } else if (std::strcmp(iface, wl_shm_interface.name) == 0) { + c->shm = static_cast<wl_shm*>(wl_registry_bind(reg, name, &wl_shm_interface, 1)); + } else if (std::strcmp(iface, xdg_wm_base_interface.name) == 0) { + c->wm_base = static_cast<xdg_wm_base*>(wl_registry_bind( + reg, name, &xdg_wm_base_interface, version < 3 ? version : 3)); + xdg_wm_base_add_listener(c->wm_base, &kWmBaseListener, c); + } +} +void registry_global_remove(void*, wl_registry*, std::uint32_t) {} +const wl_registry_listener kRegistryListener{registry_global, registry_global_remove}; + +void xsurface_configure(void* data, xdg_surface* xs, std::uint32_t serial) { + auto* w = static_cast<Window*>(data); + xdg_surface_ack_configure(xs, serial); + w->configured = true; +} +const xdg_surface_listener kXSurfaceListener{xsurface_configure}; + +void toplevel_configure(void*, xdg_toplevel*, std::int32_t, std::int32_t, wl_array*) {} +void toplevel_close(void*, xdg_toplevel*) {} +const xdg_toplevel_listener kToplevelListener{toplevel_configure, toplevel_close, {}, {}}; + +// Cooperative single-thread pump (the standard libwayland guarded-read dance). +void pump(unbox::kernel::Server& server, wl_display* client) { + wl_display_flush(client); + server.dispatch(5); + while (wl_display_prepare_read(client) != 0) { + wl_display_dispatch_pending(client); + } + wl_display_flush(client); + pollfd pfd{wl_display_get_fd(client), POLLIN, 0}; + if (poll(&pfd, 1, 5) > 0 && (pfd.revents & POLLIN)) { + wl_display_read_events(client); + } else { + wl_display_cancel_read(client); + } + wl_display_dispatch_pending(client); +} + +auto make_headless_server() -> std::unique_ptr<unbox::kernel::Server> { + setenv("WLR_BACKENDS", "headless", 1); + setenv("WLR_RENDERER", "pixman", 1); + setenv("WLR_HEADLESS_OUTPUTS", "1", 1); + return unbox::kernel::Server::create({}); +} + +// A w*h ARGB shm buffer so the surface actually maps (a toplevel maps on its +// first buffer-bearing commit after the initial configure). +auto make_buffer(Client& c, int w, int h) -> wl_buffer* { + const int stride = w * 4; + const int size = stride * h; + char name[] = "/tmp/unbox-window-field-test-XXXXXX"; + int fd = mkstemp(name); + REQUIRE(fd >= 0); + unlink(name); + REQUIRE(ftruncate(fd, size) == 0); + REQUIRE(c.shm != nullptr); + wl_shm_pool* pool = wl_shm_create_pool(c.shm, fd, size); + wl_buffer* buffer = + wl_shm_pool_create_buffer(pool, 0, w, h, stride, WL_SHM_FORMAT_ARGB8888); + wl_shm_pool_destroy(pool); + close(fd); + return buffer; +} + +// Map one toplevel from connection `c` and pump until it is mapped server-side. +// `gate` is polled until non-null (the observer's most-recently-mapped borrow), +// so the caller can confirm the new window mapped. +void map_window(unbox::kernel::Server& server, Client& c, Window& w, const char* title) { + w.surface = wl_compositor_create_surface(c.compositor); + w.xsurface = xdg_wm_base_get_xdg_surface(c.wm_base, w.surface); + xdg_surface_add_listener(w.xsurface, &kXSurfaceListener, &w); + w.toplevel = xdg_surface_get_toplevel(w.xsurface); + xdg_toplevel_add_listener(w.toplevel, &kToplevelListener, &w); + xdg_toplevel_set_title(w.toplevel, title); + wl_surface_commit(w.surface); // mandatory empty initial commit + + for (int i = 0; i < 200 && !w.configured; ++i) { + pump(server, c.display); + } + REQUIRE(w.configured); + + w.buffer = make_buffer(c, 256, 256); + REQUIRE(w.buffer != nullptr); + wl_surface_attach(w.surface, w.buffer, 0, 0); + wl_surface_damage(w.surface, 0, 0, 256, 256); + wl_surface_commit(w.surface); +} + +void destroy_window(Window& w) { + if (w.toplevel != nullptr) { + xdg_toplevel_destroy(w.toplevel); + } + if (w.xsurface != nullptr) { + xdg_surface_destroy(w.xsurface); + } + if (w.surface != nullptr) { + wl_surface_destroy(w.surface); + } + if (w.buffer != nullptr) { + wl_buffer_destroy(w.buffer); + } + w = Window{}; +} + +} // namespace + +TEST_CASE("ext-window-field installs and activates atop ext-xdg-shell") { + auto server = make_headless_server(); + server->install(unbox::ext_xdg_shell::create()); + // Topological activation runs xdg-shell first (window-field depends_on it), + // so activate() finds the Service. A missing-Service throw would propagate. + ExtensionWithProbe ewp = unbox::ext_window_field::make_extension_with_probe(); + TestProbe* probe = ewp.probe; + server->install(std::move(ewp.extension)); + server->activate_extensions(); + CHECK(!server->socket_name().empty()); + CHECK(probe->activated()); + CHECK(probe->window_count() == 0); + CHECK(probe->focused_index() == -1); + CHECK(probe->hidden_count() == 0); +} + +TEST_CASE("ext-window-field: map tracks a window + hides it; a 2nd grows the list; " + "focus flips the flag; unmap removes it") { + auto server = make_headless_server(); + server->install(unbox::ext_xdg_shell::create()); + + auto observer_owned = std::make_unique<Observer>(); + Observer* observer = observer_owned.get(); + server->install(std::move(observer_owned)); + + ExtensionWithProbe ewp = unbox::ext_window_field::make_extension_with_probe(); + TestProbe* probe = ewp.probe; + server->install(std::move(ewp.extension)); + + server->activate_extensions(); + REQUIRE(!server->socket_name().empty()); + REQUIRE(probe->activated()); + + Client c; + c.display = wl_display_connect(server->socket_name().c_str()); + REQUIRE(c.display != nullptr); + c.registry = wl_display_get_registry(c.display); + wl_registry_add_listener(c.registry, &kRegistryListener, &c); + + for (int i = 0; i < 50 && + (c.compositor == nullptr || c.wm_base == nullptr || c.shm == nullptr); + ++i) { + pump(*server, c.display); + } + REQUIRE(c.compositor != nullptr); + REQUIRE(c.wm_base != nullptr); + REQUIRE(c.shm != nullptr); + + // --- map the FIRST toplevel -> becomes a tracked window, hidden from scene + Window w1; + map_window(*server, c, w1, "unbox-field-1"); + for (int i = 0; i < 200 && observer->last_mapped() == nullptr; ++i) { + pump(*server, c.display); + } + Toplevel* tl1 = observer->last_mapped(); + REQUIRE(tl1 != nullptr); + CHECK(probe->window_count() == 1); + // hide() took it OUT of wlr_scene (the surface element is now the compositor). + CHECK(probe->hidden_count() == 1); + // ext-xdg-shell focuses a freshly-mapped window, so the field tracks it as + // focused (map-focus; on_toplevel_focused may also have fired — same value). + CHECK(probe->focused_index() == 0); + // No GL path on headless pixman: the surface element is null, so live_uri is + // empty + has_surface_element() is false (lenient-glue degrade per the brief). + CHECK(probe->has_surface_element(0) == false); + CHECK(probe->live_uri(0).empty()); + + // --- map a SECOND toplevel -> the list grows to 2, the 2nd is now focused + Window w2; + map_window(*server, c, w2, "unbox-field-2"); + for (int i = 0; i < 200 && observer->last_mapped() == tl1; ++i) { + pump(*server, c.display); + } + Toplevel* tl2 = observer->last_mapped(); + REQUIRE(tl2 != nullptr); + CHECK(tl2 != tl1); + CHECK(probe->window_count() == 2); + CHECK(probe->hidden_count() == 2); + CHECK(probe->focused_index() == 1); // map-focus moved to the 2nd window + + // --- on_toplevel_focused flips the focused flag: re-focus the FIRST window + // via Toplevel::focus() (exactly what ext-keybindings' Alt+Tab does). The + // field's focused index must follow back to window 0. + REQUIRE(observer->first_mapped() == tl1); + tl1->focus(); + for (int i = 0; i < 50 && probe->focused_index() != 0; ++i) { + pump(*server, c.display); + } + CHECK(probe->focused_index() == 0); + CHECK(probe->window_count() == 2); // focus changes membership not at all + + // --- unmap the SECOND window -> the list shrinks back to 1 + destroy_window(w2); + for (int i = 0; i < 200 && probe->window_count() != 1; ++i) { + pump(*server, c.display); + } + CHECK(probe->window_count() == 1); + // The first window is still tracked + still the focused one. + CHECK(probe->focused_index() == 0); + + // --- unmap the FIRST too -> empty + no focused window + destroy_window(w1); + for (int i = 0; i < 200 && probe->window_count() != 0; ++i) { + pump(*server, c.display); + } + CHECK(probe->window_count() == 0); + CHECK(probe->focused_index() == -1); + + // Teardown: dropping the server destroys extensions in reverse activation + // order; within the field, Subscriptions release first, then the field + // UiSurface (null here), then the windows_ vector (empty now) — all before + // the Host borrow ends (asan watches this). + if (c.wm_base != nullptr) { + xdg_wm_base_destroy(c.wm_base); + } + if (c.shm != nullptr) { + wl_shm_destroy(c.shm); + } + if (c.compositor != nullptr) { + wl_compositor_destroy(c.compositor); + } + if (c.registry != nullptr) { + wl_registry_destroy(c.registry); + } + wl_display_flush(c.display); + pump(*server, c.display); + wl_display_disconnect(c.display); +} diff --git a/packages/ext-window-field/tests/test_window_field.cpp b/packages/ext-window-field/tests/test_window_field.cpp new file mode 100644 index 0000000..f585247 --- /dev/null +++ b/packages/ext-window-field/tests/test_window_field.cpp @@ -0,0 +1,20 @@ +#define DOCTEST_CONFIG_IMPLEMENT_WITH_MAIN +#include <doctest/doctest.h> + +#include <unbox/ext-window-field/ext_window_field.hpp> +#include <unbox/kernel/extension.hpp> + +// SCAFFOLD (Wave 3). Smoke test that the factory + manifest are wired. The +// owner-agent replaces/extends this with the real headless glue test (install +// ext-xdg-shell + ext-window-field on the wlr headless backend, map a real +// in-process client toplevel, and assert it becomes a listed surface element + +// that focus routes), mirroring ext-stage-dock's test_glue.cpp client-codegen. + +TEST_CASE("ext-window-field: factory yields the window-field extension") { + auto ext = unbox::ext_window_field::create(); + REQUIRE(ext != nullptr); + CHECK(ext->manifest().id == "window-field"); + CHECK(ext->manifest().tier == unbox::kernel::Tier::core); + REQUIRE(ext->manifest().depends_on.size() == 1); + CHECK(ext->manifest().depends_on[0] == "xdg-shell"); +} diff --git a/packages/host-bin/meson.build b/packages/host-bin/meson.build index 76f0e75..2ff8559 100644 --- a/packages/host-bin/meson.build +++ b/packages/host-bin/meson.build @@ -4,6 +4,6 @@ executable( 'unbox', 'src/main.cpp', - dependencies: [kernel_dep, ext_xdg_shell_dep, ext_layer_shell_dep, ext_keybindings_dep, ext_stage_dock_dep], + dependencies: [kernel_dep, ext_xdg_shell_dep, ext_layer_shell_dep, ext_keybindings_dep, ext_stage_dock_dep, ext_window_field_dep], install: true, ) diff --git a/packages/host-bin/src/main.cpp b/packages/host-bin/src/main.cpp index 7d33ac5..507efdc 100644 --- a/packages/host-bin/src/main.cpp +++ b/packages/host-bin/src/main.cpp @@ -3,11 +3,13 @@ #include <unbox/ext-keybindings/ext_keybindings.hpp> #include <unbox/ext-layer-shell/ext_layer_shell.hpp> #include <unbox/ext-stage-dock/ext_stage_dock.hpp> +#include <unbox/ext-window-field/ext_window_field.hpp> #include <unbox/ext-xdg-shell/ext_xdg_shell.hpp> #include <unbox/kernel/kernel.hpp> #include <unbox/kernel/server.hpp> #include <cstdio> +#include <cstdlib> #include <exception> #include <optional> #include <string> @@ -16,7 +18,9 @@ namespace { void print_usage(const char* argv0) { - std::printf("usage: %s [-s <startup command>] [--config <path>] [--ui-demo]\n", argv0); + std::printf("usage: %s [-s <startup command>] [--config <path>] [--ui-demo] " + "[--rml-compositing]\n", + argv0); } } // namespace @@ -24,6 +28,13 @@ void print_usage(const char* argv0) { auto main(int argc, char* argv[]) -> int { unbox::kernel::Server::Options options; bool ui_demo = false; + // RML compositing (Phase 2): when on, install ext-window-field, which + // composites toplevels as RCSS surface elements instead of wlr_scene nodes. + // Default OFF so the proven wlr_scene path stays the default until RML + // compositing is signed off on the real seat. Enable via --rml-compositing + // or the UNBOX_RML_COMPOSITING env var (the flag is intentionally a runtime + // toggle so the two compositing paths can be A/B'd on hardware). + bool rml_compositing = std::getenv("UNBOX_RML_COMPOSITING") != nullptr; std::optional<std::string> config_path; for (int i = 1; i < argc; ++i) { const std::string_view arg = argv[i]; @@ -37,6 +48,8 @@ auto main(int argc, char* argv[]) -> int { // Slice-5 acceptance demo (temporary until the real UI extensions): // a ui surface via the public substrate contract. ui_demo = true; + } else if (arg == "--rml-compositing") { + rml_compositing = true; } else { print_usage(argv[0]); return arg == "-h" ? 0 : 1; @@ -56,6 +69,12 @@ auto main(int argc, char* argv[]) -> int { // dock of previews; tap a preview to restore. Standard tier, hidden until // it holds a minimized window (depends_on xdg-shell, topo-activated). server->install(unbox::ext_stage_dock::create()); + // RML compositing (Phase 2, opt-in): the window field composites toplevels + // as RCSS surface elements. depends_on "xdg-shell" (topologically + // activated). When off, toplevels keep compositing through wlr_scene. + if (rml_compositing) { + server->install(unbox::ext_window_field::create()); + } if (ui_demo) { server->install(unbox::host_bin::create_demo_ui()); } |
