TextEditor: select a grapheme on a double click

Change-Id: Ibb322fdd6258e42650bfc90af502a9f53ad03abb
Reviewed-on: https://skia-review.googlesource.com/c/skia/+/430219
Reviewed-by: Julia Lavrova <jlavrova@google.com>
Commit-Queue: Julia Lavrova <jlavrova@google.com>
diff --git a/experimental/sktext/editor/Editor.cpp b/experimental/sktext/editor/Editor.cpp
index f1f5faf..61f3c13 100644
--- a/experimental/sktext/editor/Editor.cpp
+++ b/experimental/sktext/editor/Editor.cpp
@@ -10,8 +10,9 @@
 const SkColor DEFAULT_FOREGROUND = SK_ColorBLACK;
 const SkScalar DEFAULT_CURSOR_WIDTH = 2;
 const SkColor DEFAULT_CURSOR_COLOR = SK_ColorGRAY;
+const SkColor DEFAULT_SELECTION_COLOR = SK_ColorCYAN;
 
-std::unique_ptr<Cursor> Cursor::Make() { return std::unique_ptr<Cursor>(new Cursor()); }
+std::unique_ptr<Cursor> Cursor::Make() { return std::make_unique<Cursor>(); }
 
 Cursor::Cursor() {
     fLinePaint.setColor(SK_ColorGRAY);
@@ -33,7 +34,48 @@
        canvas->drawRect(SkRect::MakeXYWH(fXY.fX + xy.fX, fXY.fY + xy.fY, DEFAULT_CURSOR_WIDTH, fSize.fHeight),
                 fRectPaint);
     } else {
-        canvas->drawLine(fXY + xy, fXY + xy + SkPoint::Make(1, fSize.fHeight), fLinePaint);
+        //canvas->drawLine(fXY + xy, fXY + xy + SkPoint::Make(1, fSize.fHeight), fLinePaint);
+    }
+}
+
+void Mouse::down() {
+    fMouseDown = true;
+}
+
+void Mouse::up() {
+    fMouseDown = false;
+}
+
+bool Mouse::isDoubleClick(SkPoint touch) {
+    if ((touch - fLastTouchPoint).length() > MAX_DBL_TAP_DISTANCE) {
+        fLastTouchPoint = touch;
+        fLastTouchTime = SkTime::GetMSecs();
+        return false;
+    }
+    double now = SkTime::GetMSecs();
+    if (now - fLastTouchTime > MAX_DBL_TAP_INTERVAL) {
+        fLastTouchPoint = touch;
+        fLastTouchTime = SkTime::GetMSecs();
+        return false;
+    }
+
+    clearTouchInfo();
+    return true;
+}
+
+void Selection::select(TextRange range, SkRect rect) {
+    fGlyphBoxes.clear();
+    fTextRanges.clear();
+
+    fGlyphBoxes.emplace_back(rect);
+    fTextRanges.emplace_back(range);
+}
+
+void Selection::paint(SkCanvas* canvas, SkPoint xy) {
+    for (auto& box : fGlyphBoxes) {
+        canvas->drawRect(
+                SkRect::MakeXYWH(box.fLeft + xy.fX, box.fTop + xy.fY, box.width(), box.height()),
+                fPaint);
     }
 }
 
@@ -46,6 +88,7 @@
     fText = std::move(text);
     fCursor = Cursor::Make();
     fMouse = std::make_unique<Mouse>();
+    fSelection = std::make_unique<Selection>(DEFAULT_SELECTION_COLOR);
     fUnicodeText = Text::parse(SkSpan<uint16_t>((uint16_t*)fText.data(), fText.size()));
     fShapedText = fUnicodeText->shape(fontBlocks, TEXT_DIRECTION);
     fWrappedText = fShapedText->wrap(size.width(), size.height(), fUnicodeText->getUnicode());
@@ -100,6 +143,7 @@
     auto nextPosition = fFormattedText->indexToAdjustedGraphemePosition(textIndex);
     auto rect = std::get<3>(nextPosition);
     fCursor->place(SkPoint::Make(rect.fLeft, rect.fTop), SkSize::Make(rect.width(), rect.height()));
+    this->invalidate();
 
     return true;
 }
@@ -124,6 +168,9 @@
 
 bool Editor::onChar(SkUnichar c, skui::ModifierKey modi) {
     using sknonstd::Any;
+
+    fSelection->clear();
+
     modi &= ~skui::ModifierKey::kFirstPress;
     if (!Any(modi & (skui::ModifierKey::kControl | skui::ModifierKey::kOption |
                      skui::ModifierKey::kCommand))) {
@@ -147,6 +194,9 @@
 }
 
 bool Editor::onKey(skui::Key key, skui::InputState state, skui::ModifierKey modifiers) {
+
+    fSelection->clear();
+
     if (state != skui::InputState::kDown) {
         return false;
     }
@@ -195,6 +245,33 @@
     return false;
 }
 
+bool Editor::onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers) {
+
+    //bool shiftOrDrag = sknonstd::Any(modifiers & skui::ModifierKey::kShift) || (skui::InputState::kDown != state);
+    if (skui::InputState::kDown == state) {
+        SkRect cursorRect;
+        if (fMouse->isDoubleClick(SkPoint::Make(x, y))) {
+            auto textIndex = fFormattedText->positionToAdjustedGraphemeIndex(SkPoint::Make(x, y));
+            auto nextPosition = fFormattedText->indexToAdjustedGraphemePosition(textIndex);
+            cursorRect = std::get<3>(nextPosition);
+            fSelection->select(TextRange(textIndex, textIndex + 1), cursorRect);
+            cursorRect.fLeft = cursorRect.fRight - DEFAULT_CURSOR_WIDTH;
+        } else {
+            fMouse->down();
+            fSelection->clear();
+            auto textIndex = fFormattedText->positionToAdjustedGraphemeIndex(SkPoint::Make(x, y));
+            auto nextPosition = fFormattedText->indexToAdjustedGraphemePosition(textIndex);
+            cursorRect = std::get<3>(nextPosition);
+        }
+
+        fCursor->place(SkPoint::Make(cursorRect.fLeft, cursorRect.fTop), SkSize::Make(cursorRect.width(), cursorRect.height()));
+        this->invalidate();
+        return true;
+    }
+    fMouse->up();
+    return false;
+}
+
 void Editor::onBeginLine(TextRange, float baselineY) {}
 void Editor::onEndLine(TextRange, float baselineY) {}
 void Editor::onPlaceholder(TextRange, const SkRect& bounds) {}
@@ -218,6 +295,7 @@
     fXY = xy;
     this->fFormattedText->visit(this);
 
+    fSelection->paint(canvas, xy);
     fCursor->paint(canvas, xy);
 }
 
diff --git a/experimental/sktext/editor/Editor.h b/experimental/sktext/editor/Editor.h
index 2e9b010..0278ce8 100644
--- a/experimental/sktext/editor/Editor.h
+++ b/experimental/sktext/editor/Editor.h
@@ -17,6 +17,8 @@
 class Cursor {
 public:
     static std::unique_ptr<Cursor> Make();
+    Cursor();
+    virtual ~Cursor() = default;
     void place(SkPoint xy, SkSize size) {
         fXY = xy;
         fSize = size;
@@ -34,7 +36,6 @@
     void paint(SkCanvas* canvas, SkPoint xy);
 
 private:
-    Cursor();
     SkPaint fLinePaint;
     SkPaint fRectPaint;
     SkPoint fXY;
@@ -43,7 +44,42 @@
 };
 
 class Mouse {
+    const SkMSec MAX_DBL_TAP_INTERVAL = 300;
+    const float MAX_DBL_TAP_DISTANCE = 100;
+public:
+    Mouse() : fMouseDown(false), fLastTouchPoint(), fLastTouchTime() { }
 
+    void down();
+    void up();
+    void clearTouchInfo() {
+        fLastTouchPoint = SkPoint::Make(0, 0);
+        fLastTouchTime = 0.0;
+    }
+    bool isDown() { return fMouseDown; }
+    bool isDoubleClick(SkPoint touch);
+
+private:
+    bool fMouseDown;
+    SkPoint fLastTouchPoint;
+    double fLastTouchTime;
+};
+
+class Selection {
+public:
+    Selection(SkColor color) : fGlyphBoxes(), fTextRanges() {
+        fPaint.setColor(color);
+        fPaint.setAlphaf(0.3f);
+    }
+    void select(TextRange range, SkRect rect);
+    void clear() {
+        fGlyphBoxes.clear();
+        fTextRanges.clear();
+    }
+    void paint(SkCanvas* canvas, SkPoint xy);
+private:
+    SkPaint fPaint;
+    std::vector<SkRect> fGlyphBoxes;
+    std::vector<TextRange> fTextRanges;
 };
 
 struct Style {
@@ -81,6 +117,7 @@
     void onAttach(sk_app::Window* w) override { fParent = w; }
     void onPaint(SkSurface* surface) override;
 
+    bool onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers) override;
     bool onKey(skui::Key, skui::InputState, skui::ModifierKey) override;
     bool onChar(SkUnichar c, skui::ModifierKey modifier) override;
     void invalidate() { if (fParent) { fParent->inval(); } }
@@ -99,6 +136,7 @@
 
     std::unique_ptr<Cursor> fCursor;
     std::unique_ptr<Mouse> fMouse;
+    std::unique_ptr<Selection> fSelection;
     std::u16string fText;
     std::unique_ptr<UnicodeText> fUnicodeText;
     std::unique_ptr<ShapedText> fShapedText;
diff --git a/experimental/sktext/include/Layout.h b/experimental/sktext/include/Layout.h
deleted file mode 100644
index 487d300..0000000
--- a/experimental/sktext/include/Layout.h
+++ /dev/null
@@ -1,20 +0,0 @@
-// Copyright 2021 Google LLC.
-#ifndef Layout_DEFINED
-#define Layout_DEFINED
-
-#include "experimental/sktext/include/Types.h"
-#include "experimental/sktext/src/Processor.h"
-
-namespace skia {
-namespace text {
-
-class Layout {
-
-public:
-    static std::unique_ptr<Processor> layout(std::u16string text, std::vector<FontBlock> fontBlocks, TextDirection defaultTextDirection, TextAlign textAlign, SkSize reqSize);
-    static bool layout(Processor* processor, SkSize reqSize);
-};
-}  // namespace text
-}  // namespace skia
-
-#endif  // Layout_DEFINED
diff --git a/experimental/sktext/include/Text.h b/experimental/sktext/include/Text.h
index ddda44f..a55dca5 100644
--- a/experimental/sktext/include/Text.h
+++ b/experimental/sktext/include/Text.h
@@ -114,6 +114,7 @@
     SkTArray<TextRun, false> fRuns;
     SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
 };
+
 class FormattedText;
 class WrappedText {
 public:
@@ -129,6 +130,7 @@
     SkTArray<GlyphUnitFlags, true> fGlyphUnitProperties;
     SkSize fSize;
 };
+
 class FormattedText : public SkRefCnt {
 public:
     SkSize  size() const { return fSize; }
diff --git a/experimental/sktext/include/Types.h b/experimental/sktext/include/Types.h
index 1bcf6bb..6067cff 100644
--- a/experimental/sktext/include/Types.h
+++ b/experimental/sktext/include/Types.h
@@ -25,6 +25,15 @@
     kLtr,
 };
 
+// This enum lists all possible ways to query output positioning
+enum class PositionType {
+    kGraphemeCluster,
+    kGrapheme,
+    kGlyphCluster,
+    kGlyph,
+    kGlyphPart
+};
+
 enum class LineBreakType {
   kSortLineBreakBefore,
   kHardLineBreakBefore,
diff --git a/experimental/sktext/src/Text.cpp b/experimental/sktext/src/Text.cpp
index 793185e..8f15266 100644
--- a/experimental/sktext/src/Text.cpp
+++ b/experimental/sktext/src/Text.cpp
@@ -361,6 +361,7 @@
     }
 }
 
+// TODO: The selection is not just a rectangle but a list of rectangles (for instance, over few lines or when we have ltr/rtl combo)
 std::tuple<const Line*, const TextRun*, GlyphIndex, SkRect> FormattedText::indexToAdjustedGraphemePosition(TextIndex textIndex) const {
 
     SkRect rect = SkRect::MakeEmpty();