Experimental: text editor

Proof of principle of a text editor written with Skia & SkShaper.

(Work In Progress)

Bug: skia:9020
Change-Id: I4fb837b719bc42fab8d8bdce2ca68fb9c1829d21
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/210381
Reviewed-by: Ben Wagner <bungeman@google.com>
Commit-Queue: Ben Wagner <bungeman@google.com>
diff --git a/BUILD.gn b/BUILD.gn
index 92ab115..20293b7 100644
--- a/BUILD.gn
+++ b/BUILD.gn
@@ -2453,4 +2453,16 @@
       ]
     }
   }
+
+  test_app("editor") {
+    is_shared_library = is_android
+    sources = [
+      "experimental/editor/editor_application.cpp",
+    ]
+    deps = [
+      ":sk_app",
+      ":skia",
+      "modules/skshaper",
+    ]
+  }
 }
diff --git a/experimental/editor/editor_application.cpp b/experimental/editor/editor_application.cpp
new file mode 100644
index 0000000..a176bca
--- /dev/null
+++ b/experimental/editor/editor_application.cpp
@@ -0,0 +1,193 @@
+// Copyright 2019 Google LLC.
+// Use of this source code is governed by a BSD-style license that can be found in the LICENSE file.
+
+// [Work In Progress] Proof of principle of a text editor written with Skia & SkShaper.
+// https://bugs.skia.org/9020
+
+#include "include/core/SkExecutor.h"
+#include "include/core/SkPath.h"
+#include "include/core/SkPictureRecorder.h"
+#include "include/core/SkSurface.h"
+#include "include/core/SkTime.h"
+#include "include/effects/SkGradientShader.h"
+#include "modules/skshaper/include/SkShaper.h"
+#include "tools/sk_app/Application.h"
+#include "tools/sk_app/CommandSet.h"
+#include "tools/sk_app/Window.h"
+
+#include <fstream>
+#include <iostream>
+#include <memory>
+#include <string>
+#include <vector>
+
+namespace {
+
+struct Timer {
+    double fTime;
+    const char* fDesc;
+    Timer(const char* desc = "") : fTime(SkTime::GetNSecs()), fDesc(desc) {}
+    ~Timer() { SkDebugf("%s: %d ms\n", fDesc, (int)((SkTime::GetNSecs() - fTime) * 1e-6)); }
+};
+
+struct TextLine {
+    TextLine(std::string s) : fText(std::move(s)) {}
+    std::string fText;
+    float fHeight = 0;
+    sk_sp<const SkTextBlob> fBlob;
+    bool fSelected = false;  // Will allow selection of subset of text later.
+    // Also will track presence of cursor.
+
+    void shape(const SkFont& font, const SkShaper* shaper, float width) {
+        SkTextBlobBuilderRunHandler textBlobBuilder(fText.c_str(), {0, 0});
+        shaper->shape(fText.c_str(), fText.size(), font, true, width, &textBlobBuilder);
+        fHeight = std::max(textBlobBuilder.endPoint().y(), font.getSpacing());
+        fBlob = textBlobBuilder.makeBlob();
+    }
+};
+
+std::vector<TextLine> read_file(const char* path) {
+    std::vector<TextLine> ret;
+    if (path) {
+        std::ifstream stream(path);
+        for (std::string line; std::getline(stream, line);) {
+            ret.push_back(TextLine(line));
+        }
+    }
+    return ret;
+}
+
+struct EditorLayer : public sk_app::Window::Layer {
+    sk_app::Window* fParent = nullptr;
+    std::vector<TextLine> fLines;
+    const SkShaper* fShaper = nullptr;
+    int fMargin = 10;
+    int fPos = 10;
+    int fWidth = 0;
+    int fHeight = 0;
+    SkFont fFont{nullptr, 24};
+
+    EditorLayer(std::vector<TextLine> lines, SkShaper* shaper)
+        : fLines(std::move(lines)), fShaper(shaper) {}
+
+    void onPaint(SkSurface* surface) override {
+        Timer timer("painting");
+        SkColor background = SkColorSetARGB(0xFF, 0xCC, 0xCC, 0xCC);
+        SkColor foreground = SkColorSetARGB(0xFF, 0x00, 0x00, 0x00);
+        SkCanvas* c = surface->getCanvas();
+        c->clipRect({0, 0, (float)fWidth, (float)fHeight});
+        c->clear(background);
+        float y = fPos;
+        SkPaint p;
+        p.setColor(foreground);
+        SkPaint diff;
+        diff.setColor(SK_ColorWHITE);
+        diff.setBlendMode(SkBlendMode::kDifference);
+        float width = (float)(fWidth - 2 * fMargin);
+        for (const TextLine& line : fLines) {
+            if (line.fBlob) {
+                c->drawTextBlob(line.fBlob.get(), fMargin, y, p);
+            }
+            if (line.fSelected) {
+                c->drawRect(SkRect{(float)fMargin, y, width, y + line.fHeight}, diff);
+            }
+            y += line.fHeight;
+        }
+    }
+
+    void reshape() {
+        Timer timer("shaping");
+        float width = (float)(fWidth - 2 * fMargin);
+        #ifdef SK_EDITOR_GO_FAST
+        SkSemaphore semaphore;
+        std::unique_ptr<SkExecutor> executor = SkExecutor::MakeFIFOThreadPool(100);
+        for (TextLine& line : fLines) {
+            executor->add([&]() {
+                auto shaper = SkShaper::Make();
+                line.shape(fFont, shaper.get(), width);
+                semaphore.signal();
+            });
+        }
+        for (const TextLine& l : fLines) { semaphore.wait(); }
+        #else
+        for (TextLine& line : fLines) {
+            line.shape(fFont, fShaper, width);
+        }
+        #endif
+    }
+
+    void onResize(int width, int height) override {
+        if (SkISize{fWidth, fHeight} != SkISize{width, height}) {
+            fHeight = height;
+            if (width != fWidth) {
+                fWidth = width;
+                this->reshape();
+            }
+            if (fParent) {
+                fParent->inval();
+            }
+        }
+    }
+
+    void onAttach(sk_app::Window* w) override { fParent = w; }
+
+    bool onMouseWheel(float delta, uint32_t modifiers) override {
+        int newpos = std::min(fPos + (int)(delta * fFont.getSpacing()), fMargin);
+        if (newpos != fPos) {
+            fPos = newpos;
+            if (fParent) { fParent->inval(); }
+            return true;
+        }
+        return false;
+    }
+
+    bool onMouse(int x, int y, sk_app::Window::InputState state, uint32_t modifiers) override {
+        if (sk_app::Window::kDown_InputState == state) {
+            y -= fPos;
+            if (y >= 0) {
+                for (TextLine& line : fLines) {
+                    if (y < line.fHeight) {
+                        line.fSelected = !line.fSelected;
+                        SkDebugf("  %d %d\n", x - (int)fMargin, y);
+                        fParent->inval();
+                        break;
+                    }
+                    y -= line.fHeight;
+                }
+            }
+            return true;
+        }
+        return false;
+    }
+};
+
+struct EditorApplication : public sk_app::Application {
+    std::unique_ptr<SkShaper> fShaper;
+    std::unique_ptr<sk_app::Window> fWindow;
+    std::unique_ptr<EditorLayer> fLayer;
+    sk_app::CommandSet fCommandSet;
+
+    EditorApplication(const char* path, void* platformData)
+        : fShaper(SkShaper::Make())
+        , fWindow(sk_app::Window::CreateNativeWindow(platformData))
+    {
+        fWindow->attach(sk_app::Window::kRaster_BackendType);
+        fLayer.reset(new EditorLayer(read_file(path), fShaper.get()));
+        fWindow->pushLayer(fLayer.get());
+        fCommandSet.attach(fWindow.get());
+        fWindow->show();
+        fLayer->onResize(fWindow->width(), fWindow->height());
+    }
+    ~EditorApplication() override { fWindow->detach(); }
+
+    void onIdle() override {}
+};
+
+}  // namespace
+
+sk_app::Application* sk_app::Application::Create(int argc, char** argv, void* dat) {
+    if (argc > 1) {
+        return new EditorApplication(argv[1], dat);
+    }
+    return new EditorApplication(nullptr, dat);
+}