xref: /aosp_15_r20/external/skia/modules/skottie/utils/TextEditor.cpp (revision c8dee2aa9b3f27cf6c858bd81872bdeb2c07ed17)
1 /*
2  * Copyright 2022 Google Inc.
3  *
4  * Use of this source code is governed by a BSD-style license that can be
5  * found in the LICENSE file.
6  */
7 
8 #include "modules/skottie/utils/TextEditor.h"
9 
10 #include "include/core/SkCanvas.h"
11 #include "include/core/SkColor.h"
12 #include "include/core/SkM44.h"
13 #include "include/core/SkMatrix.h"
14 #include "include/core/SkPaint.h"
15 #include "include/core/SkPath.h"
16 #include "include/core/SkPoint.h"
17 #include "include/core/SkRefCnt.h"
18 #include "include/core/SkSpan.h"
19 #include "include/core/SkString.h"
20 #include "src/base/SkUTF.h"
21 #include "tools/skui/InputState.h"
22 
23 #include <algorithm>
24 #include <limits>
25 #include <utility>
26 
27 namespace skottie_utils {
28 
29 namespace {
30 
make_cursor_path()31 SkPath make_cursor_path() {
32     // Normalized values, relative to text/font size.
33     constexpr float kWidth  = 0.2f,
34                     kHeight = 0.75f;
35 
36     SkPath p;
37 
38     p.lineTo(kWidth  , 0);
39     p.moveTo(kWidth/2, 0);
40     p.lineTo(kWidth/2, kHeight);
41     p.moveTo(0       , kHeight);
42     p.lineTo(kWidth  , kHeight);
43 
44     return p;
45 }
46 
next_utf8(const SkString & str,size_t index)47 size_t next_utf8(const SkString& str, size_t index) {
48     SkASSERT(index < str.size());
49 
50     const char* utf8_ptr = str.c_str() + index;
51 
52     if (SkUTF::NextUTF8(&utf8_ptr, str.c_str() + str.size()) < 0){
53         // Invalid UTF sequence.
54         return index;
55     }
56 
57     return utf8_ptr - str.c_str();
58 }
59 
prev_utf8(const SkString & str,size_t index)60 size_t prev_utf8(const SkString& str, size_t index) {
61     SkASSERT(index > 0);
62 
63     // Find the previous utf8 index by probing the preceding 4 offsets.  Utf8 leading bytes are
64     // always distinct from continuation bytes, so only one of these probes will succeed.
65     for (unsigned i = 1; i <= SkUTF::kMaxBytesInUTF8Sequence && i <= index; ++i) {
66         const char* utf8_ptr = str.c_str() + index - i;
67         if (SkUTF::NextUTF8(&utf8_ptr, str.c_str() + str.size()) >= 0) {
68             return index - i;
69         }
70     }
71 
72     // Invalid UTF sequence.
73     return index;
74 }
75 
76 } // namespace
77 
TextEditor(std::unique_ptr<skottie::TextPropertyHandle> && prop,std::vector<std::unique_ptr<skottie::TextPropertyHandle>> && deps)78 TextEditor::TextEditor(
79         std::unique_ptr<skottie::TextPropertyHandle>&& prop,
80         std::vector<std::unique_ptr<skottie::TextPropertyHandle>>&& deps)
81     : fTextProp(std::move(prop))
82     , fDependentProps(std::move(deps))
83     , fCursorPath(make_cursor_path())
84     , fCursorBounds(fCursorPath.computeTightBounds())
85 {}
86 
87 TextEditor::~TextEditor() = default;
88 
toggleEnabled()89 void TextEditor::toggleEnabled() {
90     fEnabled = !fEnabled;
91 
92     auto txt = fTextProp->get();
93     txt.fDecorator = fEnabled ? sk_ref_sp(this) : nullptr;
94     fTextProp->set(txt);
95 
96     if (fEnabled) {
97         // Always reset the cursor position to the end.
98         fCursorIndex = txt.fText.size();
99     }
100 
101     fTimeBase = std::chrono::steady_clock::now();
102 }
103 
setEnabled(bool enabled)104 void TextEditor::setEnabled(bool enabled) {
105     if (enabled != fEnabled) {
106         this->toggleEnabled();
107     }
108 }
109 
currentSelection() const110 std::tuple<size_t, size_t> TextEditor::currentSelection() const {
111     // Selection can be inverted.
112     return std::make_tuple(std::min(std::get<0>(fSelection), std::get<1>(fSelection)),
113                            std::max(std::get<0>(fSelection), std::get<1>(fSelection)));
114 }
115 
closestGlyph(const SkPoint & pt) const116 size_t TextEditor::closestGlyph(const SkPoint& pt) const {
117     float  min_distance = std::numeric_limits<float>::max();
118     size_t min_index    = 0;
119 
120     for (size_t i = 0; i < fGlyphData.size(); ++i) {
121         const auto dist = (fGlyphData[i].fDevBounds.center() - pt).length();
122         if (dist < min_distance) {
123             min_distance = dist;
124             min_index = i;
125         }
126     }
127 
128     return min_index;
129 }
130 
drawCursor(SkCanvas * canvas,const TextInfo & tinfo) const131 void TextEditor::drawCursor(SkCanvas* canvas, const TextInfo& tinfo) const {
132     constexpr double kCursorHz = 2;
133     const auto now_ms = std::chrono::duration_cast<std::chrono::milliseconds>(
134                             std::chrono::steady_clock::now() - fTimeBase).count();
135     const long cycle = static_cast<long>(static_cast<double>(now_ms) * 0.001 * kCursorHz);
136     if (cycle & 1) {
137         // blink
138         return;
139     }
140 
141     auto txt_prop  = fTextProp->get();
142 
143     const auto glyph_index = [&]() -> size_t {
144         if (!fCursorIndex) {
145             return 0;
146         }
147 
148         const auto prev_index = prev_utf8(txt_prop.fText, fCursorIndex);
149         for (size_t i = 0; i < tinfo.fGlyphs.size(); ++i) {
150             if (tinfo.fGlyphs[i].fCluster >= prev_index) {
151                 return i;
152             }
153         }
154 
155         return tinfo.fGlyphs.size() - 1;
156     }();
157 
158     // Cursor index mapping:
159     //   0 -> before the first char
160     //   1 -> after the first char
161     //   2 -> after the second char
162     //   ...
163     // The cursor is bottom-aligned to the baseline (y = 0), and horizontally centered to the right
164     // of the glyph advance.
165     const auto cscale = txt_prop.fTextSize * tinfo.fScale,
166                 cxpos = (fCursorIndex ? tinfo.fGlyphs[glyph_index].fAdvance : 0)
167                          - fCursorBounds.width() * cscale * 0.5f,
168                 cypos = - fCursorBounds.height() * cscale;
169     const auto cpath  = fCursorPath.makeTransform(SkMatrix::Translate(cxpos, cypos) *
170                                                   SkMatrix::Scale(cscale, cscale));
171 
172     // We stroke the cursor twice, with different colors, to ensure reasonable contrast
173     // regardless of background.
174     // The default inner stroke width is .5px for a font size of 10, and scales proportionally.
175     // The outer stroke width is slightly larger.
176     const auto inner_width = cscale * fCursorWeight * 0.05f,
177                outer_width = inner_width * 3 / 2;
178 
179     SkPaint p;
180     p.setAntiAlias(true);
181     p.setStyle(SkPaint::kStroke_Style);
182     p.setStrokeCap(SkPaint::kRound_Cap);
183 
184     SkAutoCanvasRestore acr(canvas, true);
185     canvas->concat(tinfo.fGlyphs[glyph_index].fMatrix);
186 
187     p.setColor(SK_ColorWHITE);
188     p.setStrokeWidth(outer_width);
189     canvas->drawPath(cpath, p);
190     p.setColor(SK_ColorBLACK);
191     p.setStrokeWidth(inner_width);
192     canvas->drawPath(cpath, p);
193 }
194 
updateDeps(const SkString & txt)195 void TextEditor::updateDeps(const SkString& txt) {
196     for (const auto& dep : fDependentProps) {
197         auto txt_prop = dep->get();
198         txt_prop.fText = txt;
199         dep->set(txt_prop);
200     }
201 }
202 
insertChar(SkUnichar c)203 void TextEditor::insertChar(SkUnichar c) {
204     auto txt = fTextProp->get();
205     const auto initial_size = txt.fText.size();
206 
207     txt.fText.insertUnichar(fCursorIndex, c);
208     fCursorIndex += txt.fText.size() - initial_size;
209 
210     fTextProp->set(txt);
211     this->updateDeps(txt.fText);
212 }
213 
deleteChars(size_t offset,size_t count)214 void TextEditor::deleteChars(size_t offset, size_t count) {
215     auto txt = fTextProp->get();
216 
217     txt.fText.remove(offset, count);
218     fTextProp->set(txt);
219     this->updateDeps(txt.fText);
220 
221     fCursorIndex = offset;
222 }
223 
deleteSelection()224 bool TextEditor::deleteSelection() {
225     const auto [glyph_sel_start, glyph_sel_end] = this->currentSelection();
226     if (glyph_sel_start == glyph_sel_end) {
227         return false;
228     }
229 
230     const auto utf8_sel_start = fGlyphData[glyph_sel_start].fCluster,
231                utf8_sel_end   = fGlyphData[glyph_sel_end  ].fCluster;
232     SkASSERT(utf8_sel_start < utf8_sel_end);
233 
234     this->deleteChars(utf8_sel_start, utf8_sel_end - utf8_sel_start);
235 
236     fSelection = {0,0};
237 
238     return true;
239 }
240 
onDecorate(SkCanvas * canvas,const TextInfo & tinfo)241 void TextEditor::onDecorate(SkCanvas* canvas, const TextInfo& tinfo) {
242     const auto [sel_start, sel_end] = this->currentSelection();
243 
244     fGlyphData.clear();
245 
246     for (size_t i = 0; i < tinfo.fGlyphs.size(); ++i) {
247         const auto& ginfo = tinfo.fGlyphs[i];
248 
249         SkAutoCanvasRestore acr(canvas, true);
250         canvas->concat(ginfo.fMatrix);
251 
252         // Stash some glyph info, for later use.
253         fGlyphData.push_back({
254             canvas->getLocalToDevice().asM33().mapRect(ginfo.fBounds),
255             ginfo.fCluster
256         });
257 
258         if (i < sel_start || i >= sel_end) {
259             continue;
260         }
261 
262         static constexpr SkColor4f kSelectionColor{0, 0, 1, 0.4f};
263         canvas->drawRect(ginfo.fBounds, SkPaint(kSelectionColor));
264     }
265 
266     // Only draw the cursor when there's no active selection.
267     if (sel_start == sel_end) {
268         this->drawCursor(canvas, tinfo);
269     }
270 }
271 
onMouseInput(SkScalar x,SkScalar y,skui::InputState state,skui::ModifierKey)272 bool TextEditor::onMouseInput(SkScalar x, SkScalar y, skui::InputState state,
273                                      skui::ModifierKey) {
274     if (!fEnabled || fGlyphData.empty()) {
275         return false;
276     }
277 
278     switch (state) {
279     case skui::InputState::kDown: {
280         fMouseDown = true;
281 
282         const auto closest = this->closestGlyph({x, y});
283         fSelection = {closest, closest};
284     }   break;
285     case skui::InputState::kUp:
286         fMouseDown = false;
287         break;
288     case skui::InputState::kMove:
289         if (fMouseDown) {
290             const auto closest = this->closestGlyph({x, y});
291             std::get<1>(fSelection) = closest < std::get<0>(fSelection)
292                                             ? closest
293                                             : closest + 1;
294         }
295         break;
296     default:
297         break;
298     }
299 
300     return true;
301 }
302 
onCharInput(SkUnichar c)303 bool TextEditor::onCharInput(SkUnichar c) {
304     if (!fEnabled || fGlyphData.empty()) {
305         return false;
306     }
307 
308     const auto& txt_str = fTextProp->get().fText;
309 
310     // Natural editor bindings are currently intercepted by Viewer, so we use these instead.
311     switch (c) {
312     case '|':     // commit changes and exit editing mode
313         this->toggleEnabled();
314         break;
315     case ']': {   // move right
316         if (fCursorIndex < txt_str.size()) {
317             fCursorIndex = next_utf8(txt_str, fCursorIndex);
318         }
319     } break;
320     case '[':     // move left
321         if (fCursorIndex > 0) {
322             fCursorIndex = prev_utf8(txt_str, fCursorIndex);
323         }
324         break;
325     case '\\': {  // delete
326         if (!this->deleteSelection() && fCursorIndex > 0) {
327             // Delete preceding char.
328             const auto del_index = prev_utf8(txt_str, fCursorIndex),
329                        del_count = fCursorIndex - del_index;
330 
331             this->deleteChars(del_index, del_count);
332         }
333     }   break;
334     default:
335         // Delete any selection on insert.
336         this->deleteSelection();
337         this->insertChar(c);
338         break;
339     }
340 
341     // Reset the cursor blink timer on input.
342     fTimeBase = std::chrono::steady_clock::now();
343 
344     return true;
345 }
346 
347 }  // namespace skottie_utils
348