Skip to content

tile: stop the fullscreen backdrop from masking window translucency#4034

Closed
joshsymonds wants to merge 8 commits into
niri-wm:mainfrom
joshsymonds:josh/fullscreen-backdrop-clip
Closed

tile: stop the fullscreen backdrop from masking window translucency#4034
joshsymonds wants to merge 8 commits into
niri-wm:mainfrom
joshsymonds:josh/fullscreen-backdrop-clip

Conversation

@joshsymonds
Copy link
Copy Markdown

Summary

The fullscreen backdrop is currently painted as a single rect covering the entire tile, including the area under the window. This silently overrides user-set window transparency: a fullscreen kitty with background_opacity 0.85 (or mpv with --alpha=yes, etc.) composites its alpha against the opaque backdrop instead of what's behind the tile, so the user's transparency setting becomes invisible.

This PR clips the backdrop to tile − window, preserving its documented role of covering screen-size padding when the window is smaller than the tile (e.g. fullscreening a fixed-size dialog) while no longer masking the window's own alpha channel. Opaque fullscreen windows render identically — the change is opt-in by content alpha, so no config knob is needed.

Implementation

  • New helper backdrop_clip_rects(tile, window, tile_corner_radius) decomposes tile − window into up to 4 axis-aligned rects (left bar, right bar, top middle, bottom middle), each with a CornerRadius whose non-zero corners coincide with tile-outer corners and zero corners butt against the window. Zero-area strips are dropped.
  • Steady-state path (fullscreen_progress >= 1.0) emits one SolidColorRenderElement per strip via a new from_buffer_at helper that takes an explicit geometry instead of using the buffer's natural size.
  • Animation path (fullscreen_progress < 1.0) emits one BorderRenderElement per strip. The rounded-corner shrink animation works the same; the backdrop just doesn't paint under the window at any frame.
  • When an L/R bar is absent (e.g. letterbox-shaped fullscreen), the corresponding top/bottom middle strip inherits the tile's outer corners on the relevant edge, so the rounded outer corners survive the clip.

Test plan

  • cargo test -p niri --lib backdrop_clip — 8 geometry tests covering the standard 4-strip decomposition, 2-strip / 3-strip degenerate cases, window-equals-tile, window-outside-tile, and corner-radius routing including the letterbox case.
  • cargo +nightly fmt --all -- --check and cargo clippy --all --all-targets clean.
  • Visually verified on a real desktop with a translucent kitty fullscreen — wallpaper shows through; opaque windows render identically.

Pins the contract of the geometry helper that the next commit will
introduce for the fullscreen-backdrop transparency fix. Tests cover
the standard decomposition cases + corner-radius routing.

The helper takes the tile rect, window rect, and tile_corner_radius;
returns up-to-4 (Rectangle, CornerRadius) pairs covering tile-minus-
window. Per-rect CornerRadius is non-zero only on corners that
coincide with tile-outer-corners — the L-shape backdrop keeps
animated rounded outer corners, inner edges next to the window
stay sharp.
Replace single full-tile SolidColorRenderElement with up-to-4 from
backdrop_clip_rects(tile, window, 0). At fullscreen_progress=1.0 the
backdrop no longer covers the window's geometry — translucent windows
(e.g., kitty with background_opacity<1.0) now show the wallpaper
through their transparency. Aspect-ratio padding bars unchanged.

Adds SolidColorRenderElement::from_buffer_at, a sibling of from_buffer
that takes an explicit geometry instead of deriving it from the
buffer's natural size — needed so we can paint multiple sub-rects from
one backing buffer.

Animation path (BorderRenderElement, fullscreen_progress < 1.0) is the
next task and untouched here.
Same fix as the steady-state path (commit 18c1dfb) applied to the
BorderRenderElement animation branch. Each L-shape strip is its own
BorderRenderElement with a per-corner CornerRadius — non-zero only on
tile-outer-corners, zero on inner edges next to the window. The
animated rounded-corner shrink is preserved end-to-end; backdrop
never paints under the window's geometry at any animation frame.

Also includes cargo fmt cleanup (one long-comment wrap in the test
module that fmt --check flagged).
Behavior fix (C1): top/bottom middle strips now own tile-outer corners
when the corresponding L/R bar is absent. Previously the strips
unconditionally emitted CornerRadius::default(), so a letterbox-shaped
window (full width, partial height) would render the four tile-outer
corners sharp during the fullscreen animation — wrong, since at that
point those corners ARE on the top/bottom strips. New routing checks
window_left <= tile_left / window_right >= tile_right per strip and
inherits the relevant corner radii from tile_corner_radius.

Test additions:
- backdrop_clip_rects_window_flush_one_edge_returns_3_strips: the
  3-strip case from the epic spec, previously missing. Asserts
  geometry AND corner radii on right bar + top middle + bottom middle
  for a window flush-left, partial elsewhere.
- backdrop_clip_rects_letterbox_top_bottom_own_all_tile_outer_corners:
  the C1 regression test; full-width partial-height window expects
  both top middle strip's top corners AND bottom middle's bottom
  corners to inherit tile_corner_radius.
- backdrop_clip_rects_window_outside_tile_returns_whole_tile: covers
  the early-return at the top of the helper when window doesn't
  overlap tile at all.

Test cleanup:
- Renamed backdrop_clip_rects_window_flush_left_no_left_bar to
  backdrop_clip_rects_window_flush_top_and_bottom_returns_2_strips
  (the old name was mislabeled per its own apologetic comment).
- Removed backdrop_clip_rects_zero_radius_yields_zero_radii — its
  assertion was already covered inline by
  backdrop_clip_rects_window_centered_returns_4_strips.

Style + perf:
- Vec::with_capacity(4) at the helper's allocation site (per-frame on
  fullscreen tiles; up to 4 strips means cap-0 → cap-4 wastes 1-2
  reallocations per frame).
- 0.0/16.0 → 0./16. in CornerRadius literals, matching the rest of
  tile.rs's preferred f32-literal style.
- `let (geo, _radius)` → `let (geo, _)` in the steady-state branch,
  since CornerRadius::default() is passed and the radius is unused.
@Sempyos Sempyos added area:visuals Animations, shaders, visual artifacts pr kind:feature New features and functionality labels May 10, 2026
@Uzlkav
Copy link
Copy Markdown
Contributor

Uzlkav commented May 11, 2026

If I understand them correctly, the Wayland protocol linked to in the docs you reference say that you should also draw behind the window itself?

If the fullscreened surface is not opaque, the compositor must make sure that other screen content not part of the same surface tree (made up of subsurfaces, popups or similarly coupled surfaces) are not visible below the fullscreened surface.

The fullscreen-backdrop clip diverges from xdg-shell, which says a
non-opaque fullscreen surface must not let other screen content show
through. Per upstream feedback, make this opt-in instead of unconditional:

- Default (rule absent or false): restore upstream behavior — single
  full-tile SolidColor element steady-state, single full-tile Border
  element during the un/fullscreen animation.
- Rule true: the existing 4-strip tile-minus-window clip (steady-state
  SolidColor strips, animation BorderRenderElement strips with per-strip
  corner-radius routing).

The geometry helper backdrop_clip_rects and its 8 tests are unchanged.

niri-config:
- Add `clip-fullscreen-backdrop-to-window` Option<bool> to WindowRule.

src/window/mod.rs:
- Add the same field to ResolvedWindowRules with merge logic mirroring
  clip_to_geometry.

src/layout/tile.rs:
- Read rules.clip_fullscreen_backdrop_to_window and branch in both the
  animation (border-element) and steady-state (solid-color) paths.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joshsymonds added a commit to joshsymonds/niri that referenced this pull request May 11, 2026
- josh/fullscreen-backdrop-clip: now gates behavior on opt-in
  clip-fullscreen-backdrop-to-window window rule (PR niri-wm#4034 review feedback).
- Re-adds the josh/instrument-ffm-debug entry that was dropped when the
  tooling commit was cherry-picked from main during re-derivation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joshsymonds
Copy link
Copy Markdown
Author

@Uzlkav fair point — you're right that the protocol clause covers the non-opaque case too, and a blanket clip diverges from spec. I've updated the PR to make this opt-in: a new clip-fullscreen-backdrop-to-window window rule, off by default. Default behavior matches upstream (single full-tile backdrop, spec-compliant); only windows that explicitly set the rule get the tile-minus-window clip.

The 4-strip decomposition + its 8 geometry tests are unchanged; both render paths (steady-state SolidColor and the un/fullscreen-animation BorderRenderElement) now branch on rules.clip_fullscreen_backdrop_to_window == Some(true).

If people want it they can opt-in; if maintainers absolutely don't want toggles for non-spec Wayland features, no harm no foul, close this PR and I'll just keep it on my local fork. It seems obvious to me that if someone adds alpha to their full screen window they mean it, but maybe that's wrong.

Adds clip-fullscreen-backdrop-to-window true to the existing window-rule
in the parse() snapshot test and asserts it resolves to Some(true).
Mirrors the coverage other Option<bool> rule fields (e.g. open-maximized,
open-focused) get and proves the knuffel plumbing for the new field.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joshsymonds added a commit to joshsymonds/niri that referenced this pull request May 11, 2026
- josh/fullscreen-backdrop-clip: now gates behavior on opt-in
  clip-fullscreen-backdrop-to-window window rule (PR niri-wm#4034 review feedback).
- Re-adds the josh/instrument-ffm-debug entry that gets dropped when the
  tooling commit is cherry-picked from main during re-derivation.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
joshsymonds added a commit to joshsymonds/nix-config that referenced this pull request May 11, 2026
Bumps niri-flake.inputs.niri-unstable to josh/integration@39c2331c, which
adds the new clip-fullscreen-backdrop-to-window window rule (default off,
spec-compliant). Opts kitty in via niri/extras.kdl so a translucent
fullscreen kitty (background_opacity<1.0) composes against the wallpaper
instead of the opaque black backdrop. See niri-wm/niri#4034.

Also pulls niri-flake itself (sodiboo/niri-flake) and the stable nixpkgs
along with it.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@joshsymonds
Copy link
Copy Markdown
Author

Closing this — keeping it on my fork.

@Uzlkav, thanks for the actual review. Your point on the xdg-shell non-opaque clause was right and made the patch better.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:visuals Animations, shaders, visual artifacts pr kind:feature New features and functionality

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants