popout: let open animation play on first fit

resize() and resize_and_move() gain an animate flag — when True, skip
the no_anim setprop so Hyprland's windowsIn/popin animation plays
through. Popout passes animate=_first_fit_pending so the first fit
after open animates; subsequent navigation fits still suppress anim
to avoid resize flicker.

behavior change: popout now animates in on open instead of snapping.
This commit is contained in:
pax 2026-04-13 21:49:35 -05:00
parent 9713794633
commit e004add28f
3 changed files with 13 additions and 7 deletions

View File

@ -89,7 +89,9 @@ windowrule {
popout geometry popout geometry
- `dispatch togglefloating` on the main window at launch - `dispatch togglefloating` on the main window at launch
- `dispatch setprop address:<addr> no_anim 1` applied during popout - `dispatch setprop address:<addr> no_anim 1` applied during popout
transitions transitions (skipped on the first fit after open so Hyprland's
`windowsIn` / `popin` animation can play — subsequent navigation
fits still suppress anim to avoid resize flicker)
- The startup "prime" sequence that warms Hyprland's per-window - The startup "prime" sequence that warms Hyprland's per-window
floating cache floating cache

View File

@ -54,7 +54,7 @@ def get_window(window_title: str) -> dict | None:
return None return None
def resize(window_title: str, w: int, h: int) -> None: def resize(window_title: str, w: int, h: int, animate: bool = False) -> None:
"""Ask Hyprland to resize the popout and lock its aspect ratio. """Ask Hyprland to resize the popout and lock its aspect ratio.
No-op on non-Hyprland systems. Tiled windows skip the resize No-op on non-Hyprland systems. Tiled windows skip the resize
@ -86,12 +86,12 @@ def resize(window_title: str, w: int, h: int) -> None:
if not win.get("floating"): if not win.get("floating"):
# Tiled — don't resize (fights the layout). Optionally set # Tiled — don't resize (fights the layout). Optionally set
# aspect lock and no_anim depending on the env vars. # aspect lock and no_anim depending on the env vars.
if rules_on: if rules_on and not animate:
cmds.append(f"dispatch setprop address:{addr} no_anim 1") cmds.append(f"dispatch setprop address:{addr} no_anim 1")
if aspect_on: if aspect_on:
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 1") cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 1")
else: else:
if rules_on: if rules_on and not animate:
cmds.append(f"dispatch setprop address:{addr} no_anim 1") cmds.append(f"dispatch setprop address:{addr} no_anim 1")
if aspect_on: if aspect_on:
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0") cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0")
@ -111,6 +111,7 @@ def resize_and_move(
x: int, x: int,
y: int, y: int,
win: dict | None = None, win: dict | None = None,
animate: bool = False,
) -> None: ) -> None:
"""Atomically resize and move the popout via a single hyprctl batch. """Atomically resize and move the popout via a single hyprctl batch.
@ -140,7 +141,7 @@ def resize_and_move(
if not addr: if not addr:
return return
cmds: list[str] = [] cmds: list[str] = []
if rules_on: if rules_on and not animate:
cmds.append(f"dispatch setprop address:{addr} no_anim 1") cmds.append(f"dispatch setprop address:{addr} no_anim 1")
if aspect_on: if aspect_on:
cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0") cmds.append(f"dispatch setprop address:{addr} keep_aspect_ratio 0")

View File

@ -1325,7 +1325,7 @@ class FullscreenPreview(QMainWindow):
else: else:
floating = None floating = None
if floating is False: if floating is False:
hyprland.resize(self.windowTitle(), 0, 0) # tiled: just set keep_aspect_ratio hyprland.resize(self.windowTitle(), 0, 0, animate=self._first_fit_pending) # tiled: just set keep_aspect_ratio
self._tiled_pending_content = (content_w, content_h) self._tiled_pending_content = (content_w, content_h)
return return
self._tiled_pending_content = None self._tiled_pending_content = None
@ -1373,7 +1373,10 @@ class FullscreenPreview(QMainWindow):
# Hyprland: hyprctl is the sole authority. Calling self.resize() # Hyprland: hyprctl is the sole authority. Calling self.resize()
# here would race with the batch below and produce visible flashing # here would race with the batch below and produce visible flashing
# when the window also has to move. # when the window also has to move.
hyprland.resize_and_move(self.windowTitle(), w, h, x, y, win=win) hyprland.resize_and_move(
self.windowTitle(), w, h, x, y, win=win,
animate=self._first_fit_pending,
)
else: else:
# Non-Hyprland fallback: Qt drives geometry directly. Use # Non-Hyprland fallback: Qt drives geometry directly. Use
# setGeometry with the computed top-left rather than resize() # setGeometry with the computed top-left rather than resize()