// Copyright 2021 Google LLC. #include "experimental/sktext/editor/Editor.h" #include "experimental/sktext/src/Paint.h" using namespace skia::text; namespace skia { namespace editor { std::unique_ptr Editor::Make(std::u16string text, SkSize size) { return std::make_unique(text, size); } Editor::Editor(std::u16string text, SkSize size) : fDefaultPositionType(PositionType::kGraphemeCluster) , fInsertMode(true) { fParent = nullptr; fCursor = Cursor::Make(); fMouse = std::make_unique(); { SkPaint foreground; foreground.setColor(DEFAULT_TEXT_FOREGROUND); SkPaint background; background.setColor(DEFAULT_TEXT_BACKGROUND); static FontBlock textBlock(text.size(), sk_make_sp("Roboto", 40, SkFontStyle::Normal())); static DecoratedBlock textDecor(text.size(), foreground, background); auto textSize = SkSize::Make(size.width(), size.height() - DEFAULT_STATUS_HEIGHT); fEditableText = std::make_unique( text, SkPoint::Make(0, 0), textSize, SkSpan(&textBlock, 1), SkSpan(&textDecor, 1), DEFAULT_TEXT_DIRECTION, DEFAULT_TEXT_ALIGN); } { SkPaint foreground; foreground.setColor(DEFAULT_STATUS_FOREGROUND); SkPaint background; background.setColor(DEFAULT_STATUS_BACKGROUND); std::u16string status = u"This is the status line"; static FontBlock statusBlock(status.size(), sk_make_sp("Roboto", 20, SkFontStyle::Normal())); static DecoratedBlock statusDecor(status.size(), foreground, background); auto statusPoint = SkPoint::Make(0, size.height() - DEFAULT_STATUS_HEIGHT); fStatus = std::make_unique( status, statusPoint, SkSize::Make(size.width(), SK_ScalarInfinity), SkSpan(&statusBlock, 1), SkSpan(&statusDecor, 1), DEFAULT_TEXT_DIRECTION, TextAlign::kCenter); } // Place the cursor at the end of the output text // (which is the end of the text for LTR and the beginning of the text for RTL // or possibly something in the middle for a combination of LTR & RTL) // In order to get that position we look for a position outside of the text // and that will give us the last glyph on the line auto endOfText = fEditableText->lastElement(fDefaultPositionType); //fEditableText->recalculateBoundaries(endOfText); fCursor->place(endOfText.fBoundaries); } void Editor::update() { if (fEditableText->isValid()) { return; } // Update the (shift it to point at the grapheme edge) auto position = fEditableText->adjustedPosition(fDefaultPositionType, fCursor->getCenterPosition()); //fEditableText->recalculateBoundaries(position); fCursor->place(position.fBoundaries); // TODO: Update the mouse fMouse->clearTouchInfo(); } // Moving the cursor by the output grapheme clusters (shifting to another line if necessary) // We don't want to move by the input text indexes because then we will have to take in account LTR/RTL bool Editor::moveCursor(skui::Key key) { auto cursorPosition = fCursor->getCenterPosition(); auto position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); if (key == skui::Key::kLeft) { position = fEditableText->previousElement(position); } else if (key == skui::Key::kRight) { position = fEditableText->nextElement(position); } else if (key == skui::Key::kHome) { position = fEditableText->firstElement(PositionType::kGraphemeCluster); } else if (key == skui::Key::kEnd) { position = fEditableText->lastElement(PositionType::kGraphemeCluster); } else if (key == skui::Key::kUp) { // Move one line up (if possible) if (position.fLineIndex == 0) { return false; } auto prevLine = fEditableText->getLine(position.fLineIndex - 1); cursorPosition.offset(0, - prevLine.fBounds.height()); position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); } else if (key == skui::Key::kDown) { // Move one line down (if possible) if (position.fLineIndex == fEditableText->lineCount() - 1) { return false; } auto nextLine = fEditableText->getLine(position.fLineIndex + 1); cursorPosition.offset(0, nextLine.fBounds.height()); position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); } // Place the cursor at the new position //fEditableText->recalculateBoundaries(position); fCursor->place(position.fBoundaries); this->invalidate(); return true; } void Editor::onPaint(SkSurface* surface) { SkCanvas* canvas = surface->getCanvas(); SkAutoCanvasRestore acr(canvas, true); canvas->clipRect({0, 0, (float)fWidth, (float)fHeight}); canvas->drawColor(SK_ColorWHITE); this->paint(canvas); } void Editor::onResize(int width, int height) { if (SkISize{fWidth, fHeight} != SkISize{width, height}) { fHeight = height; if (width != fWidth) { fWidth = width; } this->invalidate(); } } bool Editor::onChar(SkUnichar c, skui::ModifierKey modi) { using sknonstd::Any; modi &= ~skui::ModifierKey::kFirstPress; if (!Any(modi & (skui::ModifierKey::kControl | skui::ModifierKey::kOption | skui::ModifierKey::kCommand))) { if (((unsigned)c < 0x7F && (unsigned)c >= 0x20) || c == 0x000A) { insertCodepoint(c); return true; } } static constexpr skui::ModifierKey kCommandOrControl = skui::ModifierKey::kCommand | skui::ModifierKey::kControl; if (Any(modi & kCommandOrControl) && !Any(modi & ~kCommandOrControl)) { return false; } return false; } bool Editor::deleteElement(skui::Key key) { if (fEditableText->isEmpty()) { return false; } auto cursorPosition = fCursor->getCenterPosition(); auto position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); TextRange textRange = position.fTextRange; // IMPORTANT: We assume that a single element (grapheme cluster) does not cross the run boundaries; // It's not exactly true but we are going to enforce in by breaking the grapheme by the run boundaries if (key == skui::Key::kBack) { // TODO: Make sure previous element moves smoothly over the line break position = fEditableText->previousElement(position); textRange = position.fTextRange; fCursor->place(position.fBoundaries); } else { // The cursor stays the the same place } fEditableText->removeElement(textRange); // Find the grapheme the cursor points to position = fEditableText->adjustedPosition(fDefaultPositionType, SkPoint::Make(position.fBoundaries.fLeft, position.fBoundaries.fTop)); fCursor->place(position.fBoundaries); this->invalidate(); return true; } bool Editor::insertCodepoint(SkUnichar unichar) { auto cursorPosition = fCursor->getCenterPosition(); auto position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); if (fInsertMode) { fEditableText->insertElement(unichar, position.fTextRange.fStart); } else { fEditableText->replaceElement(unichar, position.fTextRange); } this->update(); // Find the element the cursor points to position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); // Move the cursor to the next element position = fEditableText->nextElement(position); //fEditableText->recalculateBoundaries(position); fCursor->place(position.fBoundaries); this->invalidate(); return true; } bool Editor::onKey(skui::Key key, skui::InputState state, skui::ModifierKey modifiers) { if (state != skui::InputState::kDown) { return false; } using sknonstd::Any; skui::ModifierKey ctrlAltCmd = modifiers & (skui::ModifierKey::kControl | skui::ModifierKey::kOption | skui::ModifierKey::kCommand); //bool shift = Any(modifiers & (skui::ModifierKey::kShift)); if (!Any(ctrlAltCmd)) { // no modifiers switch (key) { case skui::Key::kLeft: case skui::Key::kRight: case skui::Key::kUp: case skui::Key::kDown: case skui::Key::kHome: case skui::Key::kEnd: this->moveCursor(key); break; case skui::Key::kDelete: case skui::Key::kBack: this->deleteElement(key); return true; case skui::Key::kOK: return this->onChar(0x000A, modifiers); default: break; } } return false; } bool Editor::onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers) { if (!fEditableText->contains(x, y)) { // We only support mouse on an editable area } if (skui::InputState::kDown == state) { auto position = fEditableText->adjustedPosition(fDefaultPositionType, SkPoint::Make(x, y)); if (fMouse->isDoubleClick(SkPoint::Make(x, y))) { // Select the element fEditableText->select(position.fTextRange, position.fBoundaries); position.fBoundaries.fLeft = position.fBoundaries.fRight - DEFAULT_CURSOR_WIDTH; // Clear mouse fMouse->up(); } else { // Clear selection fMouse->down(); fEditableText->clearSelection(); } fCursor->place(position.fBoundaries); this->invalidate(); return true; } fMouse->up(); return false; } void Editor::paint(SkCanvas* canvas) { fEditableText->paint(canvas); fCursor->paint(canvas); SkPaint background; background.setColor(DEFAULT_STATUS_BACKGROUND); canvas->drawRect(SkRect::MakeXYWH(0, fHeight - DEFAULT_STATUS_HEIGHT, fWidth, DEFAULT_STATUS_HEIGHT), background); fStatus->paint(canvas); } std::unique_ptr Editor::MakeDemo(SkScalar width, SkScalar height) { std::u16string text0 = u"In a hole in the ground there lived a hobbit. Not a nasty, dirty, " "wet hole full of worms and oozy smells.\nThis was a hobbit-hole and " "that means good food, a warm hearth, and all the comforts of home."; return Editor::Make(text0, SkSize::Make(width, height)); } } // namespace editor } // namespace skia