Skip to content

overview: fix chip positioning, stale geometry, and broken bindings#188

Open
kanikamaxxing wants to merge 3 commits into
Axenide:mainfrom
kanikamaxxing:fix/multi-monitor-overview-asymmetric
Open

overview: fix chip positioning, stale geometry, and broken bindings#188
kanikamaxxing wants to merge 3 commits into
Axenide:mainfrom
kanikamaxxing:fix/multi-monitor-overview-asymmetric

Conversation

@kanikamaxxing

@kanikamaxxing kanikamaxxing commented May 23, 2026

Copy link
Copy Markdown

Summary

Three related fixes for the overview's window-thumbnail rendering. Each one is independent of the others but they all surface as "chips look wrong until you close + reopen the overview", so grouping them.

1. Render windows correctly on non-origin monitors (original commit)

On a multi-monitor setup where any monitor is positioned at a non-zero virtual-desktop offset, opening the overview on that monitor renders every window thumbnail outside its workspace cell — windows render at their global x position scaled down instead of at their monitor-local position.

Root cause: AxctlService.qml's monitor mapping discards mon.metadata.x, mon.metadata.y, and mon.metadata.transform, so monitorData.x is undefined for every monitor. OverviewWindow.qml's let base = windowData.at[0] - monitorData.x falls back to 0, which only works for a monitor at the origin.

Also drops the per-monitor window filter in Overview.qml so each overview shows windows from every monitor, looks up each window's own monitor for position math, and computes per-window scale as cell_width / window_monitor_width so cross-monitor windows fit their cell properly. After movetoworkspacesilent on drop, also dispatches moveworkspacetomonitor <ws> <drop_monitor> so windows dropped on monitor A's overview land on monitor A regardless of which monitor the workspace was previously bound to.

2. Refresh windowList from hyprctl on every axctl event

CompositorData.updateWindowList() was a no-op, relying on the inline state piggybacked onto every axctl subscribe event. That state is stale for window geometry: when a dispatch like movetoworkspacesilent causes Hyprland to re-tile, the resulting per-window resizes do not get reflected in the axctl daemon's cached state until something else (e.g. Event.WorkspaceChanged) triggers a refresh.

Visible symptom: drag a chip from cell A to cell B — the dispatch happens correctly, the real windows re-tile on the desktop, but every chip keeps showing the pre-move size, leaving white space around each window thumbnail. Closing and reopening the overview fixes it; so does switching monitors (the focus change emits a workspace event which finally flushes the daemon's state). hyprctl clients -j returns the correct sizes the whole time, axctl window list does not.

Wire updateWindowList() to actually fetch hyprctl clients -j, mapped to the same record shape AxctlService.applyState produces so other consumers are unaffected. Axctl events are still used as the "something changed" trigger (they fire reliably even when their inline state is stale), but the authoritative geometry comes from hyprctl now. Debounced at 60ms so a burst of events doesn't fork many subprocesses.

3. Restore Qt.binding after drag, gate animations until ready

Two cleanup follow-ups exposed by #2:

  • After a drag/click on a chip, onReleased did root.x = root.initX (and the same for y) to snap the chip back into place. In QML, a plain assignment destroys the property binding — so x and y stopped tracking initX/initY forever after, and subsequent windowData updates couldn't move the chip. The MouseArea's drag.target already breaks the binding during drag, so a clean release path has to restore it. Re-bind via Qt.binding(() => root.initX) in all three onReleased branches and the same in ScrollingWorkspace's drop handler.

  • Every chip rebuild (which happens on each axctl event because filteredWindowData returns a fresh array of fresh objects, so the Repeater destroys + recreates delegates) starts with Item-default x/y/width/height = 0, then the bindings kick in and Behavior on each animates 0 -> target over animDuration. With axctl-driven state churn, chips never finish settling — they perpetually look half-sized with white borders around them. Gate the Behaviors behind a ready flag flipped true in Component.onCompleted so the initial assignment skips the animation; subsequent in-place updates still animate.

Test plan

  • Dual-monitor setup (DP-1 2560×1440 at x=1920, DP-2 1920×1080 at x=0). Overview on DP-1 — windows render inside their cells, matching DP-2's already-correct behaviour.
  • Cross-monitor visibility: windows on DP-2 also show in DP-1's overview (and vice versa), each at correct per-monitor scale.
  • Drag a window in DP-1's overview, drop onto a workspace cell — window lands on DP-1's monitor.
  • Drag a window from a cell holding two windows to an empty cell on the other screen — both source and target chips immediately reflect their new sizes (source grows to full cell, target chip resizes to the new monitor's geometry). No close+reopen required.
  • Repeat in reverse (drag into a cell that already has a window) — incoming chip and existing chip both resize correctly to reflect the new split layout.

Happy to iterate if anything here should land differently. Thanks!

— robin

kanikamaxxing and others added 3 commits May 23, 2026 22:05
The mapped monitor object in AxctlService discards mon.metadata.x/y/transform
so monitorData.x is undefined for every monitor. OverviewWindow's position math
falls back to 0, which works for the monitor at the origin but renders windows
on any other monitor outside the thumbnail bounds (the global window x is used
without the monitor offset subtracted).

Also drops the per-monitor window filter in Overview so each overview shows all
windows across monitors, and computes per-window scale from the window's own
monitor width so cross-monitor windows fit the cell properly instead of being
sized for the overview's monitor.

OverviewWindow now also pins the target workspace to the drop monitor after
movetoworkspacesilent, so drops on cells in monitor A's overview actually land
on monitor A regardless of where the workspace currently lives. And imports
qs.modules.bar.workspaces so the existing CompositorData reference on drag
finalize stops throwing ReferenceError.

Repro for the primary bug: dual-monitor setup with monitors at non-zero offsets.
The overview on the second monitor renders all window thumbnails outside their
cell because monitorData.x is undefined and global x positions are used as-is.
CompositorData.updateWindowList() was a no-op, relying on the inline state
piggybacked onto every axctl subscribe event. That state is stale for window
geometry: when a dispatch like movetoworkspacesilent causes Hyprland to
re-tile, the resulting per-window resizes do not get reflected in the axctl
daemon's cached state until something else (e.g. Event.WorkspaceChanged)
triggers a refresh.

The visible symptom in the overview: drag a chip from cell A to cell B and
the dispatch happens correctly — the real windows re-tile on the desktop —
but every chip keeps showing the pre-move size, leaving white space around
each window thumbnail. Closing and reopening the overview fixes it; so does
switching monitors (the focus change emits a workspace event which finally
flushes the daemon's state). hyprctl clients -j returns the correct sizes
the whole time, axctl window list does not.

Wire updateWindowList() to actually fetch hyprctl clients -j, mapped to the
same record shape AxctlService.applyState produces so other consumers are
unaffected. Axctl events are still used as the "something changed" trigger
(they fire reliably even when their inline state is stale), but the
authoritative geometry comes from hyprctl now. Debounced at 60ms so a
burst of events doesn't fork many subprocesses.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Two related chip-update papercuts on top of the hyprctl refresh fix:

1. After a drag/click on a chip, the onReleased handler did
   root.x = root.initX and root.y = root.initY to snap the chip back into
   place. In QML, a plain assignment destroys the property binding — so x
   and y stopped tracking initX/initY forever after, and subsequent
   windowData updates couldn't move the chip. The MouseArea's drag.target
   already breaks the binding during drag, so a clean release path has to
   restore it. Re-bind via Qt.binding(() => root.initX) in all three
   onReleased branches (cross-workspace move, floating reposition, plain
   reset) and the same in ScrollingWorkspace's drop handler (bound to
   baseX/baseY, since initX there is a press-time snapshot rather than a
   reactive binding).

2. Every chip rebuild (which happens on each axctl event because
   filteredWindowData returns a fresh array of fresh objects, so the
   Repeater destroys + recreates delegates) starts with Item-default
   x/y/width/height = 0, then the bindings kick in and Behavior on each
   animates 0 -> target over animDuration. With axctl-driven state churn,
   chips never finished settling — they perpetually looked half-sized
   with white borders around them. Gate the Behaviors behind a `ready`
   flag flipped true in Component.onCompleted so the initial assignment
   skips the animation; subsequent in-place updates still animate.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@kanikamaxxing kanikamaxxing changed the title overview: render windows correctly on non-origin monitors overview: fix chip positioning, stale geometry, and broken bindings Jun 2, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant