Commit Graph
1647 Commits
Author SHA1 Message Date
LGUG2Z 699f31f8bc perf(animation): do not update a ghost that has not changed
Asking DWM to put a thumbnail where it already is, as opaque as it already
is, still has it compose the frame again, and an eased animation spends its
first and last frames where it is. Remember where a ghost was last put and
how opaque it was made, and leave it alone when that is asked of it again.
2026-10-30 15:45:37 -07:00
LGUG2Z fd6fb3047b perf(animation): do not wake the ghost thread every 8ms for nothing
The thread that owns the ghost host window looked for messages every 8ms for
as long as komorebi ran once ghost movement was on, about 125 wakeups a second
doing nothing, which keeps a processor from sleeping. Poll at that rate only
while there are ghosts, and every 250ms while there are none: it is woken at
once by a command either way.
2026-10-30 15:45:37 -07:00
LGUG2Z 388a17d682 perf(border): coalesce the rects an animation sends to a border
An animation sent the border of each window it moves a rect for every frame:
it took the global border state lock, which the border manager holds while it
handles a notification, cloned the id of the border, boxed the rect and posted
it, and the border drew every one of them in turn, resizing its render target
and drawing again for each, even when it had fallen behind, so that it lagged
and then caught up in a burst.

Look up the border once, when the ghost is made, and keep the newest rect for
each border where the animation and the border can both get at it: the border
is only told when it has none waiting, and draws the newest when it gets
there. No lock the border manager holds is taken in the middle of an animation
any more, and no box is allocated for each frame.
2026-10-30 15:45:37 -07:00
LGUG2Z 70176ee167 perf(animation): keep the swap chain nudge off the animation thread
Windows that render through a swap chain get a one pixel size change when
they are shown again, which takes as long as the window takes to answer twice
(over a hundred milliseconds for a slow one), and it was done by the
animation thread in the middle of the animation, as a hitch in every move:
drawing a frame took up to 154ms, 45ms for the worst frame of an animation on
average. Do it on the thread that resizes the window, after the window has
been resized and made to paint itself, and while it is hidden; showing the
window is then only uncloaking it. It has to come after rendering the window:
rendering leaves the compositor of a swap chain window as stale as it was, and
the window took over a hundred milliseconds after being shown to show
anything, as a gap of wallpaper where it was meant to be.
2026-10-30 15:45:37 -07:00
LGUG2Z cd1c9ce9ce perf(animation): do not build a picture to make a window paint itself
Making a window paint itself (a render of it is synchronous) went through
Snapshot::capture, which also allocated and filled a second picture of the
visible frame, copied it out of the first and cut its corners, to throw it all
away: about a quarter of what taking a picture costs, on the thread that
gates showing the window again.

Split rendering a window out of taking a picture of it and make paint_now only
render. And when taking a picture, do not run over millions of pixels to make
them opaque when a window has rendered them that way, as it does: look at a
grid of them across the picture, and only fall back to making every pixel
opaque if one of those is not.
2026-10-30 15:45:37 -07:00
LGUG2Z 126acaa074 chore(animation): trace what ghosts and animation frames cost
Time the picture a ghost takes of its window (allocating, PrintWindow and
turning it into a picture), the render a window is asked for to lay itself
out (paint_now), how long a ghost takes to make, and what the frames of a
group of animations cost: how long drawing them takes, how far apart they
come and how many are late. All at trace level, the frame statistics at
debug.
2026-10-30 15:45:37 -07:00
LGUG2Z 0b17aae86b fix(border): leave the border of a ghosted window to the animation
While a ghost stands in for a window the window is hidden and not where the
ghost is, and an animation moves its border along with the ghost. But the
border also follows the real window: every event komorebi handles ends with
a border update that reads the rect of the real window, and so does every
location change of it, and komorebi's own cloak, uncloak and resize of the
window produce both. Each of them put the border where the window is going to
be, or was, for a frame, until the next frame of the animation moved it back:
the border jumped ahead of the window, by as much as the whole width of the
screen when moves overlapped.

Say which windows a ghost stands in for, from when the ghost is made until it
is gone (or the animation is cancelled), and leave the borders of those to the
animation.

Recorded over 16 moves of mixed pace, a border was moved by something other
than its animation 21 times before; on a swap in the right column, 6 times
before (up to 127px off) and none after.
2026-10-30 15:45:37 -07:00
LGUG2Z 94cd37f6d0 feat(animation): show ghosts as a still picture on one shared host window
A ghost was a live thumbnail of its window, so it showed whatever the window
painted: the stretched old frame, then the blank frame a window paints while
it lays itself out again (Explorer shows its toolbar and nothing else for a
few hundred milliseconds), then the new layout, replacing the picture all at
once after the animation was over, and it stayed on screen about 300ms past
the end of the move while it waited for that. Every ghost also had a host
window of its own that was moved with SetWindowPos on every frame through a
channel to a thread that polls for commands every 8ms.

Take a picture of the window with PrintWindow before anything is done to it
(cropped to the visible frame, with the corners rounded) and show it through
a cloaked window of our own that DWM can take a thumbnail of and scale on the
GPU. All ghosts are composed onto one layered, click-through host window that
covers the virtual screen, and is only above other windows while a ghost is
alive so that it does not get in the way of the taskbar's auto-hide:
animating a ghost is one DwmUpdateThumbnailProperties call on the animating
thread, which has no thread affinity. The number of registered thumbnails is
counted and traced so that a leak of compositor handles can be seen.

The real window is cloaked and resized at the start of the animation, on a
thread of its own, and asked to paint itself at its new size straight away
(taking a picture of it makes it render synchronously) while the ghost is on
screen, so that it has the whole animation to lay itself out unseen. At 55%
of the way through, the window is shown again under the ghost (with the one
pixel size change that windows which render through a swap chain need for
their compositor to rebuild its render target) and the ghost fades out over it
while still moving, so the picture dissolves into the window instead of being
replaced after. A window that is slow to take its new size is not waited for:
it is shown once it is ready, or, if the animation ends first, waited for with
the ghost over it and then faded out over a few frames. Windows that are
minimized or gone are not ghosted.
2026-10-30 15:45:37 -07:00
LGUG2Z b50e21e5cd feat(animation): run the movement animations of a layout pass together
Each window that moved in a layout pass was animated on a thread of its own,
each preparing itself, drawing its frames and finishing by itself, so the
windows of a swap could be a little ahead of or behind one another, and a
ghost could appear over its window noticeably later than the ghost of the
window it was swapped with.

Collect the animations started while a workspace is being updated in an
AnimationBatch and run them on a single thread: they are prepared at once, so
that starting takes as long as the slowest of them (a ghost takes a picture of
its window) and not as long as all of them, every one is prepared before the
first frame, every frame of all of them is drawn before the compositor is
asked to present it (waiting for it with DwmFlush when a ghost is being
animated), and they finish and fade out together. Animations cancelled by a
newer one still drop out one at a time, and the wait for a cancelled one to
stop is checked every 4ms instead of every 125ms: it notices once per frame,
so the longer wait only delayed the animation replacing it.
2026-10-30 15:45:37 -07:00
LGUG2Z e3dd3c60de fix(scrolling): refocus after a window is closed with Hide
Closing a window sends Hide and not Destroy to many applications. The Hide
arm re-tiled without asking Windows to focus the surviving window, so no
window had the foreground and the border did not follow focus. Use the
same refocus rule as the Destroy arm.
2026-10-30 15:45:37 -07:00
LGUG2Z ae8e982380 fix(scrolling): show every window when a stack is spread out into a column
Spreading a stack out into a column left two things wrong, both seen in a
recording of toggle-column-stack:

- The window that stays in the original container was never restored. When it
  was the tab at the back it stayed cloaked, and its column was empty until
  focus reached it. Only the windows that moved to new containers were
  restored.
- Each new container took focus when it was inserted, so the strip scrolled a
  column to bring it into view, and did not scroll back once the containers
  were put in the one column again. The strip was left a column to the left,
  with an empty third of the screen. Focus now stays on the column that they
  are put in.

Test both, with windows that are hidden as stacking hides them.
2026-10-30 15:45:37 -07:00
LGUG2Z 5daca12e28 feat(scrolling): support more than one monitor
Remove the single-monitor restrictions on the Scrolling layout. Each
workspace keeps a strip of its own, and a window is seen on whichever monitor
it overlaps, so placement now looks at the other monitors: a window that
scrolled out of view past an edge that another monitor is next to is hidden
instead of parked, and a partly visible one is cut down to its own work area.

A window that comes back into view is shown again first, but only the one at
the front of its container: the others are the tabs at the back of a stack,
which are hidden because they are at the back.
2026-10-30 15:45:37 -07:00
LGUG2Z b8a444df5c docs(scrolling): describe the scrolling layout 2026-10-30 15:45:37 -07:00
LGUG2Z e8364f7b5b feat(scrolling): rest windows that scroll out of view at the edge
A window that scrolls out of view comes to rest at the edge of the work area
that it left by, with 5 pixels of it still on screen, keeping its size so that
nothing is resized to bring it back and an animation back into view is short.
Reuses the existing movement animations.

Also test the Workspace glue: widths, consume, expel, moving a column, and
that retile forgets the widths that windows were found to refuse.
2026-10-30 15:45:37 -07:00
LGUG2Z f170e8e2ac feat(scrolling): make room for windows that will not get as narrow
After windows are placed, ask each how wide it really is. A window that is
wider than its column was made to be widens the column to fit, and the columns
after it move along. A column never gets narrower again because of this, and
never wider than the screen.

What a window says about its size is only used once it can be trusted: with
synchronous window handling and no animation in flight, and for an overhang
of more than 16px, so that rounding, a terminal's cell grid, or a window that
is still moving does not widen a column for good. Adopting a width is logged
at info level.
2026-10-30 15:45:37 -07:00
LGUG2Z 7c79625ba7 docs(schema): regenerate the schemas for the scrolling layout
schema.json gets the center_focused_column layout option as a boolean or one
of Never, Always and OnOverflow, and schema.bar.json, which embeds the socket
messages, gets the new scrolling commands.
2026-10-30 15:45:37 -07:00
LGUG2Z 598390d9cb feat(scrolling): add commands and config for the scrolling layout
Focus, move, resize, retile and closing a window now go through the strip of
columns in the Scrolling layout. New commands: consume, expel,
toggle-column-stack, scrolling-layout-full-width and center-focus. A top
level scrolling section configures centering, centring a lone column, and
the minimum height of a window in a column of several.
2026-10-30 15:45:37 -07:00
LGUG2Z 8c7327fd31 feat(layouts): share the platform-free half of the scrolling glue
komorebi-layouts now carries what a host needs of the Scrolling layout
without touching an operating system: the scrolling config section and its
checked settings, the layout options for the model, placement of containers
in a work area, resizing a column or tile, and the hand-over of column
records between workspaces. It re-exports the model as
komorebi_layouts::scrolling::model, so komorebi-for-mac can pick all of it up
through its existing komorebi-layouts git dependency.

Also add the Workspace methods for columns, consume/expel/stack, focus and
ordering, which sit on top of it.
2026-10-30 15:45:37 -07:00
LGUG2Z 3b93035d48 feat(scrolling): lay out the scrolling layout with the scrolling model
The Scrolling layout now keeps a ScrollingSpace per workspace, keyed by
container id, instead of recomputing equal-width columns from the previous
layout on every update. Column widths follow layout_options.scrolling.columns
and persist as windows come and go.
2026-10-30 15:45:37 -07:00
LGUG2Z 6c4eb621d3 feat(scrolling): add a platform-free model of a scrollable strip of columns
Ported from komorebi-for-mac (feature/moar-scrolling). The crate has no
dependencies and no knowledge of windows; it is not used by komorebi yet.
2026-10-30 15:45:37 -07:00
LGUG2Z a4e03e3d76 test(resize): cover the top edge of the lower tile of a BSP layout
Dragging the edge two stacked tiles share moves it by the same amount from
either tile, in the layout and in the window manager. A drag of the top edge
of the lower tile that moves nothing is not for want of this.
2026-10-30 15:45:37 -07:00
LGUG2Z dc9b2e9539 fix(window): uncloak the windows a killed daemon left cloaked
A daemon that is killed does not get to uncloak the windows it hid, and the
next one skips them as cloaked, so they stay out of sight and unmanaged.

A cloak by komorebi reads the same as one for another virtual desktop or a
suspended app, so keep a record of the windows that are cloaked and uncloak
those that the last daemon left behind, before looking for windows.
2026-10-30 15:45:37 -07:00
LGUG2Z 0b57006053 chore(deps): update libz-sys to 1.1.31 for RUSTSEC-2026-0418
libz-sys before 1.1.31 logs the compiler command of its zlib probe, which
with cc 1.6.0 or later includes every environment variable of the build,
and cargo stores that output under target/. It is a build dependency of
shadow-rs via git2 and libgit2-sys; bump it in the lockfile.
2026-10-30 12:13:42 -07:00
LGUG2Z 5c8e4770dc fix(monitor): do not mistake an unplugged display for one that is still attached
Displays were matched on serial_number_id.eq(..) || device_id.eq(..), so
two displays with no serial number matched each other through None == None,
and two with no device path through the shared "UNKNOWN" device id. When
a display without either was unplugged, the one that stayed attached
"matched" it, nothing was removed, and every display change started the
3s verification again; state kept listing the display for good.

A serial number now identifies a display only when both have one and it is
not empty, and a display without a device path is told apart by its name.
The removal takes the monitors out by index instead of by id, so that two
displays sharing the id UNKNOWN are not removed together, and adding a
connected monitor no longer skips a display for sharing that id.
2026-10-30 12:13:42 -07:00
LGUG2Z 3b424dd707 fix(config): accept an integer for an initial window placement rule
PlacementTarget deserialized through String, so the integer the docs
promise ("initial_window_placement_rules": 2) made the daemon exit at
start with an untagged enum error while "2" worked. Accept integers as
well as strings.

The derived schema described an externally tagged object that neither
form matches, so describe the type by hand and regenerate both schemas.
2026-10-30 12:13:42 -07:00
LGUG2Z 837ca257d4 fix(monitor): validate the target before moving a container or workspace
move_container_to_monitor and move_workspace_to_monitor took the container
or workspace out of its place and only then looked the target monitor up,
so a monitor index that does not exist left the window on screen but in no
workspace. Check the index first so the command fails and nothing moves.
2026-10-30 12:13:42 -07:00
LGUG2Z 9b9cad997c fix(rules): keep a rule whose id another kind of rule already has
The socket handlers for manage, ignore, object-name-change, tray, layered
and title bar rules skipped a new rule when any simple rule had the same
id, whatever its kind, so ignore-rule exe foo after ignore-rule class foo
was dropped. Share one helper that compares the kind as well.
2026-10-30 12:13:42 -07:00
LGUG2Z a599e9ecc1 chore: keep the line endings LF
Everything but dependencies.json is stored with LF, which nothing said, so
a CRLF file could get in unnoticed (it did, for two files, in a round trip
that shows as a rewrite of every line of both).

* .gitattributes: normalize every text file to LF in the repository and
  check it out with LF, whatever core.autocrlf is (the Windows runner
  defaults it to true)
* rustfmt.toml: newline_style = "Unix", so cargo fmt --check fails on
  a Rust file with CRLF
* CI: fail when a file is stored with CRLF
* dependencies.json: the one file that was stored with CRLF
2026-10-30 12:13:42 -07:00
LGUG2Z 21a821aca0 fix(nix): get the build working again 2026-10-28 19:30:00 -07:00
LGUG2Z efe91ce332 chore(schema): regenerate the schemas 2026-10-28 15:18:11 -07:00
LGUG2Z f08aa55511 chore(deps): cargo update 2026-10-27 09:59:57 -07:00
LGUG2Z a3fac99a0f fix(wm): make resizing a tile by dragging its edge reliable
Measure mouse resizes from the rect the window had when the drag
started, work in edges instead of guessing from rect fields, resize the
container of the dragged window, and bound resizes by the resulting tile
sizes (granting as much as fits) rather than by the size of the
container being resized.
2026-10-27 09:59:57 -07:00
LGUG2Z e0709f02bf chore(deps): cargo update 2026-08-22 14:26:23 -07:00
Csaba 83c4e11d99 feat(wm): add initial_window_placement_rules for workspace-level
Adds a new initial_window_placement_rules workspace configuration option
that allows users to control where new tiled windows are inserted in the
container list. Supports placement by named strategy (Primary,
Secondary, BeforeFocused, AfterFocused, Last), fixed container index, or
per-application matching rules with OR/AND logic.
2026-08-22 13:52:38 -07:00
Ransomwave 4ca2ff84dc feat(bar): add default interface options to display friendly name or local IP
Adds a new configuration to the Network widget from komorebi-bar, which
allows you to toggle between viewing your Default Interface's friendly
name, IPv4 or IPv6.

Motivation: I find it very useful to check your local IP at a glance,
and missed the feature heavily from other wms in Linux.
2026-08-22 13:52:01 -07:00
Dylan Anderson 3f3fac8b65 chore(stackbar): tighten deadlock fix comment 2026-08-02 20:24:18 -07:00
Dylan Anderson 5a534eb2ac fix(stackbar): drop containers lock before repositioning stackbar
Stackbar::update held STACKBARS_CONTAINERS across the synchronous
cross-thread SetWindowPos in position_window, while the stackbar wndproc
acquires the same lock on WM_LBUTTONDOWN. A tab click racing an update
storm leaves the wndproc blocked on the lock and the updater blocked in
SetWindowPos waiting on the wndproc's message pump, deadlocking the
whole process. Scope the guard to the insert so the lock is released
before SetWindowPos.

Fixes #1734
2026-08-02 20:24:18 -07:00
dependabot[bot] 1b263b9496 chore(deps): bump shadow-rs from 1.7.1 to 2.0.0
Bumps [shadow-rs](https://github.com/baoyachi/shadow-rs) from 1.7.1 to 2.0.0.
- [Release notes](https://github.com/baoyachi/shadow-rs/releases)
- [Commits](https://github.com/baoyachi/shadow-rs/compare/v1.7.1...v2.0.0)

---
updated-dependencies:
- dependency-name: shadow-rs
  dependency-version: 2.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 16:22:35 -07:00
dependabot[bot] e4e1b6da18 chore(deps): bump sysinfo from 0.38.4 to 0.39.6
Bumps [sysinfo](https://github.com/GuillaumeGomez/sysinfo) from 0.38.4 to 0.39.6.
- [Changelog](https://github.com/GuillaumeGomez/sysinfo/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GuillaumeGomez/sysinfo/compare/v0.38.4...v0.39.6)

---
updated-dependencies:
- dependency-name: sysinfo
  dependency-version: 0.39.5
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-08-02 16:22:20 -07:00
Matt Kotsenas a10b8e27c1 fix(wm): honor layout_flip when focusing across a boundary
Cross-boundary focus arrival picked the structural leftmost/rightmost
container, ignoring layout_flip. Since layout_flip mirrors geometry
without reordering containers, focusing into a horizontally-flipped
workspace landed on the far edge instead of the container at the edge
the user crossed toward, while intra-workspace focus (via
OperationDirection::destination) already accounts for the flip.

Extract OperationDirection::cross_boundary_edge_index, which picks the
edge index against the flipped direction like destination() does, and
use it at the eight focus-arrival call sites. Add a komorebi-layouts
test that checks the pick against the rendered rectangles.
2026-07-22 14:15:07 -07:00
Matt Kotsenas 8a6868f2d2 fix(wm): refocus selection on destroy when foreground goes null
Reactive events pass trigger_focus=false so komorebi follows the OS
foreground and reconciles via the FocusChange handler once Windows
promotes a successor window. When the destroyed window was the
foreground and Windows promotes nothing, the foreground becomes null,
no FocusChange arrives, and komorebi's selection never regains OS focus,
leaving every border unfocused until the user refocuses by hand.

Add a narrow fallback in the Destroy/Unmanage handler: when the OS
foreground is null after removal, focus the current selection. Any
non-null foreground is left to FocusChange, so the OS is not fought and
the should_skip_focus_change desync cannot occur.
2026-07-22 14:14:56 -07:00
LGUG2Z fd3d52c477 chore(deps): cargo update 2026-07-21 10:53:14 -07:00
LGUG2Z d29069398b chore(cargo): address clippy warnings 2026-07-21 10:50:26 -07:00
dependabot[bot] ccb39abd2e chore(deps): bump starship-battery from 0.10.3 to 0.11.0
Bumps [starship-battery](https://github.com/starship/rust-battery) from 0.10.3 to 0.11.0.
- [Release notes](https://github.com/starship/rust-battery/releases)
- [Changelog](https://github.com/starship/rust-battery/blob/main/CHANGELOG.md)
- [Commits](https://github.com/starship/rust-battery/compare/v0.10.3...v0.11.0)

---
updated-dependencies:
- dependency-name: starship-battery
  dependency-version: 0.11.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 16:12:27 -07:00
dependabot[bot] f22fb8086c chore(deps): bump nanoid from 0.4.0 to 0.5.0
Bumps [nanoid](https://github.com/mrdimidium/nanoid) from 0.4.0 to 0.5.0.
- [Changelog](https://github.com/mrdimidium/nanoid/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mrdimidium/nanoid/compare/v0.4.0...v0.5.0)

---
updated-dependencies:
- dependency-name: nanoid
  dependency-version: 0.5.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2026-06-25 16:11:37 -07:00
LGUG2Z 538053c9c8 chore(deps): cargo update 2026-06-24 14:58:47 -07:00
ChinhLee 57b93a7404 fix(wm): verify foreground state before mutating focus state
As a defense-in-depth fix independent of the event-conversion layer, the
WindowManagerEvent::FocusChange handler should not update the focused
monitor/workspace/container/window state when the incoming window is not
the actual OS foreground window.

The issue notes that after a monitor switch komorebi focuses "the last
window focused from monitor 1" while the popout window on the other
monitor still receives input.

This is exactly the symptom of komorebi processing a stale/background
focus event and re-pointing its selection at monitor 1 even though real
focus is elsewhere. Verifying foreground state before mutating focus
state prevents the desync regardless of which win-event produced the
FocusChange.

fix #1679

Signed-off-by: ChinhLee <76194645+chinhkrb113@users.noreply.github.com>
2026-06-24 14:55:43 -07:00
LGUG2Z a3b2e28dd5 feat(animations): set the ghost_movement default to false
There are a bunch of edge cases coming up with this behaviour. Setting
the default to false for now so people on the nightly can opt in as they
feel comfortable.
2026-05-04 19:09:28 -07:00
LGUG2Z 4e7d0e337c feat(wm): add monocle_focus_behaviour to gate monocle cycling
5f629e1f made directional focus cycle the monocle container in the
underlying ring. Useful on a single monitor, but undesired when focusing
across monitors — the focus stays trapped on the monocle workspace
instead of crossing the boundary.

This commit adds a new top-level monocle_focus_behaviour option with two
variants, Cycle (the post-5f629e1f behaviour) and NoOp (the pre-5f629e1f
behaviour, now the default), along with a ToggleMonocleFocusBehaviour
socket message and corresponding komorebic subcommands to flip between
them at runtime.
2026-05-04 19:09:27 -07:00
LGUG2Z e2e5dbfcae feat(wm): use ghost windows for movement animations
This commit tries to render move/resize animations on a DWM-thumbnail
"ghost" window instead of calling MoveWindow per-frame on the real HWND.

The source is cloaked via IApplicationView::SetCloak, the thumbnail is
animated via DwmUpdateThumbnailProperties on a layered host owned by a
single "ghost owner" thread, the border for the source follows the
lerped rect via a new WM_ANIMATE_RECT message handled on the border's
own WndProc thread (preserving today's per-frame border tracking), and
the real SetWindowPos happens once at the end of the animation.

Apps repaint exactly once per animation instead of N times, which is a
substantial win for heavy renderers (browsers, IDEs, Office). For
non-Chromium sources the source is also pre-positioned to target_rect
before the thumbnail is registered so the captured texture is target-
sized and downscales to native 1:1 at the end of the animation rather
than upscaling to a stretched/blurry final frame.

Chromium-shell sources  skip the pre-paint step: their
NativeWindowOcclusionTrackerWin reads DWMWA_CLOAKED and treats any cloak
value as hidden, suspending the renderer; WM_SIZE while cloaked produces
no new frame and the post-uncloak swap chain shows stale or black
content.

For those apps we keep the source cloaked at start_rect for the whole
animation and do the SetWindowPos in post_render after uncloak, where
the visibility flip is what wakes Viz back up.

A short ease-in opacity crossfade in post_render masks the texture
transition for the Chromium path and gives slow renderers time to
present their first post-resize frame before the overlay is removed.
2026-05-03 16:09:28 -07:00