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