diff --git a/apps/SOURCES b/apps/SOURCES index 0bf42a6999..c7e3a4d9ab 100644 --- a/apps/SOURCES +++ b/apps/SOURCES @@ -11,6 +11,9 @@ core_keymap.c debug_menu.c filetypes.c fileop.c +#ifdef HAVE_TOUCHSCREEN +gesture.c +#endif language.c main.c menu.c diff --git a/apps/gesture.c b/apps/gesture.c new file mode 100644 index 0000000000..ca2e4693a0 --- /dev/null +++ b/apps/gesture.c @@ -0,0 +1,121 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2022 by Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ + +#include "gesture.h" +#include "button.h" +#include "viewport.h" +#include "system.h" + +/* could be a setting */ +#define TOUCH_LONG_PRESS_TIME (30 * HZ / 100) + +void gesture_reset(struct gesture *g) +{ + g->flags = 0; +} + +void gesture_process(struct gesture *g, const struct touchevent *ev) +{ + /* wait for the first press if we haven't seen it */ + if (!gesture_is_pressed(g) && ev->type != TOUCHEVENT_PRESS) + return; + + int dx, dy, dist_sqr; + switch (ev->type) + { + case TOUCHEVENT_PRESS: + g->flags |= GESTURE_F_PRESSED | GESTURE_F_VALID; + g->id = GESTURE_NONE; + g->ox = g->x = ev->x; + g->oy = g->y = ev->y; + g->start_tick = ev->tick; + g->last_tick = ev->tick; + break; + + case TOUCHEVENT_CONTACT: + g->x = ev->x; + g->y = ev->y; + g->last_tick = ev->tick; + + if (g->id == GESTURE_LONG_PRESS) + g->id = GESTURE_HOLD; + else if (g->id == GESTURE_DRAGSTART) + g->id = GESTURE_DRAG; + else if (g->id != GESTURE_DRAG) + { + dx = ev->x - g->ox; + dy = ev->y - g->oy; + dist_sqr = dx*dx + dy*dy; + + /* if squared distance exceeds a threshold, report as a DRAG. */ + const int thresh = touchscreen_get_scroll_threshold(); + if (dist_sqr > thresh*thresh) + g->id = GESTURE_DRAGSTART; + + /* report a LONG_PRESS if no motion occurs within a timeout */ + if (g->id == GESTURE_NONE && + TIME_AFTER(ev->tick, g->start_tick + TOUCH_LONG_PRESS_TIME)) + g->id = GESTURE_LONG_PRESS; + } + + break; + + case TOUCHEVENT_RELEASE: + /* report a RELEASE event after a continuous HOLD or DRAG */ + if (g->id == GESTURE_HOLD || + g->id == GESTURE_DRAGSTART || + g->id == GESTURE_DRAG) + g->id = GESTURE_RELEASE; + + /* report a TAP event if we got a press & release without + * triggering any other gestures */ + else if (g->id == GESTURE_NONE) + g->id = GESTURE_TAP; + + g->flags &= ~GESTURE_F_PRESSED; + g->last_tick = ev->tick; + break; + } +} + +bool gesture_get_event_in_vp(struct gesture *g, struct gesture_event *gevt, + const struct viewport *vp) +{ + if (!gesture_is_valid(g)) + return false; + + gevt->id = g->id; + gevt->x = g->x; + gevt->y = g->y; + gevt->ox = g->ox; + gevt->oy = g->oy; + gevt->start_tick = g->start_tick; + gevt->last_tick = g->last_tick; + + if (vp) { + gevt->x -= vp->x; + gevt->y -= vp->y; + gevt->ox -= vp->x; + gevt->oy -= vp->y; + } + + return !vp || viewport_point_within_vp(vp, g->ox, g->oy); +} diff --git a/apps/gesture.h b/apps/gesture.h new file mode 100644 index 0000000000..7e6c32bc6e --- /dev/null +++ b/apps/gesture.h @@ -0,0 +1,123 @@ +/*************************************************************************** + * __________ __ ___. + * Open \______ \ ____ ____ | | _\_ |__ _______ ___ + * Source | _// _ \_/ ___\| |/ /| __ \ / _ \ \/ / + * Jukebox | | ( <_> ) \___| < | \_\ ( <_> > < < + * Firmware |____|_ /\____/ \___ >__|_ \|___ /\____/__/\_ \ + * \/ \/ \/ \/ \/ + * $Id$ + * + * Copyright (C) 2022 Aidan MacDonald + * + * This program is free software; you can redistribute it and/or + * modify it under the terms of the GNU General Public License + * as published by the Free Software Foundation; either version 2 + * of the License, or (at your option) any later version. + * + * This software is distributed on an "AS IS" basis, WITHOUT WARRANTY OF ANY + * KIND, either express or implied. + * + ****************************************************************************/ +#ifndef _GESTURE_H_ +#define _GESTURE_H_ + +#include +#include + +struct viewport; +struct touchevent; + +/** Events which can be detected by the gesture API. + * + * The gesture state machine, informally, looks like this: + * + * NONE --+--> TAP ------------------------------------+ + * | | + * +--> LONG_PRESS -----------------------------+ + * | | | + * | +---> HOLD ---+ | + * | | | | + * +-------------+---> DRAG ---+--> RELEASE ----+--> NONE + * + * State transitions occur from gesture_process() in response to touch events. + * + * - The NONE "event" is returned prior to any other gesture being detected. + * Although the graph above depicts a transition back to NONE at the end of + * an event chain, that transition in fact happens on the TOUCHEVENT_PRESS + * after the end of the last event. + * + * - TAP events are reported after getting a TOUCHEVENT_RELEASE event if no + * other gestures were detected between the press and release. + * + * - LONG_PRESS events are reported on a TOUCHEVENT_CONTACT event if the long + * press timeout has expired and the touch point hasn't moved too far from + * its initial position. + * + * - HOLD events are reported on TOUCHEVENT_CONTACT events after a LONG_PRESS, + * provided the touch point hasn't moved too far from its initial position. + * + * - DRAG events are reported on TOUCHEVENT_CONTACT events after the touch + * point first moves a certain distance from its initial position. The first + * time this happens, it is reported as DRAGSTART. + * + * - RELEASE events are reported on the TOUCHEVENT_RELEASE event following a + * HOLD or DRAG gesture. + */ +enum gesture_id +{ + GESTURE_NONE = 0, /** No gesture */ + GESTURE_TAP, /** Quick press & release */ + GESTURE_LONG_PRESS, /** Start of a long press */ + GESTURE_HOLD, /** Continuation of a long press */ + GESTURE_DRAGSTART, /** Start of a DRAG event */ + GESTURE_DRAG, /** Press and drag on the screen */ + GESTURE_RELEASE, /** End of a HOLD or DRAG event */ +}; + +enum gesture_flags +{ + GESTURE_F_VALID = 0x01, + GESTURE_F_PRESSED = 0x02, +}; + +struct gesture +{ + unsigned int flags; + int id; + short x, y; + short ox, oy; + long start_tick; + long last_tick; +}; + +struct gesture_event +{ + int id; + short x, y; + short ox, oy; + long start_tick; + long last_tick; +}; + +void gesture_reset(struct gesture *g); +void gesture_process(struct gesture *g, const struct touchevent *ev); +bool gesture_get_event_in_vp(struct gesture *g, struct gesture_event *gevt, + const struct viewport *vp); + +static inline bool gesture_get_event(struct gesture *g, + struct gesture_event *gevt) +{ + return gesture_get_event_in_vp(g, gevt, NULL); +} + +static inline bool gesture_is_valid(struct gesture *g) +{ + return !!(g->flags & GESTURE_F_VALID); +} + +static inline bool gesture_is_pressed(struct gesture *g) +{ + return !!(g->flags & GESTURE_F_PRESSED); +} + +#endif /* _GESTURE_H_ */