HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 2021-2022.
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
9#pragma once
10
11#include "text_delegate.hpp"
12#include "../GUI/module.hpp"
13#include "../text/module.hpp"
14#include "../geometry/module.hpp"
15#include "../undo_stack.hpp"
16#include "../scoped_task.hpp"
17#include "../observer.hpp"
18#include "../when_any.hpp"
19#include <memory>
20#include <string>
21#include <array>
22#include <optional>
23#include <future>
24#include <limits>
25#include <chrono>
26
27namespace hi { inline namespace v1 {
28
29template<typename Context>
30concept text_widget_attribute = forward_of<Context, observer<hi::alignment>>;
31
58template<fixed_string Name = "">
59class text_widget final : public widget {
60public:
61 using super = widget;
62 using delegate_type = text_delegate;
63 constexpr static auto prefix = Name / "text";
64
66
69 observer<alignment> alignment = hi::alignment::top_flush();
70
72 {
73 hi_assert_not_null(delegate);
74 delegate->deinit(*this);
75 }
76
82 text_widget(widget *parent, std::shared_ptr<delegate_type> delegate) noexcept : super(parent), delegate(std::move(delegate))
83 {
85
86 hi_assert_not_null(this->delegate);
87 _delegate_cbt = this->delegate->subscribe([&] {
88 // Read the latest text from the delegate.
89 hi_assert_not_null(this->delegate);
90 _text_cache = this->delegate->read(*this);
91
92 // Make sure that the current selection fits the new text.
93 _selection.resize(_text_cache.size());
94
95 this->layout();
96 });
97
98 _cursor_state_cbt = _cursor_state.subscribe([&](auto...) {
99 ++global_counter<"text_widget:cursor_state:redraw">;
101 });
102
103 // If the text_widget is used as a label the blink_cursor() co-routine
104 // is only waiting on `model` and `focus`, so this is cheap.
105 _blink_cursor = blink_cursor();
106
107 this->delegate->init(*this);
108 }
109
110 text_widget(widget *parent, std::shared_ptr<delegate_type> delegate, text_widget_attribute auto&&...attributes) noexcept :
111 text_widget(parent, std::move(delegate))
112 {
113 set_attributes(hi_forward(attributes)...);
114 }
115
123 widget *parent,
124 different_from<std::shared_ptr<delegate_type>> auto&& text,
125 text_widget_attribute auto&&...attributes) noexcept
126 requires requires { make_default_text_delegate(hi_forward(text)); }
128 {
129 }
130
132 void layout() noexcept override
133 {
134 // Create a new text_shaper with the new text.
135 // Resolve as if in left-to-right mode, the grid will flip itself.
136 hilet resolved_alignment = resolve(*alignment, true);
137
138 _shaped_text = text_shaper{_text_cache, theme<prefix>.text_theme(this), resolved_alignment, os_settings::left_to_right()};
139 _cell.set_constraints(_shaped_text.constraints());
140 }
141
142 void draw(widget_draw_context& context) noexcept override
143 {
144 using namespace std::literals::chrono_literals;
145
146 // After potential constrain and layout, updating the shaped-text, ask the parent window to scroll if needed.
147 if (std::exchange(_request_scroll, false)) {
148 scroll_to_show_selection();
149 }
150
151 if (_last_drag_mouse_event) {
152 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
153 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
154
155 } else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
156 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
157
158 // The last drag mouse event was stored in window coordinate to compensate for scrolling, translate it
159 // back to local coordinates before handling the mouse event again.
160 auto new_mouse_event = _last_drag_mouse_event;
161 new_mouse_event.mouse().position = layout.from_window * _last_drag_mouse_event.mouse().position;
162
163 // When mouse is dragging a selection, start continues redraw and scroll parent views to display the selection.
164 text_widget::handle_event(new_mouse_event);
165 }
166 scroll_to_show_selection();
167 ++global_counter<"text_widget:mouse_drag:redraw">;
169 }
170
171 if (*mode > widget_mode::invisible and overlaps(context, layout)) {
172 context.draw_text(layout, _shaped_text);
173
174 context.draw_text_selection(layout, _shaped_text, _selection, theme<prefix>.selection_color(this));
175
176 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
177 context.draw_text_cursors(
178 cell.rectangle(),
179 _shaped_text,
180 _selection.cursor(),
181 _overwrite_mode,
182 to_bool(_has_dead_character),
183 theme<prefix>.caret_primary_color(this),
184 theme<prefix>.caret_secondary_color(this),
185 theme<prefix>.caret_overwrite_color(this),
186 theme<prefix>.caret_compose_color(this));
187 }
188 }
189 }
190
191 bool handle_event(gui_event const& event) noexcept override
192 {
193 hi_axiom(loop::main().on_thread());
194
195 switch (event.type()) {
196 using enum gui_event_type;
197 using enum widget_mode;
198
199 case gui_widget_next:
200 case gui_widget_prev:
201 case keyboard_exit:
202 // When the next widget is selected due to pressing the Tab key the text should be committed.
203 // The `text_widget` does not handle gui_activate, so it will be forwarded to parent widgets,
204 // such as `text_field_widget` which does.
205 process_event(gui_event_type::gui_activate);
206 return super::handle_event(event);
207
208 case keyboard_grapheme:
209 if (*mode >= partial) {
210 reset_state("BDX");
211 add_character(event.grapheme(), add_type::append);
212 return true;
213 }
214 break;
215
216 case keyboard_partial_grapheme:
217 if (*mode >= partial) {
218 reset_state("BDX");
219 add_character(event.grapheme(), add_type::dead);
220 return true;
221 }
222 break;
223
224 case text_mode_insert:
225 if (*mode >= partial) {
226 reset_state("BDX");
227 _overwrite_mode = not _overwrite_mode;
228 fix_cursor_position();
229 return true;
230 }
231 break;
232
233 case text_edit_paste:
234 if (*mode >= enabled) {
235 // Full text-edit mode, copy from the clipboard as-is.
236 reset_state("BDX");
237 replace_selection(event.clipboard_data());
238 return true;
239
240 } else if (*mode >= partial) {
241 // Line-edit mode, copy from the clipboard replacing
242 // paragraph-separators with spaces.
243 reset_state("BDX");
244 auto new_text = event.clipboard_data();
245 for (auto& c : new_text) {
246 if (c == unicode_PS) {
247 c = ' ';
248 }
249 }
250 replace_selection(new_text);
251 return true;
252 }
253 break;
254
255 case text_edit_copy:
256 if (*mode >= select) {
257 reset_state("BDX");
258 if (hilet selected_text_ = selected_text(); not selected_text_.empty()) {
260 }
261 return true;
262 }
263 break;
264
265 case text_edit_cut:
266 if (*mode >= select) {
267 reset_state("BDX");
269 if (*mode >= partial) {
270 replace_selection(text{});
271 }
272 return true;
273 }
274 break;
275
276 case text_undo:
277 if (*mode >= partial) {
278 reset_state("BDX");
279 undo();
280 return true;
281 }
282 break;
283
284 case text_redo:
285 if (*mode >= partial) {
286 reset_state("BDX");
287 redo();
288 return true;
289 }
290 break;
291
292 case text_insert_line:
293 if (*mode >= enabled) {
294 reset_state("BDX");
295 add_character(grapheme{unicode_PS}, add_type::append);
296 return true;
297 }
298 break;
299
300 case text_insert_line_up:
301 if (*mode >= enabled) {
302 reset_state("BDX");
303 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
304 add_character(grapheme{unicode_PS}, add_type::insert);
305 return true;
306 }
307 break;
308
309 case text_insert_line_down:
310 if (*mode >= enabled) {
311 reset_state("BDX");
312 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
313 add_character(grapheme{unicode_PS}, add_type::insert);
314 return true;
315 }
316 break;
317
318 case text_delete_char_next:
319 if (*mode >= partial) {
320 reset_state("BDX");
321 delete_character_next();
322 return true;
323 }
324 break;
325
326 case text_delete_char_prev:
327 if (*mode >= partial) {
328 reset_state("BDX");
329 delete_character_prev();
330 return true;
331 }
332 break;
333
334 case text_delete_word_next:
335 if (*mode >= partial) {
336 reset_state("BDX");
337 delete_word_next();
338 return true;
339 }
340 break;
341
342 case text_delete_word_prev:
343 if (*mode >= partial) {
344 reset_state("BDX");
345 delete_word_prev();
346 return true;
347 }
348 break;
349
350 case text_cursor_left_char:
351 if (*mode >= partial) {
352 reset_state("BDX");
353 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
354 request_scroll();
355 return true;
356 }
357 break;
358
359 case text_cursor_right_char:
360 if (*mode >= partial) {
361 reset_state("BDX");
362 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
363 request_scroll();
364 return true;
365 }
366 break;
367
368 case text_cursor_down_char:
369 if (*mode >= partial) {
370 reset_state("BD");
371 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
372 request_scroll();
373 return true;
374 }
375 break;
376
377 case text_cursor_up_char:
378 if (*mode >= partial) {
379 reset_state("BD");
380 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
381 request_scroll();
382 return true;
383 }
384 break;
385
386 case text_cursor_left_word:
387 if (*mode >= partial) {
388 reset_state("BDX");
389 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
390 request_scroll();
391 return true;
392 }
393 break;
394
395 case text_cursor_right_word:
396 if (*mode >= partial) {
397 reset_state("BDX");
398 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
399 request_scroll();
400 return true;
401 }
402 break;
403
404 case text_cursor_begin_line:
405 if (*mode >= partial) {
406 reset_state("BDX");
407 _selection = _shaped_text.move_begin_line(_selection.cursor());
408 request_scroll();
409 return true;
410 }
411 break;
412
413 case text_cursor_end_line:
414 if (*mode >= partial) {
415 reset_state("BDX");
416 _selection = _shaped_text.move_end_line(_selection.cursor());
417 request_scroll();
418 return true;
419 }
420 break;
421
422 case text_cursor_begin_sentence:
423 if (*mode >= partial) {
424 reset_state("BDX");
425 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
426 request_scroll();
427 return true;
428 }
429 break;
430
431 case text_cursor_end_sentence:
432 if (*mode >= partial) {
433 reset_state("BDX");
434 _selection = _shaped_text.move_end_sentence(_selection.cursor());
435 request_scroll();
436 return true;
437 }
438 break;
439
440 case text_cursor_begin_document:
441 if (*mode >= partial) {
442 reset_state("BDX");
443 _selection = _shaped_text.move_begin_document(_selection.cursor());
444 request_scroll();
445 return true;
446 }
447 break;
448
449 case text_cursor_end_document:
450 if (*mode >= partial) {
451 reset_state("BDX");
452 _selection = _shaped_text.move_end_document(_selection.cursor());
453 request_scroll();
454 return true;
455 }
456 break;
457
458 case gui_cancel:
459 if (*mode >= select) {
460 reset_state("BDX");
461 _selection.clear_selection(_shaped_text.size());
462 return true;
463 }
464 break;
465
466 case text_select_left_char:
467 if (*mode >= partial) {
468 reset_state("BDX");
469 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(), false));
470 request_scroll();
471 return true;
472 }
473 break;
474
475 case text_select_right_char:
476 if (*mode >= partial) {
477 reset_state("BDX");
478 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(), false));
479 request_scroll();
480 return true;
481 }
482 break;
483
484 case text_select_down_char:
485 if (*mode >= partial) {
486 reset_state("BD");
487 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
488 request_scroll();
489 return true;
490 }
491 break;
492
493 case text_select_up_char:
494 if (*mode >= partial) {
495 reset_state("BD");
496 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
497 request_scroll();
498 return true;
499 }
500 break;
501
502 case text_select_left_word:
503 if (*mode >= partial) {
504 reset_state("BDX");
505 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(), false));
506 request_scroll();
507 return true;
508 }
509 break;
510
511 case text_select_right_word:
512 if (*mode >= partial) {
513 reset_state("BDX");
514 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(), false));
515 request_scroll();
516 return true;
517 }
518 break;
519
520 case text_select_begin_line:
521 if (*mode >= partial) {
522 reset_state("BDX");
523 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
524 request_scroll();
525 return true;
526 }
527 break;
528
529 case text_select_end_line:
530 if (*mode >= partial) {
531 reset_state("BDX");
532 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
533 request_scroll();
534 return true;
535 }
536 break;
537
538 case text_select_begin_sentence:
539 if (*mode >= partial) {
540 reset_state("BDX");
541 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
542 request_scroll();
543 return true;
544 }
545 break;
546
547 case text_select_end_sentence:
548 if (*mode >= partial) {
549 reset_state("BDX");
550 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
551 request_scroll();
552 return true;
553 }
554 break;
555
556 case text_select_begin_document:
557 if (*mode >= partial) {
558 reset_state("BDX");
559 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
560 request_scroll();
561 return true;
562 }
563 break;
564
565 case text_select_end_document:
566 if (*mode >= partial) {
567 reset_state("BDX");
568 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
569 request_scroll();
570 return true;
571 }
572 break;
573
574 case text_select_document:
575 if (*mode >= partial) {
576 reset_state("BDX");
577 _selection = _shaped_text.move_begin_document(_selection.cursor());
578 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
579 request_scroll();
580 return true;
581 }
582 break;
583
584 case mouse_up:
585 if (*mode >= select) {
586 // Stop the continues redrawing during dragging.
587 // Also reset the time, so on drag-start it will initialize the time, which will
588 // cause a smooth startup of repeating.
589 _last_drag_mouse_event = {};
590 _last_drag_mouse_event_next_repeat = {};
591 return true;
592 }
593 break;
594
595 case mouse_down:
596 if (*mode >= select) {
597 hilet cursor = _shaped_text.get_nearest_cursor(narrow_cast<point2>(event.mouse().position));
598 switch (event.mouse().click_count) {
599 case 1:
600 reset_state("BDX");
601 _selection = cursor;
602 break;
603 case 2:
604 reset_state("BDX");
605 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
606 break;
607 case 3:
608 reset_state("BDX");
609 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
610 break;
611 case 4:
612 reset_state("BDX");
613 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
614 break;
615 case 5:
616 reset_state("BDX");
617 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
618 break;
619 default:;
620 }
621
622 ++global_counter<"text_widget:mouse_down:relayout">;
623 process_event({gui_event_type::window_relayout});
624 request_scroll();
625 return true;
626 }
627 break;
628
629 case mouse_drag:
630 if (*mode >= select) {
631 hilet cursor = _shaped_text.get_nearest_cursor(narrow_cast<point2>(event.mouse().position));
632 switch (event.mouse().click_count) {
633 case 1:
634 reset_state("BDX");
635 _selection.drag_selection(cursor);
636 break;
637 case 2:
638 reset_state("BDX");
639 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
640 break;
641 case 3:
642 reset_state("BDX");
643 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
644 break;
645 case 4:
646 reset_state("BDX");
647 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
648 break;
649 default:;
650 }
651
652 // Drag events must be repeated, so that dragging is continues when it causes scrolling.
653 // Normally mouse positions are kept in the local coordinate system, but scrolling
654 // causes this coordinate system to shift, so translate it to the window coordinate system here.
655 _last_drag_mouse_event = event;
656 _last_drag_mouse_event.mouse().position = layout.to_window * event.mouse().position;
657 ++global_counter<"text_widget:mouse_drag:redraw">;
659 return true;
660 }
661 break;
662
663 default:;
664 }
665
666 return super::handle_event(event);
667 }
668
669 hitbox hitbox_test(point2 position) const noexcept override
670 {
671 hi_axiom(loop::main().on_thread());
672
673 if (layout.contains(position)) {
674 if (*mode >= widget_mode::partial) {
675 return hitbox{id, layout.elevation, hitbox_type::text_edit};
676
677 } else if (*mode >= widget_mode::select) {
678 return hitbox{id, layout.elevation, hitbox_type::_default};
679
680 } else {
681 return hitbox{};
682 }
683 } else {
684 return hitbox{};
685 }
686 }
687
688 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
689 {
690 if (*mode >= widget_mode::partial) {
691 return to_bool(group & keyboard_focus_group::normal);
692 } else if (*mode >= widget_mode::select) {
693 return to_bool(group & keyboard_focus_group::mouse);
694 } else {
695 return false;
696 }
697 }
699private:
700 enum class add_type { append, insert, dead };
701
702 struct undo_type {
703 hi::text text;
704 text_selection selection;
705 };
706
707 enum class cursor_state_type { off, on, busy, none };
708
709 hi::text _text_cache;
710 text_shaper _shaped_text;
711
712 mutable box_constraints _constraints_cache;
713
714 delegate_type::callback_token _delegate_cbt;
715
716 text_selection _selection;
717
718 scoped_task<> _blink_cursor;
719
720 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
721 typename decltype(_cursor_state)::callback_token _cursor_state_cbt;
722
725 bool _request_scroll = false;
726
733 gui_event _last_drag_mouse_event = {};
734
737 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
738
741 float _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
742
743 bool _overwrite_mode = false;
744
750 std::optional<character> _has_dead_character = std::nullopt;
751
752 undo_stack<undo_type> _undo_stack = {1000};
753
754 void set_attributes() noexcept {}
755 void set_attributes(text_widget_attribute auto&& first, text_widget_attribute auto&&...rest) noexcept
756 {
757 if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
758 alignment = hi_forward(first);
759 } else {
761 }
762
763 set_attributes(hi_forward(rest)...);
764 }
765
768 void scroll_to_show_selection() noexcept
769 {
770 if (*mode > widget_mode::invisible and *focus) {
771 hilet cursor = _selection.cursor();
772 hilet char_it = _shaped_text.begin() + cursor.index();
773 if (char_it < _shaped_text.end()) {
774 scroll_to_show(char_it->rectangle);
775 }
776 }
777 }
778
779 void request_scroll() noexcept
780 {
781 // At a minimum we need to request a redraw so that
782 // `scroll_to_show_selection()` is called on the next frame.
783 _request_scroll = true;
784 ++global_counter<"text_widget:request_scroll:redraw">;
786 }
787
797 void reset_state(char const *states) noexcept
798 {
799 hi_assert_not_null(states);
800
801 while (*states != 0) {
802 switch (*states) {
803 case 'D':
804 delete_dead_character();
805 break;
806 case 'X':
807 _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
808 break;
809 case 'B':
810 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
811 _cursor_state = cursor_state_type::busy;
812 }
813 break;
814 default:
816 }
817 ++states;
818 }
819 }
820
821 [[nodiscard]] text selected_text() const noexcept
822 {
823 hilet[first, last] = _selection.selection_indices();
824
825 return _text_cache.substr(first, last - first);
826 }
827
828 void undo_push() noexcept
829 {
830 _undo_stack.emplace(_text_cache, _selection);
831 }
832
833 void undo() noexcept
834 {
835 if (_undo_stack.can_undo()) {
836 hilet & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
837
838 delegate->write(*this, text);
839 _selection = selection;
840 }
841 }
842
843 void redo() noexcept
844 {
845 if (_undo_stack.can_redo()) {
846 hilet & [ text, selection ] = _undo_stack.redo();
847
848 delegate->write(*this, text);
849 _selection = selection;
850 }
851 }
852
853 scoped_task<> blink_cursor() noexcept
854 {
855 while (true) {
856 if (*mode >= widget_mode::partial and *focus) {
857 switch (*_cursor_state) {
858 case cursor_state_type::busy:
859 _cursor_state = cursor_state_type::on;
860 co_await when_any(os_settings::cursor_blink_delay(), mode, focus);
861 break;
862
863 case cursor_state_type::on:
864 _cursor_state = cursor_state_type::off;
865 co_await when_any(os_settings::cursor_blink_interval() / 2, mode, focus);
866 break;
867
868 case cursor_state_type::off:
869 _cursor_state = cursor_state_type::on;
870 co_await when_any(os_settings::cursor_blink_interval() / 2, mode, focus);
871 break;
872
873 default:
874 _cursor_state = cursor_state_type::busy;
875 }
876
877 } else {
878 _cursor_state = cursor_state_type::none;
879 co_await when_any(mode, focus);
880 }
881 }
882 }
883
886 void fix_cursor_position() noexcept
887 {
888 hilet size = _text_cache.size();
889 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
890 _selection = _selection.cursor().before_neighbor(size);
891 }
892 _selection.resize(size);
893 }
894
897 void replace_selection(text const& replacement) noexcept
898 {
899 undo_push();
900
901 hilet[first, last] = _selection.selection_indices();
902
903 auto text = _text_cache;
904 text.replace(first, last - first, replacement);
905 delegate->write(*this, text);
906
907 _selection = text_cursor{first + replacement.size() - 1, true};
908 fix_cursor_position();
909 }
910
916 void add_character(grapheme c, add_type add_mode) noexcept
917 {
918 hilet original_cursor = _selection.cursor();
919 auto original_character = character{};
920
921 if (_selection.empty() and _overwrite_mode and original_cursor.before()) {
922 original_character = _text_cache[original_cursor.index()];
923
924 hilet[first, last] = _shaped_text.select_char(original_cursor);
925 _selection.drag_selection(last);
926 }
927 replace_selection(text{c});
928
929 if (add_mode == add_type::insert) {
930 // The character was inserted, put the cursor back where it was.
931 _selection = original_cursor;
932
933 } else if (add_mode == add_type::dead) {
934 _selection = original_cursor.before_neighbor(_text_cache.size());
935 _has_dead_character = original_character;
936 }
937 }
938
939 void delete_dead_character() noexcept
940 {
941 if (_has_dead_character) {
942 hi_assert(_selection.cursor().before());
943 hi_assert_bounds(_selection.cursor().index(), _text_cache);
944 if (_overwrite_mode) {
945 auto text = _text_cache;
946 text[_selection.cursor().index()] = *_has_dead_character;
947 delegate->write(*this, text);
948 } else {
949 auto text = _text_cache;
950 text.erase(_selection.cursor().index(), 1);
951 delegate->write(*this, text);
952 }
953 }
954 _has_dead_character = std::nullopt;
955 }
956
957 void delete_character_next() noexcept
958 {
959 if (_selection.empty()) {
960 auto cursor = _selection.cursor();
961 cursor = cursor.before_neighbor(_shaped_text.size());
962
963 hilet[first, last] = _shaped_text.select_char(cursor);
964 _selection.drag_selection(last);
965 }
966
967 return replace_selection(text{});
968 }
969
970 void delete_character_prev() noexcept
971 {
972 if (_selection.empty()) {
973 auto cursor = _selection.cursor();
974 cursor = cursor.after_neighbor(_shaped_text.size());
975
976 hilet[first, last] = _shaped_text.select_char(cursor);
977 _selection.drag_selection(first);
978 }
979
980 return replace_selection(hi::text{});
981 }
982
983 void delete_word_next() noexcept
984 {
985 if (_selection.empty()) {
986 auto cursor = _selection.cursor();
987 cursor = cursor.before_neighbor(_shaped_text.size());
988
989 hilet[first, last] = _shaped_text.select_word(cursor);
990 _selection.drag_selection(last);
991 }
992
993 return replace_selection(hi::text{});
994 }
995
996 void delete_word_prev() noexcept
997 {
998 if (_selection.empty()) {
999 auto cursor = _selection.cursor();
1000 cursor = cursor.after_neighbor(_shaped_text.size());
1001
1002 hilet[first, last] = _shaped_text.select_word(cursor);
1003 _selection.drag_selection(first);
1004 }
1005
1006 return replace_selection(text{});
1007 }
1008};
1009
1010}} // namespace hi::v1
Defines delegate_delegate and some default text delegates.
#define hi_static_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:323
#define hi_assert_bounds(x,...)
Assert if a value is within bounds.
Definition assert.hpp:225
#define hi_assert(expression,...)
Assert if expression is true.
Definition assert.hpp:199
#define hi_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:279
#define hi_axiom(expression,...)
Specify an axiom; an expression that is true.
Definition assert.hpp:253
#define hi_assert_not_null(x,...)
Assert if an expression is not nullptr.
Definition assert.hpp:238
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
#define hi_forward(x)
Forward a value, based on the decltype of the value.
Definition utility.hpp:29
gui_event_type
GUI event type.
Definition gui_event_type.hpp:21
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_set_clipboard
Place data on the clipboard.
@ grapheme
The gui_event has grapheme data.
std::shared_ptr< text_delegate > make_default_text_delegate(auto &&value) noexcept
Create a shared pointer to a default text delegate.
Definition text_delegate.hpp:189
widget_mode
The mode that the widget is operating at.
Definition widget_mode.hpp:20
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
@ select
The widget is selectable.
@ enabled
The widget is fully enabled.
DOXYGEN BUG.
Definition algorithm.hpp:13
geometry/margins.hpp
Definition cache.hpp:11
@ on
The border is drawn on the edge of a quad.
@ off
The widget in the off-state.
point2 position
The current position of the mouse pointer.
Definition gui_event.hpp:37
static gui_event make_clipboard_event(gui_event_type type, hi::text text) noexcept
Create clipboard event.
Definition gui_event.hpp:205
mouse_event_data & mouse() noexcept
Get the mouse event information.
Definition gui_event.hpp:257
Definition widget.hpp:26
virtual void scroll_to_show(hi::aarectangle rectangle) noexcept
Scroll to show the given rectangle on the window.
Definition widget.hpp:449
widget_id id
The numeric identifier of a widget.
Definition widget.hpp:35
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:227
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:236
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:49
observer< bool > focus
The widget has keyboard focus.
Definition widget.hpp:61
A delegate that controls the state of a text_widget.
Definition text_delegate.hpp:25
A text widget.
Definition text_widget.hpp:59
observer< alignment > alignment
The horizontal alignment of the text inside the space of the widget.
Definition text_widget.hpp:69
text_widget(widget *parent, std::shared_ptr< delegate_type > delegate) noexcept
Construct a text widget.
Definition text_widget.hpp:82
text_widget(widget *parent, different_from< std::shared_ptr< delegate_type > > auto &&text, text_widget_attribute auto &&...attributes) noexcept
Construct a text widget.
Definition text_widget.hpp:122
Definition text_widget.hpp:30
T empty(T... args)
T erase(T... args)
T move(T... args)
T quiet_NaN(T... args)
T replace(T... args)
T size(T... args)
T substr(T... args)