1 """

2 Image pan mixin for GLWidget - handles panning images within frames

3 """

4

5 from typing import TYPE_CHECKING, Optional, Tuple

6

7

8 from pyPhotoAlbum.models import ImageData

9

10

11 class 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.

17

18 This mixin handles Control+drag to pan an image within its frame by

19 adjusting the crop_info property.

20 """

21

22 def __init__(self, *args, **kwargs):

23 super().__init__(*args, **kwargs)

24

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

28

29 def _handle_image_pan_move(self, x: float, y: float, element: "ImageData"):

30 """

31 Handle image panning within a frame during mouse move.

32

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

40

41 if not self.drag_start_pos:

42 return

43

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]

47

48 # Get element size in page-local coordinates

49 elem_w, elem_h = element.size

50

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)

56

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)

61

62 # Calculate new crop_info

63 crop_width = start_crop[2] - start_crop[0]

64 crop_height = start_crop[3] - start_crop[1]

65

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

70

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

78

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

85

86 # Update element's crop_info

87 element.crop_info = (new_x_min, new_y_min, new_x_max, new_y_max)