Coverage for pyPhotoAlbum/mixins/image_pan.py: 95%
42 statements
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
« prev ^ index » next coverage.py v7.14.1, created at 2026-06-20 12:55 +0000
1"""
2Image pan mixin for GLWidget - handles panning images within frames
3"""
5from typing import TYPE_CHECKING, Optional, Tuple
8from pyPhotoAlbum.models import ImageData
11class ImagePanMixin:
12 # Type hints for expected attributes from mixing class
13 drag_start_pos: Optional[Tuple[float, float]]
14 zoom_level: float
15 """
16 Mixin providing image panning functionality.
18 This mixin handles Control+drag to pan an image within its frame by
19 adjusting the crop_info property.
20 """
22 def __init__(self, *args, **kwargs):
23 super().__init__(*args, **kwargs)
25 # Image pan state (for panning image within frame with Control key)
26 self.image_pan_mode: bool = False # True when Control+dragging an ImageData element
27 self.image_pan_start_crop: Optional[Tuple[float, float, float, float]] = None # Starting crop_info
29 def _handle_image_pan_move(self, x: float, y: float, element: "ImageData"):
30 """
31 Handle image panning within a frame during mouse move.
33 Args:
34 x: Current mouse X position in screen coordinates
35 y: Current mouse Y position in screen coordinates
36 element: The ImageData element being panned
37 """
38 if not self.image_pan_mode or not isinstance(element, ImageData):
39 return
41 if not self.drag_start_pos:
42 return
44 # Calculate mouse movement in screen pixels
45 screen_dx = x - self.drag_start_pos[0]
46 screen_dy = y - self.drag_start_pos[1]
48 # Get element size in page-local coordinates
49 elem_w, elem_h = element.size
51 # Convert screen movement to normalized crop coordinates
52 # Negative because moving mouse right should pan image left (show more of right side)
53 # Scale by zoom level and element size
54 crop_dx = -screen_dx / (elem_w * self.zoom_level)
55 crop_dy = -screen_dy / (elem_h * self.zoom_level)
57 # Get starting crop info
58 start_crop = self.image_pan_start_crop
59 if not start_crop:
60 start_crop = (0, 0, 1, 1)
62 # Calculate new crop_info
63 crop_width = start_crop[2] - start_crop[0]
64 crop_height = start_crop[3] - start_crop[1]
66 new_x_min = start_crop[0] + crop_dx
67 new_y_min = start_crop[1] + crop_dy
68 new_x_max = new_x_min + crop_width
69 new_y_max = new_y_min + crop_height
71 # Clamp to valid range (0-1) to prevent panning beyond image boundaries
72 if new_x_min < 0:
73 new_x_min = 0
74 new_x_max = crop_width
75 if new_x_max > 1:
76 new_x_max = 1
77 new_x_min = 1 - crop_width
79 if new_y_min < 0:
80 new_y_min = 0
81 new_y_max = crop_height
82 if new_y_max > 1:
83 new_y_max = 1
84 new_y_min = 1 - crop_height
86 # Update element's crop_info
87 element.crop_info = (new_x_min, new_y_min, new_x_max, new_y_max)