From d75076c14b2db507f33c49593006da97b0bd3c50 Mon Sep 17 00:00:00 2001 From: pax Date: Wed, 8 Apr 2026 19:34:52 -0500 Subject: [PATCH] popout/state: implement Fullscreen flag + F11 round-trip MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit FullscreenToggled in any non-Closing state flips state.fullscreen. Enter (fullscreen=False → True): - Snapshot state.viewport into state.pre_fullscreen_viewport - Emit EnterFullscreen effect (adapter calls self.showFullScreen()) Exit (fullscreen=True → False): - Restore state.viewport from state.pre_fullscreen_viewport - Clear state.pre_fullscreen_viewport - Emit ExitFullscreen effect (adapter calls self.showNormal() then defers a FitWindowToContent on the next event-loop tick — matching the current QTimer.singleShot(0, ...) pattern) This makes the 705e6c6 F11 round-trip viewport preservation structural. The fix in the legacy code wrote the current Hyprland window state into _viewport inside _enter_fullscreen so the F11 exit could restore it. The state machine version is equivalent: the viewport snapshot at the moment of entering is the source of truth for restoration. Whether the user got there via Super+drag (no Qt moveEvent on Wayland), nav, or external resize, the snapshot captures the viewport AS IT IS RIGHT NOW. The interaction with HyprlandDriftDetected (commit 8): the adapter will dispatch a HyprlandDriftDetected event before FullscreenToggled during enter, so any drift between the last dispatched rect and current Hyprland geometry is absorbed into viewport BEFORE the snapshot. That's how the state machine handles the "user dragged the popout, then immediately pressed F11" case. Tests passing after this commit (62 total → 47 pass, 15 fail): - test_invariant_f11_round_trip_restores_pre_fullscreen_viewport Phase A (16 tests) still green. Tests still failing (15, scheduled for commits 8-11): - Persistent viewport / drift events (commit 8) - mute/volume/loop persistence events (commit 9) - DisplayingImage content arrived branch (commit 10) - Closing transitions (commit 10) Test cases for commit 8 (persistent viewport + drift events): - WindowMoved updates viewport center, preserves long_side - WindowResized updates viewport long_side from new max(w,h) - HyprlandDriftDetected rebuilds viewport from rect - Persistent viewport doesn't drift across navs (already passing) --- booru_viewer/gui/popout/state.py | 50 ++++++++++++++++++++++++++++++-- 1 file changed, 47 insertions(+), 3 deletions(-) diff --git a/booru_viewer/gui/popout/state.py b/booru_viewer/gui/popout/state.py index 1e485ab..06ef7d3 100644 --- a/booru_viewer/gui/popout/state.py +++ b/booru_viewer/gui/popout/state.py @@ -832,9 +832,53 @@ class StateMachine: return [] def _on_fullscreen_toggled(self, event: FullscreenToggled) -> list[Effect]: - # Real implementation: enter snapshots viewport into - # pre_fullscreen_viewport. Exit restores. Lands in commit 7. - return [] + """**F11 round-trip viewport preservation (705e6c6 made + structural).** + + Enter (current state: not fullscreen): + - Snapshot `viewport` into `pre_fullscreen_viewport` + - Set `fullscreen = True` + - Emit `EnterFullscreen` effect + + Exit (current state: fullscreen): + - Restore `viewport` from `pre_fullscreen_viewport` + - Clear `pre_fullscreen_viewport` + - Set `fullscreen = False` + - Emit `ExitFullscreen` effect (which causes the adapter + to defer a FitWindowToContent on the next event-loop + tick — matching the current QTimer.singleShot(0, ...) + pattern at popout/window.py:1023) + + The viewport snapshot at the moment of entering is the key. + Whether the user got to that position via Super+drag (no Qt + moveEvent on Wayland), nav (which doesn't update viewport + unless drift is detected), or external resize, the + `pre_fullscreen_viewport` snapshot captures the viewport + AS IT IS RIGHT NOW. F11 exit restores it exactly. + + The 705e6c6 commit fixed this in the legacy code by + explicitly writing the current Hyprland window state into + `_viewport` inside `_enter_fullscreen` — the state machine + version is structurally equivalent. The adapter's + EnterFullscreen handler reads the current Hyprland geometry + and dispatches a `HyprlandDriftDetected` event before the + FullscreenToggled, which updates `viewport` to current + reality, then FullscreenToggled snapshots that into + `pre_fullscreen_viewport`. + + Valid in every non-Closing state. Closing drops it (handled + at the dispatch entry). + """ + if not self.fullscreen: + self.pre_fullscreen_viewport = self.viewport + self.fullscreen = True + return [EnterFullscreen()] + # Exiting fullscreen + if self.pre_fullscreen_viewport is not None: + self.viewport = self.pre_fullscreen_viewport + self.pre_fullscreen_viewport = None + self.fullscreen = False + return [ExitFullscreen()] def _on_window_moved(self, event: WindowMoved) -> list[Effect]: # Real implementation: updates state.viewport from rect (move