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();