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