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
8
9#pragma once
10
11#include "widget.hpp"
12#include "text_delegate.hpp"
13#include "../GUI/GUI.hpp"
14#include "../text/text.hpp"
15#include "../geometry/geometry.hpp"
16#include "../l10n/l10n.hpp"
17#include "../container/container.hpp"
18#include "../observer/observer.hpp"
19#include "../macros.hpp"
20#include <memory>
21#include <string>
22#include <array>
23#include <optional>
24#include <future>
25#include <limits>
26#include <chrono>
27
28hi_export_module(hikogui.widgets.text_widget);
29
30hi_export namespace hi {
31inline namespace v1 {
32
33template<typename Context>
35
62class text_widget : public widget {
63public:
64 using super = widget;
65 using delegate_type = text_delegate;
66
68
71 observer<alignment> alignment = hi::alignment::top_flush();
72
74 {
75 hi_assert_not_null(delegate);
76 delegate->deinit(*this);
77 }
78
85 super(), delegate(std::move(delegate))
86 {
87 set_mode(widget_mode::select);
88
89 hi_assert_not_null(this->delegate);
90 _delegate_cbt = this->delegate->subscribe([&] {
91 // On every text edit, immediately/synchronously update the shaped text.
92 // This is needed for handling multiple edit commands before the next frame update.
93 if (_layout) {
94 auto new_layout = _layout;
95 auto const old_constraints = _constraints_cache;
96
97 // Constrain and layout according to the old layout.
98 auto const new_constraints = update_constraints();
99 new_layout.shape.rectangle = aarectangle{
100 new_layout.shape.x(),
101 new_layout.shape.y(),
102 std::max(new_layout.shape.width(), new_constraints.minimum.width()),
103 std::max(new_layout.shape.height(), new_constraints.minimum.height())};
104 set_layout(new_layout);
105
106 if (new_constraints != old_constraints) {
107 // The constraints have changed, properly constrain and layout on the next frame.
108 ++global_counter<"text_widget:delegate:constrain">;
109 request_scroll();
111 }
112 } else {
113 // The layout is incomplete, properly constrain and layout on the next frame.
114 ++global_counter<"text_widget:delegate:constrain">;
115 request_scroll();
117 }
118 });
119
120 _cursor_state_cbt = _cursor_state.subscribe([&](auto...) {
121 ++global_counter<"text_widget:cursor_state:redraw">;
123 });
124
125 // If the text_widget is used as a label the blink_cursor() co-routine
126 // is only waiting on `model` and `focus`, so this is cheap.
127 _blink_cursor = blink_cursor();
128
129 this->delegate->init(*this);
130 }
131
132 template<text_widget_attribute... Attributes>
135 Attributes&&... attributes) noexcept :
136 text_widget(std::move(delegate))
137 {
138 set_attributes(std::forward<Attributes>(attributes)...);
139 }
140
147 template<incompatible_with<std::shared_ptr<delegate_type>> Text, text_widget_attribute... Attributes>
149 Text&& text,
150 Attributes&&... attributes) noexcept requires requires { make_default_text_delegate(std::forward<Text>(text)); }
152 {
153 }
154
156 [[nodiscard]] box_constraints update_constraints() noexcept override
157 {
158 _layout = {};
159
160 // Read the latest text from the delegate.
161 hi_assert_not_null(delegate);
162 _text_cache = delegate->read(*this);
163
164 // Make sure that the current selection fits the new text.
165 _selection.resize(_text_cache.size());
166
167 // Create a new text_shaper with the new text.
168 auto alignment_ = os_settings::left_to_right() ? *alignment : mirror(*alignment);
169
170 _shaped_text = text_shaper{_text_cache, theme().text_style_set(), style.pixel_density(), alignment_, os_settings::left_to_right()};
171
172 auto const shaped_text_rectangle = ceil(_shaped_text.bounding_rectangle(std::numeric_limits<float>::infinity()));
173 auto const shaped_text_size = shaped_text_rectangle.size();
174
175 if (mode() == widget_mode::partial) {
176 // In line-edit mode the text should not wrap.
177 return _constraints_cache = {
178 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), theme().margin()};
179
180 } else {
181 // Allow the text to be 550.0f pixels wide.
182 auto const preferred_shaped_text_rectangle = ceil(_shaped_text.bounding_rectangle(550.0f));
183 auto const preferred_shaped_text_size = preferred_shaped_text_rectangle.size();
184
185 auto const height = std::max(shaped_text_size.height(), preferred_shaped_text_size.height());
186 return _constraints_cache = {
187 extent2{preferred_shaped_text_size.width(), height},
188 extent2{preferred_shaped_text_size.width(), height},
189 extent2{shaped_text_size.width(), height},
190 _shaped_text.resolved_alignment(),
191 theme().margin()};
192 }
193 }
194
195 void set_layout(widget_layout const& context) noexcept override
196 {
197 if (compare_store(_layout, context)) {
198 hi_assert(context.shape.baseline);
199
200 _shaped_text.layout(context.rectangle(), *context.shape.baseline, context.sub_pixel_size);
201 }
202 }
203
204 void draw(draw_context const& context) noexcept override
205 {
206 using namespace std::literals::chrono_literals;
207
208 // After potential reconstrain and relayout, updating the shaped-text, ask the parent window to scroll if needed.
209 if (std::exchange(_request_scroll, false)) {
210 scroll_to_show_selection();
211 }
212
213 if (_last_drag_mouse_event) {
214 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
215 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
216
217 } else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
218 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
219
220 // The last drag mouse event was stored in window coordinate to compensate for scrolling, translate it
221 // back to local coordinates before handling the mouse event again.
222 auto new_mouse_event = _last_drag_mouse_event;
223 new_mouse_event.mouse().position = _layout.from_window * _last_drag_mouse_event.mouse().position;
224
225 // When mouse is dragging a selection, start continues redraw and scroll parent views to display the selection.
226 text_widget::handle_event(new_mouse_event);
227 }
228 scroll_to_show_selection();
229 ++global_counter<"text_widget:mouse_drag:redraw">;
231 }
232
233 if (mode() > widget_mode::invisible and overlaps(context, layout())) {
234 context.draw_text(layout(), _shaped_text);
235
236 context.draw_text_selection(layout(), _shaped_text, _selection, theme().text_select_color());
237
238 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
239 context.draw_text_cursors(
240 layout(),
241 _shaped_text,
242 _selection.cursor(),
243 _overwrite_mode,
244 to_bool(_has_dead_character),
245 theme().primary_cursor_color(),
246 theme().secondary_cursor_color());
247 }
248 }
249 }
250
251 bool handle_event(gui_event const& event) noexcept override
252 {
253 hi_axiom(loop::main().on_thread());
254
255 switch (event.type()) {
256 using enum gui_event_type;
257 using enum widget_mode;
258
259 case gui_widget_next:
260 case gui_widget_prev:
261 case keyboard_exit:
262 // When the next widget is selected due to pressing the Tab key the text should be committed.
263 // The `text_widget` does not handle gui_activate, so it will be forwarded to parent widgets,
264 // such as `text_field_widget` which does.
265 process_event(gui_event_type::gui_activate);
266 return super::handle_event(event);
267
268 case keyboard_grapheme:
269 if (mode() >= partial) {
270 reset_state("BDX");
271 add_character(event.grapheme(), add_type::append);
272 return true;
273 }
274 break;
275
276 case keyboard_partial_grapheme:
277 if (mode() >= partial) {
278 reset_state("BDX");
279 add_character(event.grapheme(), add_type::dead);
280 return true;
281 }
282 break;
283
284 case text_mode_insert:
285 if (mode() >= partial) {
286 reset_state("BDX");
287 _overwrite_mode = not _overwrite_mode;
288 fix_cursor_position();
289 return true;
290 }
291 break;
292
293 case text_edit_paste:
294 if (mode() >= partial) {
295 reset_state("BDX");
296 auto tmp = event.clipboard_data();
297 // Replace all paragraph separators with white-space.
298 std::replace(tmp.begin(), tmp.end(), grapheme{unicode_PS}, grapheme{' '});
299 replace_selection(tmp);
300 return true;
301
302 } else if (mode() >= enabled) {
303 reset_state("BDX");
304 replace_selection(event.clipboard_data());
305 return true;
306 }
307 break;
308
309 case text_edit_copy:
310 if (mode() >= select) {
311 reset_state("BDX");
312 if (auto const selected_text_ = selected_text(); not selected_text_.empty()) {
314 }
315 return true;
316 }
317 break;
318
319 case text_edit_cut:
320 if (mode() >= select) {
321 reset_state("BDX");
323 if (mode() >= partial) {
324 replace_selection(gstring{});
325 }
326 return true;
327 }
328 break;
329
330 case text_undo:
331 if (mode() >= partial) {
332 reset_state("BDX");
333 undo();
334 return true;
335 }
336 break;
337
338 case text_redo:
339 if (mode() >= partial) {
340 reset_state("BDX");
341 redo();
342 return true;
343 }
344 break;
345
346 case text_insert_line:
347 if (mode() >= enabled) {
348 reset_state("BDX");
349 add_character(grapheme{unicode_PS}, add_type::append);
350 return true;
351 }
352 break;
353
354 case text_insert_line_up:
355 if (mode() >= enabled) {
356 reset_state("BDX");
357 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
358 add_character(grapheme{unicode_PS}, add_type::insert);
359 return true;
360 }
361 break;
362
363 case text_insert_line_down:
364 if (mode() >= enabled) {
365 reset_state("BDX");
366 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
367 add_character(grapheme{unicode_PS}, add_type::insert);
368 return true;
369 }
370 break;
371
372 case text_delete_char_next:
373 if (mode() >= partial) {
374 reset_state("BDX");
375 delete_character_next();
376 return true;
377 }
378 break;
379
380 case text_delete_char_prev:
381 if (mode() >= partial) {
382 reset_state("BDX");
383 delete_character_prev();
384 return true;
385 }
386 break;
387
388 case text_delete_word_next:
389 if (mode() >= partial) {
390 reset_state("BDX");
391 delete_word_next();
392 return true;
393 }
394 break;
395
396 case text_delete_word_prev:
397 if (mode() >= partial) {
398 reset_state("BDX");
399 delete_word_prev();
400 return true;
401 }
402 break;
403
404 case text_cursor_left_char:
405 if (mode() >= partial) {
406 reset_state("BDX");
407 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
408 request_scroll();
409 return true;
410 }
411 break;
412
413 case text_cursor_right_char:
414 if (mode() >= partial) {
415 reset_state("BDX");
416 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
417 request_scroll();
418 return true;
419 }
420 break;
421
422 case text_cursor_down_char:
423 if (mode() >= partial) {
424 reset_state("BD");
425 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
426 request_scroll();
427 return true;
428 }
429 break;
430
431 case text_cursor_up_char:
432 if (mode() >= partial) {
433 reset_state("BD");
434 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
435 request_scroll();
436 return true;
437 }
438 break;
439
440 case text_cursor_left_word:
441 if (mode() >= partial) {
442 reset_state("BDX");
443 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
444 request_scroll();
445 return true;
446 }
447 break;
448
449 case text_cursor_right_word:
450 if (mode() >= partial) {
451 reset_state("BDX");
452 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
453 request_scroll();
454 return true;
455 }
456 break;
457
458 case text_cursor_begin_line:
459 if (mode() >= partial) {
460 reset_state("BDX");
461 _selection = _shaped_text.move_begin_line(_selection.cursor());
462 request_scroll();
463 return true;
464 }
465 break;
466
467 case text_cursor_end_line:
468 if (mode() >= partial) {
469 reset_state("BDX");
470 _selection = _shaped_text.move_end_line(_selection.cursor());
471 request_scroll();
472 return true;
473 }
474 break;
475
476 case text_cursor_begin_sentence:
477 if (mode() >= partial) {
478 reset_state("BDX");
479 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
480 request_scroll();
481 return true;
482 }
483 break;
484
485 case text_cursor_end_sentence:
486 if (mode() >= partial) {
487 reset_state("BDX");
488 _selection = _shaped_text.move_end_sentence(_selection.cursor());
489 request_scroll();
490 return true;
491 }
492 break;
493
494 case text_cursor_begin_document:
495 if (mode() >= partial) {
496 reset_state("BDX");
497 _selection = _shaped_text.move_begin_document(_selection.cursor());
498 request_scroll();
499 return true;
500 }
501 break;
502
503 case text_cursor_end_document:
504 if (mode() >= partial) {
505 reset_state("BDX");
506 _selection = _shaped_text.move_end_document(_selection.cursor());
507 request_scroll();
508 return true;
509 }
510 break;
511
512 case gui_cancel:
513 if (mode() >= select) {
514 reset_state("BDX");
515 _selection.clear_selection(_shaped_text.size());
516 return true;
517 }
518 break;
519
520 case text_select_left_char:
521 if (mode() >= partial) {
522 reset_state("BDX");
523 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(), false));
524 request_scroll();
525 return true;
526 }
527 break;
528
529 case text_select_right_char:
530 if (mode() >= partial) {
531 reset_state("BDX");
532 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(), false));
533 request_scroll();
534 return true;
535 }
536 break;
537
538 case text_select_down_char:
539 if (mode() >= partial) {
540 reset_state("BD");
541 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
542 request_scroll();
543 return true;
544 }
545 break;
546
547 case text_select_up_char:
548 if (mode() >= partial) {
549 reset_state("BD");
550 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
551 request_scroll();
552 return true;
553 }
554 break;
555
556 case text_select_left_word:
557 if (mode() >= partial) {
558 reset_state("BDX");
559 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(), false));
560 request_scroll();
561 return true;
562 }
563 break;
564
565 case text_select_right_word:
566 if (mode() >= partial) {
567 reset_state("BDX");
568 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(), false));
569 request_scroll();
570 return true;
571 }
572 break;
573
574 case text_select_begin_line:
575 if (mode() >= partial) {
576 reset_state("BDX");
577 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
578 request_scroll();
579 return true;
580 }
581 break;
582
583 case text_select_end_line:
584 if (mode() >= partial) {
585 reset_state("BDX");
586 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
587 request_scroll();
588 return true;
589 }
590 break;
591
592 case text_select_begin_sentence:
593 if (mode() >= partial) {
594 reset_state("BDX");
595 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
596 request_scroll();
597 return true;
598 }
599 break;
600
601 case text_select_end_sentence:
602 if (mode() >= partial) {
603 reset_state("BDX");
604 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
605 request_scroll();
606 return true;
607 }
608 break;
609
610 case text_select_begin_document:
611 if (mode() >= partial) {
612 reset_state("BDX");
613 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
614 request_scroll();
615 return true;
616 }
617 break;
618
619 case text_select_end_document:
620 if (mode() >= partial) {
621 reset_state("BDX");
622 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
623 request_scroll();
624 return true;
625 }
626 break;
627
628 case text_select_document:
629 if (mode() >= partial) {
630 reset_state("BDX");
631 _selection = _shaped_text.move_begin_document(_selection.cursor());
632 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
633 request_scroll();
634 return true;
635 }
636 break;
637
638 case mouse_up:
639 if (mode() >= select) {
640 // Stop the continues redrawing during dragging.
641 // Also reset the time, so on drag-start it will initialize the time, which will
642 // cause a smooth startup of repeating.
643 _last_drag_mouse_event = {};
644 _last_drag_mouse_event_next_repeat = {};
645 return true;
646 }
647 break;
648
649 case mouse_down:
650 if (mode() >= select) {
651 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
652 switch (event.mouse().click_count) {
653 case 1:
654 reset_state("BDX");
655 _selection = cursor;
656 break;
657 case 2:
658 reset_state("BDX");
659 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
660 break;
661 case 3:
662 reset_state("BDX");
663 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
664 break;
665 case 4:
666 reset_state("BDX");
667 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
668 break;
669 case 5:
670 reset_state("BDX");
671 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
672 break;
673 default:;
674 }
675
676 ++global_counter<"text_widget:mouse_down:relayout">;
678 request_scroll();
679 return true;
680 }
681 break;
682
683 case mouse_drag:
684 if (mode() >= select) {
685 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
686 switch (event.mouse().click_count) {
687 case 1:
688 reset_state("BDX");
689 _selection.drag_selection(cursor);
690 break;
691 case 2:
692 reset_state("BDX");
693 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
694 break;
695 case 3:
696 reset_state("BDX");
697 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
698 break;
699 case 4:
700 reset_state("BDX");
701 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
702 break;
703 default:;
704 }
705
706 // Drag events must be repeated, so that dragging is continues when it causes scrolling.
707 // Normally mouse positions are kept in the local coordinate system, but scrolling
708 // causes this coordinate system to shift, so translate it to the window coordinate system here.
709 _last_drag_mouse_event = event;
710 _last_drag_mouse_event.mouse().position = _layout.to_window * event.mouse().position;
711 ++global_counter<"text_widget:mouse_drag:redraw">;
713 return true;
714 }
715 break;
716
717 default:;
718 }
719
720 return super::handle_event(event);
721 }
722
723 hitbox hitbox_test(point2 position) const noexcept override
724 {
725 hi_axiom(loop::main().on_thread());
726
727 if (layout().contains(position)) {
728 if (mode() >= widget_mode::partial) {
729 return hitbox{id, _layout.elevation, hitbox_type::text_edit};
730
731 } else if (mode() >= widget_mode::select) {
732 return hitbox{id, _layout.elevation, hitbox_type::_default};
733
734 } else {
735 return hitbox{};
736 }
737 } else {
738 return hitbox{};
739 }
740 }
741
742 [[nodiscard]] bool accepts_keyboard_focus(keyboard_focus_group group) const noexcept override
743 {
744 if (mode() >= widget_mode::partial) {
745 return to_bool(group & keyboard_focus_group::normal);
746 } else if (mode() >= widget_mode::select) {
747 return to_bool(group & keyboard_focus_group::mouse);
748 } else {
749 return false;
750 }
751 }
753private:
754 enum class add_type { append, insert, dead };
755
756 struct undo_type {
757 gstring text;
758 text_selection selection;
759 };
760
761 enum class cursor_state_type { off, on, busy, none };
762
763 gstring _text_cache;
764 text_shaper _shaped_text;
765
766 mutable box_constraints _constraints_cache;
767
768 text_selection _selection;
769
770 scoped_task<> _blink_cursor;
771
772 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
773
776 bool _request_scroll = false;
777
784 gui_event _last_drag_mouse_event = {};
785
788 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
789
792 float _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
793
794 bool _overwrite_mode = false;
795
805 std::optional<grapheme> _has_dead_character = std::nullopt;
806
807 undo_stack<undo_type> _undo_stack = {1000};
808
809 callback<void()> _delegate_cbt;
810 callback<void(cursor_state_type)> _cursor_state_cbt;
811
812 void set_attributes() noexcept {}
813
814 template<text_widget_attribute First, text_widget_attribute... Rest>
815 void set_attributes(First&& first, Rest&&... rest) noexcept
816 {
817 if constexpr (forward_of<First, observer<hi::alignment>>) {
819 } else {
820 hi_static_no_default();
821 }
822
823 set_attributes(std::forward<Rest>(rest)...);
824 }
825
828 void scroll_to_show_selection() noexcept
829 {
830 if (mode() > widget_mode::invisible and focus()) {
831 auto const cursor = _selection.cursor();
832 auto const char_it = _shaped_text.begin() + cursor.index();
833 if (char_it < _shaped_text.end()) {
834 scroll_to_show(char_it->rectangle);
835 }
836 }
837 }
838
839 void request_scroll() noexcept
840 {
841 // At a minimum we need to request a redraw so that
842 // `scroll_to_show_selection()` is called on the next frame.
843 _request_scroll = true;
844 ++global_counter<"text_widget:request_scroll:redraw">;
846 }
847
857 void reset_state(char const* states) noexcept
858 {
859 hi_assert_not_null(states);
860
861 while (*states != 0) {
862 switch (*states) {
863 case 'D':
864 delete_dead_character();
865 break;
866 case 'X':
867 _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
868 break;
869 case 'B':
870 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
871 _cursor_state = cursor_state_type::busy;
872 }
873 break;
874 default:
875 hi_no_default();
876 }
877 ++states;
878 }
879 }
880
881 [[nodiscard]] gstring_view selected_text() const noexcept
882 {
883 auto const[first, last] = _selection.selection_indices();
884
885 return gstring_view{_text_cache}.substr(first, last - first);
886 }
887
888 void undo_push() noexcept
889 {
890 _undo_stack.emplace(_text_cache, _selection);
891 }
892
893 void undo() noexcept
894 {
895 if (_undo_stack.can_undo()) {
896 auto const & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
897
898 delegate->write(*this, text);
899 _selection = selection;
900 }
901 }
902
903 void redo() noexcept
904 {
905 if (_undo_stack.can_redo()) {
906 auto const & [ text, selection ] = _undo_stack.redo();
907
908 delegate->write(*this, text);
909 _selection = selection;
910 }
911 }
912
913 scoped_task<> blink_cursor() noexcept
914 {
915 while (true) {
916 if (mode() >= widget_mode::partial and focus()) {
917 switch (*_cursor_state) {
918 case cursor_state_type::busy:
919 _cursor_state = cursor_state_type::on;
920 co_await when_any(os_settings::cursor_blink_delay(), state);
921 break;
922
923 case cursor_state_type::on:
924 _cursor_state = cursor_state_type::off;
925 co_await when_any(os_settings::cursor_blink_interval() / 2, state);
926 break;
927
928 case cursor_state_type::off:
929 _cursor_state = cursor_state_type::on;
930 co_await when_any(os_settings::cursor_blink_interval() / 2, state);
931 break;
932
933 default:
934 _cursor_state = cursor_state_type::busy;
935 }
936
937 } else {
938 _cursor_state = cursor_state_type::none;
939 co_await state;
940 }
941 }
942 }
943
946 void fix_cursor_position() noexcept
947 {
948 auto const size = _text_cache.size();
949 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
950 _selection = _selection.cursor().before_neighbor(size);
951 }
952 _selection.resize(size);
953 }
954
957 void replace_selection(gstring const& replacement) noexcept
958 {
959 undo_push();
960
961 auto const[first, last] = _selection.selection_indices();
962
963 auto text = _text_cache;
964 text.replace(first, last - first, replacement);
965 delegate->write(*this, text);
966
967 _selection = text_cursor{first + replacement.size() - 1, true};
968 fix_cursor_position();
969 }
970
976 void add_character(grapheme c, add_type keyboard_mode) noexcept
977 {
978 auto const[start_selection, end_selection] = _selection.selection(_text_cache.size());
979 auto original_grapheme = grapheme{char32_t{0xffff}};
980
981 if (_selection.empty() and _overwrite_mode and start_selection.before()) {
982 original_grapheme = _text_cache[start_selection.index()];
983
984 auto const[first, last] = _shaped_text.select_char(start_selection);
985 _selection.drag_selection(last);
986 }
987 replace_selection(gstring{c});
988
989 if (keyboard_mode == add_type::insert) {
990 // The character was inserted, put the cursor back where it was.
991 _selection = start_selection;
992
993 } else if (keyboard_mode == add_type::dead) {
994 _selection = start_selection.before_neighbor(_text_cache.size());
995 _has_dead_character = original_grapheme;
996 }
997 }
998
999 void delete_dead_character() noexcept
1000 {
1001 if (_has_dead_character) {
1002 hi_assert(_selection.cursor().before());
1003 hi_assert_bounds(_selection.cursor().index(), _text_cache);
1004
1005 if (_has_dead_character != U'\uffff') {
1006 auto text = _text_cache;
1007 text[_selection.cursor().index()] = *_has_dead_character;
1008 delegate->write(*this, text);
1009 } else {
1010 auto text = _text_cache;
1011 text.erase(_selection.cursor().index(), 1);
1012 delegate->write(*this, text);
1013 }
1014 }
1015 _has_dead_character = std::nullopt;
1016 }
1017
1018 void delete_character_next() noexcept
1019 {
1020 if (_selection.empty()) {
1021 auto cursor = _selection.cursor();
1022 cursor = cursor.before_neighbor(_shaped_text.size());
1023
1024 auto const[first, last] = _shaped_text.select_char(cursor);
1025 _selection.drag_selection(last);
1026 }
1027
1028 return replace_selection(gstring{});
1029 }
1030
1031 void delete_character_prev() noexcept
1032 {
1033 if (_selection.empty()) {
1034 auto cursor = _selection.cursor();
1035 cursor = cursor.after_neighbor(_shaped_text.size());
1036
1037 auto const[first, last] = _shaped_text.select_char(cursor);
1038 _selection.drag_selection(first);
1039 }
1040
1041 return replace_selection(gstring{});
1042 }
1043
1044 void delete_word_next() noexcept
1045 {
1046 if (_selection.empty()) {
1047 auto cursor = _selection.cursor();
1048 cursor = cursor.before_neighbor(_shaped_text.size());
1049
1050 auto const[first, last] = _shaped_text.select_word(cursor);
1051 _selection.drag_selection(last);
1052 }
1053
1054 return replace_selection(gstring{});
1055 }
1056
1057 void delete_word_prev() noexcept
1058 {
1059 if (_selection.empty()) {
1060 auto cursor = _selection.cursor();
1061 cursor = cursor.after_neighbor(_shaped_text.size());
1062
1063 auto const[first, last] = _shaped_text.select_word(cursor);
1064 _selection.drag_selection(first);
1065 }
1066
1067 return replace_selection(gstring{});
1068 }
1069};
1070
1071} // namespace v1
1072} // namespace hi::v1
Defines widget.
Defines delegate_delegate and some default text delegates.
gui_event_type
GUI event type.
Definition gui_event_type.hpp:24
@ window_relayout
Request that widgets get laid out on the next frame.
Definition gui_event_type.hpp:47
@ window_reconstrain
Request that widget get constraint on the next frame.
Definition gui_event_type.hpp:48
@ window_set_clipboard
Place data on the clipboard.
Definition gui_event_type.hpp:56
@ grapheme
The gui_event has grapheme data.
Definition gui_event_variant.hpp:40
std::shared_ptr< text_delegate > make_default_text_delegate(Value &&value) noexcept
Create a shared pointer to a default text delegate.
Definition text_delegate.hpp:224
widget_mode
The mode that the widget is operating at.
Definition widget_state.hpp:26
@ partial
A widget is partially enabled.
Definition widget_state.hpp:73
@ invisible
The widget is invisible.
Definition widget_state.hpp:41
@ select
The widget is selectable.
Definition widget_state.hpp:63
@ enabled
The widget is fully enabled.
Definition widget_state.hpp:81
The HikoGUI namespace.
Definition array_generic.hpp:21
The HikoGUI API version 1.
Definition array_generic.hpp:22
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition misc.hpp:53
constexpr horizontal_alignment mirror(horizontal_alignment const &rhs) noexcept
Mirror the horizontal alignment.
Definition alignment.hpp:205
auto when_any(Args const &...args)
await on a set of objects which can be converted to an awaitable.
Definition when_any.hpp:173
Class which represents an axis-aligned rectangle.
Definition aarectangle.hpp:33
static gui_event make_clipboard_event(gui_event_type type, gstring_view text) noexcept
Create clipboard event.
Definition gui_event.hpp:212
widget_id id
The numeric identifier of a widget.
Definition widget_intf.hpp:31
widget_layout const & layout() const noexcept
Get the current layout for this widget.
Definition widget_intf.hpp:241
hi::style style
The style of this widget.
Definition widget_intf.hpp:39
observer< widget_state > state
The current state of the widget.
Definition widget_intf.hpp:49
2D constraints.
Definition box_constraints.hpp:25
A observer pointing to the whole or part of a observed_base.
Definition observer_intf.hpp:32
A delegate that controls the state of a text_widget.
Definition text_delegate.hpp:33
A text widget.
Definition text_widget.hpp:62
text_widget(std::shared_ptr< delegate_type > delegate) noexcept
Construct a text widget.
Definition text_widget.hpp:84
text_widget(Text &&text, Attributes &&... attributes) noexcept
Construct a text widget.
Definition text_widget.hpp:148
observer< alignment > alignment
The horizontal alignment of the text inside the space of the widget.
Definition text_widget.hpp:71
void scroll_to_show() noexcept
Scroll to show the important part of the widget.
Definition widget_intf.hpp:347
void request_redraw() const noexcept override
Request the widget to be redrawn on the next frame.
Definition widget.hpp:136
widget() noexcept
Constructor for creating sub views.
Definition widget.hpp:50
box_constraints update_constraints() noexcept override
Update the constraints of the widget.
Definition widget.hpp:110
bool process_event(gui_event const &event) const noexcept override
Send a event to the window.
Definition widget.hpp:125
bool handle_event(gui_event const &event) noexcept override
Handle command.
Definition widget.hpp:145
True if T is a forwarded type of Forward.
Definition concepts.hpp:137
Definition text_widget.hpp:34
T ceil(T... args)
T forward(T... args)
T infinity(T... args)
T max(T... args)
T move(T... args)
T quiet_NaN(T... args)
T replace(T... args)