#include "touch.h" #define TOUCH_ENTRY_MAX 4 #define TOUCH_HOLD_MS 500 #define TOUCH_HOLD_SLOP_PX 12 #define TOUCH_LOCK_SLOP_PX 8 #define TOUCH_TAP_MS 300 #define TOUCH_TAP_SLOP_PX 12 #define TOUCH_SWIPE_MIN_PX 50 #define TOUCH_SWIPE_MAX_MS 400 typedef enum { GESTURE_NONE, GESTURE_PAN, GESTURE_SWIPE } GestureMode; typedef struct { Window *window; TouchHandlers h; } TouchEntry; static TouchEntry s_entries[TOUCH_ENTRY_MAX]; static AppTimer *s_hold_timer; static TouchEntry *s_gesture_entry; static bool s_tracking; static bool s_hold_fired; static GestureMode s_mode; static GPoint s_down_point; static GPoint s_lock_point; static int64_t s_down_ms; static int64_t now_ms(void) { time_t sec; uint16_t ms; time_ms(&sec, &ms); return (int64_t)sec * 1000 + ms; } static int abs_delta(int16_t from, int16_t to) { int d = to - from; return d < 0 ? -d : d; } static TouchEntry *find_entry(Window *window) { for (int i = 0; i < TOUCH_ENTRY_MAX; i++) { if (s_entries[i].window == window) { return &s_entries[i]; } } return NULL; } static TouchEntry *top_entry(void) { Window *top = window_stack_get_top_window(); return top ? find_entry(top) : NULL; } static void cancel_hold_timer(void) { if (s_hold_timer) { app_timer_cancel(s_hold_timer); s_hold_timer = NULL; } } static void cancel_gesture(void) { cancel_hold_timer(); s_gesture_entry = NULL; s_tracking = false; s_mode = GESTURE_NONE; } static TouchEntry *gesture_entry(void) { if (!s_gesture_entry) { return NULL; } if (s_gesture_entry->window != window_stack_get_top_window()) { cancel_gesture(); return NULL; } return s_gesture_entry; } static void hold_timer_fired(void *data) { (void)data; s_hold_timer = NULL; TouchEntry *e = gesture_entry(); if (!e || !e->h.on_hold) { return; } s_hold_fired = true; e->h.on_hold(s_down_point, e->h.ctx); } static void touchdown(const TouchEvent *event) { cancel_gesture(); if (event->non_navigational) { return; } s_gesture_entry = top_entry(); if (!s_gesture_entry) { return; } s_tracking = true; s_hold_fired = false; s_down_point = GPoint(event->x, event->y); s_down_ms = now_ms(); s_hold_timer = app_timer_register(TOUCH_HOLD_MS, hold_timer_fired, NULL); } static void position_update(const TouchEvent *event) { if (s_hold_timer && (abs_delta(s_down_point.x, event->x) > TOUCH_HOLD_SLOP_PX || abs_delta(s_down_point.y, event->y) > TOUCH_HOLD_SLOP_PX)) { cancel_hold_timer(); } if (s_mode == GESTURE_SWIPE) { return; } TouchEntry *e = s_gesture_entry; if (s_mode == GESTURE_PAN) { if (e->h.on_pan_move) { e->h.on_pan_move(GPoint((int16_t)(event->x - s_lock_point.x), (int16_t)(event->y - s_lock_point.y)), e->h.ctx); } return; } int dx = abs_delta(s_down_point.x, event->x); int dy = abs_delta(s_down_point.y, event->y); if (dx <= TOUCH_LOCK_SLOP_PX && dy <= TOUCH_LOCK_SLOP_PX) { return; } if (dy >= dx) { s_mode = GESTURE_PAN; s_lock_point = GPoint(event->x, event->y); if (e->h.on_pan_move) { e->h.on_pan_move(GPoint(0, 0), e->h.ctx); } } else { s_mode = GESTURE_SWIPE; } } static void liftoff(const TouchEvent *event) { cancel_hold_timer(); s_tracking = false; GestureMode mode = s_mode; s_mode = GESTURE_NONE; TouchEntry *e = s_gesture_entry; s_gesture_entry = NULL; if (!e) { return; } int64_t duration = now_ms() - s_down_ms; int dx = event->x - s_down_point.x; int adx = abs_delta(s_down_point.x, event->x); int ady = abs_delta(s_down_point.y, event->y); if (mode == GESTURE_PAN) { if (e->h.on_pan_end) { e->h.on_pan_end(false, e->h.ctx); } return; } if (mode == GESTURE_SWIPE) { if (e->h.on_swipe && adx >= TOUCH_SWIPE_MIN_PX && adx > 2 * ady && duration <= TOUCH_SWIPE_MAX_MS) { e->h.on_swipe(dx < 0 ? TOUCH_SWIPE_LEFT : TOUCH_SWIPE_RIGHT, e->h.ctx); } return; } if (e->h.on_tap && !s_hold_fired && duration <= TOUCH_TAP_MS && adx <= TOUCH_TAP_SLOP_PX && ady <= TOUCH_TAP_SLOP_PX) { e->h.on_tap(s_down_point, e->h.ctx); } } static void touch_event(const TouchEvent *event, void *context) { (void)context; if (event->type == TouchEvent_Touchdown) { touchdown(event); } else if (!s_tracking || !gesture_entry()) { return; } else if (event->type == TouchEvent_PositionUpdate) { position_update(event); } else if (event->type == TouchEvent_Liftoff) { liftoff(event); } } void touch_init(void) { touch_service_subscribe(touch_event, NULL); } void touch_deinit(void) { cancel_gesture(); touch_service_unsubscribe(); } void touch_attach(Window *window, const TouchHandlers *handlers) { if (!window || !handlers) { return; } TouchEntry *e = find_entry(window); if (!e) { e = find_entry(NULL); } if (!e) { return; } e->window = window; e->h = *handlers; } void touch_detach(Window *window) { TouchEntry *e = window ? find_entry(window) : NULL; if (!e) { return; } if (s_gesture_entry == e) { cancel_gesture(); } *e = (TouchEntry){ .window = NULL }; }