Wire up mouse and keyboard events in CanvasKit viewer

Change-Id: I10b57f18edb516b48be3ba16f98a540370ec689f
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/292793
Commit-Queue: Chris Dalton <csmartdalton@google.com>
Reviewed-by: Kevin Lubick <kjlubick@google.com>
diff --git a/modules/canvaskit/canvaskit/viewer.html b/modules/canvaskit/canvaskit/viewer.html
index 10e944e..9cbaa77 100644
--- a/modules/canvaskit/canvaskit/viewer.html
+++ b/modules/canvaskit/canvaskit/viewer.html
@@ -25,7 +25,6 @@
     location.reload();
   };
 
-  var CanvasKit = null;
   CanvasKitInit({
     locateFile: (file) => '/node_modules/canvaskit/bin/'+file,
   }).then((CK) => {
@@ -54,15 +53,14 @@
         }
       });
     } else {
-      let slide = CanvasKit.MakeSlide(slideName);
-      if (!slide) {
-        throw 'Could not make slide ' + slideName;
-      }
-      ViewerMain(CanvasKit, slide);
+      ViewerMain(CanvasKit, CanvasKit.MakeSlide(slideName));
     }
   }
 
   function ViewerMain(CanvasKit, slide) {
+    if (!slide) {
+      throw 'Failed to parse slide.'
+    }
     const width = window.innerWidth;
     const height = window.innerHeight;
     const htmlCanvas = document.getElementById('viewer_canvas');
@@ -70,9 +68,7 @@
     htmlCanvas.height = height;
     slide.load(width, height);
 
-    const doMSAA = (flags.msaa > 1);
-    let surface;
-    if (doMSAA) {
+    if (flags.msaa > 1) {
         let ctx = CanvasKit.GetWebGLContext(htmlCanvas);
         let grContext = CanvasKit.MakeGrContext(ctx);
         let sampleCnt = parseInt(flags.msaa);
@@ -80,6 +76,7 @@
         if (!surface) {
           throw 'Could not create offscreen msaa render target.';
         }
+        surface.isMSAA = true;
     } else {
       surface = CanvasKit.MakeCanvasSurface(htmlCanvas);
       if (!surface) {
@@ -87,30 +84,114 @@
       }
     }
 
+    window.onmousedown = (event) => (event.button === 0) && Mouse(CanvasKit.InputState.Down, event);
+    window.onmouseup = (event) => (event.button === 0) && Mouse(CanvasKit.InputState.Up, event);
+    window.onmousemove = (event) => Mouse(CanvasKit.InputState.Move, event);
+    window.onkeypress = function(event) {
+      if (slide.onChar(event.keyCode)) {
+        ScheduleDraw();
+        return false;
+      }
+      return true;
+    }
+    window.onkeydown = function(event) {
+      if (event.keyCode == '38') {  // up arrow
+        ScaleCanvas((event.shiftKey) ? Infinity : 1.1);
+        return false;
+      }
+      if (event.keyCode == '40') {  // down arrow
+        ScaleCanvas((event.shiftKey) ? 0 : 1/1.1);
+        return false;
+      }
+      return true;
+    }
+
+    let [canvasScale, canvasTranslateX, canvasTranslateY] = [1, 0, 0];
+    function ScaleCanvas(factor) {
+      factor = Math.min(Math.max(1/(5*canvasScale), factor), 5/canvasScale);
+      canvasTranslateX *= factor;
+      canvasTranslateY *= factor;
+      canvasScale *= factor;
+      ScheduleDraw();
+    }
+    function TranslateCanvas(dx, dy) {
+      canvasTranslateX += dx;
+      canvasTranslateY += dy;
+      ScheduleDraw();
+    }
+
+    function Mouse(state, event) {
+      let modifierKeys = CanvasKit.ModifierKey.None;
+      if (event.shiftKey) {
+        modifierKeys |= CanvasKit.ModifierKey.Shift;
+      }
+      if (event.altKey) {
+        modifierKeys |= CanvasKit.ModifierKey.Option;
+      }
+      if (event.ctrlKey) {
+        modifierKeys |= CanvasKit.ModifierKey.Ctrl;
+      }
+      if (event.metaKey) {
+        modifierKeys |= CanvasKit.ModifierKey.Command;
+      }
+      let [dx, dy] = [event.pageX - this.lastX, event.pageY - this.lastY];
+      this.lastX = event.pageX;
+      this.lastY = event.pageY;
+      if (slide.onMouse(event.pageX, event.pageY, state, modifierKeys)) {
+        ScheduleDraw();
+        return false;
+      } else if (event.buttons & 1) {  // Left-button pressed.
+        TranslateCanvas(dx, dy);
+        return false;
+      }
+      return true;
+    }
+
     const fps = {
       frames: 0,
       startMs: window.performance.now()
     };
 
-    surface.requestAnimationFrame(function(canvas) {
-      slide.draw(canvas);
-      if (doMSAA) {
-        CanvasKit.BlitOffscreenFramebuffer(surface, 0, 0, width, height, 0, 0, width, height,
-                                           CanvasKit.GLFilter.Nearest);
+    function ScheduleDraw() {
+      if (this.hasPendingAnimationRequest) {
+        // It's possible for this ScheduleDraw() method to be called multiple times before an
+        // animation callback actually gets invoked. Make sure we only ever have one single
+        // requestAnimationFrame scheduled at a time, because otherwise we can get stuck in a
+        // position where multiple callbacks are coming in on a single compositing frame, and then
+        // rescheduling multiple more for the next frame.
+        return;
       }
+      this.hasPendingAnimationRequest = true;
+      surface.requestAnimationFrame((canvas) => {
+        this.hasPendingAnimationRequest = false;
 
-      ++fps.frames;
-      const ms = window.performance.now();
-      const sec = (ms - fps.startMs) / 1000;
-      if (sec > 2) {
-        console.log(Math.round(fps.frames / sec) + ' fps');
-        fps.frames = 0;
-        fps.startMs = ms;
-      }
+        canvas.save();
+        canvas.translate(canvasTranslateX, canvasTranslateY);
+        canvas.scale(canvasScale, canvasScale);
+        canvas.clear(CanvasKit.WHITE);
+        slide.draw(canvas);
+        if (surface.isMSAA) {
+          let [w, h] = [surface.width(), surface.height()];
+          CanvasKit.BlitOffscreenFramebuffer(surface, 0,0,w,h, 0,0,w,h, CanvasKit.GLFilter.Nearest);
+        }
+        canvas.restore();
 
-      if (slide.animate(ms * 1e6)) {
-        surface.requestAnimationFrame(arguments.callee);
-      }
-    });
+        ++fps.frames;
+        const ms = window.performance.now();
+        const sec = (ms - fps.startMs) / 1000;
+        if (sec > 2) {
+          console.log(Math.round(fps.frames / sec) + ' fps');
+          fps.frames = 0;
+          fps.startMs = ms;
+        }
+
+        if (slide.animate(ms * 1e6)) {
+          ScheduleDraw();
+        }
+      });
+    }
+
+    ScheduleDraw();
   }
+
 </script>
diff --git a/modules/canvaskit/viewer_bindings.cpp b/modules/canvaskit/viewer_bindings.cpp
index acb57b6..cf394f5 100644
--- a/modules/canvaskit/viewer_bindings.cpp
+++ b/modules/canvaskit/viewer_bindings.cpp
@@ -10,6 +10,8 @@
 #include "include/core/SkCanvas.h"
 #include "include/core/SkSurface.h"
 #include "include/gpu/GrContext.h"
+#include "tools/skui/InputState.h"
+#include "tools/skui/ModifierKey.h"
 #include "tools/viewer/SKPSlide.h"
 #include "tools/viewer/SampleSlide.h"
 #include "tools/viewer/SvgSlide.h"
@@ -18,21 +20,25 @@
 
 using namespace emscripten;
 
-sk_sp<Slide> MakeSlide(std::string name) {
+static sk_sp<Slide> MakeSlide(std::string name) {
     if (name == "PathText") {
         extern Sample* MakePathTextSample();
         return sk_make_sp<SampleSlide>(MakePathTextSample);
     }
+    if (name == "TessellatedWedge") {
+        extern Sample* MakeTessellatedWedgeSample();
+        return sk_make_sp<SampleSlide>(MakeTessellatedWedgeSample);
+    }
     return nullptr;
 }
 
-sk_sp<Slide> MakeSkpSlide(std::string name, std::string skpData) {
+static sk_sp<Slide> MakeSkpSlide(std::string name, std::string skpData) {
     auto stream = std::make_unique<SkMemoryStream>(skpData.data(), skpData.size(),
                                                    /*copyData=*/true);
     return sk_make_sp<SKPSlide>(SkString(name.c_str()), std::move(stream));
 }
 
-sk_sp<Slide> MakeSvgSlide(std::string name, std::string svgText) {
+static sk_sp<Slide> MakeSvgSlide(std::string name, std::string svgText) {
     auto stream = std::make_unique<SkMemoryStream>(svgText.data(), svgText.size(),
                                                    /*copyData=*/true);
     return sk_make_sp<SvgSlide>(SkString(name.c_str()), std::move(stream));
@@ -43,8 +49,8 @@
     glDeleteFramebuffers(1, &framebuffer);
 }
 
-sk_sp<SkSurface> MakeOffscreenFramebuffer(sk_sp<GrContext> grContext, int width, int height,
-                                          int sampleCnt) {
+static sk_sp<SkSurface> MakeOffscreenFramebuffer(sk_sp<GrContext> grContext, int width, int height,
+                                                 int sampleCnt) {
     GLuint colorBuffer;
     glGenRenderbuffers(1, &colorBuffer);
     glBindRenderbuffer(GL_RENDERBUFFER, colorBuffer);
@@ -87,8 +93,9 @@
     kLinear = GL_LINEAR
 };
 
-void BlitOffscreenFramebuffer(sk_sp<SkSurface> surface, int srcX0, int srcY0, int srcX1, int srcY1,
-                              int dstX0, int dstY0, int dstX1, int dstY1, GLFilter filter) {
+static void BlitOffscreenFramebuffer(sk_sp<SkSurface> surface, int srcX0, int srcY0, int srcX1, int
+                                     srcY1, int dstX0, int dstY0, int dstX1, int dstY1,
+                                     GLFilter filter) {
   surface->flush(SkSurface::BackendSurfaceAccess::kPresent, GrFlushInfo());
   GrGLFramebufferInfo glInfo;
   auto backendRT = surface->getBackendRenderTarget(SkSurface::kFlushRead_BackendHandleAccess);
@@ -112,8 +119,23 @@
         .function("animate", &Slide::animate)
         .function("draw", optional_override([](Slide& self, SkCanvas& canvas) {
             self.draw(&canvas);
-        }));
+        }))
+        .function("onChar", &Slide::onChar)
+        .function("onMouse", &Slide::onMouse);
     enum_<GLFilter>("GLFilter")
         .value("Nearest",   GLFilter::kNearest)
         .value("Linear",    GLFilter::kLinear);
+    enum_<skui::InputState>("InputState")
+        .value("Down",    skui::InputState::kDown)
+        .value("Up",      skui::InputState::kUp)
+        .value("Move",    skui::InputState::kMove)
+        .value("Right",   skui::InputState::kRight)
+        .value("Left",    skui::InputState::kLeft);
+    enum_<skui::ModifierKey>("ModifierKey")
+        .value("None",          skui::ModifierKey::kNone)
+        .value("Shift",         skui::ModifierKey::kShift)
+        .value("Control",       skui::ModifierKey::kControl)
+        .value("Option",        skui::ModifierKey::kOption)
+        .value("Command",       skui::ModifierKey::kCommand)
+        .value("FirstPress",    skui::ModifierKey::kFirstPress);
 }
diff --git a/samplecode/SampleTessellatedWedge.cpp b/samplecode/SampleTessellatedWedge.cpp
index a46adce..89731cb 100644
--- a/samplecode/SampleTessellatedWedge.cpp
+++ b/samplecode/SampleTessellatedWedge.cpp
@@ -22,9 +22,9 @@
 
 // This sample enables wireframe and visualizes the triangulation generated by
 // GrTessellateWedgeShader.
-class TessellatedWedgeView : public Sample {
+class TessellatedWedge : public Sample {
 public:
-    TessellatedWedgeView() {
+    TessellatedWedge() {
 #if 0
         fPath.moveTo(1, 0);
         int numSides = 32 * 3;
@@ -56,7 +56,7 @@
     class Click;
 };
 
-void TessellatedWedgeView::onDrawContent(SkCanvas* canvas) {
+void TessellatedWedge::onDrawContent(SkCanvas* canvas) {
     canvas->clear(SK_ColorBLACK);
 
     GrContext* ctx = canvas->getGrContext();
@@ -65,8 +65,8 @@
     SkString error;
     if (!rtc || !ctx) {
         error = "GPU Only.";
-    } else if (!ctx->priv().caps()->shaderCaps()->tessellationSupport()) {
-        error = "GPU tessellation not supported.";
+    } else if (!ctx->priv().caps()->drawInstancedSupport()) {
+        error = "Instanced rendering not supported.";
     } else if (1 == rtc->numSamples() && !ctx->priv().caps()->mixedSamplesSupport()) {
         error = "MSAA/mixed samples only.";
     }
@@ -110,7 +110,7 @@
     fLastViewMatrix = canvas->getTotalMatrix();
 }
 
-class TessellatedWedgeView::Click : public Sample::Click {
+class TessellatedWedge::Click : public Sample::Click {
 public:
     Click(int ptIdx) : fPtIdx(ptIdx) {}
 
@@ -128,7 +128,7 @@
     int fPtIdx;
 };
 
-Sample::Click* TessellatedWedgeView::onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) {
+Sample::Click* TessellatedWedge::onFindClickHandler(SkScalar x, SkScalar y, skui::ModifierKey) {
     const SkPoint* pts = SkPathPriv::PointData(fPath);
     float fuzz = 20 / fLastViewMatrix.getMaxScale();
     for (int i = 0; i < fPath.countPoints(); ++i) {
@@ -140,13 +140,13 @@
     return new Click(-1);
 }
 
-bool TessellatedWedgeView::onClick(Sample::Click* click) {
+bool TessellatedWedge::onClick(Sample::Click* click) {
     Click* myClick = (Click*)click;
     myClick->doClick(&fPath);
     return true;
 }
 
-bool TessellatedWedgeView::onChar(SkUnichar unichar) {
+bool TessellatedWedge::onChar(SkUnichar unichar) {
     switch (unichar) {
         case 'w':
             fFlags = (GrTessellatePathOp::Flags)(
@@ -160,6 +160,7 @@
     return false;
 }
 
-DEF_SAMPLE(return new TessellatedWedgeView;)
+Sample* MakeTessellatedWedgeSample() { return new TessellatedWedge; }
+static SampleRegistry gTessellatedWedgeSample(MakeTessellatedWedgeSample);
 
 #endif  // SK_SUPPORT_GPU