HikoGUI
A low latency retained GUI
Loading...
Searching...
No Matches
text_widget.hpp
Go to the documentation of this file.
1// Copyright Take Vos 2021-2022.
2// Distributed under the Boost Software License, Version 1.0.
3// (See accompanying file LICENSE_1_0.txt or copy at https://www.boost.org/LICENSE_1_0.txt)
4
9#pragma once
10
11#include "text_delegate.hpp"
12#include "../GUI/module.hpp"
13#include "../text/module.hpp"
14#include "../geometry/module.hpp"
15#include "../undo_stack.hpp"
16#include "../scoped_task.hpp"
17#include "../observer.hpp"
18#include "../when_any.hpp"
19#include <memory>
20#include <string>
21#include <array>
22#include <optional>
23#include <future>
24#include <limits>
25#include <chrono>
26
27namespace hi { inline namespace v1 {
28
29template<typename Context>
30concept text_widget_attribute = forward_of<Context, observer<hi::alignment>>;
31
58template<fixed_string Name = "">
59class text_widget final : public widget {
60public:
61 using super = widget;
62 using delegate_type = text_delegate;
63 constexpr static auto prefix = Name / "text";
64
66
69 observer<alignment> alignment = hi::alignment::top_flush();
70
72 {
73 hi_assert_not_null(delegate);
74 delegate->deinit(*this);
75 }
76
82 text_widget(widget *parent, std::shared_ptr<delegate_type> delegate) noexcept : super(parent), delegate(std::move(delegate))
83 {
85
86 hi_assert_not_null(this->delegate);
87 _delegate_cbt = this->delegate->subscribe([&] {
88 // On every text edit, immediately/synchronously update the shaped text.
89 // This is needed for handling multiple edit commands before the next frame update.
90 if (layout) {
91 auto new_layout = layout;
92 hilet old_constraints = _constraints_cache;
93
94 // Constrain and layout according to the old layout.
95 hilet new_constraints = update_constraints();
96 new_layout.shape.rectangle = aarectanglei{
97 new_layout.shape.x(),
98 new_layout.shape.y(),
99 std::max(new_layout.shape.width(), new_constraints.minimum.width()),
100 std::max(new_layout.shape.height(), new_constraints.minimum.height())};
101 set_layout(new_layout);
102
103 if (new_constraints != old_constraints) {
104 // The constraints have changed, properly constrain and layout on the next frame.
105 ++global_counter<"text_widget:delegate:constrain">;
106 request_scroll();
107 process_event({gui_event_type::window_reconstrain});
108 }
109 } else {
110 // The layout is incomplete, properly constrain and layout on the next frame.
111 ++global_counter<"text_widget:delegate:constrain">;
112 request_scroll();
113 process_event({gui_event_type::window_reconstrain});
114 }
115 });
116
117 _cursor_state_cbt = _cursor_state.subscribe([&](auto...) {
118 ++global_counter<"text_widget:cursor_state:redraw">;
120 });
121
122 // If the text_widget is used as a label the blink_cursor() co-routine
123 // is only waiting on `model` and `focus`, so this is cheap.
124 _blink_cursor = blink_cursor();
125
126 this->delegate->init(*this);
127 }
128
129 text_widget(widget *parent, std::shared_ptr<delegate_type> delegate, text_widget_attribute auto&&...attributes) noexcept :
130 text_widget(parent, std::move(delegate))
131 {
132 set_attributes(hi_forward(attributes)...);
133 }
134
142 widget *parent,
143 different_from<std::shared_ptr<delegate_type>> auto&& text,
144 text_widget_attribute auto&&...attributes) noexcept
145 requires requires { make_default_text_delegate(hi_forward(text)); }
147 {
148 }
149
151 [[nodiscard]] box_constraints update_constraints() noexcept override
152 {
153 // Read the latest text from the delegate.
154 hi_assert_not_null(delegate);
155 _text_cache = delegate->read(*this);
156
157 // Make sure that the current selection fits the new text.
158 _selection.resize(_text_cache.size());
159
160 // Create a new text_shaper with the new text.
161 auto alignment_ = os_settings::left_to_right() ? *alignment : mirror(*alignment);
162
163 auto &debug = theme<prefix>;
164 _shaped_text = text_shaper{_text_cache, theme<prefix>.text_theme(this), alignment_, os_settings::left_to_right()};
165
166 hilet shaped_text_rectangle =
167 narrow_cast<aarectanglei>(ceil(_shaped_text.bounding_rectangle(std::numeric_limits<float>::infinity())));
168 hilet shaped_text_size = shaped_text_rectangle.size();
169
170 hilet margins = theme<prefix>.margin(this);
171 if (*mode == widget_mode::partial) {
172 // In line-edit mode the text should not wrap.
173 return _constraints_cache = box_constraints{
174 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), margins};
175
176 } else {
177 // Allow the text to be 550.0f pixels wide.
178 hilet preferred_shaped_text_rectangle = narrow_cast<aarectanglei>(ceil(_shaped_text.bounding_rectangle(550.0f)));
179 hilet preferred_shaped_text_size = preferred_shaped_text_rectangle.size();
180
181 hilet height = std::max(shaped_text_size.height(), preferred_shaped_text_size.height());
182 return _constraints_cache = box_constraints{
183 extent2i{preferred_shaped_text_size.width(), height},
184 extent2i{preferred_shaped_text_size.width(), height},
185 extent2i{shaped_text_size.width(), height},
186 _shaped_text.resolved_alignment(),
187 margins};
188 }
189 }
190
191 void set_layout(widget_layout const& context) noexcept override
192 {
193 if (compare_store(layout, context)) {
194 hi_assert(context.shape.baseline);
195
196 _shaped_text.layout(
197 narrow_cast<aarectangle>(context.rectangle()),
198 narrow_cast<float>(*context.shape.baseline),
199 context.sub_pixel_size);
200 }
201 }
202
203 void draw(widget_draw_context& context) noexcept override
204 {
205 using namespace std::literals::chrono_literals;
206
207 // After potential constrain and layout, updating the shaped-text, ask the parent window to scroll if needed.
208 if (std::exchange(_request_scroll, false)) {
209 scroll_to_show_selection();
210 }
211
212 if (_last_drag_mouse_event) {
213 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
214 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
215
216 } else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
217 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
218
219 // The last drag mouse event was stored in window coordinate to compensate for scrolling, translate it
220 // back to local coordinates before handling the mouse event again.
221 auto new_mouse_event = _last_drag_mouse_event;
222 new_mouse_event.mouse().position = layout.from_window * _last_drag_mouse_event.mouse().position;
223
224 // When mouse is dragging a selection, start continues redraw and scroll parent views to display the selection.
225 text_widget::handle_event(new_mouse_event);
226 }
227 scroll_to_show_selection();
228 ++global_counter<"text_widget:mouse_drag:redraw">;
230 }
231
232 if (*mode > widget_mode::invisible and overlaps(context, layout)) {
233 context.draw_text(layout, _shaped_text);
234
235 context.draw_text_selection(layout, _shaped_text, _selection, theme<prefix>.selection_color(this));
236
237 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
238 context.draw_text_cursors(
239 layout,
240 _shaped_text,
241 _selection.cursor(),
242 _overwrite_mode,
243 to_bool(_has_dead_character),
244 theme<prefix>.caret_primary_color(this),
245 theme<prefix>.caret_secondary_color(this),
246 theme<prefix>.caret_overwrite_color(this),
247 theme<prefix>.caret_compose_color(this));
248 }
249 }
250 }
251
252 bool handle_event(gui_event const& event) noexcept override
253 {
254 hi_axiom(loop::main().on_thread());
255
256 switch (event.type()) {
257 using enum gui_event_type;
258 using enum widget_mode;
259
260 case gui_widget_next:
261 case gui_widget_prev:
262 case keyboard_exit:
263 // When the next widget is selected due to pressing the Tab key the text should be committed.
264 // The `text_widget` does not handle gui_activate, so it will be forwarded to parent widgets,
265 // such as `text_field_widget` which does.
266 process_event(gui_event_type::gui_activate);
267 return super::handle_event(event);
268
269 case keyboard_grapheme:
270 if (*mode >= partial) {
271 reset_state("BDX");
272 add_character(event.grapheme(), add_type::append);
273 return true;
274 }
275 break;
276
277 case keyboard_partial_grapheme:
278 if (*mode >= partial) {
279 reset_state("BDX");
280 add_character(event.grapheme(), add_type::dead);
281 return true;
282 }
283 break;
284
285 case text_mode_insert:
286 if (*mode >= partial) {
287 reset_state("BDX");
288 _overwrite_mode = not _overwrite_mode;
289 fix_cursor_position();
290 return true;
291 }
292 break;
293
294 case text_edit_paste:
295 if (*mode >= enabled) {
296 // Full text-edit mode, copy from the clipboard as-is.
297 reset_state("BDX");
298 replace_selection(event.clipboard_data());
299 return true;
300
301 } else if (*mode >= partial) {
302 // Line-edit mode, copy from the clipboard replacing
303 // paragraph-separators with spaces.
304 reset_state("BDX");
305 auto new_text = event.clipboard_data();
306 for (auto& c : new_text) {
307 if (c == unicode_PS) {
308 c = ' ';
309 }
310 }
311 replace_selection(new_text);
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(text{});
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(narrow_cast<point2>(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">;
684 process_event({gui_event_type::window_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(narrow_cast<point2>(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
727 return super::handle_event(event);
728 }
729
730 hitbox hitbox_test(point2i 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 hi::text text;
765 text_selection selection;
766 };
767
768 enum class cursor_state_type { off, on, busy, none };
769
770 hi::text _text_cache;
771 text_shaper _shaped_text;
772
773 mutable box_constraints _constraints_cache;
774
775 delegate_type::callback_token _delegate_cbt;
776
777 text_selection _selection;
778
779 scoped_task<> _blink_cursor;
780
781 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
782 typename decltype(_cursor_state)::callback_token _cursor_state_cbt;
783
786 bool _request_scroll = false;
787
794 gui_event _last_drag_mouse_event = {};
795
798 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
799
802 float _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
803
804 bool _overwrite_mode = false;
805
811 std::optional<character> _has_dead_character = std::nullopt;
812
813 undo_stack<undo_type> _undo_stack = {1000};
814
815 void set_attributes() noexcept {}
816 void set_attributes(text_widget_attribute auto&& first, text_widget_attribute auto&&...rest) noexcept
817 {
818 if constexpr (forward_of<decltype(first), observer<hi::alignment>>) {
819 alignment = hi_forward(first);
820 } else {
822 }
823
824 set_attributes(hi_forward(rest)...);
825 }
826
829 void scroll_to_show_selection() noexcept
830 {
831 if (*mode > widget_mode::invisible and *focus) {
832 hilet cursor = _selection.cursor();
833 hilet char_it = _shaped_text.begin() + cursor.index();
834 if (char_it < _shaped_text.end()) {
835 scroll_to_show(narrow_cast<aarectanglei>(char_it->rectangle));
836 }
837 }
838 }
839
840 void request_scroll() noexcept
841 {
842 // At a minimum we need to request a redraw so that
843 // `scroll_to_show_selection()` is called on the next frame.
844 _request_scroll = true;
845 ++global_counter<"text_widget:request_scroll:redraw">;
847 }
848
858 void reset_state(char const *states) noexcept
859 {
860 hi_assert_not_null(states);
861
862 while (*states != 0) {
863 switch (*states) {
864 case 'D':
865 delete_dead_character();
866 break;
867 case 'X':
868 _vertical_movement_x = std::numeric_limits<float>::quiet_NaN();
869 break;
870 case 'B':
871 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
872 _cursor_state = cursor_state_type::busy;
873 }
874 break;
875 default:
877 }
878 ++states;
879 }
880 }
881
882 [[nodiscard]] text selected_text() const noexcept
883 {
884 hilet[first, last] = _selection.selection_indices();
885
886 return _text_cache.substr(first, last - first);
887 }
888
889 void undo_push() noexcept
890 {
891 _undo_stack.emplace(_text_cache, _selection);
892 }
893
894 void undo() noexcept
895 {
896 if (_undo_stack.can_undo()) {
897 hilet & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
898
899 delegate->write(*this, text);
900 _selection = selection;
901 }
902 }
903
904 void redo() noexcept
905 {
906 if (_undo_stack.can_redo()) {
907 hilet & [ text, selection ] = _undo_stack.redo();
908
909 delegate->write(*this, text);
910 _selection = selection;
911 }
912 }
913
914 scoped_task<> blink_cursor() noexcept
915 {
916 while (true) {
917 if (*mode >= widget_mode::partial and *focus) {
918 switch (*_cursor_state) {
919 case cursor_state_type::busy:
920 _cursor_state = cursor_state_type::on;
921 co_await when_any(os_settings::cursor_blink_delay(), mode, focus);
922 break;
923
924 case cursor_state_type::on:
925 _cursor_state = cursor_state_type::off;
926 co_await when_any(os_settings::cursor_blink_interval() / 2, mode, focus);
927 break;
928
929 case cursor_state_type::off:
930 _cursor_state = cursor_state_type::on;
931 co_await when_any(os_settings::cursor_blink_interval() / 2, mode, focus);
932 break;
933
934 default:
935 _cursor_state = cursor_state_type::busy;
936 }
937
938 } else {
939 _cursor_state = cursor_state_type::none;
940 co_await when_any(mode, focus);
941 }
942 }
943 }
944
947 void fix_cursor_position() noexcept
948 {
949 hilet size = _text_cache.size();
950 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
951 _selection = _selection.cursor().before_neighbor(size);
952 }
953 _selection.resize(size);
954 }
955
958 void replace_selection(text const& replacement) noexcept
959 {
960 undo_push();
961
962 hilet[first, last] = _selection.selection_indices();
963
964 auto text = _text_cache;
965 text.replace(first, last - first, replacement);
966 delegate->write(*this, text);
967
968 _selection = text_cursor{first + replacement.size() - 1, true};
969 fix_cursor_position();
970 }
971
977 void add_character(grapheme c, add_type add_mode) noexcept
978 {
979 hilet original_cursor = _selection.cursor();
980 auto original_character = character{};
981
982 if (_selection.empty() and _overwrite_mode and original_cursor.before()) {
983 original_character = _text_cache[original_cursor.index()];
984
985 hilet[first, last] = _shaped_text.select_char(original_cursor);
986 _selection.drag_selection(last);
987 }
988 replace_selection(text{c});
989
990 if (add_mode == add_type::insert) {
991 // The character was inserted, put the cursor back where it was.
992 _selection = original_cursor;
993
994 } else if (add_mode == add_type::dead) {
995 _selection = original_cursor.before_neighbor(_text_cache.size());
996 _has_dead_character = original_character;
997 }
998 }
999
1000 void delete_dead_character() noexcept
1001 {
1002 if (_has_dead_character) {
1003 hi_assert(_selection.cursor().before());
1004 hi_assert_bounds(_selection.cursor().index(), _text_cache);
1005 if (_overwrite_mode) {
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 hilet[first, last] = _shaped_text.select_char(cursor);
1025 _selection.drag_selection(last);
1026 }
1027
1028 return replace_selection(text{});
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 hilet[first, last] = _shaped_text.select_char(cursor);
1038 _selection.drag_selection(first);
1039 }
1040
1041 return replace_selection(hi::text{});
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 hilet[first, last] = _shaped_text.select_word(cursor);
1051 _selection.drag_selection(last);
1052 }
1053
1054 return replace_selection(hi::text{});
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 hilet[first, last] = _shaped_text.select_word(cursor);
1064 _selection.drag_selection(first);
1065 }
1066
1067 return replace_selection(text{});
1068 }
1069};
1070
1071}} // namespace hi::v1
Defines delegate_delegate and some default text delegates.
#define hi_static_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:323
#define hi_assert_bounds(x,...)
Assert if a value is within bounds.
Definition assert.hpp:225
#define hi_assert(expression,...)
Assert if expression is true.
Definition assert.hpp:199
#define hi_no_default(...)
This part of the code should not be reachable, unless a programming bug.
Definition assert.hpp:279
#define hi_axiom(expression,...)
Specify an axiom; an expression that is true.
Definition assert.hpp:253
#define hi_assert_not_null(x,...)
Assert if an expression is not nullptr.
Definition assert.hpp:238
#define hilet
Invariant should be the default for variables.
Definition utility.hpp:23
#define hi_forward(x)
Forward a value, based on the decltype of the value.
Definition utility.hpp:29
geo::extent< int, 2 > extent2i
A 2D extent.
Definition extent.hpp:512
gui_event_type
GUI event type.
Definition gui_event_type.hpp:21
@ window_relayout
Request that widgets get laid out on the next frame.
@ window_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:189
widget_mode
The mode that the widget is operating at.
Definition widget_mode.hpp:20
@ partial
A widget is partially enabled.
@ invisible
The widget is invisible.
@ select
The widget is selectable.
@ enabled
The widget is fully enabled.
DOXYGEN BUG.
Definition algorithm.hpp:13
geometry/margins.hpp
Definition cache.hpp:11
@ on
The border is drawn on the edge of a quad.
@ off
The widget in the off-state.
bool compare_store(T &lhs, U &&rhs) noexcept
Compare then store if there was a change.
Definition utility.hpp:212
constexpr value_type & width() noexcept
Access the x-as-width element from the extent.
Definition extent.hpp:166
point2i position
The current position of the mouse pointer.
Definition gui_event.hpp:37
static gui_event make_clipboard_event(gui_event_type type, hi::text text) noexcept
Create clipboard event.
Definition gui_event.hpp:205
mouse_event_data & mouse() noexcept
Get the mouse event information.
Definition gui_event.hpp:257
Definition widget.hpp:26
widget_id id
The numeric identifier of a widget.
Definition widget.hpp:35
virtual void scroll_to_show(hi::aarectanglei rectangle) noexcept
Scroll to show the given rectangle on the window.
Definition widget.hpp:487
virtual void request_redraw() const noexcept
Request the widget to be redrawn on the next frame.
Definition widget.hpp:265
virtual bool handle_event(gui_event const &event) noexcept
Handle command.
Definition widget.hpp:274
widget * parent
Pointer to the parent widget.
Definition widget.hpp:40
observer< widget_mode > mode
The widget mode.
Definition widget.hpp:49
observer< bool > focus
The widget has keyboard focus.
Definition widget.hpp:61
translate2i from_window
This matrix transforms window coordinates to local coordinates.
Definition widget_layout.hpp:64
constexpr bool contains(point3i mouse_position) const noexcept
Check if the mouse position is inside the widget.
Definition widget_layout.hpp:126
float elevation
The elevation of the widget above the window.
Definition widget_layout.hpp:72
translate2i to_window
This matrix transforms local coordinates to window coordinates.
Definition widget_layout.hpp:60
2D constraints.
Definition box_constraints.hpp:22
A delegate that controls the state of a text_widget.
Definition text_delegate.hpp:25
A text widget.
Definition text_widget.hpp:59
observer< alignment > alignment
The horizontal alignment of the text inside the space of the widget.
Definition text_widget.hpp:69
text_widget(widget *parent, std::shared_ptr< delegate_type > delegate) noexcept
Construct a text widget.
Definition text_widget.hpp:82
text_widget(widget *parent, different_from< std::shared_ptr< delegate_type > > auto &&text, text_widget_attribute auto &&...attributes) noexcept
Construct a text widget.
Definition text_widget.hpp:141
Definition text_widget.hpp:30
T ceil(T... args)
T empty(T... args)
T erase(T... args)
T max(T... args)
T move(T... args)
T quiet_NaN(T... args)
T replace(T... args)
T size(T... args)
T substr(T... args)