popout/state: implement persistent viewport + drift events
Three event handlers, all updating state.viewport from rect data: WindowMoved (Qt moveEvent, non-Hyprland only): Move-only update — preserve existing long_side, recompute center. Moves don't change size, so the viewport's "how big does the user want it" intent stays put while its "where does the user want it" intent updates. WindowResized (Qt resizeEvent, non-Hyprland only): Full rebuild — long_side becomes new max(w, h), center becomes the rect center. Resizes change both intents. HyprlandDriftDetected (adapter, fit-time hyprctl drift check): Full rebuild from rect. This is the ONLY path that captures Hyprland Super+drag — Wayland's xdg-toplevel doesn't expose absolute window position to clients, so Qt's moveEvent never fires for external compositor-driven movement. The adapter's _derive_viewport_for_fit equivalent will dispatch this event when it sees the current Hyprland rect drifting from the last dispatched rect by more than _DRIFT_TOLERANCE. All three handlers gate on (not fullscreen) and (not Closing). Drifts and moves while in fullscreen aren't meaningful for the windowed viewport. This makes the 7d19555 persistent viewport structural. The viewport is a state field. It's only mutated by WindowMoved / WindowResized / HyprlandDriftDetected (user action) — never by FitWindowToContent reading and writing back its own dispatch. The drift accumulation that the legacy code's "recompute from current state" shortcut suffered cannot happen here because there's no read-then-write path; viewport is the source of truth, not derived from current rect. Tests passing after this commit (62 total → 50 pass, 12 fail): - test_window_moved_updates_viewport_center_only - test_window_resized_updates_viewport_long_side - test_hyprland_drift_updates_viewport_from_rect (The persistent-viewport-no-drift invariant test was already passing because the previous transition handlers don't write to viewport — the test was checking the absence of drift via the absence of writes, which the skeleton already satisfied.) Phase A (16 tests) still green. Tests still failing (12, scheduled for commits 9-11): - mute/volume/loop persistence events (commit 9) - DisplayingImage content arrived branch (commit 10) - Closing transitions (commit 10) Test cases for commit 9 (mute/volume/loop persistence): - MuteToggleRequested flips state.mute, emits ApplyMute - VolumeSet sets state.volume, emits ApplyVolume - LoopModeSet sets state.loop_mode, emits ApplyLoopMode
This commit is contained in:
parent
d75076c14b
commit
a03d0e9dc8
@ -881,20 +881,85 @@ class StateMachine:
|
|||||||
return [ExitFullscreen()]
|
return [ExitFullscreen()]
|
||||||
|
|
||||||
def _on_window_moved(self, event: WindowMoved) -> list[Effect]:
|
def _on_window_moved(self, event: WindowMoved) -> list[Effect]:
|
||||||
# Real implementation: updates state.viewport from rect (move
|
"""Qt `moveEvent` fired (non-Hyprland only — Hyprland gates
|
||||||
# only — preserves long_side). Lands in commit 8.
|
this in the adapter because Wayland doesn't expose absolute
|
||||||
|
window position to clients).
|
||||||
|
|
||||||
|
Move-only update: preserve `long_side` from the existing
|
||||||
|
viewport (moves don't change size), but recompute the center
|
||||||
|
from the new rect. If there's no existing viewport yet (first
|
||||||
|
move before any fit), build a fresh one from the rect.
|
||||||
|
|
||||||
|
Skipped during fullscreen — moves while fullscreen aren't
|
||||||
|
user intent for the windowed viewport. Skipped in Closing.
|
||||||
|
"""
|
||||||
|
if self.fullscreen or self.state == State.CLOSING:
|
||||||
|
return []
|
||||||
|
x, y, w, h = event.rect
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return []
|
||||||
|
long_side = (
|
||||||
|
self.viewport.long_side if self.viewport is not None
|
||||||
|
else float(max(w, h))
|
||||||
|
)
|
||||||
|
self.viewport = Viewport(
|
||||||
|
center_x=x + w / 2,
|
||||||
|
center_y=y + h / 2,
|
||||||
|
long_side=long_side,
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_window_resized(self, event: WindowResized) -> list[Effect]:
|
def _on_window_resized(self, event: WindowResized) -> list[Effect]:
|
||||||
# Real implementation: updates state.viewport from rect
|
"""Qt `resizeEvent` fired (non-Hyprland only).
|
||||||
# (resize — long_side becomes max(w, h)). Lands in commit 8.
|
|
||||||
|
Full rebuild from the rect: long_side becomes the new
|
||||||
|
max(w, h), center becomes the rect center. Resizes change
|
||||||
|
the user's intent for popout size.
|
||||||
|
|
||||||
|
Skipped during fullscreen and Closing.
|
||||||
|
"""
|
||||||
|
if self.fullscreen or self.state == State.CLOSING:
|
||||||
|
return []
|
||||||
|
x, y, w, h = event.rect
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return []
|
||||||
|
self.viewport = Viewport(
|
||||||
|
center_x=x + w / 2,
|
||||||
|
center_y=y + h / 2,
|
||||||
|
long_side=float(max(w, h)),
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_hyprland_drift_detected(
|
def _on_hyprland_drift_detected(
|
||||||
self, event: HyprlandDriftDetected
|
self, event: HyprlandDriftDetected
|
||||||
) -> list[Effect]:
|
) -> list[Effect]:
|
||||||
# Real implementation: rebuilds state.viewport from rect.
|
"""Hyprland-side drift detector found that the current window
|
||||||
# Lands in commit 8.
|
rect differs from the last dispatched rect by more than
|
||||||
|
`_DRIFT_TOLERANCE`. The user moved or resized the window
|
||||||
|
externally (Super+drag, corner resize, window manager
|
||||||
|
intervention).
|
||||||
|
|
||||||
|
On Wayland, Qt's moveEvent / resizeEvent never fire for
|
||||||
|
external compositor-driven movement (xdg-toplevel doesn't
|
||||||
|
expose absolute position). So this event is the only path
|
||||||
|
that captures Hyprland Super+drag.
|
||||||
|
|
||||||
|
Adopt the new state as the viewport's intent: rebuild
|
||||||
|
viewport from the current rect.
|
||||||
|
|
||||||
|
Skipped during fullscreen — drifts while in fullscreen
|
||||||
|
aren't meaningful for the windowed viewport.
|
||||||
|
"""
|
||||||
|
if self.fullscreen or self.state == State.CLOSING:
|
||||||
|
return []
|
||||||
|
x, y, w, h = event.rect
|
||||||
|
if w <= 0 or h <= 0:
|
||||||
|
return []
|
||||||
|
self.viewport = Viewport(
|
||||||
|
center_x=x + w / 2,
|
||||||
|
center_y=y + h / 2,
|
||||||
|
long_side=float(max(w, h)),
|
||||||
|
)
|
||||||
return []
|
return []
|
||||||
|
|
||||||
def _on_close_requested(self, event: CloseRequested) -> list[Effect]:
|
def _on_close_requested(self, event: CloseRequested) -> list[Effect]:
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user