Coverage for pyWebLayout/concrete/interaction_handler.py: 0%
99 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"""
2Interaction handler for managing button/link press-release lifecycle with visual feedback.
4This module provides utilities for handling interactive element states and rendering
5frames at different stages of interaction (pressed, released).
6"""
8from typing import Optional, Tuple, Callable, Any
9from PIL import Image
10import time
11import numpy as np
13from pyWebLayout.concrete.functional import LinkText, ButtonText
14from pyWebLayout.concrete.page import Page
17class InteractionHandler:
18 """
19 Manages the press-release lifecycle for interactive elements.
21 This class handles the timing and state management needed to show
22 visual feedback when buttons or links are clicked. It can generate
23 multiple rendered frames showing the pressed and released states.
25 Usage patterns:
27 Pattern A - Simple one-shot with automatic frames:
28 handler = InteractionHandler(page)
29 frames = handler.execute_with_feedback(button_element, point)
30 # Returns: [pressed_frame, released_frame]
31 # Show frames in sequence with brief delay
33 Pattern B - Manual state management for custom event loops:
34 handler = InteractionHandler(page)
35 handler.set_pressed_state(button_element, True)
36 pressed_frame = handler.render_current_state()
37 # ... show frame, wait, execute action ...
38 handler.set_pressed_state(button_element, False)
39 released_frame = handler.render_current_state()
40 """
42 def __init__(self, page: Page, press_duration_ms: int = 150):
43 """
44 Initialize the interaction handler.
46 Args:
47 page: The Page object containing the interactive elements
48 press_duration_ms: How long to show the pressed state (default: 150ms)
49 """
50 self._page = page
51 self._press_duration_ms = press_duration_ms
53 def set_pressed_state(self, element, pressed: bool):
54 """
55 Set the pressed state of an interactive element.
57 Args:
58 element: A LinkText or ButtonText object
59 pressed: True to show pressed, False to show released
60 """
61 if isinstance(element, (LinkText, ButtonText)):
62 # Ensure element has page reference for dirty flag
63 if not hasattr(element, '_page') or element._page is None:
64 element.set_page(self._page)
65 element.set_pressed(pressed)
66 else:
67 raise TypeError(
68 f"Element must be LinkText or ButtonText, got {type(element)}")
70 def set_hovered_state(self, element, hovered: bool):
71 """
72 Set the hovered state of an interactive element.
74 Args:
75 element: A LinkText or ButtonText object
76 hovered: True to show hovered, False for normal
77 """
78 if isinstance(element, (LinkText, ButtonText)):
79 # Ensure element has page reference for dirty flag
80 if not hasattr(element, '_page') or element._page is None:
81 element.set_page(self._page)
82 element.set_hovered(hovered)
83 else:
84 raise TypeError(
85 f"Element must be LinkText or ButtonText, got {type(element)}")
87 def render_current_state(self) -> Image.Image:
88 """
89 Render the page with current element states.
91 Returns:
92 PIL Image of the rendered page
93 """
94 return self._page.render()
96 def execute_with_feedback(
97 self,
98 element,
99 point: Optional[np.ndarray] = None,
100 callback: Optional[Callable] = None) -> Tuple[Image.Image, Image.Image, Any]:
101 """
102 Execute an interaction with visual feedback at each stage.
104 This is the high-level "all-in-one" method that:
105 1. Sets pressed state and renders
106 2. Waits for press_duration_ms
107 3. Executes the element's callback (or provided callback)
108 4. Sets released state and renders
110 Args:
111 element: A LinkText or ButtonText object
112 point: Optional point where interaction occurred
113 callback: Optional custom callback (overrides element's callback)
115 Returns:
116 Tuple of (pressed_frame, released_frame, callback_result)
117 """
118 # Step 1: Render pressed state
119 self.set_pressed_state(element, True)
120 pressed_frame = self.render_current_state()
122 # Step 2: Wait for visual feedback duration
123 time.sleep(self._press_duration_ms / 1000.0)
125 # Step 3: Execute callback
126 callback_result = None
127 if callback:
128 callback_result = callback(point) if point is not None else callback()
129 elif hasattr(element, 'interact'):
130 callback_result = element.interact(point)
132 # Step 4: Render released state
133 self.set_pressed_state(element, False)
134 released_frame = self.render_current_state()
136 return pressed_frame, released_frame, callback_result
138 def execute_async_with_feedback(
139 self,
140 element,
141 point: Optional[np.ndarray] = None) -> Tuple[Image.Image, Callable, Image.Image]:
142 """
143 Execute an interaction with visual feedback, returning frames immediately
144 without blocking.
146 This method returns the frames and a callback to execute later, allowing
147 the caller to control when the action actually happens.
149 Args:
150 element: A LinkText or ButtonText object
151 point: Optional point where interaction occurred
153 Returns:
154 Tuple of (pressed_frame, execute_callback, released_frame)
155 where execute_callback is a function that will execute the interaction
156 """
157 # Render pressed state
158 self.set_pressed_state(element, True)
159 pressed_frame = self.render_current_state()
161 # Create callback that will execute the interaction and reset state
162 def execute_callback():
163 result = None
164 if hasattr(element, 'interact'):
165 result = element.interact(point)
166 self.set_pressed_state(element, False)
167 return result
169 # Pre-render the released state (element state is still pressed)
170 # We'll return this frame but the caller controls when to show it
171 self.set_pressed_state(element, False)
172 released_frame = self.render_current_state()
174 # Reset back to pressed for consistency
175 # (caller will call execute_callback which sets to False)
176 self.set_pressed_state(element, True)
178 return pressed_frame, execute_callback, released_frame
181class InteractionStateManager:
182 """
183 Manages interaction states for multiple elements on a page.
185 Useful for applications that need to track hover/press states
186 across many interactive elements simultaneously.
187 """
189 def __init__(self, page: Page):
190 """
191 Initialize the state manager.
193 Args:
194 page: The Page object containing interactive elements
195 """
196 self._page = page
197 self._hovered_element = None
198 self._pressed_element = None
200 def update_hover(self, point: Tuple[int, int]) -> Optional[Image.Image]:
201 """
202 Update hover state based on cursor position.
204 Queries the page to find what's under the cursor and updates
205 hover states accordingly.
207 Args:
208 point: Cursor position (x, y)
210 Returns:
211 New rendered frame if hover state changed, None otherwise
212 """
213 # Query what's at this point
214 result = self._page.query_point(point)
216 if not result or not result.is_interactive:
217 # Nothing interactive under cursor
218 if self._hovered_element:
219 # Clear previous hover
220 if isinstance(self._hovered_element, (LinkText, ButtonText)):
221 self._hovered_element.set_hovered(False)
222 self._hovered_element = None
223 return self._page.render()
224 return None
226 # Something interactive is under cursor
227 element = result.object
228 if element != self._hovered_element:
229 # Hover changed
230 # Clear old hover
231 if self._hovered_element and isinstance(
232 self._hovered_element, (LinkText, ButtonText)):
233 self._hovered_element.set_hovered(False)
235 # Set new hover
236 if isinstance(element, (LinkText, ButtonText)):
237 element.set_hovered(True)
239 self._hovered_element = element
240 return self._page.render()
242 return None
244 def handle_mouse_down(self, point: Tuple[int, int]) -> Optional[Image.Image]:
245 """
246 Handle mouse button press at a point.
248 Args:
249 point: Click position (x, y)
251 Returns:
252 New rendered frame showing pressed state, or None if nothing interactive
253 """
254 result = self._page.query_point(point)
256 if not result or not result.is_interactive:
257 return None
259 element = result.object
260 if isinstance(element, (LinkText, ButtonText)):
261 element.set_pressed(True)
262 self._pressed_element = element
263 return self._page.render()
265 return None
267 def handle_mouse_up(
268 self,
269 point: Tuple[int,
270 int]) -> Tuple[Optional[Image.Image],
271 Any]:
272 """
273 Handle mouse button release at a point.
275 Args:
276 point: Release position (x, y)
278 Returns:
279 Tuple of (rendered_frame, callback_result)
280 Frame shows released state, result is from executing the callback
281 """
282 if not self._pressed_element:
283 return None, None
285 # Execute the interaction
286 callback_result = None
287 if hasattr(self._pressed_element, 'interact'):
288 callback_result = self._pressed_element.interact(
289 np.array(point))
291 # Release the pressed state
292 if isinstance(self._pressed_element, (LinkText, ButtonText)):
293 self._pressed_element.set_pressed(False)
295 self._pressed_element = None
297 return self._page.render(), callback_result
299 def reset(self):
300 """Reset all interaction states."""
301 if self._hovered_element and isinstance(
302 self._hovered_element, (LinkText, ButtonText)):
303 self._hovered_element.set_hovered(False)
305 if self._pressed_element and isinstance(
306 self._pressed_element, (LinkText, ButtonText)):
307 self._pressed_element.set_pressed(False)
309 self._hovered_element = None
310 self._pressed_element = None