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