diff --git a/README.md b/README.md index a105012..fb7c032 100644 --- a/README.md +++ b/README.md @@ -112,6 +112,7 @@ These commands do not install or switch the active session. See [Development](do | [Development](docs/DEVELOPMENT.md) | Reproducible build and validation workflow | | [Known limitations](docs/KNOWN_LIMITATIONS.md) | Unsupported or unvalidated behavior | | [Roadmap](docs/ROADMAP.md) | Ordered steps and exit criteria | +| [STEP20C viewport proposal](docs/design/STEP20C_VIEWPORT_DESIGN.md) | Proposed clipping and input design; feasibility still open | | [STEP20A](docs/STEP20A.md) · [STEP20B](docs/STEP20B.md) | Historical implementation evidence | | [Changelog](CHANGELOG.md) · [Contributing](CONTRIBUTING.md) | Evolution and contribution rules | diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 7e29203..e98dfad 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -49,6 +49,12 @@ Sway's pending/current transaction model separates requested geometry from commi The output renders a physical viewport of a wider logical row. Single-output headless tests demonstrate visual translation. A generic scene-tree clip at workspace/output boundaries is not yet implemented. Adjacent outputs may expose content outside a workspace's intended viewport; correct clipping and matching hit testing are a STEP20C concern. Do not infer multi-output safety from output-buffer cropping on one monitor. +## STEP20C viewport design status + +The [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md) audits the actual SwayFX/SceneFX paths and recommends a **PROPOSED** Mosaic-owned viewport projection with matching input boundary. It is not implemented or accepted as a frozen contract. SceneFX 0.5 has no public generic scene-tree clip API; the proposed node-specific projection still needs a focused headless feasibility probe covering effects, saved buffers, popups, damage and hit testing. Multi-output Mosaic remains unsupported until visual and interactive isolation are demonstrated together. + +The proposal distinguishes logical row order and computed geometry from the workspace viewport, logical camera target, and any future presented camera position. During a transaction, scene and pointer input must refer to the same applied geometry/viewport generation. The current `scroll_x` is not copied into Sway's transaction state; a future implementation must define that boundary explicitly. These are design requirements for STEP20C, not claims about current behavior. + ## IPC On a Mosaic workspace, `get_tree` reports `layout: "mosaic"` and adds a `mosaic` object with `active_row` and `rows`. Each row entry contains ordered child node IDs, `scroll_x`, and focused node ID or null. Existing Sway fields retain their shapes; normal workspace/window events and `get_workspaces` paths remain in use. This is an extension to the Sway layout vocabulary: strict IPC consumers with a closed layout enum must recognize `mosaic`. Additional rows in the model are not a promise of usable multi-row IPC behavior. diff --git a/docs/KNOWN_LIMITATIONS.md b/docs/KNOWN_LIMITATIONS.md index 2775043..4bbd54f 100644 --- a/docs/KNOWN_LIMITATIONS.md +++ b/docs/KNOWN_LIMITATIONS.md @@ -16,4 +16,6 @@ This document describes the development tree at the documentation foundation pas | Forward port to a later SwayFX release | PLANNED | The patch is pinned to SwayFX 0.6; a new release requires manual review and validation. | | Very wide logical coordinates | NOT YET SUPPORTED | Scene positions use bounded integer coordinates; extremely wide canvases are not qualified. | +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. + These limits are reflected in [UX](UX.md), [architecture](ARCHITECTURE.md), and [roadmap](ROADMAP.md). A future step must update those contracts before claiming support. diff --git a/docs/README.md b/docs/README.md index 279e322..400abbb 100644 --- a/docs/README.md +++ b/docs/README.md @@ -13,7 +13,8 @@ The [root README](../README.md) is the public summary. For a specific topic, use 5. [Development](DEVELOPMENT.md): reproducible workflow. 6. [Known limitations](KNOWN_LIMITATIONS.md): current support boundary. 7. [Roadmap](ROADMAP.md): ordered future work, not proof of implementation. -8. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence. +8. [STEP20C viewport proposal](design/STEP20C_VIEWPORT_DESIGN.md): proposed design, subordinate to accepted canonical contracts. +9. [STEP20A](STEP20A.md) and [STEP20B](STEP20B.md): historical evidence. ## Project @@ -40,6 +41,14 @@ The [root README](../README.md) is the public summary. For a specific topic, use | [Packaging](PACKAGING.md) | Normative | File ownership, versioning, fallback | | [Upstream maintenance](UPSTREAM.md) | Normative | SwayFX relationship, pinned base, forward ports | +## Design proposals + +| Document | Role | Purpose | +| --- | --- | --- | +| [STEP20C viewport design](design/STEP20C_VIEWPORT_DESIGN.md) | Proposed, not yet accepted | Source audit, clipping alternatives, test plan, feasibility gate | + +A proposal does not supersede accepted canonical contracts or assert implementation status. Its open feasibility items must be resolved before implementation. + ## Roadmap and historical reports | Document | Role | Purpose | diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index afd3926..564eab0 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -22,7 +22,7 @@ This is the canonical order of work, not proof that a feature exists. **A step i ## 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:** specify clipping ownership, animation semantics, and edge-case UX in Architecture and UX; update Known Limitations before implementation. This documentation pass does not implement STEP20C. +**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), prove its clipping/input feasibility on installed SceneFX, then settle the resulting contract in Architecture and UX and update Known Limitations before implementation. The proposal is not implementation approval. This documentation pass does not implement STEP20C. ## STEP20D — multi-row Spatial Canvas · PLANNED diff --git a/docs/UX.md b/docs/UX.md index 53449d2..1005b8c 100644 --- a/docs/UX.md +++ b/docs/UX.md @@ -50,6 +50,10 @@ Each row conceptually remembers `scroll_x` and its last focused container. Chang The viewport must remain aligned with rendered content and hit testing. Robust clipping across adjacent outputs is **PLANNED**. +## Proposed STEP20C viewport behavior + +The [STEP20C design proposal](design/STEP20C_VIEWPORT_DESIGN.md) recommends **minimal focus reveal**: a fully visible focused cell would not move the camera, while an off-screen cell would be revealed by the smallest useful movement. A cell wider than the viewport needs a separate edge/partial-visibility rule. This policy is **PROPOSED**, not yet accepted. A future visual camera value may animate toward the logical target while preserving row order; animation duration and easing remain **TBD**. Multi-output visual and input isolation, including popups and partial cells, remains unimplemented and unsupported pending a SceneFX feasibility probe. + ## Floating, fullscreen, and scratchpad These remain ordinary Sway concepts. Mosaic does not redefine them. Headless tests have exercised basic floating, fullscreen, scratchpad, insertion, and layout transitions; longer multi-output and resize interactions are not fully validated. See [known limitations](KNOWN_LIMITATIONS.md). diff --git a/docs/design/STEP20C_VIEWPORT_DESIGN.md b/docs/design/STEP20C_VIEWPORT_DESIGN.md new file mode 100644 index 0000000..dcf8431 --- /dev/null +++ b/docs/design/STEP20C_VIEWPORT_DESIGN.md @@ -0,0 +1,208 @@ +# STEP20C viewport architecture — design proposal + +**Status: PROPOSED, documentation only.** Baseline: project commit `eeba4b7ac72866f9e48e6f002f39eb962fdbdd3e`, SwayFX 0.6, installed wlroots 0.20.2 and SceneFX 0.5.0 headers. No STEP20C behavior exists yet. This proposal preserves [Architecture](../ARCHITECTURE.md), [UX](../UX.md), [Packaging](../PACKAGING.md), and the [historical STEP20B evidence](../STEP20B.md). It does **not** freeze the final cell width, key bindings, or animation curve. + +## 1. Design status and feasibility gate + +**Recommendation: PROPOSED Approach C, a Mosaic-owned viewport projection over the existing real Sway scene nodes, with one shared viewport predicate for rendering and input.** The compositor remains the sole authority for order, geometry, camera, focus, and input. Standard layouts and the standard SwayFX binary must retain their existing paths. + +**Critical unresolved feasibility:** SceneFX 0.5 has no public generic `wlr_scene_tree` clip API. It offers node-specific clip/crop operations, but their combined behavior for all Mosaic node types, damage, and hit testing has not been demonstrated. In particular, global popup scene trees and effect/saved-buffer nodes need a focused disposable headless probe before production implementation. Until that proof passes, this is a recommended architecture, **not an implementation-ready clipping contract**. If the probe fails, revisit the architecture; do not silently substitute an output-only mask or claim multi-output safety. + +## 2. Existing implementation and exact call paths + +The paths below were checked in this repository. References identify current functions; the change column describes **proposed future work**, not a present API. + +| Source/function | Current responsibility | Required STEP20C change | Risk | +| --- | --- | --- | --- | +| `sway/tree/arrange.c:apply_mosaic_layout` | Sets Row 0 pending logical rectangles, temporary 640 px minimum, clamps scroll | Produce/retain computed row extent and viewport geometry independently of sizing policy | Overflow, changing width while scrolled | +| `sway/tree/arrange.c:arrange_children`, `arrange_workspace`, `arrange_output` | Routes Mosaic only for workspace tiling; sets workspace origin from `output->lx/ly + usable_area`; handles fullscreen/floating | Recompute viewport on usable-area/output change and bind it to workspace | Stale origin or camera after migration | +| `sway/tree/workspace.c:workspace_get_box`, `workspace_move_to_output` | Returns post-gap workspace rectangle; moves workspace between outputs | Rebind/reclamp viewport and cancel/rebase animation during move | Old-output scene/input leak | +| `sway/config/output.c:output_update_position` | Copies output layout box into `lx/ly/width/height` | Trigger viewport recomputation through existing arrangement path | Negative origins, transforms, scale | +| `sway/tree/mosaic.c:mosaic_row_max_scroll`, `mosaic_row_clamp_scroll` | Bounds row camera using count, width, gap and temporary cell width | Bound from computed logical row extent instead of duplicating sizing math | Divergent geometry/camera limit | +| `sway/commands/layout.c:cmd_layout` | `layout mosaic scroll`, toggle, state creation and arrangement | Send camera target/reveal through a single Mosaic path; preserve restore target | Target visible in IPC before scene catches up | +| `sway/input/seat.c:seat_set_workspace_focus` | Saves Row 0 focus and minimally reveals via pending rectangles | Use viewport-aware reveal for oversized/partial cells; defer visual update consistently | Reentrant arrange, focus/scene mismatch | +| `sway/desktop/transaction.c:transaction_commit_dirty`, `transaction_commit_pending`, `transaction_progress`, `transaction_apply` | Copies pending state, waits for client commits or timeout, applies current, arranges scene | Define camera target snapshot/commit boundary and invalidation on transitions | Mixed pending geometry and current scene | +| `sway/desktop/transaction.c:workspace_tiling_scene_x`, `arrange_output`, `arrange_root` | Places tiling tree at area/gap origin minus Row 0 `scroll_x`; reparents workspace/output layers | Apply visual camera and viewport projection only for active Mosaic tiling; clear it on standard/fullscreen paths | Cross-output leakage or faded old workspace | +| `sway/desktop/transaction.c:arrange_workspace_floating`, `arrange_fullscreen`, `disable_workspace`, `arrange_popups` | Floating/fullscreen/scratchpad and global popup scene handling | Preserve ordinary modes; associate Mosaic-origin popups with viewport policy | Popup bypass of clip; stale nodes | +| `sway/desktop/output.c:output_repaint_timer_handler`, `output_configure_scene`, `handle_frame` | Configures effects, calls `wlr_scene_output_build_state`, commits output and sends frame done | Verify projected clip/damage per output; avoid changing global render path for standard layouts | Redraw cost, blur/shadow spill | +| `sway/input/cursor.c:node_at_coords`, `pointer_motion`, `dispatch_cursor_button` | Walks global scene layers with `wlr_scene_node_at`; output lookup is fallback after scene hit | Reject Mosaic-owned hits outside their viewport before delivery; preserve `sx/sy` from real scene hit | Invisible target or wrong client coordinates | +| `sway/input/seatop_move_tiling.c`, `seatop_resize_tiling.c` | Use pointer/global coordinates and Sway boxes during drag/resize | Validate viewport boundary and cancellation/migration semantics | Drag target outside visible region | +| `sway/desktop/xdg_shell.c:popup_create`, `popup_unconstrain`; `transaction.c:arrange_popups` | Creates global popup scene tree, unconstrains to output, positions relative to view | Scope Mosaic-origin popup visibility/input to its owner viewport or explicitly reposition it inside | Popup can escape tiling subtree | +| `sway/ipc-json.c:ipc_json_describe_workspace` | Emits `layout: "mosaic"` and `{active_row, rows:[{children, scroll_x, focused}]}` | Keep stable target semantics; avoid publishing transient visual offset as logical order | Strict consumers, inconsistent snapshots | +| `include/sway/tree/mosaic.h` | Owns Row 0 scroll/focus and model-only future rows | Specify target/visual camera and viewport lifetime if implementation adds state | Dangling output pointer or duplicate authority | + +Current high-level paths: + +```text +IPC scroll / keyboard focus -> row scroll_x -> arrange_workspace -> node_set_dirty + -> eventual transaction_commit_dirty -> pending transaction -> client commit or timeout + -> transaction_apply -> arrange_root -> workspace_tiling_scene_x + -> wlr_scene_node_set_position -> SceneFX output build/commit + +Pointer global lx/ly -> node_at_coords -> global scene-layer walk + -> wlr_scene_node_at -> surface/node and sx/sy -> seat operation +``` + +`scroll_x` is a workspace-row model value, **not** part of copied Sway transaction state today. `cmd_layout` and focus can update it before a transaction applies; `get_tree` reads current workspace model values while ordinary rectangles may represent another transaction phase. The existing design must therefore avoid claiming that every intermediate IPC snapshot is atomic. The proposal below makes the target explicit and aligns presented scene/input with a committed geometry generation. + +## 3. Coordinate spaces and transform + +| Space | Meaning and owner | Used by | +| --- | --- | --- | +| Row logical | Ordered cell rectangles and extent relative to a row origin, independent of camera | Mosaic layout, clamp, reveal | +| Workspace layout | `pending`/`current` container boxes in Sway layout coordinates; `workspace_get_box` includes output origin and gaps | Arrangement, Sway transactions, IPC rectangles | +| Scene parent-local | Integer node positions relative to ancestors; output layer at `output->lx/ly`, workspace tiling layer translated by camera | `wlr_scene_node_set_position`, effects | +| Scene/root global | Sum of node transforms in output-layout logical coordinates | `wlr_scene_node_coords`, global scene hit testing | +| Output-local logical | Global coordinate minus output layout origin, before buffer scale/transform | Output viewport intersection, output IPC conversion | +| Output-buffer pixels | Output-local logical geometry transformed/scaled/rounded by wlroots/SceneFX | `wlr_scene_output_build_state`, screenshots | +| Pointer global | `wlr_cursor` layout coordinates; surface-local `sx/sy` comes from scene hit | `node_at_coords`, seat operations | + +For a simple untransformed row, `screen_x = logical_x - visual_camera_x + viewport_origin_x`. Here `logical_x` must be row-relative; `viewport_origin_x` is the post-gap workspace origin in global logical coordinates. In current code, container `pending.x` is already workspace/global logical, and `workspace_tiling_scene_x` subtracts `scroll_x` from the workspace tiling scene parent. **Do not subtract the output origin or camera twice.** Parent-local positions, nested trees, output negative positions, and gap offsets must be checked with `wlr_scene_node_coords`/the actual scene hierarchy. Output scale and rotation belong to the output-render conversion, not to `scroll_x`; clipping uses integer logical boxes with conservative edge rounding and test evidence at buffer pixels. Fractional scale must not be implemented by scaling camera pixels manually. + +The viewport is the **workspace's tiled usable rectangle after gaps**, intersected with its active output logical box. The physical output box is a safety boundary; it is not the logical canvas extent. Scene projection and hit testing must use the same committed viewport box and owner workspace/output identity. + +## 4. Clipping alternatives on installed APIs + +Installed `scenefx/types/wlr_scene.h` provides `wlr_scene_node_set_position`, `wlr_scene_node_at`, `wlr_scene_subsurface_tree_set_clip` (subsurface descendants in root-surface coordinates), `wlr_scene_rect_set_clipped_region`, shadow/blur clipped-region calls, and `wlr_scene_buffer_set_source_box`. The latter crops a buffer's source; it is **not** a general workspace clip. `wlr_scene_output_build_state` options expose color transform/swapchain/timer, **no per-subtree viewport filter**. There is no declared generic tree clip. These are header observations, not a proof of all rendering behavior. + +| Approach | API feasibility | Render/input correctness | Transaction/damage/performance | STEP20D/rebase/standard-layout impact | +| --- | --- | --- | --- | --- | +| **A. Generic scene-tree clip** | **Unsupported as stated** on SceneFX 0.5; no public `wlr_scene_tree` clip | Would be attractive if available, but cannot be assumed | Would rely on dependency behavior not present | Low conceptual cost, unavailable without dependency change | +| **B. Output-render restriction** | Requires custom renderer traversal or SceneFX extension; build-state options do not offer it | Could clip visual output; global `node_at_coords` still needs separate input policy | High risk to damage, effects, scanout, frame callbacks and output-specific work | Broad SceneFX/upstream maintenance; risk to standard layouts | +| **C. Mosaic viewport projection** | Node-specific public clip/crop APIs exist; complete coverage remains unproven | Proposed shared viewport predicate plus explicit scene-node projection and input guard | Medium/high compositor complexity; must test damage and transitions | Can stay Mosaic-scoped and generalize from one row to per-row regions; no standard path change intended | + +**Recommend C, PROPOSED.** Keep the existing real container scene ownership, add a workspace-owned viewport descriptor and a Mosaic-scoped projection of intersected content. Use the public node-specific operations where they are valid; handle decorations, shadows, blur, saved buffers, and global popups explicitly. Do not replace real containers with copies or a second layout tree. The same committed viewport descriptor guards Mosaic scene hits. When a required node type cannot be projected safely, fail the feasibility gate and revisit B or a deliberately versioned SceneFX extension; never ship visual-only clipping. + +**Focused feasibility investigation required before implementation:** in a disposable headless build/instance, clip one partly visible XDG surface including subsurfaces and popup, border, shadow/blur, and saved transaction buffer; inspect both outputs and `wlr_scene_node_at` behavior at every boundary. Instrument damage/frame callbacks. Establish that any per-node crop preserves client-local input coordinates. This probe is not authorized by this documentation-only pass. + +## 5. Viewport ownership and hit testing + +**PROPOSED:** a Mosaic viewport descriptor belongs to the workspace's Mosaic state. It references no output permanently: resolve the current `workspace->output` and its usable/output boxes when arranging and applying scene state. An output disconnect invalidates the descriptor and stops camera animation before scene nodes are reused. Destruction clears projection state before destroying workspace scene nodes. Future rows can each supply a region, while a workspace-level owner prevents a row from drawing on another output. + +For Mosaic tiled nodes, visual and interactive regions are the intersection of node content with the committed viewport. An entirely hidden node must be absent from visible composition and ineligible for pointer focus. A partly visible node can receive left/right click, hover, and drag only within its visible part; `sx/sy` are derived from the real scene hit, without inventing translated client coordinates. The input guard identifies the hit's owner by walking scene descriptors/ancestors, checks the current output from `wlr_output_layout_output_at`, active workspace, committed viewport and visible geometry, then rejects an out-of-viewport Mosaic hit. It must continue the normal scene-layer search below rejected content where needed, rather than return a hidden surface or swallow an unrelated layer-shell hit. `wlr_scene_node_at` returns only the topmost hit; the inspected API does not provide a generic "next hit" iterator. A scoped traversal or scene organization that avoids rejected hits is therefore another required feasibility proof, not a trivial post-filter. A bare fallback to `output_get_active_workspace` is insufficient because `node_at_coords` tests scene layers **before** output lookup. + +Global popups are not children of `workspace->layers.tiling`; `arrange_popups` places them in a root popup layer. **PROPOSED:** a popup owned by a Mosaic tiled view is constrained to the owner's active output/viewport, including input, while ordinary floating/fullscreen popups keep Sway behavior. Popup reposition/unconstrain and nested popup edges require the feasibility probe. If exact clipping of popups cannot be demonstrated, multi-output support remains blocked. Pointer crossing outputs must resolve to the actually visible surface on the destination output. + +Tiling drag, move indicators, resize, hover and touch/tablet paths that use `node_at_coords` or global scene nodes need the same boundary policy. An in-progress drag may cross outputs according to ordinary Sway semantics, but its **source** cannot be an invisible Mosaic hit and its drop target cannot be inferred from a clipped-away cell. Pointer constraints and popups must be rechecked when camera/output changes. No input guard should alter ordinary floating, fullscreen, scratchpad or standard-layout hit testing. + +## 6. Focus and reveal + +| Policy | Benefit | Cost | +| --- | --- | --- | +| Minimal movement | Stable spatial context; no motion for fully visible cell | Large cells cannot be made wholly visible | +| Center focused cell | Predictable focus position | Repeated focus causes unnecessary camera motion | +| Snap to cell/viewport | Easy coarse navigation | Jarring motion and partial visibility at odd widths | + +**Recommend minimal movement, PROPOSED.** If a cell fits, reveal just enough to make it wholly visible; if already fully visible, keep camera target unchanged. If a cell is wider than the viewport, reveal its focused/entry edge or keep the visible portion stable, then allow explicit camera scroll within it; do not oscillate trying to satisfy an impossible full-visibility condition. Clamp to `[0, row_extent - viewport_width]`. Focus changes never alter row order. Tests must verify both directions and the oversized-cell rule before UX acceptance. + +## 7. Transactions, camera target and animation + +**PROPOSED authoritative split:** logical row order/focus and `target_scroll_x` live in workspace Mosaic state. A committed viewport/geometry generation plus `presented_scroll_x` describes what the scene and input currently show. During an in-flight Sway transaction, pending geometry may change, but the presented scene and input remain paired to the last applied geometry; target can be queued or retargeted. On `transaction_apply`, recompute the viewport from current output/current geometry, clamp target, update projection, then begin/retarget visual camera movement. IPC `scroll_x` remains the **logical target**; a future optional presented offset would need explicit naming and compatibility review. Do not serialize an interpolated pixel position as logical order. + +The existing `transaction_progress` applies after client readiness or timeout and calls `arrange_root` then `cursor_rebase_all`; a timeout still applies a transaction. A failed/late client commit must not mutate order or row focus. When queued geometry is superseded, derive the next target from the latest valid logical state and do not animate toward a destroyed node. On empty/disabled output, suspend presentation and retain only valid workspace-local logical state until an output returns. The transaction generation/viewport descriptor must be an implementation detail, not another durable model. + +```text +Normal camera move: +IPC delta -> validate/clamp target -> no order change -> dirty/commit path +-> scene generation applies -> projection updates -> visual camera reaches target +-> cursor rebase; IPC continues to describe target + +Focus reveal: +seat focus -> remember real tiled cell -> calculate minimal target from pending geometry +-> transaction applies current geometry -> re-evaluate/clamp target +-> project scene and input together -> animate or jump per motion setting + +Output resize/migration: +output config/move -> invalidate old viewport and stop/retarget animation +-> arrange pending geometry and new output box -> transaction applies +-> clamp target to new row extent -> project on new output -> rebase pointer + +Standard -> Mosaic -> Standard: +remember standard layout -> create/reuse Mosaic state -> arrange real nodes +-> apply Mosaic viewport/projection -> toggle back -> clear Mosaic projection +-> restore standard layout on same workspace -> preserve node identities +``` + +### Animation approaches + +| Approach | Assessment | +| --- | --- | +| Move scene tree directly on a timer | Simple but risks bypassing transaction and input/projection updates | +| Store visual camera separately from target | Clear authority; every frame can derive scene translation, clip and hit boundary from one presented state | +| Use existing SwayFX animation manager | Reuses scheduling/easing infrastructure, but needs a Mosaic-specific state callback and lifecycle/cancel rules | + +**Recommend a separate visual camera value driven by the existing animation manager, PROPOSED.** Keep target and presented offset distinct. At each tick, interpolate from current presented position toward the latest valid target; update scene translation, projected clip/input region and damage together; schedule only affected output frames. Rapid repeated focus/navigation retargets from the **currently presented** position, never from the stale original start. Cancel/snap on layout exit, workspace destruction, output loss, fullscreen entry, or geometry generation change that invalidates the path. Recompute from current geometry on output resize/migration. Avoid extra redraws when target equals presented or workspace is inactive. Measure CPU/GPU/damage behavior before choosing a duration or easing. **Reduced-motion proposal:** snap immediately when compositor animation is disabled or a future explicit reduced-motion setting is active. Duration and curve remain **TBD**, not permanent constants. + +## 8. Output isolation and transitions + +The output box and workspace viewport are distinct. The projection uses their intersection so a row on eDP-1 cannot paint or hit HDMI-A-1. Horizontal adjacency, vertical stacking and negative global origins share the same logical-box intersection rule. Differing scales/transforms are handled only after verifying SceneFX conversion from global logical coordinates to buffer pixels; tests must include boundary pixels and transformed input. These scenarios are **target acceptance cases**, not currently supported. + +On workspace switch, old workspace fade animation may keep its tiling scene enabled in `transaction.c:arrange_output`; its Mosaic projection must remain bound to the old output viewport until disabled. On output move, bind to the new output only after the transaction has consistent current geometry; never display old-output content on the destination by stale projection. On disconnect, disable visible projection and animation; on reconnect, recompute from the new output. Empty workspace has extent zero and camera zero. One-cell workspace cannot scroll unless that cell is wider than the viewport. Closing the focused/first/last cell clears stale focus, reflows and reclamps. Inserting while scrolled keeps logical insertion after active cell and reveals only as focus policy requires. Resizing at the end clamps the target and presented position to the new range. + +Fullscreen disables Mosaic tiling projection while the standard fullscreen tree is active, then restores/reclamps it on exit. Floating and scratchpad nodes remain in their ordinary Sway/root scene paths; converting floating to tiled re-enters Row 0 through normal tree mutation. Layout toggle preserves identities and workspace-local previous layout. None of these paths may leave a hidden interactive tiling node. + +## 9. Cell sizing and future rows + +STEP20C needs **computed cell rectangles**, their checked logical row extent, the viewport rectangle, and a camera target. It must not read a hard-coded cell width to clip or hit-test. `mosaic_row_max_scroll` currently repeats the temporary sizing calculation; STEP20C should derive the clamp from the same arranged geometry/extent used for scene placement. Future sizing discussions may compare equal-width cells, content-informed widths, and user-set width classes; no choice is frozen here. A policy change should alter layout geometry, not the clipping architecture. + +STEP20C projects Row 0 only. STEP20D may supply separate row geometry/regions and `scroll_x` values (for example 1200, 400, 0), plus each row's order and focus memory. The viewport owner should accept row-scoped regions/camera values rather than embed `rows[0]` in the clip primitive. STEP20D, not this step, wires additional rows into Sway runtime tree, transactions, input and IPC. + +## 10. Headless test design + +Use isolated headless compositor instances, temporary config/runtime directories and separate IPC sockets. Place colored test clients with identifiable IDs; capture each output independently (for example with `grim`), compare expected pixel regions, query `get_tree`, and inject pointer events through a headless input device/test harness. **Screenshots prove pixels, not click routing.** Log target surface/seat focus and client-received button/hover coordinates. Assertions use both IPC and observed input. No test runs against the normal desktop session. + +In the matrix, `IDs` means Sway node IDs and ordered `mosaic.rows[0].children`; `hit` means observed client/seat target, including button side and local coordinates. Each case also checks no compositor crash and no unexpected window migration. + +| Scenario and initial state | Action | Expected IPC | Expected rendering | Expected hit result / regression condition | +| --- | --- | --- | --- | --- | +| One output, 4 cells | Scroll to middle | IDs/order fixed; bounded target | Only viewport intersection appears | Hidden side has no hit; any spill fails | +| Two horizontal outputs: Mosaic left, standard right | Scroll past left edge of right output | Both workspace IDs fixed | Right output shows only its workspace | Click/right-click right output reaches right surface; Mosaic hit fails | +| Two vertically stacked outputs | Scroll and move pointer across boundary | Output/workspace ownership fixed | No vertical spill, including shadows/popups | Destination hit correct; hidden Mosaic target fails | +| Negative output origin | Put Mosaic output left/up of origin, scroll | Same IDs and valid target | Clip follows negative box | Correct target across zero boundary; offset error fails | +| Different scales and rotated output | Scroll across boundary | Logical target unchanged by scale | Boundary pixels confined after transform | Hit maps to visible client-local coordinates; off-by-one fails | +| Partly visible cell, popup and border | Open context menu; left/right click visible and clipped sides | Same cell ID; focus as expected | Visible fraction only; popup bounded/repositioned | Only visible fraction/menu receives input; wrong `sx/sy` fails | +| Entirely hidden cell | Move/click/hover over where its translated geometry crosses neighbor | Same focus unless explicit keyboard action | No pixels/effects on neighbor | No hover/button/focus to hidden client; any hit fails | +| Scroll left/right repeatedly | Explicit scroll commands | IDs/order/geometry fixed; target clamped | Camera changes only | Visible target matches frame; reordered ID fails | +| Focus off-screen, then fully visible cell | Directional focus | Focused ID and target update only when needed | First reveals; second does not pan | Keyboard focus correct; needless camera move fails | +| Rapid camera moves | Alternate focus/scroll before animation ends | Latest target wins | Continuous path, final frame at target | Hits follow presented scene; stale final position fails | +| Resize output while at row end | Change headless mode/usable area | Target bounded to new extent | No void/excess content | Boundary hit correct; overscroll fails | +| Move workspace to another output, then hotplug | IPC workspace move/disconnect/reconnect | Same workspace/container IDs | Only new active output shows it | No old-output hit; stale viewport fails | +| `splith/tabbed/stacking -> mosaic -> prior` | Toggle each populated workspace | Same IDs/order; previous layout restored | Standard scene returns without Mosaic clip | Standard hit path unchanged; identity loss fails | +| Floating/fullscreen/scratchpad | Toggle each mode while scrolled | Ordinary mode and IDs | Standard scene ownership; tiling hidden under fullscreen | Expected mode receives input; clipped floating fails | +| Close focused/first/last; insert while scrolled | Close/open test clients | No stale focus ID; contiguous order | Reflow and bounded camera | New visible cell hit correct; ghost node fails | +| Client stalls during resize | Delay commit past transaction timeout | Final target/IDs coherent | No permanent mixed geometry or leak | Input follows applied scene; stuck transaction fails | +| Drag/resize at clipped edge | Tiling drag and resize from visible part | Order changes only on valid operation | Indicator bounded; no neighbor spill | Hidden edge cannot initiate drag; wrong target fails | + +Automate exact pixel-color masks and event logs where possible; use screenshots for corroboration. Run standard-layout controls on the same isolated binary. Stress with long rows, many windows, rapid retargets, output hotplug, popup creation/destruction and client stalls. A single-output screenshot or IPC-only test cannot qualify multi-output correctness. + +## 11. Performance and safety + +A projection layer or per-node clipping can increase scene traversal, clip recomputation and scene graph depth; measure them. Camera movement damages old and new visible regions and may trigger more frequent output composition, blur, shadows and saved-buffer work. Restrict redraw to affected outputs where SceneFX permits, and verify damage rather than promising a frame rate. Long rows should not allocate one output-sized buffer per cell. Check 64-bit extent calculations before converting to SceneFX integer node positions; reject or safely bound extreme widths and non-finite camera values. Test output hotplug, negative origins, fractional scale, transforms, many logical pixels, hundreds of containers, popups and transaction timeouts. Do not compare only total process RSS to a camera-state estimate. Keep the future production patch small and reviewable; preserve standard-layout code paths and package isolation. + +## 12. Implementation decomposition and acceptance + +This is a **future work plan**, not authorization to edit source in this pass. + +1. **Feasibility probe:** prove complete render/input behavior of node-specific clipping on installed SceneFX 0.5 with two headless outputs, partial surface, effects, saved buffer and popup. Record API behavior and damage. **Block implementation** if it fails. +2. **Geometry contract:** expose checked row extent and committed viewport intersection independently of cell sizing; define workspace ownership/lifetime and output invalidation. +3. **Scene projection:** Mosaic-only clip/crop for all relevant scene node types and output boundaries; preserve standard/floating/fullscreen paths. +4. **Input guard:** use the same committed viewport predicate in `node_at_coords` and related pointer/drag paths; test local coordinates and popup routing. +5. **Transaction/camera path:** separate target from presented camera; align scene/input with applied geometry, handle timeout/rebase/cancel. +6. **Animation:** reuse existing manager only after static clipping passes; implement retarget/reduced-motion behavior without hard-coded UX duration. +7. **Headless matrix:** qualify horizontal/vertical/negative/scaled/transformed outputs, transitions, special modes and stress; update canonical docs and limitations from evidence. + +**Acceptance criteria:** no Mosaic pixels or input leak outside its viewport on qualified output arrangements; partial cells and popups have matching visible/interactive regions; target and presented camera converge without changing order; scene and IPC recover after transaction timeout/output change; standard layouts and ordinary SwayFX paths remain unaffected; tests are reproducible and report actual client input, not screenshots alone. Until feasibility and these tests pass, multi-output Mosaic remains unsupported. + +## 13. Decision register + +| Decision | Options | Recommendation | Status | +| --- | --- | --- | --- | +| Viewport clipping | A generic tree / B output renderer / C Mosaic projection | C, conditional on node-coverage probe | **PROPOSED; feasibility open** | +| Hit testing | Trust scene alone / separate guard | Shared committed viewport predicate and explicit input guard | **PROPOSED** | +| Focus reveal | Minimal / center / snap | Minimal; special rule for oversized cell | **PROPOSED** | +| Camera animation | Direct scene timer / separate visual state / existing manager | Separate visual value driven by existing manager | **PROPOSED** | +| Reduced motion | Animate / snap | Snap when animations disabled; explicit setting later | **PROPOSED** | +| Cell sizing | Equal/content-based/user classes | Defer final choice | **TBD** | +| Exact navigation keys, duration, curve | Multiple valid UX choices | Resolve in UX review after feasibility | **TBD** | + +The only material architectural approval requested before implementation is the clipping strategy after its feasibility probe. UX review can settle motion and oversized-cell details without changing real-container ownership. This design does not authorize STEP20D or a SwayFX/SceneFX dependency fork.