docs(mosaic): preserve STEP20C valid grab probe evidence

This commit is contained in:
fy59 2026-10-11 08:26:03 +02:00
parent 76a765a996
commit 285239a58e
17 changed files with 1286 additions and 14 deletions

2
.gitattributes vendored Normal file
View file

@ -0,0 +1,2 @@
# Preserved experimental patches include diff markers and source-context tabs.
tooling/probes/step20c-valid-grab/*.patch -whitespace

View file

@ -26,8 +26,9 @@ choose a convenient interpretation.
`docs/KNOWN_LIMITATIONS.md`, and `docs/ROADMAP.md`. `docs/KNOWN_LIMITATIONS.md`, and `docs/ROADMAP.md`.
4. For build/source work: `docs/DEVELOPMENT.md` and `docs/UPSTREAM.md`. 4. For build/source work: `docs/DEVELOPMENT.md` and `docs/UPSTREAM.md`.
For packaging: `docs/PACKAGING.md`. For packaging: `docs/PACKAGING.md`.
5. For STEP20C: all four reports under `docs/design/STEP20C_*.md`, 5. For STEP20C: all reports under `docs/design/STEP20C_*.md`,
especially `STEP20C_POPUP_OWNERSHIP_PROBE.md`. especially `STEP20C_POPUP_OWNERSHIP_PROBE.md` and
`STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md`.
Canonical architecture/UX/packaging/upstream/development documents govern their Canonical architecture/UX/packaging/upstream/development documents govern their
own topics. Design documents are proposals or isolated evidence; historical own topics. Design documents are proposals or isolated evidence; historical
@ -60,14 +61,16 @@ Resolve contradictory canonical documents **before** implementing behavior.
validated as a daily-driver compositor. Do not claim otherwise. validated as a daily-driver compositor. Do not claim otherwise.
- The isolated SceneFX subtree clip covered sampled tiled visual/input cases. - The isolated SceneFX subtree clip covered sampled tiled visual/input cases.
A later isolated owner-derived root-popup clip covered sampled GTK root and A later isolated owner-derived root-popup clip covered sampled GTK root and
nested popup cases on two horizontal headless outputs. Both are only nested popup cases on two horizontal headless outputs. A further private
**PARTIALLY_FEASIBLE**, not a production dependency decision. probe measured a valid-serial XDG grab, found a fully hidden owner leak in
- Next evidence required: reproducible **valid-serial XDG grabs** (distinguish that initial clip, and closed the sampled leak with a disposable owner
popup dismissal/grab behavior from invisible scene hits), **fully hidden visibility guard. All remain **PARTIALLY_FEASIBLE**, not a production
popup owner**, vertical/negative-origin/scaled/rotated outputs, output dependency or UX decision.
migration/resize, transaction-generation consistency, damage/stale pixels, - Next evidence required: valid-grab nested popup behavior, vertical/negative-
decorations/shadows/blur/saved buffers, and SceneFX/wlroots ABI/package origin/scaled/rotated outputs, output migration/resize, transaction-generation
maintenance. Mark any untested matrix cell **NOT TESTED**. consistency, complete damage/stale-pixel coverage, decorations/shadows/blur/
saved buffers, and SceneFX/wlroots ABI/package maintenance. Mark any untested
matrix cell **NOT TESTED**.
- For popup semantics, compare the modified private compositor against an - For popup semantics, compare the modified private compositor against an
unmodified headless SwayFX control. Do not promise universal click-through. unmodified headless SwayFX control. Do not promise universal click-through.
Offscreen-popup hide/dismiss/reposition UX remains undecided. Offscreen-popup hide/dismiss/reposition UX remains undecided.
@ -76,6 +79,12 @@ Resolve contradictory canonical documents **before** implementing behavior.
production readiness without an explicit decision and closed feasibility production readiness without an explicit decision and closed feasibility
gates. Use disposable private checkouts, libraries, sockets and logs for gates. Use disposable private checkouts, libraries, sockets and logs for
bounded probes. bounded probes.
- A bounded probe may preserve reviewed source fixtures and private patch files
under `tooling/probes/` with base commits, hashes, reproduction instructions
and an explicit experimental status. Keep generated files, binaries, build
trees, sockets, large logs and transient captures outside Git. Fixture
preservation does not add a production build dependency or authorize merging
the experimental patch.
## Safe development workflow ## Safe development workflow

View file

@ -2,6 +2,10 @@
## Unreleased ## Unreleased
### STEP20C experimental evidence
- Preserved the valid-serial XDG grab / hidden-owner probe's original C and Python sources, private patches and protocol XML with hashes and a disposable headless replay procedure. Its result remains **PARTIALLY_FEASIBLE**; no production source, package or UX policy changed.
### Patch export tooling ### Patch export tooling
- Fixed cumulative Mosaic patch export to diff against the verified pristine SwayFX 0.6 import, preserve the real Git index, validate clean-base application and source equivalence, and replace the patch atomically only after successful checks. - Fixed cumulative Mosaic patch export to diff against the verified pristine SwayFX 0.6 import, preserve the real Git index, validate clean-base application and source equivalence, and replace the patch atomically only after successful checks.

View file

@ -59,7 +59,11 @@ The isolated [STEP20C feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) co
The follow-up SceneFX subtree spike is also **PARTIALLY FEASIBLE**. It confirms that a tree-local render/hit clip can cover ordinary tiled descendants in an isolated binary, including pointer pass-through to another output. The globally parented popup path violates that boundary. Its temporary static link demonstrates a possible package shape, not a decided SceneFX dependency or ABI contract. The next design must bind popups to the owner's viewport without changing ordinary floating, fullscreen or standard-layout popup behavior. The follow-up SceneFX subtree spike is also **PARTIALLY FEASIBLE**. It confirms that a tree-local render/hit clip can cover ordinary tiled descendants in an isolated binary, including pointer pass-through to another output. The globally parented popup path violates that boundary. Its temporary static link demonstrates a possible package shape, not a decided SceneFX dependency or ABI contract. The next design must bind popups to the owner's viewport without changing ordinary floating, fullscreen or standard-layout popup behavior.
The later [popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) demonstrated, in an isolated private build, a root-popup scene clip derived from the tiled owner’s applied Mosaic viewport. A tested nested popup inherited the clip; sampled visible popup input worked and the clipped-away popup did not intercept the ordinary client on the adjacent output. Standard `splith` behavior was restored after layout exit. Valid-serial XDG grabs, fully hidden popup owners, additional output geometries, complete damage/effect coverage and package maintenance remain unresolved. This evidence does not authorize production STEP20C or multi-output Mosaic support. The later [popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) demonstrated, in an isolated private build, a root-popup scene clip derived from the tiled owner’s applied Mosaic viewport. A tested nested popup inherited the clip; sampled visible popup input worked and the clipped-away popup did not intercept the ordinary client on the adjacent output. Standard `splith` behavior was restored after layout exit. Valid-serial XDG grabs and fully hidden popup owners were untested in that probe; additional output geometries, complete damage/effect coverage and package maintenance also remained unresolved. This evidence does not authorize production STEP20C or multi-output Mosaic support.
The [valid-grab and hidden-owner probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md) measured a real button serial passed to `xdg_popup.grab` and compared unmodified SwayFX, private standard layout and private Mosaic. It found that the initial viewport-only popup clip leaked pixels and hits when the owner was entirely offscreen. A further disposable guard based on the owner's applied scene visibility suppressed the sampled leak while preserving the popup for camera return. The private guard does not establish a production transaction-generation contract, offscreen-popup UX policy or SceneFX ABI/package decision; broader STEP20C gates remain open.
The [preserved source fixture](../tooling/probes/step20c-valid-grab/README.md) reconstructs the private guard from pinned bases and patches. It supplies reproducible evidence, not a compositor-owned STEP20C implementation.
## IPC ## IPC

View file

@ -46,6 +46,16 @@ git diff --check
The headless script starts its own compositor socket; use its documented `MOSAIC_BINARY` environment variable for a nondefault build path. Do not send test commands to the running desktop's IPC socket. Record compiler/test failures, not only passes. The headless script starts its own compositor socket; use its documented `MOSAIC_BINARY` environment variable for a nondefault build path. Do not send test commands to the running desktop's IPC socket. Record compiler/test failures, not only passes.
## Preserved STEP20C experimental fixture
The [valid-grab source fixture](../tooling/probes/step20c-valid-grab/README.md)
records the exact private patch bases, hashes, dependencies and temporary
headless replay commands for the [bounded probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md).
These source files are experimental evidence. Rebuild in a new temporary
laboratory; leave generated protocol files, private libraries, compositor
binaries, sockets, logs and screenshots outside Git. This fixture is not part
of the ordinary Mosaic build, cumulative production patch or Arch package.
## Staging and package checks ## Staging and package checks
```sh ```sh

View file

@ -18,7 +18,11 @@ This document describes the development tree at the documentation foundation pas
The [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md) identifies an additional **design feasibility blocker**: installed SceneFX 0.5 exposes no generic scene-tree clip. Its proposed Mosaic-only projection requires a disposable headless proof that all relevant nodes, popups, damage and pointer hits respect one viewport boundary. The proposal does not change any current support status. The [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md) identifies an additional **design feasibility blocker**: installed SceneFX 0.5 exposes no generic scene-tree clip. Its proposed Mosaic-only projection requires a disposable headless proof that all relevant nodes, popups, damage and pointer hits respect one viewport boundary. The proposal does not change any current support status.
The later [popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) is **PARTIALLY_FEASIBLE**. A private owner-derived root-popup clip kept tested root and nested GTK popup pixels and pointer hits inside the Mosaic viewport on two horizontal headless outputs. Valid-serial grabs, fully hidden owners, other output arrangements, complete damage and effects remain unqualified. This does not change the current **NOT YET SUPPORTED** multi-output status. The later [popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) is **PARTIALLY_FEASIBLE**. A private owner-derived root-popup clip kept tested root and nested GTK popup pixels and pointer hits inside the Mosaic viewport on two horizontal headless outputs. Valid-serial grabs and fully hidden owners were untested in that probe; other output arrangements, complete damage and effects also remained unqualified. This does not change the current **NOT YET SUPPORTED** multi-output status.
The [valid-grab and hidden-owner follow-up](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md) is also **PARTIALLY_FEASIBLE**. A genuine pointer-button serial produced an interactive grabbed XDG popup in unmodified SwayFX, private standard layout and private Mosaic. The previous owner-derived viewport clip leaked popup pixels and button hits when the owner was fully hidden. A further disposable owner-visibility guard removed that sampled leak while the popup stayed mapped and returned with the camera. Valid-grab nested popups, other output arrangements, transaction-generation consistency, complete damage/effect coverage and ABI/package maintenance remain unqualified. Multi-output Mosaic is still **NOT YET SUPPORTED**.
The [retained source fixture](../tooling/probes/step20c-valid-grab/README.md) permits bounded replay; it does not close these qualification gaps.
The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; the disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage too. Popups, effects, saved buffers, vertical Y-crossing behavior, pointer behavior in those additional arrangements and complete damage correctness remain unqualified. Multi-output Mosaic remains **NOT YET SUPPORTED**. The [isolated feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) is **PARTIALLY FEASIBLE**, not a release qualification. On two horizontal headless outputs, real Mosaic content leaked visually into an empty adjacent output; the disposable candidate also delivered pointer enter from a partly visible client outside its viewport. Negative-origin, scaled and rotated arrangements showed visual leakage too. Popups, effects, saved buffers, vertical Y-crossing behavior, pointer behavior in those additional arrangements and complete damage correctness remain unqualified. Multi-output Mosaic remains **NOT YET SUPPORTED**.

View file

@ -17,7 +17,9 @@ The [root README](../README.md) is the public summary. For a specific topic, use
9. [STEP20C feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md): isolated technical evidence and an unresolved multi-output gate. 9. [STEP20C feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md): isolated technical evidence and an unresolved multi-output gate.
10. [STEP20C SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md): isolated clipping, popup and packaging evidence; production gate remains open. 10. [STEP20C SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md): isolated clipping, popup and packaging evidence; production gate remains open.
11. [STEP20C popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md): isolated owner-derived root/nested popup evidence and remaining grab, damage and packaging gates. 11. [STEP20C popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md): isolated owner-derived root/nested popup evidence and remaining grab, damage and packaging gates.
11. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence. 12. [STEP20C valid-grab and hidden-owner probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md): isolated valid-serial XDG grab comparison, fully hidden owner leak and private guard evidence; production gates remain open.
[Preserved source fixtures](../tooling/probes/step20c-valid-grab/README.md) record exact inputs and a disposable replay procedure.
13. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence.
## Project ## Project
@ -52,6 +54,7 @@ The [root README](../README.md) is the public summary. For a specific topic, use
| [STEP20C feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) | Experimental evidence | PARTIALLY FEASIBLE; visual and input boundary failed in a two-output headless candidate | | [STEP20C feasibility probe](design/STEP20C_FEASIBILITY_PROBE.md) | Experimental evidence | PARTIALLY FEASIBLE; visual and input boundary failed in a two-output headless candidate |
| [STEP20C SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) | Experimental evidence | PARTIALLY FEASIBLE; tiled clip worked privately, global popup leaked pixels and input | | [STEP20C SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md) | Experimental evidence | PARTIALLY FEASIBLE; tiled clip worked privately, global popup leaked pixels and input |
| [STEP20C popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; owner-derived root/nested popup clipping worked in a horizontal private headless case, with grab/damage/output gates open | | [STEP20C popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; owner-derived root/nested popup clipping worked in a horizontal private headless case, with grab/damage/output gates open |
| [STEP20C valid-grab and hidden-owner probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md) | Experimental evidence | PARTIALLY_FEASIBLE; valid-serial popup grab measured, hidden-owner leak found and closed only in a private guard; broader STEP20C gates remain open |
A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation. A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation.

View file

@ -22,7 +22,9 @@ This is the canonical order of work, not proof that a feature exists. **A step i
## STEP20C — viewport correctness · NEXT ## STEP20C — viewport correctness · NEXT
**Goal:** make the single-row viewport correct at workspace and output boundaries. **Planned deliverables:** clipping or an equivalent rendering strategy, matching hit testing, adjacent-output tests, resize/migration tests, and transaction-safe camera animation. **Exit criteria:** no cross-output visual/input leakage in the tested scenarios; camera and IPC remain consistent across transactions; standard layouts still pass. **Docs first:** review the [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md), resolve the visual/input gaps found by the [PARTIALLY FEASIBLE isolated probe](design/STEP20C_FEASIBILITY_PROBE.md) and the [PARTIALLY FEASIBLE SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md), then settle the resulting contract in Architecture and UX and update Known Limitations before implementation. The subtree spike proves a narrow tiled path but exposes a popup visual/input leak and unresolved damage, effect and package-maintenance gates. The follow-up [PARTIALLY_FEASIBLE popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) closes the sampled root/nested popup leak in an isolated build but leaves valid-serial grabs, hidden-owner behavior, other output geometries, complete damage/effects and packaging unresolved. None of these probes authorizes production STEP20C development or general multi-output support. **Goal:** make the single-row viewport correct at workspace and output boundaries. **Planned deliverables:** clipping or an equivalent rendering strategy, matching hit testing, adjacent-output tests, resize/migration tests, and transaction-safe camera animation. **Exit criteria:** no cross-output visual/input leakage in the tested scenarios; camera and IPC remain consistent across transactions; standard layouts still pass. **Docs first:** review the [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md), resolve the visual/input gaps found by the [PARTIALLY FEASIBLE isolated probe](design/STEP20C_FEASIBILITY_PROBE.md) and the [PARTIALLY FEASIBLE SceneFX subtree spike](design/STEP20C_SCENEFX_CLIPPING_SPIKE.md), then settle the resulting contract in Architecture and UX and update Known Limitations before implementation. The subtree spike proves a narrow tiled path but exposes a popup visual/input leak and unresolved damage, effect and package-maintenance gates. The [PARTIALLY_FEASIBLE popup ownership probe](design/STEP20C_POPUP_OWNERSHIP_PROBE.md) closes the sampled root/nested popup leak in an isolated build. The later [PARTIALLY_FEASIBLE valid-grab and hidden-owner probe](design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md) measures a real XDG grab and exposes a hidden-owner leak in that initial private clip; a further private guard closes the sampled leak. Valid-grab nested popups, other output geometries, transaction/output migration, complete damage/effects and packaging remain unresolved. None of these probes authorizes production STEP20C development or general multi-output support.
The [source fixture for the valid-grab probe](../tooling/probes/step20c-valid-grab/README.md) preserves a bounded replay path. Its existence does not change STEP20C's **NEXT** status or any exit criterion.
## STEP20D — multi-row Spatial Canvas · PLANNED ## STEP20D — multi-row Spatial Canvas · PLANNED

View file

@ -0,0 +1,146 @@
# STEP20C valid grab and hidden popup owner probe
**Status: EXPERIMENTAL / PARTIALLY_FEASIBLE — valid-grab fixture demonstrated; hidden-owner leak found in the retained private candidate and closed at sampled points by a further private guard.** This is an isolated feasibility probe, not a production STEP20C implementation or a multi-output support claim. The canonical [architecture](../ARCHITECTURE.md), [UX](../UX.md), [limitations](../KNOWN_LIMITATIONS.md), and [roadmap](../ROADMAP.md) retain authority.
## Problem and existing evidence
SwayFX attaches a root XDG popup scene below `root->layers.popup`, outside its owner's tiled scene tree. The earlier private SceneFX inherited clip, applied to the tiled tree alone, therefore allowed the popup to render and receive pointer events on HEADLESS-2. The later [popup ownership probe](STEP20C_POPUP_OWNERSHIP_PROBE.md) applied a clip to the root popup scene derived from its Mosaic owner's applied tiled viewport. Sampled visible and clipped-away root and nested popup points behaved as intended on two horizontal headless outputs; a standard `splith` return restored sampled ordinary behavior. That probe's GTK request used `xdg_popup.grab(wl_seat, 0)`, so valid-grab behavior remains **NOT TESTED**. Its attempt to hide the owner changed focus and dismissed the popup before measurement; a fully hidden owner remains **NOT TESTED**.
The relevant paths are `sway/desktop/xdg_shell.c` (owner, popup scene creation and unconstrain), `sway/desktop/transaction.c` (applied tiled and popup scene positions), `sway/input/cursor.c` (scene hit traversal), wlroots 0.20 XDG shell grab and pointer handling, and private SceneFX 0.5 inherited render/hit/damage clipping. XDG unconstrain currently reads pending content geometry, while the private clip reads applied scene/current state. Their agreement across transaction generations has not been proved.
## Hypotheses and expected boundary
1. A popup created after a real `wl_pointer.button` serial and `xdg_popup.grab(seat, serial)` remains interactive where visible. At a clipped-away scene position, its surface receives no pointer enter or button. A valid grab may still dismiss the popup or consume an outside click; compare this with unmodified SwayFX instead of assuming click-through.
2. A root popup can remain mapped after its tiled Mosaic owner is translated entirely outside the applied viewport. The owner retains the same Sway container identity and workspace. The private clip should then prevent popup pixels and scene hits outside the owner's viewport. If `popup_done`, unmap, focus change or client teardown occurs first, the test is inconclusive for clipping.
3. Returning the owner to view or leaving Mosaic restores the appropriate sampled popup rendering/input without stale pixels. Comprehensive transaction, damage, effects and output-geometry coverage remains a later gate.
## Experimental protocol and acceptance
Use a small Wayland client with a real XDG toplevel and popup. The client must log pointer enter/leave, both button codes, the exact serial received in a `wl_pointer.button`, the same serial passed to `xdg_popup.grab`, XDG configure acknowledgements, popup mapping, popup pointer events and `popup_done`. Request the grab after the input event and before the first popup buffer commit, in protocol order. Preserve `WAYLAND_DEBUG=1` traces. An arbitrary nonzero serial or a grab request without an interactive configured popup is **FAIL** as a fixture. A popup that closed before the hidden-owner measurement is **INCONCLUSIVE**, not a clip PASS.
Run three private headless cases with two horizontal 800×600 outputs: **A** unmodified SwayFX with a standard layout; **B** private SceneFX/Mosaic binary with a standard layout; **C** that private binary with Mosaic on HEADLESS-1 and an ordinary client on HEADLESS-2. Place the owner and popup so the popup is fully visible, partly visible and physically extends beyond the Mosaic viewport. Inject left and right clicks at visible, clipped-away and ordinary outside positions. Record both output screenshots, global/output/client-local coordinates, client and compositor logs, protocol trace, IPC tree/outputs, applied clip logs, hit and button events, dismissal and `popup_done`. Compare outside-click consumption and dismissal in A/B/C; an invisible popup scene hit is a failure even if a legitimate grab consumes an outside click.
For the hidden-owner case, open and verify the popup first. Move only the Mosaic camera so the owner's *applied* tiled rectangle is entirely outside the viewport, without opening new windows or transferring focus as part of the maneuver. Record its container ID, logical rectangle, `scroll_x`, output viewport, popup configure/map state, scene clip, two-output pixels and pointer events. Move the camera back, then separately test workspace switch and owner destruction. Distinguish clip removal from XDG dismissal and unmap. Check appearance, movement and disappearance for sampled stale pixels and SceneFX damage records, without claiming exhaustive damage correctness.
| Scenario | Control A | Private standard B | Private Mosaic C | Acceptance state |
| --- | --- | --- | --- | --- |
| Valid-serial XDG grab | PASS: button/grab serial 61 | PASS: serial 61 | PASS: serial 55 | PASS: configured, interactive popup and outside-click dismissal without protocol error |
| Entire popup inside HEADLESS-1 | PASS: 250×190, both buttons | PASS: same | PASS: same | PASS at `(350,150)`; no `popup_done` |
| Visible region of crossing popup | PASS at `(700,150)` | PASS at `(700,150)` | PASS at `(700,150)` | PASS for sampled left/right popup events |
| Partly clipped popup | Ordinary cross-output popup visible | Same as control | Visible HEADLESS-1, clipped HEADLESS-2 | PASS at sampled pixels and pointer positions |
| Input outside viewport `(900,150)` | Popup receives left/right | Popup receives left/right | No popup enter/button; `wev` receives left/right | PASS for sampled clipped region; valid grab also sends `popup_done` |
| True exterior `(900,400)` under grab | `wev` gets left/right; `popup_done` | Same as control | Same as control | PASS for sampled dismissal and delivery |
| Nested popup | NOT TESTED in this probe | NOT TESTED | NOT TESTED | Previous non-valid-grab nested case does not qualify this row |
| Fully hidden owner with popup still mapped | No camera analogue | No camera analogue | Baseline FAIL; private guard PASS at sampled point | Owner stayed ID 8; no `popup_done` before hidden measurement |
| Return of hidden owner | — | — | PASS at sampled point with same popup | Camera 1280→400 restored yellow popup without configure/recreation |
| Return to standard layout | Ordinary reference | Ordinary reference | PASS at `(900,150)` | Clip cleared; popup pixel and right-button delivery returned |
| Workspace switch / owner destruction | NOT TESTED | NOT TESTED | Measured separately | Switch sent `popup_done`; destroy ended client without `popup_done` |
| Damage and stale pixels | Sampled screenshots | Sampled screenshots | PARTIAL | No yellow residue at tested points; logs show damage, no complete effect/transaction proof |
Any persistent visible/hittable leak or unavoidable ordinary-layout regression is **BLOCKED** for this approach. Positive sampled cases with open gates are **PARTIALLY_FEASIBLE**. If a reliable fixture cannot be obtained, the affected cases are **INCONCLUSIVE** or **NOT TESTED**. This matrix does not qualify STEP20C as complete.
## Isolation and starting provenance
No production source, package, dotfile, installed library, session or live IPC socket may change. Use a dedicated `/tmp` laboratory with private source/build directories, `XDG_RUNTIME_DIR`, `WAYLAND_DISPLAY`, explicit `SWAYSOCK`, headless outputs and process-local `LD_LIBRARY_PATH` only for the private compositor. Do not alter the public `struct wlr_scene_tree` layout. Transient logs, screenshots, generated protocol code and builds stay outside the repository. The later evidence-freeze task retained only reviewed source fixtures under [`tooling/probes/step20c-valid-grab/`](../../tooling/probes/step20c-valid-grab/README.md); they are not production dependencies.
At probe start, local `main`, `origin/main` and `github/main` were `76a765a9965815a52586cc10e56e9d99f2720bcb`; the worktree was clean. Installed `wayland-client`, SceneFX and wlroots pkg-config versions were 1.26.0, 0.5.0 and 0.20.2. `/usr/bin/sway` reported SwayFX 0.6. The retained `/tmp/swayfx-mosaic-popup-yEamPX/` laboratory exists: private Mosaic source HEAD `cd550382b311efdbfe3ed892f4e1d281d852e754`, SceneFX source HEAD `37ccd723bef49e6891156ffafce8f549f01446cc`; complete private patch SHA-256 values match the prior report (`d416f6413ea097a48a44d231e50ffba363914f46c8687f0ea2e4dcacd5f31fe9` and `c74c515b98da8f5f637e260e6a3755c7a75bd78d1c39e54a67850f1c383e30e7`). Private binaries exist but will be copied or rebuilt in a new disposable laboratory before use. Their presence alone is not a new result.
## Running observations
The new laboratory is `/tmp/swayfx-mosaic-valid-grab-7SA2yO/`. It holds copies of the verified private sources, fresh `scenefx-build/` and `mosaic-build/`, `grab-client.c`, generated XDG protocol files, `pointer-daemon.c`, `run.py`, and per-case logs/screenshots. The client initially observed `wl_seat.capabilities(0)` before a virtual pointer existed; a first attempt without a persistent pointer produced no usable input and is not counted. The corrected fixture starts a virtual-pointer process before the client, retains it for the full scenario and logs injected events. A second initial Mosaic attempt used logical IPC `rect.x` as if it were rendered x; correcting by the measured `scroll_x` gave the real input point. These failed fixture attempts do not count as scientific observations.
In `run-control`, `run-private_standard` and `run-private_mosaic`, the client received a real left `wl_pointer.button` press serial (61, 61 and 55 respectively) on its toplevel and passed the identical serial in `xdg_popup.grab(wl_seat, serial)` before the popup's first commit. Each run received `xdg_popup.configure` followed by `xdg_surface.configure` (62 in control and private standard, 56 in private Mosaic), acknowledged it and mapped a 500×190 yellow SHM popup. The client stayed alive and received no protocol error. At global `(900,150)`, the standard control and private standard screenshots show yellow popup pixels on HEADLESS-2; the private Mosaic screenshot shows the ordinary output client. This is initial valid-serial and visual evidence, pending more pointer comparisons.
The retained private candidate **fails the fully hidden owner case**. The owner remained container ID 8 with logical rectangle `(640,25,640,575)` and the same focused Mosaic row. The applied camera changed from `scroll_x=400` to `1280`, putting the owner horizontally at `[-640,0)` against viewport `[0,800)`. No `popup_done` occurred before or during the measurement; the client remained alive. The popup remained yellow at HEADLESS-1 `(100,150)` while the normal background at `(300,150)` was dark. A virtual-pointer move entered the popup (`POINTER_ENTER target=popup`), and a right click at `(100,150)` generated popup button 273 press/release. The private log changed the root clip from `local=-242,-25 800x600` to `local=638,-25 800x600`, a viewport-only clip that still intersects the popup despite its owner being completely outside. Returning to `scroll_x=400` preserved the mapped popup and generated no `popup_done`; the later ordinary outside right click delivered to `wev` and then `popup_done`. Screenshots and logs are in `run-private_mosaic_hit/`. This is a real scene pixel/hit leak, not a grab-only outside-click effect. It blocks accepting the current owner-derived viewport clip *as implemented* for fully hidden owners.
To discriminate whether the failure was inherent to the architecture, a second private variant checked the owner's applied scene rectangle before setting the popup clip. That change is a feasibility discriminant, not a production patch or a settled UX policy.
That discriminant was implemented only in the copied private `mosaic/sway/desktop/transaction.c`, with the previous file retained as `transaction-before-hidden.c`. After rebuilding the private compositor, `run-private_mosaic/` and `run-private_mosaic_hit/` repeated the same 400→1280→400 camera sequence. The guard checks whether the owner's applied scene rectangle intersects the applied viewport and sets an empty root-popup clip when it does not. At hidden `(100,150)`, the yellow baseline pixel `(224,192,32)` became ordinary dark background `(30,30,46)`; the hover produced no popup enter and the hidden right click produced no popup button. The compositor logged `PROBE_POPUP_HIDDEN_OWNER` once for the hide transition. The popup remained mapped and no `popup_done` occurred during hide or hover; with no hidden click, returning the camera restored its visible yellow region without a new popup configure or recreation. In the separate hidden-click run, the valid grab generated `popup_done` on the outside click, as expected from grab dismissal, while the popup surface received no button. These are sampled visual/input results for the private guard; they do not settle whether offscreen popups should be hidden, dismissed or repositioned in production.
The final hidden-owner log recorded the applied owner scene rectangle `(-640,0,640,600)` and applied viewport `(0,0,800,600)`, directly confirming their empty horizontal intersection. IPC retained the owner's logical rectangle `(640,25,640,575)` and container identity; the scene coordinates differ because camera translation and decorations are applied by the compositor. This is why the guard uses scene position rather than treating IPC `rect.x` as physical x.
## Comparative pointer and lifecycle results
The XDG positioner reported popup geometry `(350,80,500,190)` relative to the owner in all three compositor configurations. The private Mosaic owner was logical `(640,25,640,575)` with camera 400 at popup creation. The control and private standard layout rendered the popup at global `(900,150)` (HEADLESS-2 local `(100,150)`), where screenshots read `(224,192,32)`. They delivered both left button 272 and right button 273 to the popup at that point; `wev` received neither, and the popup was not dismissed. In Mosaic, HEADLESS-2 local `(100,150)` read ordinary client `(238,238,238)` with no popup enter/button at global `(900,150)`. `wev` received both left and right buttons in separate runs; each exterior click also caused `popup_done`. At visible Mosaic popup `(700,150)`, left and right clicks reached the popup in separate runs without `popup_done`. Thus the clipped-away surface was not receiving events, while the valid grab still handled dismissal. The pointer entered and left the popup on camera changes; the client-local coordinates remained reported by Wayland, but exhaustive coordinate transformations were not tested.
A second XDG positioner variant `(50,80,250,190)` kept the entire popup within HEADLESS-1. In control, private standard and private Mosaic runs, global `(350,150)` was yellow `(224,192,32)` and the popup received both left and right buttons without `popup_done`; HEADLESS-2 local `(100,150)` remained ordinary `(238,238,238)`. These separate `fully_visible` runs establish the whole-popup case instead of inferring it from the visible portion of a crossing popup.
At the true exterior `(900,400)`, both button codes reached `wev` and caused `popup_done` in **all** three configurations. The direct control comparison is important: a `popup_done` alone does not mean that a hidden scene surface received the click. Returning from Mosaic to `splith` while the popup remained open made HEADLESS-2 local `(100,150)` yellow again and delivered right button 273 to the popup, with no `popup_done` before that click. Switching workspaces while the owner was hidden sent `popup_done` and left the client alive. Destroying the hidden owner instead terminated the client; no `popup_done` was observed in that path. Neither mechanism is counted as a clip proof.
Sampled damage observations are limited. On HEADLESS-1, exact yellow popup-color pixels counted 39,520 while open, 0 with the private guard and owner hidden, and 39,520 after camera return. The unguarded baseline had 40,280 yellow pixels while the owner was hidden. The private SceneFX log contains `SPIKE_DAMAGE` entries on both outputs during the transitions, including large regions; these logs do not isolate popup-only invalidation. The captured frames show no yellow residue at the sampled point after hiding or after `popup_done`, but do not prove all vacated pixels, frames, effects or saved transaction buffers are correct. The client remained mapped during the no-click hide/return run; no new XDG configure was required for its return.
## Reproduction and retained artifacts
The original laboratory root is `/tmp/swayfx-mosaic-valid-grab-7SA2yO/`; this path is historical and **not required** for the preserved [replay fixture](../../tooling/probes/step20c-valid-grab/README.md). Its three private source patches are `mosaic-popup-plus-base.patch` (SHA-256 `d416f6413ea097a48a44d231e50ffba363914f46c8687f0ea2e4dcacd5f31fe9`), `scenefx-popup-plus-base.patch` (`c74c515b98da8f5f637e260e6a3755c7a75bd78d1c39e54a67850f1c383e30e7`) and `hidden-guard.patch` (`35785a8f3e863b3c1fd87932d8ff0c2949b03cb04382cc34e1126170968f802b`). The first Mosaic patch applies to the already-Mosaic project commit `cd550382b311efdbfe3ed892f4e1d281d852e754`, **not** directly to pristine upstream SwayFX `fd71a6bd...`; an initial attempted `git apply --check` on the upstream commit failed at `transaction.c:974`. The SceneFX patch applies to `37ccd723bef49e6891156ffafce8f549f01446cc`. Applying the patches in that order on clean archives, then comparing `include/`, `sway/`, Meson files, the Mosaic model test, and SceneFX `include/` and `types/` against the laboratory sources passed. The archive check is a source reproducibility check, not a package/ABI qualification.
`grab-client.c` SHA-256 is `202afdc5dae0ee4aba570eef4a563f8b298474eb84e33af9510c4c81c05d16e4`; `pointer-daemon.c` is `a00fcedc38ccb7ca4c25c982d9f38e8bde3168a3833f0c9cf2056e841d720938`; `run.py` is `b2aaa90188ea387cb8a69aa69935b56e63f5ce70eb98e74783b0fb3c9236a41f`. These six source files are now byte-identically preserved in Git with the three patches. The added upstream `wlr-virtual-pointer-unstable-v1.xml` at `wlr-protocols` commit `b010a03648b88d143236de193bddbfea0c08bc84` has SHA-256 `3ff6d540be0bc5228195bf072bde42117ea17945a5c2061add5d3cf97d6bb524` and regenerates the original virtual-pointer C/header byte-for-byte. Generated XDG and virtual-pointer C/header files, both private builds, the earlier unguarded `transaction-before-hidden.c`, and per-run `client.log` (including `WAYLAND_DEBUG=1`), `pointer.log`, `wev.log`, `compositor.log`, IPC JSON and two-output PNG files remain outside Git in the original laboratory. `run-private_mosaic_hit_baseline/` is the retained unguarded leak; `run-private_mosaic_hit/` is the guarded hidden-click comparison. `run-control/`, `run-private_standard/`, `run-private_mosaic/`, the six `boundary_{left,right}` directories, `outside_left`, `visible_left`, `fully_visible`, `standard_return`, `lifecycle_switch` and `lifecycle_destroy` directories retain the other cases.
The core commands used, after copying or reconstructing the two verified private sources and applying `hidden-guard.patch`, were:
```sh
R=/tmp/swayfx-mosaic-valid-grab-7SA2yO
meson setup "$R/scenefx-build" "$R/scenefx" -Dexamples=false -Dwerror=false
meson compile -C "$R/scenefx-build" -j 8
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson setup "$R/mosaic-build" "$R/mosaic" \
-Dmosaic-package=true -Ddefault-wallpaper=false -Dswaybar=false -Dswaynag=false \
-Dman-pages=disabled -Dsd-bus-provider=libsystemd -Dwerror=false
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson compile -C "$R/mosaic-build" -j 8
wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml "$R/xdg-shell-client.h"
wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml "$R/xdg-shell-protocol.c"
cc -std=c17 -Wall -Wextra -Wno-unused-parameter -I"$R" "$R/grab-client.c" "$R/xdg-shell-protocol.c" -o "$R/grab-client" $(pkg-config --cflags --libs wayland-client)
cc -std=c17 -Wall -Wextra -Wno-unused-parameter -I"$R" "$R/pointer-daemon.c" "$R/virtual-pointer-protocol.c" -o "$R/pointer-daemon" $(pkg-config --cflags --libs wayland-client)
python "$R/run.py" control boundary_left
python "$R/run.py" private_standard boundary_right
python "$R/run.py" private_mosaic boundary_left
python "$R/run.py" private_mosaic boundary_right
python "$R/run.py" private_mosaic fully_visible
python "$R/run.py" private_mosaic
python "$R/run.py" private_mosaic_hit
```
The runner gives each case its own mode-0700 headless `XDG_RUNTIME_DIR`, discovers that case's Wayland/IPC sockets, and always passes the private socket with `swaymsg -s`. It sets private `LD_LIBRARY_PATH` only on the private compositor process; clients and injectors do not inherit it. Its virtual pointer remains alive until the case ends. Installed packages were `swayfx 0.6-1`, `scenefx 0.5-1` and `wlroots0.20 0.20.2-1`; `pkg-config` reported Wayland client 1.26.0, SceneFX 0.5.0 and wlroots 0.20.2. `ldd` with process-local `LD_LIBRARY_PATH` resolved the private `libscenefx-0.5.so` and system `libwlroots-0.20.so`. `nm -D` found the private `wlr_scene_tree_set_clip` and `wlr_scene_tree_get_clip` symbols. The copied SceneFX source matched the verified prior private source byte-for-byte in the modified files; no installed SceneFX or wlroots file changed.
### Evidence freeze and independent replay
The later evidence-freeze task preserved the six original source files and one
upstream protocol XML in [the Git fixture](../../tooling/probes/step20c-valid-grab/README.md).
The original six SHA-256 values above matched both the lab and the Git copies.
The XML at a pinned `wlr-protocols` commit regenerated the lab's virtual-pointer
header and C source byte-for-byte. No original fixture was edited, so there is
no adapted-source hash. The XDG protocol code, build directories, binaries,
logs, IPC JSON, screenshots, sockets and `transaction-before-hidden.c` remain
temporary artifacts outside Git; the latter is reconstructible by applying
the first Mosaic patch before the guard. The original `/tmp` lab remains
historical evidence, while the linked README can create a fresh lab at any
temporary path.
A fresh archive of Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754`
and a fresh clone of SceneFX `37ccd723bef49e6891156ffafce8f549f01446cc`
accepted their respective private patches with `git apply --check` and no
rejects. The guard then applied to the patched Mosaic source. The four changed
Mosaic/SceneFX files and the pre-guard transaction file matched the original
lab byte-for-byte. Both guarded and unguarded private compositors, the private
SceneFX library and both C clients compiled in the new lab. The replay runner
set per-case headless sockets and explicit `swaymsg -s`; inspection found no
path to the active Sway IPC socket.
The bounded replay ran `control boundary_left`, `private_standard
boundary_right`, `private_mosaic boundary_left`, guarded `private_mosaic`,
guarded `private_mosaic_hit`, and unguarded `private_mosaic_hit`. The control
and private standard used button serial 61 for both receipt and `xdg_popup.grab`;
private Mosaic used 55 for both. All received popup configure. The right-output
sample `(100,150)` stayed yellow `(224,192,32)` in control/private standard
and ordinary `(238,238,238)` in private Mosaic. With the owner hidden, the
unguarded left-output sample `(100,150)` stayed yellow and the popup received
right button 273; the guarded sample became background `(30,30,46)`, with no
hidden popup enter/button. The guarded no-click hide/return counted 39,520
exact yellow pixels before hiding, 0 while hidden and 39,520 after return,
without `popup_done` before return. The guarded hidden-click run did cause
`popup_done`, as expected for the valid grab's outside dismissal. These are
repeated **sampled** observations, not a wider qualification matrix.
## Decision and remaining gates
**PARTIALLY_FEASIBLE for this bounded probe.** The serial was genuinely received and used, the configured grabbed popup was interactive, and the private owner-visibility guard removed the demonstrated fully hidden owner pixel/hit leak while keeping the popup mapped for camera return. It also preserved sampled standard-layout behavior. The unguarded owner-derived viewport clip alone is **FAIL** for the hidden-owner invariant. A production policy for hiding versus dismissing or repositioning an offscreen popup is still undecided, and the private guard's use of `animation_state.current_width/height` needs a committed-generation/lifetime contract before any implementation. The probe did not exercise a valid-grab nested popup, vertical/negative/scaled/rotated outputs, output migration/resize, complete damage/effect/saved-buffer coverage, exact pending/current generation synchronization, or SceneFX/wlroots ABI/package maintenance. Those cells remain **NOT TESTED** or **PARTIAL**; multi-output Mosaic remains **NOT YET SUPPORTED**.
The smallest next experiment is a controlled valid-grab nested popup plus transaction/output-migration and damage measurements against the same unmodified SwayFX control, followed by a separate scientific/UX decision about offscreen popup lifecycle. Do not import either private patch into production on this evidence.

View file

@ -0,0 +1,143 @@
# STEP20C valid-grab / hidden-owner fixture (EXPERIMENTAL)
This fixture preserves the bounded headless comparison reported in
[the STEP20C probe](../../../docs/design/STEP20C_VALID_GRAB_HIDDEN_OWNER_PROBE.md).
It is not part of the Mosaic build or package. Its private SceneFX symbols and
Mosaic edits are feasibility evidence, not production interfaces. The result is
**PARTIALLY_FEASIBLE** at the measured pixels and pointer locations only.
## Provenance and exact inputs
| Input | Base / origin | SHA-256 |
| --- | --- | --- |
| `mosaic-popup-plus-base.patch` | Complete private delta against Mosaic `cd550382b311efdbfe3ed892f4e1d281d852e754` | `d416f6413ea097a48a44d231e50ffba363914f46c8687f0ea2e4dcacd5f31fe9` |
| `scenefx-popup-plus-base.patch` | Complete private delta against [SceneFX](https://github.com/wlrfx/scenefx) tag 0.5, `37ccd723bef49e6891156ffafce8f549f01446cc` | `c74c515b98da8f5f637e260e6a3755c7a75bd78d1c39e54a67850f1c383e30e7` |
| `hidden-guard.patch` | Incremental private delta after the Mosaic patch | `35785a8f3e863b3c1fd87932d8ff0c2949b03cb04382cc34e1126170968f802b` |
| `grab-client.c` | Original valid-serial XDG client from the probe lab | `202afdc5dae0ee4aba570eef4a563f8b298474eb84e33af9510c4c81c05d16e4` |
| `pointer-daemon.c` | Original persistent virtual pointer from the probe lab | `a00fcedc38ccb7ca4c25c982d9f38e8bde3168a3833f0c9cf2056e841d720938` |
| `run.py` | Original headless runner from the probe lab | `b2aaa90188ea387cb8a69aa69935b56e63f5ce70eb98e74783b0fb3c9236a41f` |
| `wlr-virtual-pointer-unstable-v1.xml` | [wlr-protocols](https://github.com/swaywm/wlr-protocols), `b010a03648b88d143236de193bddbfea0c08bc84`, `unstable/` | `3ff6d540be0bc5228195bf072bde42117ea17945a5c2061add5d3cf97d6bb524` |
The six lab files above are byte-identical to the originals in
`/tmp/swayfx-mosaic-valid-grab-7SA2yO/`; none was adapted. The protocol XML
regenerates that lab's `virtual-pointer-client.h` and
`virtual-pointer-protocol.c` byte-for-byte. `xdg-shell.xml` comes from the
installed `wayland-protocols` package. The pinned pristine SwayFX base is
`fd71a6bdc061bd633b488ae7b83e8a6981d22f86`; **do not apply the Mosaic
private patch to that base**. The Mosaic commit above already includes the
project's prior Mosaic changes.
The original environment used SwayFX 0.6 (`swayfx` 0.6-1), SceneFX 0.5
(`scenefx` 0.5-1), wlroots 0.20.2 (`wlroots0.20` 0.20.2-1),
`wayland-client` 1.26.0, and the installed `wayland-protocols` XML. Meson,
Ninja, `wayland-scanner`, a C17 compiler, `pkg-config`, Python, `kitty`,
`swaymsg`, `grim`, `wev`, and `/usr/bin/sway` are required. Package releases
and scanner versions may change; record them when reproducing.
## Rebuild in a new temporary laboratory
Run from this repository root. The commands copy only source fixtures into a
new `mktemp` directory; all compiled files, sockets, logs and captures stay
there. Keep `R` for the commands below, then remove it during cleanup.
```sh
set -eu
R=$(mktemp -d /tmp/swayfx-mosaic-valid-grab-replay-XXXXXXXX)
F="$PWD/tooling/probes/step20c-valid-grab"
cp "$F/grab-client.c" "$F/pointer-daemon.c" "$F/run.py" "$F"/*.patch \
"$F/wlr-virtual-pointer-unstable-v1.xml" "$R/"
mkdir "$R/mosaic" "$R/scenefx"
git archive cd550382b311efdbfe3ed892f4e1d281d852e754 | tar -xf - -C "$R/mosaic"
git clone --quiet https://github.com/wlrfx/scenefx.git "$R/scenefx-git"
git -C "$R/scenefx-git" checkout --quiet --detach 37ccd723bef49e6891156ffafce8f549f01446cc
git -C "$R/scenefx-git" archive HEAD | tar -xf - -C "$R/scenefx"
(cd "$R/scenefx" && git apply --check "$R/scenefx-popup-plus-base.patch" && git apply "$R/scenefx-popup-plus-base.patch")
(cd "$R/mosaic" && git apply --check "$R/mosaic-popup-plus-base.patch" && git apply "$R/mosaic-popup-plus-base.patch")
# This is the unguarded baseline. Save it only in the temporary lab.
cp "$R/mosaic/sway/desktop/transaction.c" "$R/transaction-before-hidden.c"
(cd "$R/mosaic" && git apply --check "$R/hidden-guard.patch" && git apply "$R/hidden-guard.patch")
meson setup "$R/scenefx-build" "$R/scenefx" -Dexamples=false -Dwerror=false
meson compile -C "$R/scenefx-build" -j 8
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson setup "$R/mosaic-build" "$R/mosaic" \
-Dmosaic-package=true -Ddefault-wallpaper=false -Dswaybar=false -Dswaynag=false \
-Dman-pages=disabled -Dsd-bus-provider=libsystemd -Dwerror=false
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson compile -C "$R/mosaic-build" -j 8
wayland-scanner client-header /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml "$R/xdg-shell-client.h"
wayland-scanner private-code /usr/share/wayland-protocols/stable/xdg-shell/xdg-shell.xml "$R/xdg-shell-protocol.c"
wayland-scanner client-header "$R/wlr-virtual-pointer-unstable-v1.xml" "$R/virtual-pointer-client.h"
wayland-scanner private-code "$R/wlr-virtual-pointer-unstable-v1.xml" "$R/virtual-pointer-protocol.c"
cc -std=c17 -Wall -Wextra -Wno-unused-parameter -I"$R" "$R/grab-client.c" "$R/xdg-shell-protocol.c" -o "$R/grab-client" $(pkg-config --cflags --libs wayland-client)
cc -std=c17 -Wall -Wextra -Wno-unused-parameter -I"$R" "$R/pointer-daemon.c" "$R/virtual-pointer-protocol.c" -o "$R/pointer-daemon" $(pkg-config --cflags --libs wayland-client)
```
For the **unguarded** comparison, reconstruct a second disposable Mosaic
source/build directory from the same Mosaic commit and apply only the first
Mosaic patch. Keep the guarded binary intact for restoration:
```sh
mkdir "$R/mosaic-unguarded"
git archive cd550382b311efdbfe3ed892f4e1d281d852e754 | tar -xf - -C "$R/mosaic-unguarded"
(cd "$R/mosaic-unguarded" && git apply --check "$R/mosaic-popup-plus-base.patch" && git apply "$R/mosaic-popup-plus-base.patch")
cmp "$R/mosaic-unguarded/sway/desktop/transaction.c" "$R/transaction-before-hidden.c"
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson setup "$R/mosaic-unguarded-build" "$R/mosaic-unguarded" \
-Dmosaic-package=true -Ddefault-wallpaper=false -Dswaybar=false -Dswaynag=false \
-Dman-pages=disabled -Dsd-bus-provider=libsystemd -Dwerror=false
PKG_CONFIG_PATH="$R/scenefx-build/meson-uninstalled" meson compile -C "$R/mosaic-unguarded-build" -j 8
```
The original runner always selects `R/mosaic-build/sway/swayfx-mosaic`. To run
the unguarded case, first save that *temporary* guarded binary and replace it
with the unguarded build; restore it afterward. Never modify a system binary
or library.
## Run the original reference cases
`run.py` resolves its laboratory from its own copied path. It removes inherited
`WAYLAND_DISPLAY` and `SWAYSOCK`, creates a mode-0700 run directory for each
case, starts a two-output headless compositor, discovers its new sockets, and
passes that socket explicitly to every `swaymsg -s` call. The private SceneFX
library path is process-local to the private compositor. It never targets the
active desktop socket. Run only these previously reported cases:
```sh
python "$R/run.py" control boundary_left
python "$R/run.py" private_standard boundary_right
python "$R/run.py" private_mosaic boundary_left
python "$R/run.py" private_mosaic # guarded, no hidden click; tests mapped return
python "$R/run.py" private_mosaic_hit # guarded hidden-click case
cp "$R/mosaic-build/sway/swayfx-mosaic" "$R/guarded-swayfx-mosaic"
cp "$R/mosaic-unguarded-build/sway/swayfx-mosaic" "$R/mosaic-build/sway/swayfx-mosaic"
mv "$R/run-private_mosaic_hit" "$R/run-private_mosaic_hit-guarded"
python "$R/run.py" private_mosaic_hit # unguarded baseline
cp "$R/guarded-swayfx-mosaic" "$R/mosaic-build/sway/swayfx-mosaic"
```
The runner reuses `run-private_mosaic_hit/` for both variants; rename that
directory within `R` after the unguarded run to retain both result sets.
Check `client.log` for the same received/used nonzero serial and XDG configure;
inspect `pointer.log`, `wev.log`, IPC JSON and the left/right PNGs. Expected
sampled observations: standard control and private standard deliver popup
input at `(900,150)`; Mosaic clips that point; unguarded hidden owner leaks a
yellow pixel and popup enter/button at `(100,150)`; guarded hidden owner shows
ordinary background and no popup enter/button there; the no-click guarded
case shows the same mapped popup after camera return. A hidden outside click
may trigger `popup_done` under the valid grab without delivering a popup
button, as it did in the original report. Treat any disagreement as a new
observation, not as a reason to rewrite the recorded result.
The probe does not establish universal leak freedom, full transaction/damage
correctness, effects/saved-buffer behavior, ABI durability, general
multi-output safety, or an offscreen-popup UX policy. Valid-grab nested
popups and other output geometries remain **NOT TESTED**.
## Cleanup
After all processes started by `run.py` have exited, remove only the new lab:
```sh
rm -rf -- "$R"
```
Do not clean the original probe laboratory as part of this procedure.

View file

@ -0,0 +1,186 @@
#define _POSIX_C_SOURCE 200809L
#include <errno.h>
#include <fcntl.h>
#include <signal.h>
#include <stdbool.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <sys/mman.h>
#include <unistd.h>
#include <wayland-client.h>
#include "xdg-shell-client.h"
static struct wl_display *display;
static struct wl_compositor *compositor;
static struct wl_shm *shm;
static struct xdg_wm_base *wm;
static struct wl_seat *seat;
static struct wl_pointer *pointer;
static struct wl_surface *top_surface, *popup_surface;
static struct xdg_surface *top_xdg, *popup_xdg;
static struct xdg_toplevel *top_role;
static struct xdg_popup *popup_role;
static bool top_drawn, popup_drawn;
static int popup_width = 500, popup_anchor_x = 350;
static volatile sig_atomic_t running = 1;
static void draw(struct wl_surface *surface, int width, int height, uint32_t color) {
int size = width * height * 4;
char path[] = "/tmp/mosaic-grab-buffer-XXXXXX";
int fd = mkstemp(path);
if (fd < 0 || ftruncate(fd, size) < 0) { perror("buffer"); exit(1); }
unlink(path);
uint32_t *pixels = mmap(NULL, size, PROT_READ | PROT_WRITE, MAP_SHARED, fd, 0);
if (pixels == MAP_FAILED) { perror("mmap"); exit(1); }
for (int i = 0; i < width * height; ++i) pixels[i] = color;
struct wl_shm_pool *pool = wl_shm_create_pool(shm, fd, size);
struct wl_buffer *buffer = wl_shm_pool_create_buffer(pool, 0, width, height,
width * 4, WL_SHM_FORMAT_XRGB8888);
wl_surface_attach(surface, buffer, 0, 0);
wl_surface_damage_buffer(surface, 0, 0, width, height);
wl_surface_commit(surface);
wl_buffer_destroy(buffer);
wl_shm_pool_destroy(pool);
munmap(pixels, size);
close(fd);
}
static void wm_ping(void *data, struct xdg_wm_base *base, uint32_t serial) {
xdg_wm_base_pong(base, serial);
}
static const struct xdg_wm_base_listener wm_listener = {.ping = wm_ping};
static void xdg_configure(void *data, struct xdg_surface *surface, uint32_t serial) {
xdg_surface_ack_configure(surface, serial);
if (surface == top_xdg && !top_drawn) {
top_drawn = true;
fprintf(stderr, "TOP_CONFIGURE serial=%u map=1\n", serial);
draw(top_surface, 420, 320, 0x002344cc);
} else if (surface == popup_xdg && !popup_drawn) {
popup_drawn = true;
fprintf(stderr, "POPUP_XDG_CONFIGURE serial=%u map=1\n", serial);
draw(popup_surface, popup_width, 190, 0x00e0c020);
}
}
static const struct xdg_surface_listener surface_listener = {.configure = xdg_configure};
static void top_configure(void *data, struct xdg_toplevel *top, int32_t width,
int32_t height, struct wl_array *states) {
fprintf(stderr, "TOP_ROLE_CONFIGURE width=%d height=%d\n", width, height);
}
static void top_close(void *data, struct xdg_toplevel *top) {
fprintf(stderr, "TOP_CLOSE\n"); running = 0;
}
static const struct xdg_toplevel_listener top_listener = {
.configure = top_configure, .close = top_close,
};
static void popup_configure(void *data, struct xdg_popup *popup, int32_t x,
int32_t y, int32_t width, int32_t height) {
fprintf(stderr, "POPUP_ROLE_CONFIGURE x=%d y=%d w=%d h=%d\n", x, y, width, height);
}
static void popup_done(void *data, struct xdg_popup *popup) {
fprintf(stderr, "POPUP_DONE mapped=%d\n", popup_drawn);
}
static const struct xdg_popup_listener popup_listener = {
.configure = popup_configure, .popup_done = popup_done,
};
static void open_popup(uint32_t serial) {
if (popup_role) return;
popup_surface = wl_compositor_create_surface(compositor);
popup_xdg = xdg_wm_base_get_xdg_surface(wm, popup_surface);
xdg_surface_add_listener(popup_xdg, &surface_listener, NULL);
struct xdg_positioner *positioner = xdg_wm_base_create_positioner(wm);
xdg_positioner_set_size(positioner, popup_width, 190);
xdg_positioner_set_anchor_rect(positioner, popup_anchor_x, 80, 1, 1);
xdg_positioner_set_anchor(positioner, XDG_POSITIONER_ANCHOR_TOP_LEFT);
xdg_positioner_set_gravity(positioner, XDG_POSITIONER_GRAVITY_BOTTOM_RIGHT);
popup_role = xdg_surface_get_popup(popup_xdg, top_xdg, positioner);
xdg_popup_add_listener(popup_role, &popup_listener, NULL);
fprintf(stderr, "POPUP_GRAB_REQUEST received_button_serial=%u used_serial=%u\n", serial, serial);
xdg_popup_grab(popup_role, seat, serial);
wl_surface_commit(popup_surface);
xdg_positioner_destroy(positioner);
}
static void pointer_enter(void *data, struct wl_pointer *ptr, uint32_t serial,
struct wl_surface *surface, wl_fixed_t x, wl_fixed_t y) {
fprintf(stderr, "POINTER_ENTER target=%s serial=%u x=%.1f y=%.1f\n",
surface == top_surface ? "top" : surface == popup_surface ? "popup" : "other",
serial, wl_fixed_to_double(x), wl_fixed_to_double(y));
}
static void pointer_leave(void *data, struct wl_pointer *ptr, uint32_t serial,
struct wl_surface *surface) {
fprintf(stderr, "POINTER_LEAVE target=%s serial=%u\n",
surface == top_surface ? "top" : surface == popup_surface ? "popup" : "other", serial);
}
static void pointer_motion(void *data, struct wl_pointer *ptr, uint32_t time,
wl_fixed_t x, wl_fixed_t y) {
fprintf(stderr, "POINTER_MOTION x=%.1f y=%.1f\n", wl_fixed_to_double(x), wl_fixed_to_double(y));
}
static void pointer_button(void *data, struct wl_pointer *ptr, uint32_t serial,
uint32_t time, uint32_t button, uint32_t state) {
fprintf(stderr, "POINTER_BUTTON serial=%u button=%u state=%u popup=%d\n",
serial, button, state, popup_role != NULL);
if (state == WL_POINTER_BUTTON_STATE_PRESSED && button == 272 && !popup_role)
open_popup(serial);
}
static void pointer_axis(void *data, struct wl_pointer *ptr, uint32_t time,
uint32_t axis, wl_fixed_t value) {}
static void pointer_frame(void *data, struct wl_pointer *ptr) {}
static const struct wl_pointer_listener pointer_listener = {
.enter = pointer_enter, .leave = pointer_leave, .motion = pointer_motion,
.button = pointer_button, .axis = pointer_axis, .frame = pointer_frame,
};
static void seat_capabilities(void *data, struct wl_seat *s, uint32_t caps) {
if ((caps & WL_SEAT_CAPABILITY_POINTER) && !pointer) {
pointer = wl_seat_get_pointer(s);
wl_pointer_add_listener(pointer, &pointer_listener, NULL);
}
}
static void seat_name(void *data, struct wl_seat *s, const char *name) {}
static const struct wl_seat_listener seat_listener = {
.capabilities = seat_capabilities, .name = seat_name,
};
static void global(void *data, struct wl_registry *reg, uint32_t name,
const char *interface, uint32_t version) {
if (!strcmp(interface, wl_compositor_interface.name))
compositor = wl_registry_bind(reg, name, &wl_compositor_interface, version < 4 ? version : 4);
else if (!strcmp(interface, wl_shm_interface.name))
shm = wl_registry_bind(reg, name, &wl_shm_interface, 1);
else if (!strcmp(interface, xdg_wm_base_interface.name))
wm = wl_registry_bind(reg, name, &xdg_wm_base_interface, version < 3 ? version : 3);
else if (!strcmp(interface, wl_seat_interface.name)) {
seat = wl_registry_bind(reg, name, &wl_seat_interface, version < 5 ? version : 5);
wl_seat_add_listener(seat, &seat_listener, NULL);
}
}
static void removed(void *data, struct wl_registry *reg, uint32_t name) {}
static const struct wl_registry_listener registry_listener = {.global = global, .global_remove = removed};
static void stop(int signal_number) { running = 0; }
int main(void) {
signal(SIGTERM, stop);
if (getenv("PROBE_POPUP_FULLY_VISIBLE")) {
popup_width = 250;
popup_anchor_x = 50;
}
display = wl_display_connect(NULL);
if (!display) { fprintf(stderr, "cannot connect Wayland\n"); return 1; }
struct wl_registry *registry = wl_display_get_registry(display);
wl_registry_add_listener(registry, &registry_listener, NULL);
wl_display_roundtrip(display);
wl_display_roundtrip(display);
if (!compositor || !shm || !wm || !seat || !pointer) {
fprintf(stderr, "missing Wayland globals\n"); return 1;
}
xdg_wm_base_add_listener(wm, &wm_listener, NULL);
top_surface = wl_compositor_create_surface(compositor);
top_xdg = xdg_wm_base_get_xdg_surface(wm, top_surface);
xdg_surface_add_listener(top_xdg, &surface_listener, NULL);
top_role = xdg_surface_get_toplevel(top_xdg);
xdg_toplevel_add_listener(top_role, &top_listener, NULL);
xdg_toplevel_set_title(top_role, "Mosaic Valid Grab Probe");
xdg_toplevel_set_app_id(top_role, "org.mosaic.ValidGrabProbe");
wl_surface_commit(top_surface);
while (running && wl_display_dispatch(display) >= 0) {}
fprintf(stderr, "CLIENT_EXIT popup_created=%d popup_mapped=%d\n", popup_role != NULL, popup_drawn);
wl_display_disconnect(display);
return 0;
}

View file

@ -0,0 +1,32 @@
--- a/sway/desktop/transaction.c
+++ b/sway/desktop/transaction.c
@@ -1069,6 +1069,29 @@
wlr_scene_tree_set_clip(popup_tree, &empty);
return;
}
+ // EXPERIMENTAL INVARIANT: the viewport alone does not imply that its
+ // popup owner is visible. Use the already-applied scene position and size,
+ // not pending layout geometry, to suppress a fully hidden owner's popup.
+ int owner_x = INT_MIN, owner_y = INT_MIN;
+ struct wlr_box owner_visible;
+ if (!wlr_scene_node_coords(&owner->scene_tree->node, &owner_x, &owner_y) ||
+ owner->animation_state.current_width <= 0 ||
+ owner->animation_state.current_height <= 0 ||
+ !wlr_box_intersection(&owner_visible, &global,
+ &(struct wlr_box){
+ .x = owner_x, .y = owner_y,
+ .width = owner->animation_state.current_width,
+ .height = owner->animation_state.current_height,
+ })) {
+ struct wlr_box empty = {0};
+ wlr_scene_tree_set_clip(popup_tree, &empty);
+ sway_log(SWAY_INFO, "PROBE_POPUP_HIDDEN_OWNER owner=%p applied=%d,%d %dx%d viewport=%d,%d %dx%d",
+ (void *)con, owner_x, owner_y,
+ owner->animation_state.current_width,
+ owner->animation_state.current_height,
+ global.x, global.y, global.width, global.height);
+ return;
+ }
int64_t px = (int64_t)global.x - popup_x;
int64_t py = (int64_t)global.y - popup_y;
if (px < INT_MIN || px > INT_MAX || py < INT_MIN || py > INT_MAX) {

View file

@ -0,0 +1,115 @@
diff --git a/sway/desktop/transaction.c b/sway/desktop/transaction.c
index 47e3e179..bb7bea16 100644
--- a/sway/desktop/transaction.c
+++ b/sway/desktop/transaction.c
@@ -974,6 +974,20 @@ static void arrange_output(struct sway_output *output, int width, int height) {
wlr_scene_node_set_position(&child->layers.tiling->node,
workspace_tiling_scene_x(child, area, gaps),
gaps->top + area->y);
+ // Experimental: tiling tree clip follows the output viewport,
+ // while the tree itself follows the Mosaic camera.
+ if (child->current.layout == L_MOSAIC && child->mosaic) {
+ struct mosaic_row *row = child->mosaic->rows->items[0];
+ struct wlr_box clip = {
+ .x = row->state.scroll_x,
+ .y = 0,
+ .width = area->width - gaps->left - gaps->right,
+ .height = area->height - gaps->top - gaps->bottom,
+ };
+ wlr_scene_tree_set_clip(child->layers.tiling, &clip);
+ } else {
+ wlr_scene_tree_set_clip(child->layers.tiling, NULL);
+ }
arrange_workspace_tiling(child,
area->width - gaps->left - gaps->right,
@@ -1006,6 +1020,69 @@ static void arrange_output(struct sway_output *output, int width, int height) {
output->prev_active_workspace = new_active;
}
+// Experimental: derive a root popup clip from its actual tiled owner and the
+// already-applied Mosaic tiling clip. This avoids pending XDG geometry as a
+// source for the visible viewport; no owner pointer is stored on the clip.
+static void probe_clip_popup(struct wlr_scene_node *node,
+ struct sway_popup_desc *popup) {
+ struct wlr_scene_tree *popup_tree = wlr_scene_tree_from_node(node);
+ struct sway_container *con = popup->view ? popup->view->container : NULL;
+ struct sway_workspace *ws = con ? con->current.workspace : NULL;
+ if (!ws || ws->current.layout != L_MOSAIC || !ws->mosaic) {
+ wlr_scene_tree_set_clip(popup_tree, NULL);
+ return;
+ }
+ struct sway_container *owner = con;
+ while (owner->current.parent) owner = owner->current.parent;
+ if (!ws->current.tiling || list_find(ws->current.tiling, owner) < 0 ||
+ ws->current.fullscreen || con->current.fullscreen_mode) {
+ wlr_scene_tree_set_clip(popup_tree, NULL);
+ return;
+ }
+ struct wlr_box local_viewport;
+ struct sway_output *output = ws->current.output;
+ int tile_x, tile_y, popup_x, popup_y;
+ if (!output || output->current.active_workspace != ws ||
+ !wlr_scene_tree_get_clip(ws->layers.tiling, &local_viewport) ||
+ !wlr_scene_node_coords(&ws->layers.tiling->node, &tile_x, &tile_y) ||
+ !wlr_scene_node_coords(node, &popup_x, &popup_y)) {
+ struct wlr_box empty = {0};
+ wlr_scene_tree_set_clip(popup_tree, &empty);
+ return;
+ }
+ struct wlr_box global = local_viewport;
+ int64_t gx = (int64_t)tile_x + global.x;
+ int64_t gy = (int64_t)tile_y + global.y;
+ if (gx < INT_MIN || gx > INT_MAX || gy < INT_MIN || gy > INT_MAX) {
+ struct wlr_box empty = {0};
+ wlr_scene_tree_set_clip(popup_tree, &empty);
+ return;
+ }
+ global.x = (int)gx;
+ global.y = (int)gy;
+ struct wlr_box output_box = {
+ .x = output->lx, .y = output->ly,
+ .width = output->width, .height = output->height,
+ };
+ if (!wlr_box_intersection(&global, &global, &output_box)) {
+ struct wlr_box empty = {0};
+ wlr_scene_tree_set_clip(popup_tree, &empty);
+ return;
+ }
+ int64_t px = (int64_t)global.x - popup_x;
+ int64_t py = (int64_t)global.y - popup_y;
+ if (px < INT_MIN || px > INT_MAX || py < INT_MIN || py > INT_MAX) {
+ struct wlr_box empty = {0};
+ wlr_scene_tree_set_clip(popup_tree, &empty);
+ return;
+ }
+ global.x = (int)px;
+ global.y = (int)py;
+ wlr_scene_tree_set_clip(popup_tree, &global);
+ sway_log(SWAY_INFO, "PROBE_POPUP_CLIP owner=%p local=%d,%d %dx%d",
+ (void *)con, global.x, global.y, global.width, global.height);
+}
+
void arrange_popups(struct wlr_scene_tree *popups) {
struct wlr_scene_node *node;
wl_list_for_each(node, &popups->children, link) {
@@ -1016,6 +1093,7 @@ void arrange_popups(struct wlr_scene_tree *popups) {
int lx, ly;
wlr_scene_node_coords(popup->relative, &lx, &ly);
wlr_scene_node_set_position(node, lx, ly);
+ probe_clip_popup(node, popup);
}
}
}
diff --git a/sway/desktop/xdg_shell.c b/sway/desktop/xdg_shell.c
index c3b4e4f2..2bf2cab5 100644
--- a/sway/desktop/xdg_shell.c
+++ b/sway/desktop/xdg_shell.c
@@ -380,6 +380,7 @@ static void handle_new_popup(struct wl_listener *listener, void *data) {
int lx, ly;
wlr_scene_node_coords(&popup->view->content_tree->node, &lx, &ly);
wlr_scene_node_set_position(&popup->scene_tree->node, lx, ly);
+ arrange_popups(root->layers.popup);
}
static void handle_request_maximize(struct wl_listener *listener, void *data) {

View file

@ -0,0 +1,56 @@
#define _DEFAULT_SOURCE
#include <linux/input-event-codes.h>
#include <stdint.h>
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <unistd.h>
#include <wayland-client.h>
#include "virtual-pointer-client.h"
static struct zwlr_virtual_pointer_manager_v1 *manager;
static void global(void *data, struct wl_registry *registry, uint32_t name,
const char *interface, uint32_t version) {
if (!strcmp(interface, zwlr_virtual_pointer_manager_v1_interface.name))
manager=wl_registry_bind(registry,name,&zwlr_virtual_pointer_manager_v1_interface,
version < 2 ? version : 2);
}
static void removed(void *data, struct wl_registry *registry, uint32_t name) {}
int main(void) {
struct wl_display *display=wl_display_connect(NULL);
if (!display) return 1;
struct wl_registry *registry=wl_display_get_registry(display);
struct wl_registry_listener listener={global,removed};
wl_registry_add_listener(registry,&listener,NULL);
wl_display_roundtrip(display);
if (!manager) return 2;
struct zwlr_virtual_pointer_v1 *pointer=
zwlr_virtual_pointer_manager_v1_create_virtual_pointer(manager,NULL);
wl_display_roundtrip(display);
fprintf(stderr,"POINTER_READY\n");fflush(stderr);
char line[128];
uint32_t stamp=1000;
while (fgets(line,sizeof(line),stdin)) {
char action[16];int x,y;
if (sscanf(line,"%15s %d %d",action,&x,&y)!=3) continue;
fprintf(stderr,"INJECT %s %d,%d\n",action,x,y);fflush(stderr);
zwlr_virtual_pointer_v1_motion_absolute(pointer,stamp++,x,y,1600,600);
zwlr_virtual_pointer_v1_frame(pointer);
wl_display_flush(display);usleep(250000);
if (!strcmp(action,"left") || !strcmp(action,"right")) {
uint32_t button=!strcmp(action,"left") ? BTN_LEFT : BTN_RIGHT;
zwlr_virtual_pointer_v1_button(pointer,stamp++,button,WL_POINTER_BUTTON_STATE_PRESSED);
zwlr_virtual_pointer_v1_frame(pointer);
wl_display_flush(display);usleep(80000);
zwlr_virtual_pointer_v1_button(pointer,stamp++,button,WL_POINTER_BUTTON_STATE_RELEASED);
zwlr_virtual_pointer_v1_frame(pointer);
wl_display_flush(display);
}
wl_display_roundtrip(display);
fprintf(stderr,"INJECT_DONE %s %d,%d\n",action,x,y);fflush(stderr);
}
zwlr_virtual_pointer_v1_destroy(pointer);
zwlr_virtual_pointer_manager_v1_destroy(manager);
wl_registry_destroy(registry);
wl_display_disconnect(display);
return 0;
}

View file

@ -0,0 +1,193 @@
import json, os, pathlib, signal, subprocess, sys, time
LAB=pathlib.Path(__file__).resolve().parent
case=sys.argv[1]
assert case in ('control','private_standard','private_mosaic','private_mosaic_hit')
mode=sys.argv[2] if len(sys.argv)>2 else ''
assert mode in ('','boundary_left','boundary_right','standard_return',
'lifecycle_switch','lifecycle_destroy','outside_left','visible_left',
'fully_visible')
run=LAB/('run-'+case+('-'+mode if mode else ''))
run.mkdir(exist_ok=True)
run.chmod(0o700)
config=('output HEADLESS-1 resolution 800x600 position 0 0\n'
'output HEADLESS-2 resolution 800x600 position 800 0\n'
'workspace 1 output HEADLESS-1\nworkspace 2 output HEADLESS-2\n'
'focus_follows_mouse no\n')
(run/'config').write_text(config)
e=os.environ.copy()
e.update(XDG_RUNTIME_DIR=str(run),WLR_BACKENDS='headless',WLR_HEADLESS_OUTPUTS='2',WLR_RENDERER='pixman')
e.pop('WAYLAND_DISPLAY',None);e.pop('SWAYSOCK',None)
compositor_env=e.copy()
if case.startswith('private'):
binary=LAB/'mosaic-build/sway/swayfx-mosaic'
compositor_env['LD_LIBRARY_PATH']=str(LAB/'scenefx-build')
else:
binary=pathlib.Path('/usr/bin/sway')
compositor_env.pop('LD_LIBRARY_PATH',None)
e.pop('LD_LIBRARY_PATH',None)
files=[];children=[]
def spawn(args,name,debug=False):
f=(run/name).open('w');files.append(f)
env=e.copy()
if debug:env['WAYLAND_DEBUG']='1'
p=subprocess.Popen(args,env=env,stdout=f,stderr=subprocess.STDOUT,start_new_session=True)
children.append(p)
return p
def wait(fn,seconds=10):
end=time.monotonic()+seconds
while time.monotonic()<end:
v=fn()
if v:return v
if compositor.poll() is not None:raise RuntimeError('compositor exited '+str(compositor.returncode))
time.sleep(.05)
raise RuntimeError('timeout')
def ipc(*args):
return json.loads(subprocess.run(['swaymsg','-s',str(sock),*args],env=e,text=True,
capture_output=True,timeout=5,check=True).stdout)
def tree():return ipc('-t','get_tree')
def find_app(node,app):
if isinstance(node,dict):
if node.get('app_id')==app:return node
for child in node.get('nodes',[])+node.get('floating_nodes',[]):
found=find_app(child,app)
if found:return found
return None
def workspace(node,name):
if isinstance(node,dict):
if node.get('type')=='workspace' and node.get('name')==name:return node
for child in node.get('nodes',[]):
found=workspace(child,name)
if found:return found
return None
def shot(tag):
(run/f'tree-{tag}.json').write_text(json.dumps(tree(),indent=2))
(run/f'outputs-{tag}.json').write_text(json.dumps(ipc('-t','get_outputs'),indent=2))
for out,name in [('HEADLESS-1','left'),('HEADLESS-2','right')]:
subprocess.run(['grim','-o',out,str(run/f'{name}-{tag}.png')],env=e,check=True,timeout=8,
stdout=subprocess.DEVNULL)
def inject(button,x,y):
marker=f'INJECT_DONE {button} {x},{y}'
injector.stdin.write(f'{button} {x} {y}\n');injector.stdin.flush()
wait(lambda:marker in (run/'pointer.log').read_text())
def logged(pattern):
path=run/'client.log'
return pattern in path.read_text() if path.exists() else False
log=(run/'compositor.log').open('w');files.append(log)
compositor=subprocess.Popen([str(binary),'-c',str(run/'config'),'-d'],env=compositor_env,
stdout=log,stderr=subprocess.STDOUT,start_new_session=True)
try:
def sockets():
s=next(iter(run.glob('sway-ipc.*.sock')),None)
w=next((x for x in run.glob('wayland-*') if x.suffix!='.lock'),None)
return (s,w) if s and w else None
sock,way=wait(sockets)
e.update(SWAYSOCK=str(sock),WAYLAND_DISPLAY=way.name)
pointer_log=(run/'pointer.log').open('w');files.append(pointer_log)
injector=subprocess.Popen([str(LAB/'pointer-daemon')],env=e,stdin=subprocess.PIPE,
text=True,stdout=pointer_log,stderr=subprocess.STDOUT,start_new_session=True)
children.append(injector)
wait(lambda:'POINTER_READY' in (run/'pointer.log').read_text())
ipc('workspace 1');ipc('layout mosaic' if case.startswith('private_mosaic') else 'layout splith')
spawn(['kitty','--class','A','-o','background=#aa2222','/usr/bin/sleep','60'],'A.log')
wait(lambda:len(workspace(tree(),'1')['nodes'])>=1)
if mode=='fully_visible':e['PROBE_POPUP_FULLY_VISIBLE']='1'
client=spawn([str(LAB/'grab-client')],'client.log',True)
wait(lambda:find_app(tree(),'org.mosaic.ValidGrabProbe'))
for label in ('C','D'):
spawn(['kitty','--class',label,'/usr/bin/sleep','60'],label+'.log')
time.sleep(.35)
ipc('workspace 2')
spawn(['stdbuf','-oL','wev'],'wev.log')
time.sleep(.5)
ipc('workspace 1')
ipc('[app_id="org.mosaic.ValidGrabProbe"] focus')
if case.startswith('private_mosaic'):
current=workspace(tree(),'1')['mosaic']['rows'][0]['scroll_x']
ipc(f'layout mosaic scroll {400-current:g}')
time.sleep(.5)
ws=workspace(tree(),'1');owner=find_app(ws,'org.mosaic.ValidGrabProbe')
(run/'owner-before.json').write_text(json.dumps({'owner':owner,'workspace':ws.get('mosaic')},indent=2))
rect=owner['rect']
scroll=ws['mosaic']['rows'][0]['scroll_x'] if case.startswith('private_mosaic') else 0
x=int(max(30,min(770,rect['x']-scroll+int(rect['width']*.55))));y=rect['y']+130
print('OPEN_POINT',x,y,'OWNER_RECT',rect,flush=True)
inject('left',x,y)
wait(lambda:logged('POPUP_XDG_CONFIGURE'))
time.sleep(.35)
shot('open')
print('POPUP_OPEN',logged('POPUP_GRAB_REQUEST'),'CLIENT_ALIVE',client.poll() is None,flush=True)
if mode.startswith('boundary_'):
inject('move',700,150);time.sleep(.2);shot('visible-hover')
inject('right',700,150);time.sleep(.2);shot('visible-click')
inject('move',900,150);time.sleep(.2);shot('boundary-hover')
inject(mode.split('_')[1],900,150)
time.sleep(.4);shot('boundary-click')
print('BOUNDARY',mode,'POPUP_DONE',logged('POPUP_DONE'),
'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if mode=='standard_return':
print('STANDARD_RESULT',ipc('layout splith'),flush=True)
time.sleep(.4);shot('standard-open')
inject('move',900,150);inject('right',900,150)
time.sleep(.35);shot('standard-click')
print('STANDARD_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if mode=='outside_left':
inject('left',900,400)
time.sleep(.4);shot('outside-left')
print('OUTSIDE_LEFT_POPUP_DONE',logged('POPUP_DONE'),
'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if mode=='visible_left':
inject('move',700,150);inject('left',700,150)
time.sleep(.3);shot('visible-left')
print('VISIBLE_LEFT_POPUP_DONE',logged('POPUP_DONE'),
'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if mode=='fully_visible':
inject('move',350,150);inject('left',350,150);inject('right',350,150)
time.sleep(.3);shot('fully-visible-clicks')
print('FULLY_VISIBLE_POPUP_DONE',logged('POPUP_DONE'),
'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if case.startswith('private_mosaic'):
print('SCROLL_RESULT',ipc('layout mosaic scroll 880'),flush=True)
time.sleep(.6)
shot('hidden')
ws=workspace(tree(),'1');owner=find_app(ws,'org.mosaic.ValidGrabProbe')
print('HIDDEN_OWNER_RECT',owner['rect'],'MOSAIC',ws.get('mosaic'),flush=True)
print('POPUP_DONE_BEFORE_CLICK',logged('POPUP_DONE'),flush=True)
if mode=='lifecycle_switch':
print('SWITCH_RESULT',ipc('workspace 2'),flush=True)
time.sleep(.45);shot('switched')
print('SWITCH_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
if mode=='lifecycle_destroy':
print('DESTROY_RESULT',ipc(f'[con_id={owner["id"]}] kill'),flush=True)
time.sleep(.45);shot('destroyed')
print('DESTROY_POPUP_DONE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True)
sys.exit(0)
inject('move',100,150)
time.sleep(.25);shot('hidden-hover')
print('POPUP_DONE_AFTER_HOVER',logged('POPUP_DONE'),flush=True)
if case.endswith('_hit'):
inject('right',100,150)
time.sleep(.25);shot('hidden-click')
print('POPUP_DONE_AFTER_HIDDEN_CLICK',logged('POPUP_DONE'),flush=True)
print('RETURN_RESULT',ipc('layout mosaic scroll -880'),flush=True)
time.sleep(.5);shot('returned')
print('POPUP_DONE_AFTER_RETURN',logged('POPUP_DONE'),flush=True)
# A click outside the popup is compared against the control. Capture before
# and after separately so dismissal cannot masquerade as clipping evidence.
inject('right',900,400)
time.sleep(.5);shot('outside')
print('POPUP_DONE_OUTSIDE',logged('POPUP_DONE'),'CLIENT_ALIVE',client.poll() is None,flush=True)
if case.startswith('private_mosaic'):
print('STANDARD_RESULT',ipc('layout splith'),flush=True)
time.sleep(.4);shot('standard')
finally:
for child in children:
if child.poll() is None:os.killpg(child.pid,signal.SIGTERM)
if compositor.poll() is None:os.killpg(compositor.pid,signal.SIGTERM)
compositor.wait(timeout=5)
for f in files:f.close()

View file

@ -0,0 +1,211 @@
diff --git a/include/scenefx/types/wlr_scene.h b/include/scenefx/types/wlr_scene.h
index ac1bc49..edb68a2 100644
--- a/include/scenefx/types/wlr_scene.h
+++ b/include/scenefx/types/wlr_scene.h
@@ -462,6 +462,11 @@ void wlr_scene_set_color_manager_v1(struct wlr_scene *scene, struct wlr_color_ma
*/
struct wlr_scene_tree *wlr_scene_tree_create(struct wlr_scene_tree *parent);
+/** Experimental: copy a tree-local clip; NULL removes it. Width/height must be nonnegative. */
+void wlr_scene_tree_set_clip(struct wlr_scene_tree *tree, const struct wlr_box *clip);
+/** Experimental: copy the currently applied tree-local clip; false if unset. */
+bool wlr_scene_tree_get_clip(struct wlr_scene_tree *tree, struct wlr_box *clip);
+
/**
* Add a node displaying a single surface to the scene-graph.
*
diff --git a/types/scene/wlr_scene.c b/types/scene/wlr_scene.c
index 9a9fcc0..b4409d3 100644
--- a/types/scene/wlr_scene.c
+++ b/types/scene/wlr_scene.c
@@ -40,6 +40,36 @@
#define DMABUF_FEEDBACK_DEBOUNCE_FRAMES 30
#define HIGHLIGHT_DAMAGE_FADEOUT_TIME 250
+// Experimental sidecar preserves the public scene structs shared with wlroots.
+// A production primitive needs an upstream-owned, scalable attachment design.
+struct scene_tree_clip {
+ struct scene_tree_clip *next;
+ struct wlr_scene_tree *tree;
+ struct wlr_box box;
+ struct wl_listener destroy;
+};
+static struct scene_tree_clip *scene_tree_clips;
+
+static struct scene_tree_clip *scene_tree_clip_find(struct wlr_scene_tree *tree) {
+ for (struct scene_tree_clip *it = scene_tree_clips; it; it = it->next) {
+ if (it->tree == tree) return it;
+ }
+ return NULL;
+}
+
+static void scene_tree_clip_remove(struct scene_tree_clip *clip) {
+ struct scene_tree_clip **link = &scene_tree_clips;
+ while (*link != clip) link = &(*link)->next;
+ *link = clip->next;
+ wl_list_remove(&clip->destroy.link);
+ free(clip);
+}
+
+static void scene_tree_clip_destroy(struct wl_listener *listener, void *data) {
+ struct scene_tree_clip *clip = wl_container_of(listener, clip, destroy);
+ scene_tree_clip_remove(clip);
+}
+
struct wlr_scene_tree *wlr_scene_tree_from_node(struct wlr_scene_node *node) {
assert(node->type == WLR_SCENE_NODE_TREE);
struct wlr_scene_tree *tree = wl_container_of(node, tree, node);
@@ -477,6 +507,9 @@ static void scene_output_damage(struct wlr_scene_output *scene_output,
pixman_region32_intersect_rect(&clipped, damage, 0, 0, output->width, output->height);
if (!pixman_region32_empty(&clipped)) {
+ const pixman_box32_t *extent = pixman_region32_extents(&clipped);
+ wlr_log(WLR_INFO, "SPIKE_DAMAGE output=%s extent=%d,%d..%d,%d",
+ output->name, extent->x1, extent->y1, extent->x2, extent->y2);
wlr_output_schedule_frame(scene_output->output);
wlr_damage_ring_add(&scene_output->damage_ring, &clipped);
@@ -673,6 +706,51 @@ static void restack_xwayland_surface(struct wlr_scene_node *node,
}
#endif
+// WHY: leaf visibility is also the render/damage boundary for SceneFX effects.
+// CONTRACT: clips are tree-local and intersect across all ancestors.
+// INVARIANT: a descendant cannot expose pixels beyond any ancestor clip.
+static void scene_node_intersect_clip(struct wlr_scene_node *node,
+ pixman_region32_t *region) {
+ for (struct wlr_scene_node *ancestor = node; ancestor != NULL;
+ ancestor = ancestor->parent ? &ancestor->parent->node : NULL) {
+ if (ancestor->type != WLR_SCENE_NODE_TREE) {
+ continue;
+ }
+ struct scene_tree_clip *clip = scene_tree_clip_find(wlr_scene_tree_from_node(ancestor));
+ if (!clip) {
+ continue;
+ }
+ int x, y;
+ wlr_scene_node_coords(ancestor, &x, &y);
+ pixman_region32_intersect_rect(region, region,
+ x + clip->box.x, y + clip->box.y,
+ clip->box.width, clip->box.height);
+ }
+}
+
+static bool scene_node_point_inside_clip(struct wlr_scene_node *node,
+ double lx, double ly) {
+ for (struct wlr_scene_node *ancestor = node; ancestor != NULL;
+ ancestor = ancestor->parent ? &ancestor->parent->node : NULL) {
+ if (ancestor->type != WLR_SCENE_NODE_TREE) {
+ continue;
+ }
+ struct scene_tree_clip *entry = scene_tree_clip_find(wlr_scene_tree_from_node(ancestor));
+ if (!entry) {
+ continue;
+ }
+ int x, y;
+ wlr_scene_node_coords(ancestor, &x, &y);
+ struct wlr_box clip = entry->box;
+ clip.x += x;
+ clip.y += y;
+ if (!wlr_box_contains_point(&clip, lx, ly)) {
+ return false;
+ }
+ }
+ return true;
+}
+
static bool scene_node_update_iterator(struct wlr_scene_node *node,
int lx, int ly, void *_data) {
struct scene_update_data *data = _data;
@@ -695,11 +773,13 @@ static bool scene_node_update_iterator(struct wlr_scene_node *node,
pixman_region32_union(&node->visible, &node->visible, data->visible);
pixman_region32_intersect_rect(&node->visible, &node->visible,
lx, ly, box.width, box.height);
+ scene_node_intersect_clip(node, &node->visible);
if (data->calculate_visibility) {
pixman_region32_t opaque;
pixman_region32_init(&opaque);
scene_node_opaque_region(node, lx, ly, &opaque);
+ scene_node_intersect_clip(node, &opaque);
pixman_region32_subtract(data->visible, data->visible, &opaque);
pixman_region32_fini(&opaque);
}
@@ -749,7 +829,11 @@ static void scene_node_bounds(struct wlr_scene_node *node,
int width, height;
scene_node_get_size(node, &width, &height);
- pixman_region32_union_rect(visible, visible, x, y, width, height);
+ pixman_region32_t bounds;
+ pixman_region32_init_rect(&bounds, x, y, width, height);
+ scene_node_intersect_clip(node, &bounds);
+ pixman_region32_union(visible, visible, &bounds);
+ pixman_region32_fini(&bounds);
}
static void scene_update_region(struct wlr_scene *scene,
@@ -863,6 +947,40 @@ static void scene_node_update(struct wlr_scene_node *node,
pixman_region32_fini(damage);
}
+void wlr_scene_tree_set_clip(struct wlr_scene_tree *tree, const struct wlr_box *clip) {
+ assert(tree);
+ assert(!clip || (clip->width >= 0 && clip->height >= 0));
+ struct scene_tree_clip *entry = scene_tree_clip_find(tree);
+ if (!clip && !entry) return;
+ if (clip && entry && memcmp(&entry->box, clip, sizeof(*clip)) == 0) return;
+ wlr_log(WLR_INFO, "SPIKE_CLIP tree=%p rect=%d,%d %dx%d",
+ (void *)tree, clip ? clip->x : 0, clip ? clip->y : 0,
+ clip ? clip->width : 0, clip ? clip->height : 0);
+ if (clip && !entry) {
+ entry = calloc(1, sizeof(*entry));
+ if (!entry) return;
+ entry->tree = tree;
+ entry->destroy.notify = scene_tree_clip_destroy;
+ wl_signal_add(&tree->node.events.destroy, &entry->destroy);
+ entry->next = scene_tree_clips;
+ scene_tree_clips = entry;
+ }
+ if (clip) {
+ entry->box = *clip;
+ } else {
+ scene_tree_clip_remove(entry);
+ }
+ scene_node_update(&tree->node, NULL);
+}
+
+bool wlr_scene_tree_get_clip(struct wlr_scene_tree *tree, struct wlr_box *clip) {
+ assert(tree && clip);
+ struct scene_tree_clip *entry = scene_tree_clip_find(tree);
+ if (!entry) return false;
+ *clip = entry->box;
+ return true;
+}
+
struct wlr_scene_rect *wlr_scene_rect_create(struct wlr_scene_tree *parent,
int width, int height, const float color[static 4]) {
assert(parent);
@@ -1883,6 +2001,9 @@ struct node_at_data {
static bool scene_node_at_iterator(struct wlr_scene_node *node,
int lx, int ly, void *data) {
struct node_at_data *at_data = data;
+ if (!scene_node_point_inside_clip(node, at_data->lx, at_data->ly)) {
+ return false;
+ }
double rx = at_data->lx - lx;
double ry = at_data->ly - ly;
@@ -2739,6 +2860,13 @@ static enum scene_direct_scanout_result scene_entry_try_direct_scanout(
const struct render_data *data) {
struct wlr_scene_output *scene_output = data->output;
struct wlr_scene_node *node = entry->node;
+ for (struct wlr_scene_node *ancestor = node; ancestor != NULL;
+ ancestor = ancestor->parent ? &ancestor->parent->node : NULL) {
+ if (ancestor->type == WLR_SCENE_NODE_TREE &&
+ scene_tree_clip_find(wlr_scene_tree_from_node(ancestor))) {
+ return SCANOUT_INELIGIBLE;
+ }
+ }
if (!scene_output->scene->direct_scanout) {
return SCANOUT_INELIGIBLE;

View file

@ -0,0 +1,152 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="wlr_virtual_pointer_unstable_v1">
<copyright>
Copyright © 2019 Josef Gajdusek
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice (including the next
paragraph) shall be included in all copies or substantial portions of the
Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
DEALINGS IN THE SOFTWARE.
</copyright>
<interface name="zwlr_virtual_pointer_v1" version="2">
<description summary="virtual pointer">
This protocol allows clients to emulate a physical pointer device. The
requests are mostly mirror opposites of those specified in wl_pointer.
</description>
<enum name="error">
<entry name="invalid_axis" value="0"
summary="client sent invalid axis enumeration value" />
<entry name="invalid_axis_source" value="1"
summary="client sent invalid axis source enumeration value" />
</enum>
<request name="motion">
<description summary="pointer relative motion event">
The pointer has moved by a relative amount to the previous request.
Values are in the global compositor space.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="dx" type="fixed" summary="displacement on the x-axis"/>
<arg name="dy" type="fixed" summary="displacement on the y-axis"/>
</request>
<request name="motion_absolute">
<description summary="pointer absolute motion event">
The pointer has moved in an absolute coordinate frame.
Value of x can range from 0 to x_extent, value of y can range from 0
to y_extent.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="x" type="uint" summary="position on the x-axis"/>
<arg name="y" type="uint" summary="position on the y-axis"/>
<arg name="x_extent" type="uint" summary="extent of the x-axis"/>
<arg name="y_extent" type="uint" summary="extent of the y-axis"/>
</request>
<request name="button">
<description summary="button event">
A button was pressed or released.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="button" type="uint" summary="button that produced the event"/>
<arg name="state" type="uint" enum="wl_pointer.button_state" summary="physical state of the button"/>
</request>
<request name="axis">
<description summary="axis event">
Scroll and other axis requests.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/>
<arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/>
</request>
<request name="frame">
<description summary="end of a pointer event sequence">
Indicates the set of events that logically belong together.
</description>
</request>
<request name="axis_source">
<description summary="axis source event">
Source information for scroll and other axis.
</description>
<arg name="axis_source" type="uint" enum="wl_pointer.axis_source" summary="source of the axis event"/>
</request>
<request name="axis_stop">
<description summary="axis stop event">
Stop notification for scroll and other axes.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="the axis stopped with this event"/>
</request>
<request name="axis_discrete">
<description summary="axis click event">
Discrete step information for scroll and other axes.
This event allows the client to extend data normally sent using the axis
event with discrete value.
</description>
<arg name="time" type="uint" summary="timestamp with millisecond granularity"/>
<arg name="axis" type="uint" enum="wl_pointer.axis" summary="axis type"/>
<arg name="value" type="fixed" summary="length of vector in touchpad coordinates"/>
<arg name="discrete" type="int" summary="number of steps"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual pointer object"/>
</request>
</interface>
<interface name="zwlr_virtual_pointer_manager_v1" version="2">
<description summary="virtual pointer manager">
This object allows clients to create individual virtual pointer objects.
</description>
<request name="create_virtual_pointer">
<description summary="Create a new virtual pointer">
Creates a new virtual pointer. The optional seat is a suggestion to the
compositor.
</description>
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
</request>
<request name="destroy" type="destructor" since="1">
<description summary="destroy the virtual pointer manager"/>
</request>
<!-- Version 2 additions -->
<request name="create_virtual_pointer_with_output" since="2">
<description summary="Create a new virtual pointer">
Creates a new virtual pointer. The seat and the output arguments are
optional. If the seat argument is set, the compositor should assign the
input device to the requested seat. If the output argument is set, the
compositor should map the input device to the requested output.
</description>
<arg name="seat" type="object" interface="wl_seat" allow-null="true"/>
<arg name="output" type="object" interface="wl_output" allow-null="true"/>
<arg name="id" type="new_id" interface="zwlr_virtual_pointer_v1"/>
</request>
</interface>
</protocol>