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