add gestures from pyweblayout
This commit is contained in:
parent
06c4a8504b
commit
60426432a0
127
dreader/gesture.py
Normal file
127
dreader/gesture.py
Normal file
@ -0,0 +1,127 @@
|
|||||||
|
"""
|
||||||
|
Gesture event types for touch input.
|
||||||
|
|
||||||
|
This module defines touch gestures that can be received from a HAL (Hardware Abstraction Layer)
|
||||||
|
or touch input system, and the response format for actions to be performed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
from enum import Enum
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Optional, Dict, Any
|
||||||
|
|
||||||
|
|
||||||
|
class GestureType(Enum):
|
||||||
|
"""Touch gesture types from HAL"""
|
||||||
|
TAP = "tap" # Single finger tap
|
||||||
|
LONG_PRESS = "long_press" # Hold for 500ms+
|
||||||
|
SWIPE_LEFT = "swipe_left" # Swipe left (page forward)
|
||||||
|
SWIPE_RIGHT = "swipe_right" # Swipe right (page back)
|
||||||
|
SWIPE_UP = "swipe_up" # Swipe up (scroll down)
|
||||||
|
SWIPE_DOWN = "swipe_down" # Swipe down (scroll up)
|
||||||
|
PINCH_IN = "pinch_in" # Pinch fingers together (zoom out)
|
||||||
|
PINCH_OUT = "pinch_out" # Spread fingers apart (zoom in)
|
||||||
|
DRAG_START = "drag_start" # Start dragging/selection
|
||||||
|
DRAG_MOVE = "drag_move" # Continue dragging
|
||||||
|
DRAG_END = "drag_end" # End dragging/selection
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class TouchEvent:
|
||||||
|
"""
|
||||||
|
Touch event from HAL.
|
||||||
|
|
||||||
|
Represents a single touch gesture with its coordinates and metadata.
|
||||||
|
"""
|
||||||
|
gesture: GestureType
|
||||||
|
x: int # Primary touch point X coordinate
|
||||||
|
y: int # Primary touch point Y coordinate
|
||||||
|
x2: Optional[int] = None # Secondary point X (for pinch/drag)
|
||||||
|
y2: Optional[int] = None # Secondary point Y (for pinch/drag)
|
||||||
|
timestamp_ms: float = 0 # Timestamp in milliseconds
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def from_hal(cls, hal_data: dict) -> 'TouchEvent':
|
||||||
|
"""
|
||||||
|
Parse a touch event from HAL format.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
hal_data: Dictionary with gesture data from HAL
|
||||||
|
Expected keys: 'gesture', 'x', 'y', optionally 'x2', 'y2', 'timestamp'
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
TouchEvent instance
|
||||||
|
|
||||||
|
Example:
|
||||||
|
>>> event = TouchEvent.from_hal({
|
||||||
|
... 'gesture': 'tap',
|
||||||
|
... 'x': 450,
|
||||||
|
... 'y': 320
|
||||||
|
... })
|
||||||
|
"""
|
||||||
|
return cls(
|
||||||
|
gesture=GestureType(hal_data['gesture']),
|
||||||
|
x=hal_data['x'],
|
||||||
|
y=hal_data['y'],
|
||||||
|
x2=hal_data.get('x2'),
|
||||||
|
y2=hal_data.get('y2'),
|
||||||
|
timestamp_ms=hal_data.get('timestamp', 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""Convert to dictionary for serialization"""
|
||||||
|
return {
|
||||||
|
'gesture': self.gesture.value,
|
||||||
|
'x': self.x,
|
||||||
|
'y': self.y,
|
||||||
|
'x2': self.x2,
|
||||||
|
'y2': self.y2,
|
||||||
|
'timestamp_ms': self.timestamp_ms
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class GestureResponse:
|
||||||
|
"""
|
||||||
|
Response from handling a gesture.
|
||||||
|
|
||||||
|
This encapsulates the action that should be performed by the UI
|
||||||
|
in response to a gesture, keeping all business logic in the library.
|
||||||
|
"""
|
||||||
|
action: str # Action type: "navigate", "define", "select", "zoom", "page_turn", "none", etc.
|
||||||
|
data: Dict[str, Any] # Action-specific data
|
||||||
|
|
||||||
|
def to_dict(self) -> dict:
|
||||||
|
"""
|
||||||
|
Convert to dictionary for Flask JSON response.
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dictionary with action and data
|
||||||
|
"""
|
||||||
|
return {
|
||||||
|
'action': self.action,
|
||||||
|
'data': self.data
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# Action type constants for clarity
|
||||||
|
class ActionType:
|
||||||
|
"""Constants for gesture response action types"""
|
||||||
|
NONE = "none"
|
||||||
|
PAGE_TURN = "page_turn"
|
||||||
|
NAVIGATE = "navigate"
|
||||||
|
DEFINE = "define"
|
||||||
|
SELECT = "select"
|
||||||
|
ZOOM = "zoom"
|
||||||
|
BOOK_LOADED = "book_loaded"
|
||||||
|
WORD_SELECTED = "word_selected"
|
||||||
|
SHOW_MENU = "show_menu"
|
||||||
|
SELECTION_START = "selection_start"
|
||||||
|
SELECTION_UPDATE = "selection_update"
|
||||||
|
SELECTION_COMPLETE = "selection_complete"
|
||||||
|
AT_START = "at_start"
|
||||||
|
AT_END = "at_end"
|
||||||
|
ERROR = "error"
|
||||||
|
OVERLAY_OPENED = "overlay_opened"
|
||||||
|
OVERLAY_CLOSED = "overlay_closed"
|
||||||
|
CHAPTER_SELECTED = "chapter_selected"
|
||||||
287
tests/test_gesture.py
Normal file
287
tests/test_gesture.py
Normal file
@ -0,0 +1,287 @@
|
|||||||
|
"""
|
||||||
|
Unit tests for gesture event system.
|
||||||
|
|
||||||
|
Tests TouchEvent, GestureType, GestureResponse, and HAL integration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import unittest
|
||||||
|
from dreader.gesture import (
|
||||||
|
GestureType,
|
||||||
|
TouchEvent,
|
||||||
|
GestureResponse,
|
||||||
|
ActionType
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGestureType(unittest.TestCase):
|
||||||
|
"""Test GestureType enum"""
|
||||||
|
|
||||||
|
def test_gesture_types_exist(self):
|
||||||
|
"""Test all gesture types are defined"""
|
||||||
|
self.assertEqual(GestureType.TAP.value, "tap")
|
||||||
|
self.assertEqual(GestureType.LONG_PRESS.value, "long_press")
|
||||||
|
self.assertEqual(GestureType.SWIPE_LEFT.value, "swipe_left")
|
||||||
|
self.assertEqual(GestureType.SWIPE_RIGHT.value, "swipe_right")
|
||||||
|
self.assertEqual(GestureType.SWIPE_UP.value, "swipe_up")
|
||||||
|
self.assertEqual(GestureType.SWIPE_DOWN.value, "swipe_down")
|
||||||
|
self.assertEqual(GestureType.PINCH_IN.value, "pinch_in")
|
||||||
|
self.assertEqual(GestureType.PINCH_OUT.value, "pinch_out")
|
||||||
|
self.assertEqual(GestureType.DRAG_START.value, "drag_start")
|
||||||
|
self.assertEqual(GestureType.DRAG_MOVE.value, "drag_move")
|
||||||
|
self.assertEqual(GestureType.DRAG_END.value, "drag_end")
|
||||||
|
|
||||||
|
|
||||||
|
class TestTouchEvent(unittest.TestCase):
|
||||||
|
"""Test TouchEvent dataclass"""
|
||||||
|
|
||||||
|
def test_init_basic(self):
|
||||||
|
"""Test basic TouchEvent creation"""
|
||||||
|
event = TouchEvent(
|
||||||
|
gesture=GestureType.TAP,
|
||||||
|
x=450,
|
||||||
|
y=320
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.TAP)
|
||||||
|
self.assertEqual(event.x, 450)
|
||||||
|
self.assertEqual(event.y, 320)
|
||||||
|
self.assertIsNone(event.x2)
|
||||||
|
self.assertIsNone(event.y2)
|
||||||
|
self.assertEqual(event.timestamp_ms, 0)
|
||||||
|
|
||||||
|
def test_init_with_secondary_point(self):
|
||||||
|
"""Test TouchEvent with secondary point (pinch/drag)"""
|
||||||
|
event = TouchEvent(
|
||||||
|
gesture=GestureType.PINCH_OUT,
|
||||||
|
x=400,
|
||||||
|
y=300,
|
||||||
|
x2=450,
|
||||||
|
y2=350,
|
||||||
|
timestamp_ms=12345.678
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
|
||||||
|
self.assertEqual(event.x, 400)
|
||||||
|
self.assertEqual(event.y, 300)
|
||||||
|
self.assertEqual(event.x2, 450)
|
||||||
|
self.assertEqual(event.y2, 350)
|
||||||
|
self.assertEqual(event.timestamp_ms, 12345.678)
|
||||||
|
|
||||||
|
def test_from_hal_basic(self):
|
||||||
|
"""Test parsing TouchEvent from HAL format"""
|
||||||
|
hal_data = {
|
||||||
|
'gesture': 'tap',
|
||||||
|
'x': 450,
|
||||||
|
'y': 320
|
||||||
|
}
|
||||||
|
|
||||||
|
event = TouchEvent.from_hal(hal_data)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.TAP)
|
||||||
|
self.assertEqual(event.x, 450)
|
||||||
|
self.assertEqual(event.y, 320)
|
||||||
|
|
||||||
|
def test_from_hal_complete(self):
|
||||||
|
"""Test parsing TouchEvent with all fields from HAL"""
|
||||||
|
hal_data = {
|
||||||
|
'gesture': 'pinch_out',
|
||||||
|
'x': 400,
|
||||||
|
'y': 300,
|
||||||
|
'x2': 450,
|
||||||
|
'y2': 350,
|
||||||
|
'timestamp': 12345.678
|
||||||
|
}
|
||||||
|
|
||||||
|
event = TouchEvent.from_hal(hal_data)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
|
||||||
|
self.assertEqual(event.x, 400)
|
||||||
|
self.assertEqual(event.y, 300)
|
||||||
|
self.assertEqual(event.x2, 450)
|
||||||
|
self.assertEqual(event.y2, 350)
|
||||||
|
self.assertEqual(event.timestamp_ms, 12345.678)
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
"""Test TouchEvent serialization"""
|
||||||
|
event = TouchEvent(
|
||||||
|
gesture=GestureType.SWIPE_LEFT,
|
||||||
|
x=600,
|
||||||
|
y=400,
|
||||||
|
timestamp_ms=12345.0
|
||||||
|
)
|
||||||
|
|
||||||
|
d = event.to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(d['gesture'], 'swipe_left')
|
||||||
|
self.assertEqual(d['x'], 600)
|
||||||
|
self.assertEqual(d['y'], 400)
|
||||||
|
self.assertIsNone(d['x2'])
|
||||||
|
self.assertIsNone(d['y2'])
|
||||||
|
self.assertEqual(d['timestamp_ms'], 12345.0)
|
||||||
|
|
||||||
|
|
||||||
|
class TestGestureResponse(unittest.TestCase):
|
||||||
|
"""Test GestureResponse dataclass"""
|
||||||
|
|
||||||
|
def test_init(self):
|
||||||
|
"""Test GestureResponse creation"""
|
||||||
|
response = GestureResponse(
|
||||||
|
action="page_turn",
|
||||||
|
data={"direction": "forward", "progress": 0.42}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.action, "page_turn")
|
||||||
|
self.assertEqual(response.data['direction'], "forward")
|
||||||
|
self.assertEqual(response.data['progress'], 0.42)
|
||||||
|
|
||||||
|
def test_to_dict(self):
|
||||||
|
"""Test GestureResponse serialization"""
|
||||||
|
response = GestureResponse(
|
||||||
|
action="define",
|
||||||
|
data={"word": "ephemeral", "bounds": (100, 200, 50, 20)}
|
||||||
|
)
|
||||||
|
|
||||||
|
d = response.to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(d['action'], "define")
|
||||||
|
self.assertEqual(d['data']['word'], "ephemeral")
|
||||||
|
self.assertEqual(d['data']['bounds'], (100, 200, 50, 20))
|
||||||
|
|
||||||
|
def test_to_dict_empty_data(self):
|
||||||
|
"""Test GestureResponse with empty data"""
|
||||||
|
response = GestureResponse(action="none", data={})
|
||||||
|
|
||||||
|
d = response.to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(d['action'], "none")
|
||||||
|
self.assertEqual(d['data'], {})
|
||||||
|
|
||||||
|
|
||||||
|
class TestActionType(unittest.TestCase):
|
||||||
|
"""Test ActionType constants"""
|
||||||
|
|
||||||
|
def test_action_types_defined(self):
|
||||||
|
"""Test all action type constants are defined"""
|
||||||
|
self.assertEqual(ActionType.NONE, "none")
|
||||||
|
self.assertEqual(ActionType.PAGE_TURN, "page_turn")
|
||||||
|
self.assertEqual(ActionType.NAVIGATE, "navigate")
|
||||||
|
self.assertEqual(ActionType.DEFINE, "define")
|
||||||
|
self.assertEqual(ActionType.SELECT, "select")
|
||||||
|
self.assertEqual(ActionType.ZOOM, "zoom")
|
||||||
|
self.assertEqual(ActionType.BOOK_LOADED, "book_loaded")
|
||||||
|
self.assertEqual(ActionType.WORD_SELECTED, "word_selected")
|
||||||
|
self.assertEqual(ActionType.SHOW_MENU, "show_menu")
|
||||||
|
self.assertEqual(ActionType.SELECTION_START, "selection_start")
|
||||||
|
self.assertEqual(ActionType.SELECTION_UPDATE, "selection_update")
|
||||||
|
self.assertEqual(ActionType.SELECTION_COMPLETE, "selection_complete")
|
||||||
|
self.assertEqual(ActionType.AT_START, "at_start")
|
||||||
|
self.assertEqual(ActionType.AT_END, "at_end")
|
||||||
|
self.assertEqual(ActionType.ERROR, "error")
|
||||||
|
|
||||||
|
|
||||||
|
class TestHALIntegration(unittest.TestCase):
|
||||||
|
"""Test HAL integration scenarios"""
|
||||||
|
|
||||||
|
def test_hal_tap_flow(self):
|
||||||
|
"""Test complete HAL tap event flow"""
|
||||||
|
# Simulate HAL sending tap event
|
||||||
|
hal_data = {
|
||||||
|
'gesture': 'tap',
|
||||||
|
'x': 450,
|
||||||
|
'y': 320,
|
||||||
|
'timestamp': 1234567890.123
|
||||||
|
}
|
||||||
|
|
||||||
|
# Parse event
|
||||||
|
event = TouchEvent.from_hal(hal_data)
|
||||||
|
|
||||||
|
# Verify event
|
||||||
|
self.assertEqual(event.gesture, GestureType.TAP)
|
||||||
|
self.assertEqual(event.x, 450)
|
||||||
|
self.assertEqual(event.y, 320)
|
||||||
|
|
||||||
|
# Simulate business logic response
|
||||||
|
response = GestureResponse(
|
||||||
|
action=ActionType.WORD_SELECTED,
|
||||||
|
data={"word": "hello", "bounds": (440, 310, 50, 20)}
|
||||||
|
)
|
||||||
|
|
||||||
|
# Serialize for Flask
|
||||||
|
response_dict = response.to_dict()
|
||||||
|
|
||||||
|
self.assertEqual(response_dict['action'], "word_selected")
|
||||||
|
self.assertEqual(response_dict['data']['word'], "hello")
|
||||||
|
|
||||||
|
def test_hal_pinch_flow(self):
|
||||||
|
"""Test complete HAL pinch event flow"""
|
||||||
|
# Simulate HAL sending pinch event with two touch points
|
||||||
|
hal_data = {
|
||||||
|
'gesture': 'pinch_out',
|
||||||
|
'x': 400,
|
||||||
|
'y': 500,
|
||||||
|
'x2': 500,
|
||||||
|
'y2': 500,
|
||||||
|
'timestamp': 1234567891.456
|
||||||
|
}
|
||||||
|
|
||||||
|
event = TouchEvent.from_hal(hal_data)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.PINCH_OUT)
|
||||||
|
self.assertEqual(event.x, 400)
|
||||||
|
self.assertEqual(event.x2, 500)
|
||||||
|
|
||||||
|
def test_hal_swipe_flow(self):
|
||||||
|
"""Test complete HAL swipe event flow"""
|
||||||
|
hal_data = {
|
||||||
|
'gesture': 'swipe_left',
|
||||||
|
'x': 600,
|
||||||
|
'y': 400
|
||||||
|
}
|
||||||
|
|
||||||
|
event = TouchEvent.from_hal(hal_data)
|
||||||
|
|
||||||
|
self.assertEqual(event.gesture, GestureType.SWIPE_LEFT)
|
||||||
|
|
||||||
|
# Expected response
|
||||||
|
response = GestureResponse(
|
||||||
|
action=ActionType.PAGE_TURN,
|
||||||
|
data={"direction": "forward", "progress": 0.25}
|
||||||
|
)
|
||||||
|
|
||||||
|
self.assertEqual(response.action, "page_turn")
|
||||||
|
|
||||||
|
def test_hal_drag_selection_flow(self):
|
||||||
|
"""Test complete drag selection flow"""
|
||||||
|
# Drag start
|
||||||
|
start_data = {
|
||||||
|
'gesture': 'drag_start',
|
||||||
|
'x': 100,
|
||||||
|
'y': 200
|
||||||
|
}
|
||||||
|
|
||||||
|
start_event = TouchEvent.from_hal(start_data)
|
||||||
|
self.assertEqual(start_event.gesture, GestureType.DRAG_START)
|
||||||
|
|
||||||
|
# Drag move
|
||||||
|
move_data = {
|
||||||
|
'gesture': 'drag_move',
|
||||||
|
'x': 300,
|
||||||
|
'y': 250
|
||||||
|
}
|
||||||
|
|
||||||
|
move_event = TouchEvent.from_hal(move_data)
|
||||||
|
self.assertEqual(move_event.gesture, GestureType.DRAG_MOVE)
|
||||||
|
|
||||||
|
# Drag end
|
||||||
|
end_data = {
|
||||||
|
'gesture': 'drag_end',
|
||||||
|
'x': 500,
|
||||||
|
'y': 300
|
||||||
|
}
|
||||||
|
|
||||||
|
end_event = TouchEvent.from_hal(end_data)
|
||||||
|
self.assertEqual(end_event.gesture, GestureType.DRAG_END)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
unittest.main()
|
||||||
Loading…
x
Reference in New Issue
Block a user