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 "../ranges.hpp"
11#include "../gap_buffer.hpp"
12#include <string>
13#include <vector>
14
15namespace tt {
16
18
19
20public:
22 _font_book(font_book), _text(), _shaped_text(), _current_style(style)
23 {
24 }
25
26 [[nodiscard]] operator std::string() const noexcept
27 {
28 auto r = std::string{};
29
30 for (ttlet &c : _text) {
31 r += to_string(c.grapheme.NFC());
32 }
33
34 return r;
35 }
36
37 editable_text &operator=(std::string_view str) noexcept
38 {
39 tt_axiom(is_valid());
40 cancel_partial_grapheme();
41
42 gstring gstr = to_gstring(str);
43
44 _text.clear();
45 _text.reserve(std::ssize(gstr));
46 for (ttlet &g : gstr) {
47 _text.emplace_back(g, _current_style);
48 }
49
50 _selection_index = _cursor_index = 0;
51
53 tt_axiom(is_valid());
54 return *this;
55 }
56
59 void update_shaped_text() noexcept {
60 auto text_ = make_vector(_text);
61
62 // Make sure there is an end-paragraph marker in the _text.
63 // This allows the shaped_text to figure out the style of the _text of an empty paragraph.
64 if (std::ssize(_text) == 0) {
65 text_.emplace_back(grapheme::PS(), _current_style, 0);
66 } else {
67 text_.emplace_back(grapheme::PS(), text_.back().style, 0);
68 }
69
70 _shaped_text = tt::shaped_text{_font_book, text_, _width, alignment::top_left, false};
71 }
72
73 [[nodiscard]] shaped_text shaped_text() const noexcept {
74 return _shaped_text;
75 }
76
77 void set_width(float width) noexcept {
78 _width = width;
80 }
81
82 void set_current_style(text_style style) noexcept {
83 this->_current_style = style;
84 }
85
88 void set_style_of_all(text_style style) noexcept {
89 set_current_style(style);
90 for (auto &c: _text) {
91 c.style = style;
92 }
94 }
95
96 size_t size() const noexcept {
97 return _text.size();
98 }
99
102 decltype(auto) it(ssize_t index) noexcept {
103 tt_axiom(index >= 0);
104 // Index should never be at _text.cend();
105 tt_axiom(index < std::ssize(_text));
106
107 return _text.begin() + index;
108 }
109
112 decltype(auto) cit(ssize_t index) const noexcept {
113 tt_axiom(index >= 0);
114 // Index should never be beyond _text.cend();
115 tt_axiom(index <= std::ssize(_text));
116
117 return _text.cbegin() + index;
118 }
119
120 decltype(auto) it(ssize_t index) const noexcept {
121 return cit(index);
122 }
123
127 tt_axiom(is_valid());
128
129 if (_has_partial_grapheme) {
130 tt_axiom(_cursor_index != 0);
131 return _shaped_text.left_to_right_caret(_cursor_index - 1, false);
132 } else {
133 return {};
134 }
135 }
136
140 tt_axiom(is_valid());
141 return _shaped_text.left_to_right_caret(_cursor_index, _insert_mode);
142 }
143
147 tt_axiom(is_valid());
148 auto r = std::vector<aarectangle>{};
149 if (_selection_index < _cursor_index) {
150 r = _shaped_text.selection_rectangles(_selection_index, _cursor_index);
151 } else if (_selection_index > _cursor_index) {
152 r = _shaped_text.selection_rectangles(_cursor_index, _selection_index);
153 }
154 tt_axiom(is_valid());
155 return r;
156 }
157
162 void delete_selection() noexcept {
163 tt_axiom(is_valid());
164
165 if (_selection_index < _cursor_index) {
166 _text.erase(cit(_selection_index), cit(_cursor_index));
167 _cursor_index = _selection_index;
169 } else if (_selection_index > _cursor_index) {
170 _text.erase(cit(_cursor_index), cit(_selection_index));
171 _selection_index = _cursor_index;
173 }
174 tt_axiom(is_valid());
175 }
176
180
181 void set_cursor_at_coordinate(point2 coordinate) noexcept
182 {
183 tt_axiom(is_valid());
184 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
185 _selection_index = _cursor_index = *new_cursor_position;
186 }
187 tt_axiom(is_valid());
188 }
189
190 void select_word_at_coordinate(point2 coordinate) noexcept
191 {
192 tt_axiom(is_valid());
193
194 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
195 std::tie(_selection_index, _cursor_index) = _shaped_text.indices_of_word(*new_cursor_position);
196 }
197
198 tt_axiom(is_valid());
199 }
200
201 void select_paragraph_at_coordinate(point2 coordinate) noexcept
202 {
203 tt_axiom(is_valid());
204
205 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
206 std::tie(_selection_index, _cursor_index) = _shaped_text.indices_of_paragraph(*new_cursor_position);
207 }
208
209 tt_axiom(is_valid());
210 }
211
212 void drag_cursor_at_coordinate(point2 coordinate) noexcept
213 {
214 tt_axiom(is_valid());
215
216 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
217 _cursor_index = *new_cursor_position;
218 }
219 tt_axiom(is_valid());
220 }
221
222 void drag_word_at_coordinate(point2 coordinate) noexcept
223 {
224 tt_axiom(is_valid());
225
226 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
227 ttlet[a, b] = _shaped_text.indices_of_word(*new_cursor_position);
228
229 if (_selection_index <= _cursor_index) {
230 if (a < _selection_index) {
231 // Reverse selection
232 _selection_index = _cursor_index;
233 _cursor_index = a;
234 } else {
235 _cursor_index = b;
236 }
237 } else {
238 if (b > _selection_index) {
239 // Reverse selection
240 _selection_index = _cursor_index;
241 _cursor_index = b;
242 } else {
243 _cursor_index = a;
244 }
245 }
246 }
247 tt_axiom(is_valid());
248 }
249
250 void drag_paragraph_at_coordinate(point2 coordinate) noexcept
251 {
252 tt_axiom(is_valid());
253
254 if (ttlet new_cursor_position = _shaped_text.index_of_grapheme_at_coordinate(coordinate)) {
255 ttlet[a, b] = _shaped_text.indices_of_paragraph(*new_cursor_position);
256
257 if (_selection_index <= _cursor_index) {
258 if (a < _selection_index) {
259 // Reverse selection
260 _selection_index = _cursor_index;
261 _cursor_index = a;
262 } else {
263 _cursor_index = b;
264 }
265 } else {
266 if (b > _selection_index) {
267 // Reverse selection
268 _selection_index = _cursor_index;
269 _cursor_index = b;
270 } else {
271 _cursor_index = a;
272 }
273 }
274 }
275
276 tt_axiom(is_valid());
277 }
278
279 void cancel_partial_grapheme() noexcept {
280 tt_axiom(is_valid());
281
282 if (_has_partial_grapheme) {
283 tt_axiom(_cursor_index >= 1);
284
285 _selection_index = --_cursor_index;
286
287 _text.erase(cit(_cursor_index));
288 _has_partial_grapheme = false;
289
291 }
292
293 tt_axiom(is_valid());
294 }
295
301 void insert_partial_grapheme(grapheme character) noexcept {
302 tt_axiom(is_valid());
303
304 cancel_partial_grapheme();
306
307 _text.emplace_before(cit(_cursor_index), character, _current_style);
308 _selection_index = ++_cursor_index;
309
310 _has_partial_grapheme = true;
312
313 tt_axiom(is_valid());
314 }
315
319 void insert_grapheme(grapheme character) noexcept {
320 tt_axiom(is_valid());
321
322 cancel_partial_grapheme();
324
325 if (!_insert_mode) {
326 handle_event(command::text_delete_char_next);
327 }
328 _text.emplace_before(cit(_cursor_index), character, _current_style);
329 _selection_index = ++_cursor_index;
330
332
333 tt_axiom(is_valid());
334 }
335
336 void handle_paste(std::string str) noexcept {
337 tt_axiom(is_valid());
338
339 cancel_partial_grapheme();
341
342 gstring gstr = to_gstring(str);
343
344 auto str_attr = std::vector<attributed_grapheme>{};
345 str_attr.reserve(std::ssize(gstr));
346 for (ttlet &g: gstr) {
347 str_attr.emplace_back(g, _current_style);
348 }
349
350 _text.insert_after(cit(_cursor_index), str_attr.cbegin(), str_attr.cend());
351 _selection_index = _cursor_index += std::ssize(str_attr);
352
354 tt_axiom(is_valid());
355 }
356
357 std::string handle_copy() noexcept {
358 tt_axiom(is_valid());
359
360 auto r = std::string{};
361
362 if (_selection_index < _cursor_index) {
363 r.reserve(_cursor_index - _selection_index);
364 for (auto i = cit(_selection_index); i != cit(_cursor_index); ++i) {
365 r += to_string(i->grapheme);
366 }
367 } else if (_selection_index > _cursor_index) {
368 r.reserve(_selection_index - _cursor_index);
369 for (auto i = cit(_cursor_index); i != cit(_selection_index); ++i) {
370 r += to_string(i->grapheme);
371 }
372 }
373
374 tt_axiom(is_valid());
375 return r;
376 }
377
378 std::string handle_cut() noexcept {
379 tt_axiom(is_valid());
380
381 auto r = handle_copy();
382 cancel_partial_grapheme();
384
385 tt_axiom(is_valid());
386 return r;
387 }
388
389 bool handle_event(command command) noexcept {
390 tt_axiom(is_valid());
391
392 auto handled = false;
393 switch (command) {
394 case command::text_cursor_char_left:
395 cancel_partial_grapheme();
396 handled = true;
397 if (ttlet new_cursor_position = _shaped_text.indexOfCharOnTheLeft(_cursor_index)) {
398 // XXX Change _current_style based on the grapheme at the new cursor position.
399 _selection_index = _cursor_index = *new_cursor_position;
400 }
401 break;
402
403 case command::text_cursor_char_right:
404 cancel_partial_grapheme();
405 handled = true;
406 if (ttlet new_cursor_position = _shaped_text.indexOfCharOnTheRight(_cursor_index)) {
407 _selection_index = _cursor_index = *new_cursor_position;
408 }
409 break;
410
411 case command::text_cursor_word_left:
412 cancel_partial_grapheme();
413 handled = true;
414 if (ttlet new_cursor_position = _shaped_text.indexOfWordOnTheLeft(_cursor_index)) {
415 _selection_index = _cursor_index = *new_cursor_position;
416 }
417 break;
418
419 case command::text_cursor_word_right:
420 cancel_partial_grapheme();
421 handled = true;
422 if (ttlet new_cursor_position = _shaped_text.indexOfWordOnTheRight(_cursor_index)) {
423 _selection_index = _cursor_index = *new_cursor_position;
424 }
425 break;
426
427 case command::text_cursor_line_end:
428 cancel_partial_grapheme();
429 handled = true;
430 _selection_index = _cursor_index = size();
431 break;
432
433 case command::text_cursor_line_begin:
434 cancel_partial_grapheme();
435 handled = true;
436 _selection_index = _cursor_index = 0;
437 break;
438
439 case command::text_select_char_left:
440 cancel_partial_grapheme();
441 handled = true;
442 if (ttlet new_cursor_position = _shaped_text.indexOfCharOnTheLeft(_cursor_index)) {
443 _cursor_index = *new_cursor_position;
444 }
445 break;
446
447 case command::text_select_char_right:
448 cancel_partial_grapheme();
449 handled = true;
450 if (ttlet new_cursor_position = _shaped_text.indexOfCharOnTheRight(_cursor_index)) {
451 _cursor_index = *new_cursor_position;
452 }
453 break;
454
455 case command::text_select_word_left:
456 cancel_partial_grapheme();
457 handled = true;
458 if (ttlet new_cursor_position = _shaped_text.indexOfWordOnTheLeft(_cursor_index)) {
459 _cursor_index = *new_cursor_position;
460 }
461 break;
462
463 case command::text_select_word_right:
464 cancel_partial_grapheme();
465 handled = true;
466 if (ttlet new_cursor_position = _shaped_text.indexOfWordOnTheRight(_cursor_index)) {
467 _cursor_index = *new_cursor_position;
468 }
469 break;
470
471 case command::text_select_word:
472 cancel_partial_grapheme();
473 handled = true;
474 std::tie(_selection_index, _cursor_index) = _shaped_text.indices_of_word(_cursor_index);
475 break;
476
477 case command::text_select_line_end:
478 cancel_partial_grapheme();
479 handled = true;
480 _cursor_index = size();
481 break;
482
483 case command::text_select_line_begin:
484 cancel_partial_grapheme();
485 handled = true;
486 _cursor_index = 0;
487 break;
488
489 case command::text_select_document:
490 cancel_partial_grapheme();
491 handled = true;
492 _selection_index = 0;
493 _cursor_index = size();
494 break;
495
496 case command::text_mode_insert:
497 cancel_partial_grapheme();
498 handled = true;
499 _insert_mode = !_insert_mode;
500 break;
501
502 case command::text_delete_char_prev:
503 cancel_partial_grapheme();
504 handled = true;
505 if (_cursor_index != _selection_index) {
507
508 } else if (_cursor_index >= 1) {
509 _selection_index = --_cursor_index;
510 _text.erase(cit(_cursor_index));
512 }
513 break;
514
515 case command::text_delete_char_next:
516 cancel_partial_grapheme();
517 handled = true;
518 if (_cursor_index != _selection_index) {
520
521 } else if (_cursor_index < std::ssize(_text)) {
522 // Don't delete the trailing paragraph separator.
523 _text.erase(cit(_cursor_index));
525 }
526 default:;
527 }
528
529 tt_axiom(is_valid());
530 return handled;
531 }
532
533 bool is_valid() const noexcept
534 {
535 return _selection_index >= 0 && _selection_index <= std::ssize(_text) && _cursor_index >= 0 && _cursor_index <= std::ssize(_text);
536 }
537
538private:
539 font_book const &_font_book;
540 gap_buffer<attributed_grapheme> _text;
541 tt::shaped_text _shaped_text;
542
546 float _width = 0.0f;
547
550 bool _insert_mode = true;
551
554 ssize_t _cursor_index = 0;
555
559 ssize_t _selection_index = 0;
560
561 text_style _current_style;
562
565 bool _has_partial_grapheme = false;
566};
567
568
569}
570
Class which represents an axis-aligned rectangle.
Definition axis_aligned_rectangle.hpp:20
Definition editable_text.hpp:17
void set_style_of_all(text_style style) noexcept
Change the text style of all graphemes.
Definition editable_text.hpp:88
decltype(auto) cit(ssize_t index) const noexcept
Return the _text iterator at index.
Definition editable_text.hpp:112
aarectangle left_to_right_caret() const noexcept
Get carets at the cursor position.
Definition editable_text.hpp:139
void delete_selection() noexcept
Delete a selection.
Definition editable_text.hpp:162
std::vector< aarectangle > selection_rectangles() const noexcept
Get a set of rectangles for which _text is selected.
Definition editable_text.hpp:146
void update_shaped_text() noexcept
Update the shaped _text after changed to _text.
Definition editable_text.hpp:59
decltype(auto) it(ssize_t index) noexcept
Return the _text iterator at index.
Definition editable_text.hpp:102
void insert_partial_grapheme(grapheme character) noexcept
Definition editable_text.hpp:301
ssize_t character_index_at_position(point2 position) const noexcept
void insert_grapheme(grapheme character) noexcept
Definition editable_text.hpp:319
aarectangle partial_grapheme_caret() const noexcept
Get carets at the cursor position.
Definition editable_text.hpp:126
font_book keeps track of multiple fonts.
Definition font_book.hpp:30
Definition grapheme.hpp:21
static grapheme PS() noexcept
Paragraph separator.
Definition grapheme.hpp:198
Definition gstring.hpp:13
shaped_text represent a piece of text shaped to be displayed.
Definition shaped_text.hpp:23
std::optional< ssize_t > index_of_grapheme_at_coordinate(point2 coordinate) const noexcept
Get the character close to a coordinate.
std::vector< aarectangle > selection_rectangles(ssize_t first, ssize_t last) const noexcept
Return a list of merged rectangles to display for the selection.
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.
aarectangle left_to_right_caret(ssize_t index, bool overwrite) const noexcept
Return the cursor-carets.
std::pair< ssize_t, ssize_t > indices_of_word(ssize_t logical_index) const noexcept
Get the word with the given character.
std::pair< ssize_t, ssize_t > indices_of_paragraph(ssize_t logical_index) const noexcept
Get the character right of the given character.
std::optional< ssize_t > indexOfCharOnTheLeft(ssize_t logical_index) const noexcept
Get the character left of the given character.
Definition text_style.hpp:17
T reserve(T... args)
T tie(T... args)
T to_string(T... args)