1// Copyright 2021 Google LLC. 2#include "experimental/sktext/editor/Editor.h" 3#include "experimental/sktext/src/Paint.h" 4 5using namespace skia::text; 6 7namespace skia { 8namespace editor { 9 10std::unique_ptr<Editor> Editor::Make(std::u16string text, SkSize size) { 11 return std::make_unique<Editor>(text, size); 12} 13 14Editor::Editor(std::u16string text, SkSize size) 15 : fDefaultPositionType(PositionType::kGraphemeCluster) 16 , fInsertMode(true) { 17 18 fParent = nullptr; 19 fCursor = Cursor::Make(); 20 fMouse = std::make_unique<Mouse>(); 21 { 22 SkPaint foreground; foreground.setColor(DEFAULT_TEXT_FOREGROUND); 23 SkPaint background; background.setColor(DEFAULT_TEXT_BACKGROUND); 24 static FontBlock textBlock(text.size(), sk_make_sp<TrivialFontChain>("Roboto", 40, SkFontStyle::Normal())); 25 static DecoratedBlock textDecor(text.size(), foreground, background); 26 auto textSize = SkSize::Make(size.width(), size.height() - DEFAULT_STATUS_HEIGHT); 27 fEditableText = std::make_unique<EditableText>( 28 text, SkPoint::Make(0, 0), textSize, 29 SkSpan<FontBlock>(&textBlock, 1), SkSpan<DecoratedBlock>(&textDecor, 1), 30 DEFAULT_TEXT_DIRECTION, DEFAULT_TEXT_ALIGN); 31 } 32 { 33 SkPaint foreground; foreground.setColor(DEFAULT_STATUS_FOREGROUND); 34 SkPaint background; background.setColor(DEFAULT_STATUS_BACKGROUND); 35 std::u16string status = u"This is the status line"; 36 static FontBlock statusBlock(status.size(), sk_make_sp<TrivialFontChain>("Roboto", 20, SkFontStyle::Normal())); 37 static DecoratedBlock statusDecor(status.size(), foreground, background); 38 auto statusPoint = SkPoint::Make(0, size.height() - DEFAULT_STATUS_HEIGHT); 39 fStatus = std::make_unique<DynamicText>( 40 status, statusPoint, SkSize::Make(size.width(), SK_ScalarInfinity), 41 SkSpan<FontBlock>(&statusBlock, 1), SkSpan<DecoratedBlock>(&statusDecor, 1), 42 DEFAULT_TEXT_DIRECTION, TextAlign::kCenter); 43 } 44 // Place the cursor at the end of the output text 45 // (which is the end of the text for LTR and the beginning of the text for RTL 46 // or possibly something in the middle for a combination of LTR & RTL) 47 // In order to get that position we look for a position outside of the text 48 // and that will give us the last glyph on the line 49 auto endOfText = fEditableText->lastElement(fDefaultPositionType); 50 //fEditableText->recalculateBoundaries(endOfText); 51 fCursor->place(endOfText.fBoundaries); 52} 53 54void Editor::update() { 55 56 if (fEditableText->isValid()) { 57 return; 58 } 59 60 // Update the (shift it to point at the grapheme edge) 61 auto position = fEditableText->adjustedPosition(fDefaultPositionType, fCursor->getCenterPosition()); 62 //fEditableText->recalculateBoundaries(position); 63 fCursor->place(position.fBoundaries); 64 65 // TODO: Update the mouse 66 fMouse->clearTouchInfo(); 67} 68 69// Moving the cursor by the output grapheme clusters (shifting to another line if necessary) 70// We don't want to move by the input text indexes because then we will have to take in account LTR/RTL 71bool Editor::moveCursor(skui::Key key) { 72 auto cursorPosition = fCursor->getCenterPosition(); 73 auto position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); 74 75 if (key == skui::Key::kLeft) { 76 position = fEditableText->previousElement(position); 77 } else if (key == skui::Key::kRight) { 78 position = fEditableText->nextElement(position); 79 } else if (key == skui::Key::kHome) { 80 position = fEditableText->firstElement(PositionType::kGraphemeCluster); 81 } else if (key == skui::Key::kEnd) { 82 position = fEditableText->lastElement(PositionType::kGraphemeCluster); 83 } else if (key == skui::Key::kUp) { 84 // Move one line up (if possible) 85 if (position.fLineIndex == 0) { 86 return false; 87 } 88 auto prevLine = fEditableText->getLine(position.fLineIndex - 1); 89 cursorPosition.offset(0, - prevLine.fBounds.height()); 90 position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); 91 } else if (key == skui::Key::kDown) { 92 // Move one line down (if possible) 93 if (position.fLineIndex == fEditableText->lineCount() - 1) { 94 return false; 95 } 96 auto nextLine = fEditableText->getLine(position.fLineIndex + 1); 97 cursorPosition.offset(0, nextLine.fBounds.height()); 98 position = fEditableText->adjustedPosition(PositionType::kGraphemeCluster, cursorPosition); 99 } 100 101 // Place the cursor at the new position 102 //fEditableText->recalculateBoundaries(position); 103 fCursor->place(position.fBoundaries); 104 this->invalidate(); 105 106 return true; 107} 108 109void Editor::onPaint(SkSurface* surface) { 110 SkCanvas* canvas = surface->getCanvas(); 111 SkAutoCanvasRestore acr(canvas, true); 112 canvas->clipRect({0, 0, (float)fWidth, (float)fHeight}); 113 canvas->drawColor(SK_ColorWHITE); 114 this->paint(canvas); 115} 116 117void Editor::onResize(int width, int height) { 118 if (SkISize{fWidth, fHeight} != SkISize{width, height}) { 119 fHeight = height; 120 if (width != fWidth) { 121 fWidth = width; 122 } 123 this->invalidate(); 124 } 125} 126 127bool Editor::onChar(SkUnichar c, skui::ModifierKey modi) { 128 using sknonstd::Any; 129 130 modi &= ~skui::ModifierKey::kFirstPress; 131 if (!Any(modi & (skui::ModifierKey::kControl | 132 skui::ModifierKey::kOption | 133 skui::ModifierKey::kCommand))) { 134 if (((unsigned)c < 0x7F && (unsigned)c >= 0x20) || c == 0x000A) { 135 insertCodepoint(c); 136 return true; 137 } 138 } 139 static constexpr skui::ModifierKey kCommandOrControl = 140 skui::ModifierKey::kCommand | skui::ModifierKey::kControl; 141 if (Any(modi & kCommandOrControl) && !Any(modi & ~kCommandOrControl)) { 142 return false; 143 } 144 return false; 145} 146 147bool Editor::deleteElement(skui::Key key) { 148 149 if (fEditableText->isEmpty()) { 150 return false; 151 } 152 153 auto cursorPosition = fCursor->getCenterPosition(); 154 auto position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); 155 TextRange textRange = position.fTextRange; 156 157 // IMPORTANT: We assume that a single element (grapheme cluster) does not cross the run boundaries; 158 // It's not exactly true but we are going to enforce in by breaking the grapheme by the run boundaries 159 if (key == skui::Key::kBack) { 160 // TODO: Make sure previous element moves smoothly over the line break 161 position = fEditableText->previousElement(position); 162 textRange = position.fTextRange; 163 fCursor->place(position.fBoundaries); 164 } else { 165 // The cursor stays the the same place 166 } 167 168 fEditableText->removeElement(textRange); 169 170 // Find the grapheme the cursor points to 171 position = fEditableText->adjustedPosition(fDefaultPositionType, SkPoint::Make(position.fBoundaries.fLeft, position.fBoundaries.fTop)); 172 fCursor->place(position.fBoundaries); 173 this->invalidate(); 174 175 return true; 176} 177 178bool Editor::insertCodepoint(SkUnichar unichar) { 179 auto cursorPosition = fCursor->getCenterPosition(); 180 auto position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); 181 182 if (fInsertMode) { 183 fEditableText->insertElement(unichar, position.fTextRange.fStart); 184 } else { 185 fEditableText->replaceElement(unichar, position.fTextRange); 186 } 187 188 this->update(); 189 190 // Find the element the cursor points to 191 position = fEditableText->adjustedPosition(fDefaultPositionType, cursorPosition); 192 193 // Move the cursor to the next element 194 position = fEditableText->nextElement(position); 195 //fEditableText->recalculateBoundaries(position); 196 fCursor->place(position.fBoundaries); 197 198 this->invalidate(); 199 200 return true; 201} 202 203bool Editor::onKey(skui::Key key, skui::InputState state, skui::ModifierKey modifiers) { 204 205 if (state != skui::InputState::kDown) { 206 return false; 207 } 208 using sknonstd::Any; 209 skui::ModifierKey ctrlAltCmd = modifiers & (skui::ModifierKey::kControl | 210 skui::ModifierKey::kOption | 211 skui::ModifierKey::kCommand); 212 //bool shift = Any(modifiers & (skui::ModifierKey::kShift)); 213 if (!Any(ctrlAltCmd)) { 214 // no modifiers 215 switch (key) { 216 case skui::Key::kLeft: 217 case skui::Key::kRight: 218 case skui::Key::kUp: 219 case skui::Key::kDown: 220 case skui::Key::kHome: 221 case skui::Key::kEnd: 222 this->moveCursor(key); 223 break; 224 case skui::Key::kDelete: 225 case skui::Key::kBack: 226 this->deleteElement(key); 227 return true; 228 case skui::Key::kOK: 229 return this->onChar(0x000A, modifiers); 230 default: 231 break; 232 } 233 } 234 return false; 235} 236 237bool Editor::onMouse(int x, int y, skui::InputState state, skui::ModifierKey modifiers) { 238 239 if (!fEditableText->contains(x, y)) { 240 // We only support mouse on an editable area 241 } 242 if (skui::InputState::kDown == state) { 243 auto position = fEditableText->adjustedPosition(fDefaultPositionType, SkPoint::Make(x, y)); 244 if (fMouse->isDoubleClick(SkPoint::Make(x, y))) { 245 // Select the element 246 fEditableText->select(position.fTextRange, position.fBoundaries); 247 position.fBoundaries.fLeft = position.fBoundaries.fRight - DEFAULT_CURSOR_WIDTH; 248 // Clear mouse 249 fMouse->up(); 250 } else { 251 // Clear selection 252 fMouse->down(); 253 fEditableText->clearSelection(); 254 } 255 256 fCursor->place(position.fBoundaries); 257 this->invalidate(); 258 return true; 259 } 260 fMouse->up(); 261 return false; 262} 263 264void Editor::paint(SkCanvas* canvas) { 265 266 fEditableText->paint(canvas); 267 fCursor->paint(canvas); 268 269 SkPaint background; background.setColor(DEFAULT_STATUS_BACKGROUND); 270 canvas->drawRect(SkRect::MakeXYWH(0, fHeight - DEFAULT_STATUS_HEIGHT, fWidth, DEFAULT_STATUS_HEIGHT), background); 271 fStatus->paint(canvas); 272} 273 274std::unique_ptr<Editor> Editor::MakeDemo(SkScalar width, SkScalar height) { 275 276 std::u16string text0 = u"In a hole in the ground there lived a hobbit. Not a nasty, dirty, " 277 "wet hole full of worms and oozy smells.\nThis was a hobbit-hole and " 278 "that means good food, a warm hearth, and all the comforts of home."; 279 280 return Editor::Make(text0, SkSize::Make(width, height)); 281} 282} // namespace editor 283} // namespace skia 284