From 3d0f88fcf9a262680c19bd5c44e407a02c409dcf Mon Sep 17 00:00:00 2001 From: Jaakko Keränen Date: Thu, 18 Feb 2021 21:41:46 +0200 Subject: iOS: Working on touch event handling Handle finger down, motion, and up events to implement basic taps, long presses, and inertia scrolling. Much finetuning still to be done, and certain widgets like input fields and scrollbars need a direct drag mode (they were working fine via the "mouse" events already). --- CMakeLists.txt | 5 + res/MacOSXBundleInfo.plist.in | 13 ++ src/app.c | 6 + src/ios.h | 1 + src/ios.m | 12 ++ src/ui/documentwidget.c | 9 +- src/ui/indicatorwidget.c | 1 + src/ui/sidebarwidget.c | 2 +- src/ui/text.c | 2 +- src/ui/touch.c | 278 ++++++++++++++++++++++++++++++++++++++++++ src/ui/touch.h | 29 +++++ src/ui/widget.c | 3 + src/ui/widget.h | 1 + src/ui/window.c | 4 + 14 files changed, 361 insertions(+), 5 deletions(-) create mode 100644 src/ui/touch.c create mode 100644 src/ui/touch.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ccdb1cc1..216ccfc0 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -150,6 +150,8 @@ set (SOURCES src/ui/sidebarwidget.h src/ui/text.c src/ui/text.h + src/ui/touch.c + src/ui/touch.h src/ui/util.c src/ui/util.h src/ui/visbuf.c @@ -230,6 +232,9 @@ target_link_libraries (app PUBLIC ${SDL2_LDFLAGS}) if (APPLE) if (IOS) target_link_libraries (app PUBLIC "-framework UIKit") + set_target_properties (app PROPERTIES + XCODE_ATTRIBUTE_TARGETED_DEVICE_FAMILY 2 + ) else () target_link_libraries (app PUBLIC "-framework AppKit") endif () diff --git a/res/MacOSXBundleInfo.plist.in b/res/MacOSXBundleInfo.plist.in index 1d769768..1883db5f 100644 --- a/res/MacOSXBundleInfo.plist.in +++ b/res/MacOSXBundleInfo.plist.in @@ -34,6 +34,19 @@ NSRequiresAquaSystemAppearance + UISupportedInterfaceOrientations + + UIInterfaceOrientationPortrait + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + + UISupportedInterfaceOrientations~ipad + + UIInterfaceOrientationPortrait + UIInterfaceOrientationPortraitUpsideDown + UIInterfaceOrientationLandscapeLeft + UIInterfaceOrientationLandscapeRight + CFBundleDocumentTypes diff --git a/src/app.c b/src/app.c index b5eb5688..ae0633e5 100644 --- a/src/app.c +++ b/src/app.c @@ -64,6 +64,9 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #if defined (iPlatformAppleDesktop) # include "macos.h" #endif +#if defined (iPlatformAppleMobile) +# include "ios.h" +#endif #if defined (iPlatformMsys) # include "win32.h" #endif @@ -464,6 +467,9 @@ static void init_App_(iApp *d, int argc, char **argv) { #endif #if defined (iPlatformAppleDesktop) setupApplication_MacOS(); +#endif +#if defined (iPlatformAppleMobile) + setupApplication_iOS(); #endif init_Keys(); loadPrefs_App_(d); diff --git a/src/ios.h b/src/ios.h index 7217e74a..09c514b0 100644 --- a/src/ios.h +++ b/src/ios.h @@ -24,4 +24,5 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ui/util.h" +void setupApplication_iOS (void); diff --git a/src/ios.m b/src/ios.m index 029bd068..37f62b09 100644 --- a/src/ios.m +++ b/src/ios.m @@ -21,3 +21,15 @@ ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "ios.h" + +#include + +static void enableMouse_(iBool yes) { + SDL_EventState(SDL_MOUSEBUTTONDOWN, yes); + SDL_EventState(SDL_MOUSEMOTION, yes); + SDL_EventState(SDL_MOUSEBUTTONUP, yes); +} + +void setupApplication_iOS(void) { + enableMouse_(iFalse); +} diff --git a/src/ui/documentwidget.c b/src/ui/documentwidget.c index 232b4140..0a282f1b 100644 --- a/src/ui/documentwidget.c +++ b/src/ui/documentwidget.c @@ -2351,7 +2351,7 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e which device is sending the event. */ if (ev->wheel.which == 0) { /* Trackpad with precise scrolling w/inertia. */ stop_Anim(&d->scrollY); - iInt2 wheel = init_I2(ev->wheel.x, ev->wheel.y); + iInt2 wheel = mulf_I2(init_I2(ev->wheel.x, ev->wheel.y), get_Window()->pixelRatio); /* Only scroll on one axis at a time. */ if (iAbs(wheel.x) > iAbs(wheel.y)) { wheel.y = 0; @@ -2359,8 +2359,11 @@ static iBool processEvent_DocumentWidget_(iDocumentWidget *d, const SDL_Event *e else { wheel.x = 0; } - scroll_DocumentWidget_(d, -wheel.y * get_Window()->pixelRatio); - scrollWideBlock_DocumentWidget_(d, mouseCoord, wheel.x * get_Window()->pixelRatio, 0); + scroll_DocumentWidget_(d, -wheel.y); +#if defined (iPlatformAppleMobile) + wheel.x = -wheel.x; +#endif + scrollWideBlock_DocumentWidget_(d, mouseCoord, wheel.x, 0); } else #endif diff --git a/src/ui/indicatorwidget.c b/src/ui/indicatorwidget.c index 96556912..4a829ae3 100644 --- a/src/ui/indicatorwidget.c +++ b/src/ui/indicatorwidget.c @@ -72,6 +72,7 @@ void init_IndicatorWidget(iIndicatorWidget *d) { iWidget *w = &d->widget; init_Widget(w); init_Anim(&d->pos, 0); + setFlags_Widget(w, unhittable_WidgetFlag, iTrue); } static void startTimer_IndicatorWidget_(iIndicatorWidget *d) { diff --git a/src/ui/sidebarwidget.c b/src/ui/sidebarwidget.c index 679d8e6f..32727703 100644 --- a/src/ui/sidebarwidget.c +++ b/src/ui/sidebarwidget.c @@ -322,7 +322,7 @@ static void updateItems_SidebarWidget_(iSidebarWidget *d) { iConstForEach(PtrArray, i, identities_GmCerts(certs_App())) { const iGmIdentity *ident = i.ptr; iSidebarItem *item = new_SidebarItem(); - item->id = index_PtrArrayConstIterator(&i); + item->id = (uint32_t) index_PtrArrayConstIterator(&i); item->icon = ident->icon; set_String(&item->label, collect_String(subject_TlsCertificate(ident->cert))); iDate until; diff --git a/src/ui/text.c b/src/ui/text.c index 65c7a256..7bb65bdc 100644 --- a/src/ui/text.c +++ b/src/ui/text.c @@ -668,7 +668,7 @@ static iChar nextChar_(const char **chPos, const char *end) { } static enum iFontId fontId_Text_(const iFont *font) { - return font - text_.fonts; + return (enum iFontId) (font - text_.fonts); } iLocalDef iBool isWrapBoundary_(iChar prevC, iChar c) { diff --git a/src/ui/touch.c b/src/ui/touch.c new file mode 100644 index 00000000..12cd1745 --- /dev/null +++ b/src/ui/touch.c @@ -0,0 +1,278 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#include "touch.h" +#include "window.h" +#include "app.h" + +#include +#include +#include + +iDeclareType(Touch) +iDeclareType(TouchState) +iDeclareType(Momentum) + +struct Impl_Touch { + SDL_FingerID id; + iWidget *affinity; /* widget on which the touch started */ + iBool hasMoved; + uint32_t startTime; + iFloat3 startPos; + uint32_t posTime[2]; + iFloat3 pos[2]; + iFloat3 remainder; +}; + +iLocalDef void pushPos_Touch_(iTouch *d, const iFloat3 pos, uint32_t time) { + d->posTime[1] = d->posTime[0]; + d->posTime[0] = time; + d->pos[1] = d->pos[0]; + d->pos[0] = pos; +} + +struct Impl_Momentum { + iWidget *affinity; + uint32_t releaseTime; + iFloat3 velocity; + iFloat3 accum; +}; + +struct Impl_TouchState { + iArray *touches; + iArray *moms; + double lastMomTime; +}; + +static iTouchState *touchState_(void) { + static iTouchState state_; + iTouchState *d = &state_; + if (!d->touches) { + d->touches = new_Array(sizeof(iTouch)); + d->moms = new_Array(sizeof(iMomentum)); + d->lastMomTime = SDL_GetTicks(); + } + return d; +} + +static iTouch *find_TouchState_(iTouchState *d, SDL_FingerID id) { + iConstForEach(Array, i, d->touches) { + iTouch *touch = (iTouch *) i.value; + if (touch->id == id) { + return touch; + } + } + return NULL; +} + +static uint32_t longPressSpanMs_ = 500; +static int tapRadiusPt_ = 15; + +static iBool isStationary_Touch_(const iTouch *d) { + return !d->hasMoved && + length_F3(sub_F3(d->pos[0], d->startPos)) < tapRadiusPt_ * get_Window()->pixelRatio; +} + +static void dispatchClick_Touch_(const iTouch *d, int button) { + const iFloat3 tapPos = divf_F3(add_F3(d->pos[0], d->startPos), 2); + SDL_MouseButtonEvent btn = { + .type = SDL_MOUSEBUTTONDOWN, + .button = button, + .clicks = 1, + .state = SDL_PRESSED, + .timestamp = SDL_GetTicks(), + .which = SDL_TOUCH_MOUSEID, + .x = x_F3(tapPos), + .y = y_F3(tapPos) + }; + dispatchEvent_Widget(get_Window()->root, (SDL_Event *) &btn); + /* Immediately released, too. */ + btn.type = SDL_MOUSEBUTTONUP; + btn.state = SDL_RELEASED; + btn.timestamp = SDL_GetTicks(); + dispatchEvent_Widget(get_Window()->root, (SDL_Event *) &btn); +} + +static void clearWidgetMomentum_TouchState_(iTouchState *d, iWidget *widget) { + if (!widget) return; + iForEach(Array, m, d->moms) { + iMomentum *mom = m.value; + if (mom->affinity == widget) { + remove_ArrayIterator(&m); + } + } +} + +static void update_TouchState_(void *ptr) { + iTouchState *d = ptr; + const uint32_t nowTime = SDL_GetTicks(); + /* Check for long presses to simulate right clicks. */ + iForEach(Array, i, d->touches) { + iTouch *touch = i.value; + /* Holding a touch will reset previous momentum for this widget. */ + if (isStationary_Touch_(touch)) { + if (nowTime - touch->startTime > 25) { + clearWidgetMomentum_TouchState_(d, touch->affinity); + } + if (nowTime - touch->startTime >= longPressSpanMs_ && touch->affinity) { + dispatchClick_Touch_(touch, SDL_BUTTON_RIGHT); + remove_ArrayIterator(&i); + } + } + } + /* Update/cancel momentum scrolling. */ { + const float minSpeed = 10.0f; + const float momFriction = 0.975f; + const float stepDurationMs = 1000.0f / 120.0f; + double momAvailMs = nowTime - d->lastMomTime; + int numSteps = iMin((int) (momAvailMs / stepDurationMs), 10); + d->lastMomTime += numSteps * stepDurationMs; +// printf("mom steps:%d\n", numSteps); + iForEach(Array, m, d->moms) { + if (numSteps == 0) break; + iMomentum *mom = m.value; + for (int step = 0; step < numSteps; step++) { + mulvf_F3(&mom->velocity, momFriction); + addv_F3(&mom->accum, mulf_F3(mom->velocity, stepDurationMs / 1000.0f)); + } + const iInt2 pixels = initF3_I2(mom->accum); + if (pixels.x || pixels.y) { + subv_F3(&mom->accum, initI2_F3(pixels)); + dispatchEvent_Widget(mom->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ + .type = SDL_MOUSEWHEEL, + .timestamp = SDL_GetTicks(), + .which = 0, /* means "precise scrolling" in DocumentWidget */ + .x = pixels.x, + .y = pixels.y + }); + } + //printf("mom vel:%f\n", length_F3(mom->velocity)); + if (length_F3(mom->velocity) < minSpeed) { + remove_ArrayIterator(&m); + } + } + } + /* Keep updating if interaction is still ongoing. */ + if (!isEmpty_Array(d->touches) || !isEmpty_Array(d->moms)) { + addTicker_App(update_TouchState_, ptr); + } +} + +iBool processEvent_Touch(const SDL_Event *ev) { + /* We only handle finger events here. */ + if (ev->type != SDL_FINGERDOWN && ev->type != SDL_FINGERMOTION && ev->type != SDL_FINGERUP) { + return iFalse; + } + iTouchState *d = touchState_(); + const SDL_TouchFingerEvent *fing = &ev->tfinger; + iWindow *window = get_Window(); + const iInt2 rootSize = rootSize_Window(window); + const iFloat3 pos = init_F3(fing->x * rootSize.x, fing->y * rootSize.y, 0); + //printf("%2d: %f: touch %f, %f\n", ev->type, z_F3(pos), x_F3(pos), y_F3(pos)); + //fflush(stdout); + const uint32_t nowTime = SDL_GetTicks(); + if (ev->type == SDL_FINGERDOWN) { + /* Register the new touch. */ + iWidget *aff = hitChild_Widget(window->root, init_I2(iRound(x_F3(pos)), iRound(y_F3(pos)))); + pushBack_Array(d->touches, &(iTouch){ + .id = fing->fingerId, + .affinity = aff, + .startTime = nowTime, + .startPos = pos, + .pos = pos + }); + /* Some widgets rely on hover state. */ + dispatchEvent_Widget(window->root, (SDL_Event *) &(SDL_MouseMotionEvent){ + .type = SDL_MOUSEMOTION, + .timestamp = SDL_GetTicks(), + .which = SDL_TOUCH_MOUSEID, + .x = x_F3(pos), + .y = y_F3(pos) + }); + addTicker_App(update_TouchState_, d); + } + else if (ev->type == SDL_FINGERMOTION) { + iTouch *touch = find_TouchState_(d, fing->fingerId); + if (touch && touch->affinity) { + /* TODO: Update touch position. */ + const iFloat3 amount = add_F3(touch->remainder, + divf_F3(mul_F3(init_F3(fing->dx, fing->dy, 0), + init_F3(rootSize.x, rootSize.y, 0)), + window->pixelRatio)); + const iInt2 pixels = init_I2(iRound(x_F3(amount)), iRound(y_F3(amount))); + iFloat3 remainder = sub_F3(amount, initI2_F3(pixels)); + touch->remainder = remainder; + pushPos_Touch_(touch, pos, nowTime); + if (!touch->hasMoved && !isStationary_Touch_(touch)) { + touch->hasMoved = iTrue; + } + if (pixels.x || pixels.y) { +// printf("%p (%s) wy: %f\n", touch->affinity, class_Widget(touch->affinity)->name, +// fing->dy * rootSize.y / window->pixelRatio); + dispatchEvent_Widget(touch->affinity, (SDL_Event *) &(SDL_MouseWheelEvent){ + .type = SDL_MOUSEWHEEL, + .timestamp = SDL_GetTicks(), + .which = 0, /* means "precise scrolling" in DocumentWidget */ + .x = pixels.x, + .y = pixels.y + }); + /* TODO: Keep increasing movement if the direction is the same. */ + clearWidgetMomentum_TouchState_(d, touch->affinity); + } + } + } + else if (ev->type == SDL_FINGERUP) { + iTouch *touch = find_TouchState_(d, fing->fingerId); + iForEach(Array, i, d->touches) { + iTouch *touch = i.value; + if (touch->id != fing->fingerId) { + continue; + } + const uint32_t elapsed = nowTime - touch->posTime[1]; + iFloat3 velocity = zero_F3(); + if (elapsed < 50) { + velocity = divf_F3(sub_F3(pos, touch->pos[1]), (float) elapsed / 1000.0f); + } + pushPos_Touch_(touch, pos, nowTime); + iBool wasUsed = iFalse; + const uint32_t duration = nowTime - touch->startTime; + /* If short and didn't move far, do a tap (left click). */ + if (duration < longPressSpanMs_ && isStationary_Touch_(touch)) { + dispatchClick_Touch_(touch, SDL_BUTTON_LEFT); + } + else if (length_F3(velocity) > 10.0f) { + clearWidgetMomentum_TouchState_(d, touch->affinity); + iMomentum mom = { + .affinity = touch->affinity, + .releaseTime = nowTime, + .velocity = velocity + }; + if (isEmpty_Array(d->moms)) { + d->lastMomTime = nowTime; + } + pushBack_Array(d->moms, &mom); + } + remove_ArrayIterator(&i); + } + } + return iTrue; +} diff --git a/src/ui/touch.h b/src/ui/touch.h new file mode 100644 index 00000000..fb8ff555 --- /dev/null +++ b/src/ui/touch.h @@ -0,0 +1,29 @@ +/* Copyright 2021 Jaakko Keränen + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND +ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED +WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR +ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES +(INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; +LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON +ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ + +#pragma once + +#include +#include + +iBool processEvent_Touch (const SDL_Event *); +void update_Touch (void); diff --git a/src/ui/widget.c b/src/ui/widget.c index b60c67e3..5afb004f 100644 --- a/src/ui/widget.c +++ b/src/ui/widget.c @@ -731,6 +731,9 @@ size_t childIndex_Widget(const iWidget *d, const iAnyObject *child) { } iAny *hitChild_Widget(const iWidget *d, iInt2 coord) { + if (d->flags & unhittable_WidgetFlag) { + return NULL; + } iConstForEach(ObjectList, i, d->children) { iAny *found = hitChild_Widget(constAs_Widget(i.object), coord); if (found) return found; diff --git a/src/ui/widget.h b/src/ui/widget.h index 79f68b3c..a9756793 100644 --- a/src/ui/widget.h +++ b/src/ui/widget.h @@ -91,6 +91,7 @@ enum iWidgetFlag { #define borderTop_WidgetFlag iBit64(37) #define overflowScrollable_WidgetFlag iBit64(38) #define focusRoot_WidgetFlag iBit64(39) +#define unhittable_WidgetFlag iBit64(40) enum iWidgetAddPos { back_WidgetAddPos, diff --git a/src/ui/window.c b/src/ui/window.c index 563d57ae..d64e7ebf 100644 --- a/src/ui/window.c +++ b/src/ui/window.c @@ -33,6 +33,7 @@ SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "paint.h" #include "util.h" #include "keys.h" +#include "touch.h" #include "../app.h" #include "../visited.h" #include "../gmcerts.h" @@ -1246,6 +1247,9 @@ iBool processEvent_Window(iWindow *d, const SDL_Event *ev) { postRefresh_App(); return iTrue; } + if (processEvent_Touch(&event)) { + return iTrue; + } if (event.type == SDL_KEYDOWN && SDL_GetTicks() - d->focusGainedAt < 10) { /* Suspiciously close to when input focus was received. For example under openbox, closing xterm with Ctrl+D will cause the keydown event to "spill" over to us. -- cgit v1.2.3