""" Gesture routing and handling. This module handles all touch event routing and gesture logic. """ from __future__ import annotations from typing import TYPE_CHECKING, Optional, Tuple from ..gesture import TouchEvent, GestureType, GestureResponse, ActionType if TYPE_CHECKING: from ..application import EbookReader from pyWebLayout.core.query import SelectionRange class GestureRouter: """ Routes and handles all gestures. This class centralizes all gesture handling logic, making it easier to test and maintain gesture interactions. """ def __init__(self, reader: 'EbookReader'): """ Initialize the gesture router. Args: reader: EbookReader instance to route gestures for """ self.reader = reader # Selection state (for text selection gestures) self._selection_start: Optional[Tuple[int, int]] = None self._selection_end: Optional[Tuple[int, int]] = None self._selected_range: Optional['SelectionRange'] = None def handle_touch(self, event: TouchEvent) -> GestureResponse: """ Handle a touch event from HAL. This is the main entry point for all touch interactions. Args: event: TouchEvent from HAL with gesture type and coordinates Returns: GestureResponse with action and data for UI to process """ if not self.reader.is_loaded(): return GestureResponse(ActionType.ERROR, {"message": "No book loaded"}) # Handle overlay-specific gestures first if self.reader.is_overlay_open(): if event.gesture == GestureType.TAP: return self._handle_overlay_tap(event.x, event.y) elif event.gesture == GestureType.SWIPE_DOWN: return self._handle_overlay_close() # Dispatch based on gesture type for normal reading mode if event.gesture == GestureType.TAP: return self._handle_tap(event.x, event.y) elif event.gesture == GestureType.LONG_PRESS: return self._handle_long_press(event.x, event.y) elif event.gesture == GestureType.SWIPE_LEFT: return self._handle_page_forward() elif event.gesture == GestureType.SWIPE_RIGHT: return self._handle_page_back() elif event.gesture == GestureType.SWIPE_UP: return self._handle_swipe_up(event.y) elif event.gesture == GestureType.SWIPE_DOWN: return self._handle_swipe_down(event.y) elif event.gesture == GestureType.PINCH_IN: return self._handle_zoom_out() elif event.gesture == GestureType.PINCH_OUT: return self._handle_zoom_in() elif event.gesture == GestureType.DRAG_START: return self._handle_selection_start(event.x, event.y) elif event.gesture == GestureType.DRAG_MOVE: return self._handle_selection_move(event.x, event.y) elif event.gesture == GestureType.DRAG_END: return self._handle_selection_end(event.x, event.y) elif event.gesture == GestureType.TILT_FORWARD: return self._handle_page_forward() elif event.gesture == GestureType.TILT_BACKWARD: return self._handle_page_back() return GestureResponse(ActionType.NONE, {}) # =================================================================== # Reading Mode Gesture Handlers # =================================================================== def _handle_tap(self, x: int, y: int) -> GestureResponse: """Handle tap gesture - activates links or selects words""" page = self.reader.manager.get_current_page() result = page.query_point((x, y)) if not result or result.object_type == "empty": return GestureResponse(ActionType.NONE, {}) # If it's a link, navigate if result.is_interactive and result.link_target: # Handle different link types if result.link_target.endswith('.epub'): # Open new book success = self.reader.load_epub(result.link_target) if success: return GestureResponse(ActionType.BOOK_LOADED, { "title": self.reader.book_title, "author": self.reader.book_author, "path": result.link_target }) else: return GestureResponse(ActionType.ERROR, { "message": f"Failed to load {result.link_target}" }) else: # Internal navigation (chapter) self.reader.jump_to_chapter(result.link_target) return GestureResponse(ActionType.NAVIGATE, { "target": result.link_target, "chapter": self.reader.get_current_chapter_info() }) # Just a tap on text - select word if result.text: return GestureResponse(ActionType.WORD_SELECTED, { "word": result.text, "bounds": result.bounds }) return GestureResponse(ActionType.NONE, {}) def _handle_long_press(self, x: int, y: int) -> GestureResponse: """Handle long-press - show definition or menu""" page = self.reader.manager.get_current_page() result = page.query_point((x, y)) if result and result.text: return GestureResponse(ActionType.DEFINE, { "word": result.text, "bounds": result.bounds }) # Long-press on empty - show menu return GestureResponse(ActionType.SHOW_MENU, { "options": ["bookmark", "settings", "toc", "search"] }) def _handle_page_forward(self) -> GestureResponse: """Handle swipe left - next page""" img = self.reader.next_page() if img: return GestureResponse(ActionType.PAGE_TURN, { "direction": "forward", "progress": self.reader.get_reading_progress(), "chapter": self.reader.get_current_chapter_info() }) return GestureResponse(ActionType.AT_END, {}) def _handle_page_back(self) -> GestureResponse: """Handle swipe right - previous page""" img = self.reader.previous_page() if img: return GestureResponse(ActionType.PAGE_TURN, { "direction": "back", "progress": self.reader.get_reading_progress(), "chapter": self.reader.get_current_chapter_info() }) return GestureResponse(ActionType.AT_START, {}) def _handle_zoom_in(self) -> GestureResponse: """Handle pinch out - increase font""" self.reader.increase_font_size() return GestureResponse(ActionType.ZOOM, { "direction": "in", "font_scale": self.reader.base_font_scale }) def _handle_zoom_out(self) -> GestureResponse: """Handle pinch in - decrease font""" self.reader.decrease_font_size() return GestureResponse(ActionType.ZOOM, { "direction": "out", "font_scale": self.reader.base_font_scale }) def _handle_selection_start(self, x: int, y: int) -> GestureResponse: """Start text selection""" self._selection_start = (x, y) self._selection_end = None self._selected_range = None return GestureResponse(ActionType.SELECTION_START, { "start": (x, y) }) def _handle_selection_move(self, x: int, y: int) -> GestureResponse: """Update text selection""" if not self._selection_start: return GestureResponse(ActionType.NONE, {}) self._selection_end = (x, y) # Query range page = self.reader.manager.get_current_page() self._selected_range = page.query_range( self._selection_start, self._selection_end ) return GestureResponse(ActionType.SELECTION_UPDATE, { "start": self._selection_start, "end": self._selection_end, "text_count": len(self._selected_range.results), "bounds": self._selected_range.bounds_list }) def _handle_selection_end(self, x: int, y: int) -> GestureResponse: """End text selection and return selected text""" if not self._selection_start: return GestureResponse(ActionType.NONE, {}) self._selection_end = (x, y) page = self.reader.manager.get_current_page() self._selected_range = page.query_range( self._selection_start, self._selection_end ) return GestureResponse(ActionType.SELECTION_COMPLETE, { "text": self._selected_range.text, "word_count": len(self._selected_range.results), "bounds": self._selected_range.bounds_list }) def _handle_swipe_up(self, y: int) -> GestureResponse: """Handle swipe up gesture - opens Navigation overlay (TOC + Bookmarks)""" # Open navigation overlay from anywhere on screen overlay_image = self.reader.open_navigation_overlay(active_tab="contents") if overlay_image: return GestureResponse(ActionType.OVERLAY_OPENED, { "overlay_type": "navigation", "active_tab": "contents", "chapters": self.reader.get_chapters() }) return GestureResponse(ActionType.NONE, {}) def _handle_swipe_down(self, y: int) -> GestureResponse: """Handle swipe down gesture - opens Settings overlay (only from top 20% of screen)""" # Only open settings overlay if swipe starts from top 20% of screen top_threshold = self.reader.page_size[1] * 0.2 if y > top_threshold: return GestureResponse(ActionType.NONE, {}) overlay_image = self.reader.open_settings_overlay() if overlay_image: return GestureResponse(ActionType.OVERLAY_OPENED, { "overlay_type": "settings", "font_scale": self.reader.base_font_scale, "line_spacing": self.reader.page_style.line_spacing, "inter_block_spacing": self.reader.page_style.inter_block_spacing }) return GestureResponse(ActionType.NONE, {}) # =================================================================== # Overlay Mode Gesture Handlers # =================================================================== def _handle_overlay_tap(self, x: int, y: int) -> GestureResponse: """Handle tap when overlay is open - delegates to EbookReader overlay handlers""" # This remains in EbookReader because it's tightly coupled with overlay state return self.reader._handle_overlay_tap(x, y) def _handle_overlay_close(self) -> GestureResponse: """Handle overlay close gesture (swipe down)""" self.reader.close_overlay() return GestureResponse(ActionType.OVERLAY_CLOSED, {})