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