diff --git a/booru_viewer/gui/popout/__init__.py b/booru_viewer/gui/popout/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/booru_viewer/gui/popout/viewport.py b/booru_viewer/gui/popout/viewport.py new file mode 100644 index 0000000..7972e8a --- /dev/null +++ b/booru_viewer/gui/popout/viewport.py @@ -0,0 +1,36 @@ +"""Popout viewport math: persistent intent + drift tolerance.""" + +from __future__ import annotations + +from typing import NamedTuple + + +class Viewport(NamedTuple): + """Where and how large the user wants popout content to appear. + + Three numbers, no aspect. Aspect is a property of the currently- + displayed post and is recomputed from actual content on every + navigation. The viewport stays put across navigations; the window + rect is a derived projection (Viewport, content_aspect) → (x,y,w,h). + + `long_side` is the binding edge length: for landscape it becomes + width, for portrait it becomes height. Symmetric across the two + orientations, which is the property that breaks the + width-anchor ratchet that the previous `_fit_to_content` had. + """ + center_x: float + center_y: float + long_side: float + + +# Maximum drift between our last-dispatched window rect and the current +# Hyprland-reported rect that we still treat as "no user action happened." +# Anything within this tolerance is absorbed (Hyprland gap rounding, +# subpixel accumulation, decoration accounting). Anything beyond it is +# treated as "the user dragged or resized the window externally" and the +# persistent viewport gets updated from current state. +# +# 2px is small enough not to false-positive on real user drags (which +# are always tens of pixels minimum) and large enough to absorb the +# 1-2px per-nav drift that compounds across many navigations. +_DRIFT_TOLERANCE = 2 diff --git a/booru_viewer/gui/preview.py b/booru_viewer/gui/preview.py index 9eb9eb4..f02c35a 100644 --- a/booru_viewer/gui/preview.py +++ b/booru_viewer/gui/preview.py @@ -18,37 +18,6 @@ import mpv as mpvlib _log = logging.getLogger("booru") -class Viewport(NamedTuple): - """Where and how large the user wants popout content to appear. - - Three numbers, no aspect. Aspect is a property of the currently- - displayed post and is recomputed from actual content on every - navigation. The viewport stays put across navigations; the window - rect is a derived projection (Viewport, content_aspect) → (x,y,w,h). - - `long_side` is the binding edge length: for landscape it becomes - width, for portrait it becomes height. Symmetric across the two - orientations, which is the property that breaks the - width-anchor ratchet that the previous `_fit_to_content` had. - """ - center_x: float - center_y: float - long_side: float - - -# Maximum drift between our last-dispatched window rect and the current -# Hyprland-reported rect that we still treat as "no user action happened." -# Anything within this tolerance is absorbed (Hyprland gap rounding, -# subpixel accumulation, decoration accounting). Anything beyond it is -# treated as "the user dragged or resized the window externally" and the -# persistent viewport gets updated from current state. -# -# 2px is small enough not to false-positive on real user drags (which -# are always tens of pixels minimum) and large enough to absorb the -# 1-2px per-nav drift that compounds across many navigations. -_DRIFT_TOLERANCE = 2 - - ## Overlay styling for the popout's translucent toolbar / controls bar ## now lives in the bundled themes (themes/*.qss). The widgets get their ## object names set in code (FullscreenPreview / VideoPlayer) so theme QSS @@ -2269,3 +2238,4 @@ class ImagePreview(QWidget): # -- Refactor compatibility shims (deleted in commit 14) -- from .media.constants import VIDEO_EXTENSIONS, _is_video # re-export for refactor compat +from .popout.viewport import Viewport, _DRIFT_TOLERANCE # re-export for refactor compat