HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
editable_text.hpp
1// Copyright Take Vos 2019-2021.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
5#pragma once
6
7#include "attributed_grapheme.hpp"
8#include "shaped_text.hpp"
9#include "font.hpp"
10#include <string>
11#include <vector>
12
13namespace tt {
14
17 shaped_text _shapedText;
18
22 float width = 0.0f;
23
26 bool insertMode = true;
27
30 ssize_t cursorIndex = 0;
31
35 ssize_t selectionIndex = 0;
36
37 text_style currentStyle;
38
41 bool hasPartialgrapheme = false;
42
43public:
45 text(), _shapedText(), currentStyle(style)
46 {
47 }
48
49 [[nodiscard]] operator std::string() const noexcept
50 {
51 auto r = std::string{};
52
53 for (ttlet &c : text) {
54 r += to_string(c.grapheme.NFC());
55 }
56
57 return r;
58 }
59
60 editable_text &operator=(std::string_view str) noexcept
61 {
62 cancelPartialgrapheme();
63
64 gstring gstr = to_gstring(str);
65
66 text.clear();
67 text.reserve(std::ssize(gstr));
68 for (ttlet &g : gstr) {
69 text.emplace_back(g, currentStyle);
70 }
71
72 selectionIndex = cursorIndex = 0;
73 tt_axiom(selectionIndex >= 0);
74 tt_axiom(selectionIndex <= std::ssize(text));
75 tt_axiom(cursorIndex >= 0);
76 tt_axiom(cursorIndex <= std::ssize(text));
77
79 return *this;
80 }
81
84 void updateshaped_text() noexcept {
85 auto text_ = text;
86
87 // Make sure there is an end-paragraph marker in the text.
88 // This allows the shapedText to figure out the style of the text of an empty paragraph.
89 if (std::ssize(text) == 0) {
90 text_.emplace_back(grapheme::PS(), currentStyle, 0);
91 } else {
92 text_.emplace_back(grapheme::PS(), text_.back().style, 0);
93 }
94
95 _shapedText = shaped_text(text_, width, alignment::top_left, false);
96 }
97
98 [[nodiscard]] shaped_text shapedText() const noexcept {
99 return _shapedText;
100 }
101
102 void setWidth(float _width) noexcept {
103 width = _width;
105 }
106
107 void setCurrentStyle(text_style style) noexcept {
108 this->currentStyle = style;
109 }
110
113 void setStyleOfAll(text_style style) noexcept {
114 setCurrentStyle(style);
115 for (auto &c: text) {
116 c.style = style;
117 }
119 }
120
121 size_t size() const noexcept {
122 return text.size();
123 }
124
127 decltype(auto) it(ssize_t index) noexcept {
128 tt_axiom(index >= 0);
129 // Index should never be at text.cend();
130 tt_axiom(index < std::ssize(text));
131
132 return text.begin() + index;
133 }
134
137 decltype(auto) cit(ssize_t index) const noexcept {
138 tt_axiom(index >= 0);
139 // Index should never be beyond text.cend();
140 tt_axiom(index <= std::ssize(text));
141
142 return text.cbegin() + index;
143 }
144
145 decltype(auto) it(ssize_t index) const noexcept {
146 return cit(index);
147 }
148
152 if (hasPartialgrapheme) {
153 tt_axiom(cursorIndex != 0);
154 return _shapedText.leftToRightCaret(cursorIndex - 1, false);
155 } else {
156 return {};
157 }
158 }
159
162 aarectangle leftToRightCaret() const noexcept {
163 return _shapedText.leftToRightCaret(cursorIndex, insertMode);
164 }
165
169 auto r = std::vector<aarectangle>{};
170 if (selectionIndex < cursorIndex) {
171 r = _shapedText.selectionRectangles(selectionIndex, cursorIndex);
172 } else if (selectionIndex > cursorIndex) {
173 r = _shapedText.selectionRectangles(cursorIndex, selectionIndex);
174 }
175 return r;
176 }
177
182 void deleteSelection() noexcept {
183 if (selectionIndex < cursorIndex) {
184 text.erase(cit(selectionIndex), cit(cursorIndex));
185 cursorIndex = selectionIndex;
187 } else if (selectionIndex > cursorIndex) {
188 text.erase(cit(cursorIndex), cit(selectionIndex));
189 selectionIndex = cursorIndex;
191 }
192 }
193
196 ssize_t characterIndexAtPosition(point2 position) const noexcept;
197
198 void setmouse_cursorAtCoordinate(point2 coordinate) noexcept
199 {
200 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
201 selectionIndex = cursorIndex = *newmouse_cursorPosition;
202 tt_axiom(selectionIndex >= 0);
203 tt_axiom(selectionIndex <= std::ssize(text));
204 }
205 }
206
207 void selectWordAtCoordinate(point2 coordinate) noexcept
208 {
209 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
210 std::tie(selectionIndex, cursorIndex) = _shapedText.indicesOfWord(*newmouse_cursorPosition);
211 tt_axiom(selectionIndex >= 0);
212 tt_axiom(selectionIndex <= std::ssize(text));
213 tt_axiom(cursorIndex >= 0);
214 tt_axiom(cursorIndex <= std::ssize(text));
215 }
216 }
217
218 void selectParagraphAtCoordinate(point2 coordinate) noexcept
219 {
220 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
221 std::tie(selectionIndex, cursorIndex) = _shapedText.indicesOfParagraph(*newmouse_cursorPosition);
222 tt_axiom(selectionIndex >= 0);
223 tt_axiom(selectionIndex <= std::ssize(text));
224 tt_axiom(cursorIndex >= 0);
225 tt_axiom(cursorIndex <= std::ssize(text));
226 }
227 }
228
229 void dragmouse_cursorAtCoordinate(point2 coordinate) noexcept
230 {
231 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
232 cursorIndex = *newmouse_cursorPosition;
233 tt_axiom(cursorIndex >= 0);
234 tt_axiom(cursorIndex <= std::ssize(text));
235 }
236 }
237
238 void dragWordAtCoordinate(point2 coordinate) noexcept
239 {
240 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
241 ttlet [a, b] = _shapedText.indicesOfWord(*newmouse_cursorPosition);
242
243 if (selectionIndex <= cursorIndex) {
244 if (a < selectionIndex) {
245 // Reverse selection
246 selectionIndex = cursorIndex;
247 cursorIndex = a;
248 } else {
249 cursorIndex = b;
250 }
251 } else {
252 if (b > selectionIndex) {
253 // Reverse selection
254 selectionIndex = cursorIndex;
255 cursorIndex = b;
256 } else {
257 cursorIndex = a;
258 }
259 }
260
261 tt_axiom(selectionIndex >= 0);
262 tt_axiom(selectionIndex <= std::ssize(text));
263 tt_axiom(cursorIndex >= 0);
264 tt_axiom(cursorIndex <= std::ssize(text));
265 }
266 }
267
268 void dragParagraphAtCoordinate(point2 coordinate) noexcept
269 {
270 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharAtCoordinate(coordinate)) {
271 ttlet [a, b] = _shapedText.indicesOfParagraph(*newmouse_cursorPosition);
272
273 if (selectionIndex <= cursorIndex) {
274 if (a < selectionIndex) {
275 // Reverse selection
276 selectionIndex = cursorIndex;
277 cursorIndex = a;
278 } else {
279 cursorIndex = b;
280 }
281 } else {
282 if (b > selectionIndex) {
283 // Reverse selection
284 selectionIndex = cursorIndex;
285 cursorIndex = b;
286 } else {
287 cursorIndex = a;
288 }
289 }
290
291 tt_axiom(selectionIndex >= 0);
292 tt_axiom(selectionIndex <= std::ssize(text));
293 tt_axiom(cursorIndex >= 0);
294 tt_axiom(cursorIndex <= std::ssize(text));
295 }
296 }
297
298 void cancelPartialgrapheme() noexcept {
299 if (hasPartialgrapheme) {
300 tt_axiom(cursorIndex >= 1);
301
302 selectionIndex = --cursorIndex;
303 tt_axiom(selectionIndex >= 0);
304 tt_axiom(selectionIndex <= std::ssize(text));
305 tt_axiom(cursorIndex >= 0);
306 tt_axiom(cursorIndex <= std::ssize(text));
307
308 text.erase(cit(cursorIndex));
309 hasPartialgrapheme = false;
310
312 }
313 }
314
320 void insertPartialgrapheme(grapheme character) noexcept {
321 cancelPartialgrapheme();
323
324 text.emplace(cit(cursorIndex), character, currentStyle);
325 selectionIndex = ++cursorIndex;
326 tt_axiom(selectionIndex >= 0);
327 tt_axiom(selectionIndex <= std::ssize(text));
328 tt_axiom(cursorIndex >= 0);
329 tt_axiom(cursorIndex <= std::ssize(text));
330
331 hasPartialgrapheme = true;
333 }
334
338 void insertgrapheme(grapheme character) noexcept {
339 cancelPartialgrapheme();
341
342 if (!insertMode) {
343 handle_event(command::text_delete_char_next);
344 }
345 text.emplace(cit(cursorIndex), character, currentStyle);
346 selectionIndex = ++cursorIndex;
347 tt_axiom(selectionIndex >= 0);
348 tt_axiom(selectionIndex <= std::ssize(text));
349 tt_axiom(cursorIndex >= 0);
350 tt_axiom(cursorIndex <= std::ssize(text));
351
353 }
354
355 void handlePaste(std::string str) noexcept {
356 cancelPartialgrapheme();
358
359 gstring gstr = to_gstring(str);
360
361 auto str_attr = std::vector<attributed_grapheme>{};
362 str_attr.reserve(std::ssize(gstr));
363 for (ttlet &g: gstr) {
364 str_attr.emplace_back(g, currentStyle);
365 }
366
367 text.insert(cit(cursorIndex), str_attr.cbegin(), str_attr.cend());
368 selectionIndex = cursorIndex += std::ssize(str_attr);
369 tt_axiom(selectionIndex >= 0);
370 tt_axiom(selectionIndex <= std::ssize(text));
371 tt_axiom(cursorIndex >= 0);
372 tt_axiom(cursorIndex <= std::ssize(text));
373
375 }
376
377 std::string handleCopy() noexcept {
378 auto r = std::string{};
379
380 if (selectionIndex < cursorIndex) {
381 r.reserve(cursorIndex - selectionIndex);
382 for (auto i = cit(selectionIndex); i != cit(cursorIndex); ++i) {
383 r += to_string(i->grapheme);
384 }
385 } else if (selectionIndex > cursorIndex) {
386 r.reserve(selectionIndex - cursorIndex);
387 for (auto i = cit(cursorIndex); i != cit(selectionIndex); ++i) {
388 r += to_string(i->grapheme);
389 }
390 }
391 return r;
392 }
393
394 std::string handleCut() noexcept {
395 auto r = handleCopy();
396 cancelPartialgrapheme();
398 return r;
399 }
400
401 bool handle_event(command command) noexcept {
402 auto handled = false;
403
404 tt_axiom(cursorIndex <= std::ssize(text));
405 cancelPartialgrapheme();
406
407 switch (command) {
408 case command::text_cursor_char_left:
409 handled = true;
410 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharOnTheLeft(cursorIndex)) {
411 // XXX Change currentStyle based on the grapheme at the new cursor position.
412 selectionIndex = cursorIndex = *newmouse_cursorPosition;
413 }
414 break;
415
416 case command::text_cursor_char_right:
417 handled = true;
418 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharOnTheRight(cursorIndex)) {
419 selectionIndex = cursorIndex = *newmouse_cursorPosition;
420 }
421 break;
422
423 case command::text_cursor_word_left:
424 handled = true;
425 if (ttlet newmouse_cursorPosition = _shapedText.indexOfWordOnTheLeft(cursorIndex)) {
426 selectionIndex = cursorIndex = *newmouse_cursorPosition;
427 }
428 break;
429
430 case command::text_cursor_word_right:
431 handled = true;
432 if (ttlet newmouse_cursorPosition = _shapedText.indexOfWordOnTheRight(cursorIndex)) {
433 selectionIndex = cursorIndex = *newmouse_cursorPosition;
434 }
435 break;
436
437 case command::text_cursor_line_end:
438 handled = true;
439 selectionIndex = cursorIndex = size() - 1;
440 break;
441
442 case command::text_cursor_line_begin:
443 handled = true;
444 selectionIndex = cursorIndex = 0;
445 break;
446
447 case command::text_select_char_left:
448 handled = true;
449 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharOnTheLeft(cursorIndex)) {
450 cursorIndex = *newmouse_cursorPosition;
451 }
452 break;
453
454 case command::text_select_char_right:
455 handled = true;
456 if (ttlet newmouse_cursorPosition = _shapedText.indexOfCharOnTheRight(cursorIndex)) {
457 cursorIndex = *newmouse_cursorPosition;
458 }
459 break;
460
461 case command::text_select_word_left:
462 handled = true;
463 if (ttlet newmouse_cursorPosition = _shapedText.indexOfWordOnTheLeft(cursorIndex)) {
464 cursorIndex = *newmouse_cursorPosition;
465 }
466 break;
467
468 case command::text_select_word_right:
469 handled = true;
470 if (ttlet newmouse_cursorPosition = _shapedText.indexOfWordOnTheRight(cursorIndex)) {
471 cursorIndex = *newmouse_cursorPosition;
472 }
473 break;
474
475 case command::text_select_word:
476 handled = true;
477 std::tie(selectionIndex, cursorIndex) = _shapedText.indicesOfWord(cursorIndex);
478 break;
479
480 case command::text_select_line_end:
481 handled = true;
482 cursorIndex = size() - 1;
483 break;
484
485 case command::text_select_line_begin:
486 handled = true;
487 cursorIndex = 0;
488 break;
489
490 case command::text_select_document:
491 handled = true;
492 selectionIndex = 0;
493 cursorIndex = size() - 1; // Upto end-of-paragraph marker.
494 break;
495
496 case command::text_mode_insert:
497 handled = true;
498 insertMode = !insertMode;
499 break;
500
501 case command::text_delete_char_prev:
502 handled = true;
503 if (cursorIndex != selectionIndex) {
505
506 } else if (cursorIndex >= 1) {
507 selectionIndex = --cursorIndex;
508 text.erase(cit(cursorIndex));
510 }
511 break;
512
513 case command::text_delete_char_next:
514 handled = true;
515 if (cursorIndex != selectionIndex) {
517
518 } else if (cursorIndex < (std::ssize(text) - 1)) {
519 // Don't delete the trailing paragraph separator.
520 text.erase(cit(cursorIndex));
522 }
523 default:;
524 }
525
526 tt_axiom(selectionIndex >= 0);
527 tt_axiom(selectionIndex <= std::ssize(text));
528 tt_axiom(cursorIndex >= 0);
529 tt_axiom(cursorIndex <= std::ssize(text));
530 return handled;
531 }
532};
533
534
535}
536
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:18
Definition editable_text.hpp:15
decltype(auto) cit(ssize_t index) const noexcept
Return the text iterator at index.
Definition editable_text.hpp:137
void setStyleOfAll(text_style style) noexcept
Change the text style of all graphemes.
Definition editable_text.hpp:113
void updateshaped_text() noexcept
Update the shaped text after changed to text.
Definition editable_text.hpp:84
void deleteSelection() noexcept
Delete a selection.
Definition editable_text.hpp:182
decltype(auto) it(ssize_t index) noexcept
Return the text iterator at index.
Definition editable_text.hpp:127
std::vector< aarectangle > selectionRectangles() const noexcept
Get a set of rectangles for which text is selected.
Definition editable_text.hpp:168
ssize_t characterIndexAtPosition(point2 position) const noexcept
void insertgrapheme(grapheme character) noexcept
Definition editable_text.hpp:338
aarectangle partialgraphemeCaret() const noexcept
Get carets at the cursor position.
Definition editable_text.hpp:151
void insertPartialgrapheme(grapheme character) noexcept
Definition editable_text.hpp:320
aarectangle leftToRightCaret() const noexcept
Get carets at the cursor position.
Definition editable_text.hpp:162
Definition grapheme.hpp:21
static grapheme PS() noexcept
Paragraph separator.
Definition grapheme.hpp:196
Definition gstring.hpp:13
shaped_text represent a piece of text shaped to be displayed.
Definition shaped_text.hpp:23
std::optional< ssize_t > indexOfWordOnTheLeft(ssize_t logical_index) const noexcept
Get the first character of the word on the left.
std::optional< ssize_t > indexOfCharOnTheRight(ssize_t logical_index) const noexcept
Get the character right of the given character.
std::optional< ssize_t > indexOfWordOnTheRight(ssize_t logical_index) const noexcept
Get the last character of the word on the right.
std::vector< aarectangle > selectionRectangles(ssize_t first, ssize_t last) const noexcept
Return a list of merged rectangles to display for the selection.
std::pair< ssize_t, ssize_t > indicesOfWord(ssize_t logical_index) const noexcept
Get the word with the given character.
std::pair< ssize_t, ssize_t > indicesOfParagraph(ssize_t logical_index) const noexcept
Get the character right of the given character.
aarectangle leftToRightCaret(ssize_t index, bool overwrite) const noexcept
Return the cursor-carets.
std::optional< ssize_t > indexOfCharAtCoordinate(point2 coordinate) const noexcept
Get the character close to a coordinate.
std::optional< ssize_t > indexOfCharOnTheLeft(ssize_t logical_index) const noexcept
Get the character left of the given character.
Definition text_style.hpp:16
T begin(T... args)
T clear(T... args)
T emplace_back(T... args)
T emplace(T... args)
T erase(T... args)
T insert(T... args)
T reserve(T... args)
T size(T... args)
T tie(T... args)
T to_string(T... args)