Flutter Engine
The Flutter Engine
TextEditor.cpp
Go to the documentation of this file.
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
9
12#include "include/core/SkM44.h"
15#include "include/core/SkPath.h"
18#include "include/core/SkSpan.h"
20#include "src/base/SkUTF.h"
22
23#include <algorithm>
24#include <limits>
25#include <utility>
26
27namespace skottie_utils {
28
29namespace {
30
31SkPath 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
47size_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
60size_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
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
87TextEditor::~TextEditor() = default;
88
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
104void TextEditor::setEnabled(bool enabled) {
105 if (enabled != fEnabled) {
106 this->toggleEnabled();
107 }
108}
109
110std::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
116size_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
131void 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
195void 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
203void 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
214void 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
224bool 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
241void 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
274 if (!fEnabled || fGlyphData.empty()) {
275 return false;
276 }
277
278 switch (state) {
280 fMouseDown = true;
281
282 const auto closest = this->closestGlyph({x, y});
283 fSelection = {closest, closest};
284 } break;
286 fMouseDown = false;
287 break;
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
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
int count
Definition: FontMgrTest.cpp:50
#define SkASSERT(cond)
Definition: SkAssert.h:116
constexpr SkColor SK_ColorBLACK
Definition: SkColor.h:103
constexpr SkColor SK_ColorWHITE
Definition: SkColor.h:122
sk_sp< T > sk_ref_sp(T *obj)
Definition: SkRefCnt.h:381
int32_t SkUnichar
Definition: SkTypes.h:175
void drawRect(const SkRect &rect, const SkPaint &paint)
Definition: SkCanvas.cpp:1673
SkM44 getLocalToDevice() const
Definition: SkCanvas.cpp:1633
void drawPath(const SkPath &path, const SkPaint &paint)
Definition: SkCanvas.cpp:1747
void concat(const SkMatrix &matrix)
Definition: SkCanvas.cpp:1318
static SkMatrix Scale(SkScalar sx, SkScalar sy)
Definition: SkMatrix.h:75
static SkMatrix Translate(SkScalar dx, SkScalar dy)
Definition: SkMatrix.h:91
@ kRound_Cap
adds circle
Definition: SkPaint.h:335
@ kStroke_Style
set to stroke geometry
Definition: SkPaint.h:194
Definition: SkPath.h:59
SkPath makeTransform(const SkMatrix &m, SkApplyPerspectiveClip pc=SkApplyPerspectiveClip::kYes) const
Definition: SkPath.h:1400
size_t size() const
Definition: SkString.h:131
const char * c_str() const
Definition: SkString.h:133
bool onCharInput(SkUnichar c)
Definition: TextEditor.cpp:303
TextEditor(std::unique_ptr< skottie::TextPropertyHandle > &&, std::vector< std::unique_ptr< skottie::TextPropertyHandle > > &&)
Definition: TextEditor.cpp:78
bool onMouseInput(SkScalar x, SkScalar y, skui::InputState state, skui::ModifierKey)
Definition: TextEditor.cpp:272
void onDecorate(SkCanvas *, const TextInfo &) override
Definition: TextEditor.cpp:241
static const char * prev_utf8(const char *p, const char *begin)
Definition: editor.cpp:127
static const char * next_utf8(const char *p, const char *end)
Definition: editor.cpp:111
float SkScalar
Definition: extension.cpp:12
AtkStateType state
static float max(float r, float g, float b)
Definition: hsl.cpp:49
static float min(float r, float g, float b)
Definition: hsl.cpp:48
size_t length
double y
double x
constexpr unsigned kMaxBytesInUTF8Sequence
Definition: SkUTF.h:59
SK_SPI SkUnichar NextUTF8(const char **ptr, const char *end)
Definition: SkUTF.cpp:118
const myers::Point & get< 1 >(const myers::Segment &s)
Definition: Myers.h:81
const myers::Point & get< 0 >(const myers::Segment &s)
Definition: Myers.h:80
InputState
Definition: InputState.h:6
ModifierKey
Definition: ModifierKey.h:9
Definition: ref_ptr.h:256
static double now_ms()
Definition: nanobench.cpp:214
static SkScalar prop(SkScalar radius, SkScalar newSize, SkScalar oldSize)
Definition: rrect.cpp:83
SeparatedVector2 offset
constexpr float height() const
Definition: SkRect.h:769
constexpr float width() const
Definition: SkRect.h:762
SkSpan< const GlyphInfo > fGlyphs
constexpr size_t kHeight
constexpr size_t kWidth