Hook up pinch-zoom and swipe gestures.

Sets UIPinchGestureRecognizer and UISwipeGestureRecognizers and
passes the result down to the sk_app::Window. To simplify detection,
swipes take precedence over pans, and pans require a single touch.
This is less flexible for the app, but in most cases I think is
what we want.

Bug: skia:8737
Change-Id: Ib031b6ad465d3a353da29d7e0b48a666d4ff8b9a
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/239776
Commit-Queue: Jim Van Verth <jvanverth@google.com>
Reviewed-by: Brian Osman <brianosman@google.com>
diff --git a/samplecode/Sample.cpp b/samplecode/Sample.cpp
index ab655b1..c2ba788 100644
--- a/samplecode/Sample.cpp
+++ b/samplecode/Sample.cpp
@@ -97,6 +97,10 @@
                 return result;
             }
             return false;
+        default:
+            // Ignore other cases
+            SkASSERT(false);
+            break;
     }
     SkASSERT(false);
     return false;
diff --git a/tools/sk_app/Window.cpp b/tools/sk_app/Window.cpp
index 56ffbb6..2ccad55 100644
--- a/tools/sk_app/Window.cpp
+++ b/tools/sk_app/Window.cpp
@@ -60,6 +60,14 @@
     return this->signalLayers([=](Layer* layer) { return layer->onTouch(owner, state, x, y); });
 }
 
+bool Window::onFling(skui::InputState state) {
+    return this->signalLayers([=](Layer* layer) { return layer->onFling(state); });
+}
+
+bool Window::onPinch(skui::InputState state, float scale, float x, float y) {
+    return this->signalLayers([=](Layer* layer) { return layer->onPinch(state, scale, x, y); });
+}
+
 void Window::onUIStateChanged(const SkString& stateName, const SkString& stateValue) {
     this->visitLayers([=](Layer* layer) { layer->onUIStateChanged(stateName, stateValue); });
 }
diff --git a/tools/sk_app/Window.h b/tools/sk_app/Window.h
index b46a13b..76be662 100644
--- a/tools/sk_app/Window.h
+++ b/tools/sk_app/Window.h
@@ -87,6 +87,9 @@
         virtual bool onMouse(int x, int y, skui::InputState, skui::ModifierKey) { return false; }
         virtual bool onMouseWheel(float delta, skui::ModifierKey) { return false; }
         virtual bool onTouch(intptr_t owner, skui::InputState, float x, float y) { return false; }
+        // Platform-detected gesture events
+        virtual bool onFling(skui::InputState state) { return false; }
+        virtual bool onPinch(skui::InputState state, float scale, float x, float y) { return false; }
         virtual void onUIStateChanged(const SkString& stateName, const SkString& stateValue) {}
         virtual void onPrePaint() {}
         virtual void onPaint(SkSurface*) {}
@@ -108,6 +111,9 @@
     bool onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers);
     bool onMouseWheel(float delta, skui::ModifierKey modifiers);
     bool onTouch(intptr_t owner, skui::InputState state, float x, float y);  // multi-owner = multi-touch
+    // Platform-detected gesture events
+    bool onFling(skui::InputState state);
+    bool onPinch(skui::InputState state, float scale, float x, float y);
     void onUIStateChanged(const SkString& stateName, const SkString& stateValue);
     void onPaint();
     void onResize(int width, int height);
diff --git a/tools/sk_app/ios/Window_ios.mm b/tools/sk_app/ios/Window_ios.mm
index 39037c4..82d8a73 100644
--- a/tools/sk_app/ios/Window_ios.mm
+++ b/tools/sk_app/ios/Window_ios.mm
@@ -43,7 +43,7 @@
         return true;
     }
 
-    // Create a delegate to track certain events
+    // Create a view controller to track certain events
     WindowViewController* viewController = [[WindowViewController alloc] initWithWindow:this];
     if (nil == viewController) {
         return false;
@@ -145,7 +145,7 @@
     switch (sender.state) {
         case UIGestureRecognizerStateBegan:
             fWindow->onMouse(location.x, location.y,
-                             skui::InputState::kDown,skui::ModifierKey::kNone);
+                             skui::InputState::kDown, skui::ModifierKey::kNone);
             break;
         case UIGestureRecognizerStateChanged:
             fWindow->onMouse(location.x, location.y,
@@ -178,10 +178,45 @@
     }
 }
 
+- (IBAction)pinchGestureAction:(UIGestureRecognizer*)sender {
+    CGPoint location = [sender locationInView:self];
+    UIPinchGestureRecognizer* pinchGestureRecognizer = (UIPinchGestureRecognizer*) sender;
+    float scale = pinchGestureRecognizer.scale;
+    switch (sender.state) {
+        case UIGestureRecognizerStateBegan:
+            fWindow->onPinch(skui::InputState::kDown, scale, location.x, location.y);
+            break;
+        case UIGestureRecognizerStateChanged:
+            fWindow->onPinch(skui::InputState::kMove, scale, location.x, location.y);
+            break;
+        case UIGestureRecognizerStateEnded:
+            fWindow->onPinch(skui::InputState::kUp, scale, location.x, location.y);
+            break;
+        case UIGestureRecognizerStateCancelled:
+            fWindow->onPinch(skui::InputState::kUp, scale, location.x, location.y);
+            break;
+        default:
+            break;
+    }
+}
+
+- (IBAction)swipeRightGestureAction:(UIGestureRecognizer*)sender {
+    if (UIGestureRecognizerStateEnded == sender.state) {
+        fWindow->onFling(skui::InputState::kRight);
+    }
+}
+
+- (IBAction)swipeLeftGestureAction:(UIGestureRecognizer*)sender {
+    if (UIGestureRecognizerStateEnded == sender.state) {
+        fWindow->onFling(skui::InputState::kLeft);
+    }
+}
+
 - (MainView*)initWithWindow:(sk_app::Window_ios *)initWindow {
     self = [super init];
 
     UIPanGestureRecognizer* panGestureRecognizer = [[UIPanGestureRecognizer alloc] init];
+    panGestureRecognizer.maximumNumberOfTouches = 1;
     [panGestureRecognizer addTarget:self action:@selector(panGestureAction:)];
     [self addGestureRecognizer:panGestureRecognizer];
 
@@ -189,6 +224,24 @@
     [tapGestureRecognizer addTarget:self action:@selector(tapGestureAction:)];
     [self addGestureRecognizer:tapGestureRecognizer];
 
+    UIPinchGestureRecognizer* pinchGestureRecognizer = [[UIPinchGestureRecognizer alloc] init];
+    [pinchGestureRecognizer addTarget:self action:@selector(pinchGestureAction:)];
+    [self addGestureRecognizer:pinchGestureRecognizer];
+
+    UISwipeGestureRecognizer* swipeRightGestureRecognizer = [[UISwipeGestureRecognizer alloc] init];
+    swipeRightGestureRecognizer.direction = UISwipeGestureRecognizerDirectionRight;
+    [swipeRightGestureRecognizer addTarget:self action:@selector(swipeRightGestureAction:)];
+    [self addGestureRecognizer:swipeRightGestureRecognizer];
+
+    UISwipeGestureRecognizer* swipeLeftGestureRecognizer = [[UISwipeGestureRecognizer alloc] init];
+    swipeLeftGestureRecognizer.direction = UISwipeGestureRecognizerDirectionLeft;
+    [swipeLeftGestureRecognizer addTarget:self action:@selector(swipeLeftGestureAction:)];
+    [self addGestureRecognizer:swipeLeftGestureRecognizer];
+
+    // disable pan recognition until swipes fail
+    [panGestureRecognizer requireGestureRecognizerToFail:swipeLeftGestureRecognizer];
+    [panGestureRecognizer requireGestureRecognizerToFail:swipeRightGestureRecognizer];
+
     fWindow = initWindow;
 
     return self;
diff --git a/tools/skui/InputState.h b/tools/skui/InputState.h
index 75821d4..90d5c6c 100644
--- a/tools/skui/InputState.h
+++ b/tools/skui/InputState.h
@@ -6,7 +6,9 @@
 enum class InputState {
     kDown,
     kUp,
-    kMove   // only valid for mouse
+    kMove,   // only valid for mouse
+    kRight,  // only valid for fling
+    kLeft,   // only valid for fling
 };
 }
 #endif  // skui_inputstate_DEFINED
diff --git a/tools/viewer/SKPSlide.cpp b/tools/viewer/SKPSlide.cpp
index 5b4d312..1c03a5e 100644
--- a/tools/viewer/SKPSlide.cpp
+++ b/tools/viewer/SKPSlide.cpp
@@ -49,7 +49,9 @@
 
 void SKPSlide::load(SkScalar, SkScalar) {
     fPic = read_picture(fPath.c_str());
-    fCullRect = fPic->cullRect().roundOut();
+    if (fPic) {
+        fCullRect = fPic->cullRect().roundOut();
+    }
 }
 
 void SKPSlide::unload() {
diff --git a/tools/viewer/TouchGesture.cpp b/tools/viewer/TouchGesture.cpp
index 249aa44..4bef1f2 100644
--- a/tools/viewer/TouchGesture.cpp
+++ b/tools/viewer/TouchGesture.cpp
@@ -169,7 +169,7 @@
             fState = kTranslate_State;
             break;
         case 2:
-            fState = kZoom_State;
+            this->startZoom();
             break;
         default:
             break;
@@ -204,6 +204,24 @@
     return scale;
 }
 
+void TouchGesture::startZoom() {
+    fState = kZoom_State;
+}
+
+void TouchGesture::updateZoom(float scale, float startX, float startY, float lastX, float lastY) {
+    scale = this->limitTotalZoom(scale);
+
+    fLocalM.setTranslate(-startX, -startY);
+    fLocalM.postScale(scale, scale);
+    fLocalM.postTranslate(lastX, lastY);
+}
+
+void TouchGesture::endZoom() {
+    this->flushLocalM();
+    SkASSERT(kZoom_State == fState);
+    fState = kEmpty_State;
+}
+
 void TouchGesture::touchMoved(void* owner, float x, float y) {
 //    SkDebugf("--- %d touchMoved %p %g %g\n", fTouches.count(), owner, x, y);
 
@@ -246,13 +264,11 @@
             const Rec& rec1 = fTouches[1];
 
             float scale = this->computePinch(rec0, rec1);
-            scale = this->limitTotalZoom(scale);
-
-            fLocalM.setTranslate(-center(rec0.fStartX, rec1.fStartX),
-                                 -center(rec0.fStartY, rec1.fStartY));
-            fLocalM.postScale(scale, scale);
-            fLocalM.postTranslate(center(rec0.fLastX, rec1.fLastX),
-                                  center(rec0.fLastY, rec1.fLastY));
+            this->updateZoom(scale,
+                             center(rec0.fStartX, rec1.fStartX),
+                             center(rec0.fStartY, rec1.fStartY),
+                             center(rec0.fLastX, rec1.fLastX),
+                             center(rec0.fLastY, rec1.fLastY));
         } break;
         default:
             break;
@@ -286,9 +302,7 @@
             fState = kEmpty_State;
         } break;
         case 2:
-            this->flushLocalM();
-            SkASSERT(kZoom_State == fState);
-            fState = kEmpty_State;
+            this->endZoom();
             break;
         default:
             SkASSERT(kZoom_State == fState);
diff --git a/tools/viewer/TouchGesture.h b/tools/viewer/TouchGesture.h
index f6cdb3a..475b438 100644
--- a/tools/viewer/TouchGesture.h
+++ b/tools/viewer/TouchGesture.h
@@ -27,6 +27,10 @@
     bool isBeingTouched() { return kEmpty_State != fState; }
     bool isFling(SkPoint* dir);
 
+    void startZoom();
+    void updateZoom(float scale, float startX, float startY, float lastX, float lastY);
+    void endZoom();
+
     const SkMatrix& localM();
     const SkMatrix& globalM() const { return fGlobalM; }
 
diff --git a/tools/viewer/Viewer.cpp b/tools/viewer/Viewer.cpp
index 601856c..6349113 100644
--- a/tools/viewer/Viewer.cpp
+++ b/tools/viewer/Viewer.cpp
@@ -1444,6 +1444,11 @@
             fGesture.touchMoved(castedOwner, x, y);
             break;
         }
+        default: {
+            // kLeft and kRight are only for swipes
+            SkASSERT(false);
+            break;
+        }
     }
     fGestureDevice = fGesture.isBeingTouched() ? GestureDevice::kTouch : GestureDevice::kNone;
     fWindow->inval();
@@ -1474,6 +1479,10 @@
             fGesture.touchMoved(nullptr, x, y);
             break;
         }
+        default: {
+            SkASSERT(false); // shouldn't see kRight or kLeft here
+            break;
+        }
     }
     fGestureDevice = fGesture.isBeingTouched() ? GestureDevice::kMouse : GestureDevice::kNone;
 
@@ -1483,6 +1492,39 @@
     return true;
 }
 
+bool Viewer::onFling(skui::InputState state) {
+    if (skui::InputState::kRight == state) {
+        this->setCurrentSlide(fCurrentSlide > 0 ? fCurrentSlide - 1 : fSlides.count() - 1);
+        return true;
+    } else if (skui::InputState::kLeft == state) {
+        this->setCurrentSlide(fCurrentSlide < fSlides.count() - 1 ? fCurrentSlide + 1 : 0);
+        return true;
+    }
+    return false;
+}
+
+bool Viewer::onPinch(skui::InputState state, float scale, float x, float y) {
+    switch (state) {
+        case skui::InputState::kDown:
+            fGesture.startZoom();
+            return true;
+            break;
+        case skui::InputState::kMove:
+            fGesture.updateZoom(scale, x, y, x, y);
+            return true;
+            break;
+        case skui::InputState::kUp:
+            fGesture.endZoom();
+            return true;
+            break;
+        default:
+            SkASSERT(false);
+            break;
+    }
+
+    return false;
+}
+
 static void ImGui_Primaries(SkColorSpacePrimaries* primaries, SkPaint* gamutPaint) {
     // The gamut image covers a (0.8 x 0.9) shaped region
     ImGui::DragCanvas dc(primaries, { 0.0f, 0.9f }, { 0.8f, 0.0f });
diff --git a/tools/viewer/Viewer.h b/tools/viewer/Viewer.h
index 3567218..be25ef0 100644
--- a/tools/viewer/Viewer.h
+++ b/tools/viewer/Viewer.h
@@ -42,6 +42,8 @@
     void onUIStateChanged(const SkString& stateName, const SkString& stateValue) override;
     bool onKey(skui::Key key, skui::InputState state, skui::ModifierKey modifiers) override;
     bool onChar(SkUnichar c, skui::ModifierKey modifiers) override;
+    bool onPinch(skui::InputState state, float scale, float x, float y) override;
+    bool onFling(skui::InputState state) override;
 
     struct SkFontFields {
         bool fTypeface = false;