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
« prev ^ index » next coverage.py v7.11.2, created at 2025-11-12 12:02 +0000
1"""
2Interactive and queryable image for pyWebLayout.
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"""
9from typing import Optional, Callable, Tuple
10import numpy as np
12from .block import Image
13from ..core.base import Interactable, Queriable
16class InteractiveImage(Image, Interactable, Queriable):
17 """
18 An image that can be interacted with and queried for hit detection.
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
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 """
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.
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)
64 # Initialize Interactable
65 Interactable.__init__(self, callback=callback)
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
71 def interact(self, point: np.generic) -> Optional[any]:
72 """
73 Handle interaction at the given point.
75 Only triggers the callback if the point is within the image bounds.
77 Args:
78 point: The coordinates of the interaction (x, y)
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)
89 return None
91 def in_object(self, point: np.generic) -> bool:
92 """
93 Check if a point is within the image bounds.
95 Args:
96 point: The coordinates to check (x, y)
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))
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.
118 This is a convenience method that mimics the Image.create_and_add_to API
119 but creates an InteractiveImage instead.
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
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 )
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)
150 return img
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.
156 This should be called by the renderer after it places the image.
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