Continue the background wipe across every output - #10677
Draft
sven-strothoff wants to merge 4 commits into
Draft
Conversation
maybeStartReveal required revealProgress to still be 0, but each output decodes its own copy of the incoming image and finishes at a different time. The first output to decode called startReveal, which starts the animation and moves revealProgress off 0, so every other output failed the guard and never set maskReady. Its incomingLayer stayed invisible until revealProgress reached 1, leaving it on the old wallpaper for the length of the transition and then jumping straight to the new one. Drop the revealProgress condition. startReveal already restarts the animation only once per backgroundVersion, so a late output sets its own mask and picks the front up where it is.
The reveal mask was a visible: false item captured through its own layer. An item outside the render tree can change its geometry without dirtying the window, so growing the mask shape scheduled no frame on an output whose scene was otherwise static. That output kept the old wallpaper for the whole transition and jumped to the new one when the reveal finished, while an output that happened to be repainting for another reason showed the wipe. Capture the mask through a ShaderEffectSource with hideSource instead. The mask stays live in the tree, so its geometry changes schedule frames on every output, and hideSource keeps it off the screen.
The wipe opened at the centre of each output independently, so a multi-output layout showed several unrelated fronts starting at once instead of one gesture crossing the desk. Treat the layout as one coordinate space. The front starts at the centre of the output the change was made on and continues onto the others at the position and vertical offset Hyprland gives them, so a stacked or offset layout, or one with a gap between outputs, is followed rather than ignored. The mask geometry reduces to the previous centre-out wipe on the origin output. Scale the duration by how much further the front has to travel to clear the whole layout, capped at 900ms, so the edge keeps roughly the speed it had on a single screen rather than racing to cover every output in 420ms.
The wipe across the whole output layout is the right default, but it is a visible change to a default, and a very wide layout or a slow machine may want something else. Read two keys the way the idle service reads its own: background.transition selects "layout" (one front across every output) or "output" (a wipe per output, each from its own centre), and background.transitionDuration pins the reveal in milliseconds instead of deriving it from the layout. Unset or unrecognised values keep the defaults, and "output" reproduces the previous per-output geometry exactly.
Author
|
Raised as a suggestion for discussion too, per |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The wipe is the thing that made me stop and look the first time I changed a
theme on Omarchy. It isn't a crossfade and it isn't a swap — it's a tilted
front that opens out of the middle of the screen and pushes the old
wallpaper off both edges. It reads as one deliberate gesture, and it makes
changing a theme feel like an event rather than a settings change.
On a single screen it is perfect. On two, the illusion breaks: each output
opens its own front at its own centre, so instead of one gesture you get
two unrelated ones firing simultaneously, and the effect collapses back
into "the wallpaper changed."
This makes the layout the stage instead of the individual output. The front
starts at the centre of the output you were working on when you changed the
wallpaper or theme, and keeps going onto the others — arriving from
whichever edge faces the output you started on, at the vertical offset your
layout implies. What was two competing animations becomes one front
crossing the desk, which is what the effect was always reaching for. It's a
small change to what the mask is measured against and the whole thing
scales up to however many screens you own.
Three things worth calling out about how it behaves:
It starts where you are. The origin is the output Hyprland has focused
when the change is made, snapshotted at the start of the transition so
moving focus mid-wipe can't drag it. Change your theme on the laptop and
the front opens there and travels out to the external; do it on the
external and it goes the other way. The wipe begins where your attention
already is, which is the whole reason it works on one screen.
It honours your physical layout. All of it is done in Hyprland's own
layout coordinates, so a stacked, offset, mismatched-height or
mismatched-resolution arrangement is followed rather than approximated —
including a gap, where the front spends a moment crossing the space
between two outputs, exactly as it would if the desk were one surface. The
flip side is the obvious one: it is only as right as your layout is. If
your outputs are not arranged in
monitors.luathe way they physicallysit on your desk, the front will cross them in the order the compositor
believes in, not the order you see. That is arguably a feature — it gives
you an immediate, visible reason to fix your layout.
It preserves the original on the output it starts from. The mask
geometry reduces algebraically to the existing single-screen formula for
the origin output, so nothing about the effect you already know changes on
the screen you triggered it from. The other outputs are the continuation
that was previously missing. Single-monitor setups are unaffected.
Configuration
Both the behaviour and the timing are configurable, read the same way the
idle service reads its own keys:
background.transitionlayout,outputlayoutbackground.transitionDurationoutputrestores the previous per-output wipe, and reproduces its geometryexactly rather than approximating it. Unset or unrecognised values fall
back to the defaults.
I'd argue for the defaults as they stand.
layoutis the point of thechange, and the per-output behaviour is the thing this fixes rather than a
taste people are likely to hold. The derived duration matters more:
covering a much longer path in the original 420ms makes the front move
several times faster than it does today, which on my layout turned the
wipe into something closer to a flicker. So the duration scales with how
much further the front actually has to travel, keeping the edge at roughly
the speed it has on one screen.
That scaling is capped at 900ms, and the cap is the one number here I hold
loosely. Constant edge velocity on my two outputs wants about 1180ms,
which felt long enough to notice as waiting; 900ms keeps it a gesture. On
a three- or four-output wall the cap will bind harder and the front will
move faster than it does on one screen — a deliberate trade against a
transition that overstays. Happy to change either number, or drop the cap
and let velocity stay constant, if you'd rather.
Verification
The geometry is checked against three invariants, over eight layouts
(aligned, offset, gapped, stacked, secondary-on-the-left, negative
coordinates, three outputs with mixed offsets, and a portrait secondary),
for every choice of origin in each:
screen.xequals the global front at that y, exactlyrevealProgress == 1the mask covers every output completely,densely sampled down each output's height rather than only at corners
All hold. I also compared the numbers the running shell computes against an
independent implementation of the same formulas, for both origin choices on
my hardware —
originX/originY, per-outputdx,originDy,centerTop,centerBottom,reachanddurationall match to floating point.Configuration was exercised end to end through
shell.json: default,transition=output, a pinned duration, both together, and deliberatelyinvalid values.
transition=outputreproduces the original geometryexactly (
centerTop 1335.0,centerBottom 1065.0,reach 1339on myinternal panel, which is what the pre-change expression gives).
Two limits on that, stated plainly. My machine runs 4.0.2, whose shell
predates
Ui/BackgroundMedia.qml, soquattro'sBackground.qmlwill notload here — I verified against the 4.0.2 copy with the same edits applied,
as in #10660. And
hyprmoncfgdowns my display layout and reapplies itover
monitors.lua, so I could not rearrange my outputs to test thealignments on real hardware; the eight layouts above are verified at the
formula level, and only one real two-output arrangement (side by side,
748px vertical offset, different heights and resolutions) was exercised
live, in both origin directions.
./test/allfails the same 5 of 234 files asquattrodoes on thismachine with the patch reverted, and no others:
config-test.sh,snapper-test.shandunowned-system-paths-test.shwant anomarchy-pkgscheckout I don't have,
locate-test.shdies in aUnicodeDecodeErrorinits own helper, and
runtime-smoke-test.shtrips #9975's per-screenIpcHandler assertion.
config-test.shoutput is byte-identical to thebase-commit run, so the new
background.*keys don't disturb it.Setup: Hyprland, eDP-1 at 0,748 (2400x1500 logical) and DP-12 at 2400,0
(2400x1350 logical), both at scale 1.6.