Coverage for pyWebLayout/abstract/interactive_image.py: 80%

34 statements  

« prev     ^ index     » next       coverage.py v7.11.2, created at 2025-11-12 12:02 +0000

1""" 

2Interactive and queryable image for pyWebLayout. 

3 

4Provides an InteractiveImage class that combines Image with Interactable 

5and Queriable capabilities, allowing images to respond to tap events with 

6proper bounding box detection. 

7""" 

8 

9from typing import Optional, Callable, Tuple 

10import numpy as np 

11 

12from .block import Image 

13from ..core.base import Interactable, Queriable 

14 

15 

16class InteractiveImage(Image, Interactable, Queriable): 

17 """ 

18 An image that can be interacted with and queried for hit detection. 

19 

20 This combines pyWebLayout's Image block with Interactable and Queriable 

21 capabilities, allowing the image to: 

22 - Have a callback that fires when tapped 

23 - Know its rendered position (origin) 

24 - Detect if a point is within its bounds 

25 

26 Example: 

27 >>> img = InteractiveImage( 

28 ... source="cover.png", 

29 ... alt_text="Book Title", 

30 ... callback=lambda point: "/path/to/book.epub" 

31 ... ) 

32 >>> # After rendering, origin is set automatically 

33 >>> # Check if tap is inside 

34 >>> result = img.interact((120, 250)) 

35 >>> # Returns "/path/to/book.epub" if inside, None if outside 

36 """ 

37 

38 def __init__( 

39 self, 

40 source: str = "", 

41 alt_text: str = "", 

42 width: Optional[int] = None, 

43 height: Optional[int] = None, 

44 callback: Optional[Callable] = None 

45 ): 

46 """ 

47 Initialize an interactive image. 

48 

49 Args: 

50 source: The image source URL or path 

51 alt_text: Alternative text for accessibility 

52 width: Optional image width in pixels 

53 height: Optional image height in pixels 

54 callback: Function to call when image is tapped (receives point coordinates) 

55 """ 

56 # Initialize Image 

57 Image.__init__( 

58 self, 

59 source=source, 

60 alt_text=alt_text, 

61 width=width, 

62 height=height) 

63 

64 # Initialize Interactable 

65 Interactable.__init__(self, callback=callback) 

66 

67 # Initialize position tracking 

68 self._origin = np.array([0, 0]) # Will be set during rendering 

69 self.size = (width or 0, height or 0) # Will be updated during rendering 

70 

71 def interact(self, point: np.generic) -> Optional[any]: 

72 """ 

73 Handle interaction at the given point. 

74 

75 Only triggers the callback if the point is within the image bounds. 

76 

77 Args: 

78 point: The coordinates of the interaction (x, y) 

79 

80 Returns: 

81 The result of the callback if point is inside, None otherwise 

82 """ 

83 # Check if point is inside this image 

84 if self.in_object(point): 

85 # Point is inside, trigger callback 

86 if self._callback is not None: 

87 return self._callback(point) 

88 

89 return None 

90 

91 def in_object(self, point: np.generic) -> bool: 

92 """ 

93 Check if a point is within the image bounds. 

94 

95 Args: 

96 point: The coordinates to check (x, y) 

97 

98 Returns: 

99 True if point is inside the image, False otherwise 

100 """ 

101 point_array = np.array(point) 

102 relative_point = point_array - self._origin 

103 return np.all((0 <= relative_point) & (relative_point < self.size)) 

104 

105 @classmethod 

106 def create_and_add_to( 

107 cls, 

108 parent, 

109 source: str, 

110 alt_text: str = "", 

111 width: Optional[int] = None, 

112 height: Optional[int] = None, 

113 callback: Optional[Callable] = None 

114 ) -> 'InteractiveImage': 

115 """ 

116 Create an interactive image and add it to a parent block. 

117 

118 This is a convenience method that mimics the Image.create_and_add_to API 

119 but creates an InteractiveImage instead. 

120 

121 Args: 

122 parent: Parent block to add this image to 

123 source: The image source URL or path 

124 alt_text: Alternative text for accessibility 

125 width: Optional image width in pixels 

126 height: Optional image height in pixels 

127 callback: Function to call when image is tapped 

128 

129 Returns: 

130 The created InteractiveImage instance 

131 """ 

132 img = cls( 

133 source=source, 

134 alt_text=alt_text, 

135 width=width, 

136 height=height, 

137 callback=callback 

138 ) 

139 

140 # Add to parent using its add_block method 

141 if hasattr(parent, 'add_block'): 141 ↛ 142line 141 didn't jump to line 142 because the condition on line 141 was never true

142 parent.add_block(img) 

143 elif hasattr(parent, 'add_child'): 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true

144 parent.add_child(img) 

145 elif hasattr(parent, '_children'): 145 ↛ 147line 145 didn't jump to line 147 because the condition on line 145 was always true

146 parent._children.append(img) 

147 elif hasattr(parent, '_blocks'): 

148 parent._blocks.append(img) 

149 

150 return img 

151 

152 def set_rendered_bounds(self, origin: Tuple[int, int], size: Tuple[int, int]): 

153 """ 

154 Set the rendered position and size of this image. 

155 

156 This should be called by the renderer after it places the image. 

157 

158 Args: 

159 origin: (x, y) coordinates of top-left corner 

160 size: (width, height) of the rendered image 

161 """ 

162 self._origin = np.array(origin) 

163 self.size = size