63 constexpr static auto prefix = Name /
"text";
69 observer<alignment>
alignment = hi::alignment::top_flush();
74 delegate->deinit(*
this);
87 _delegate_cbt = this->delegate->subscribe([&] {
90 _text_cache = this->delegate->read(*
this);
93 _selection.resize(_text_cache.
size());
98 _cursor_state_cbt = _cursor_state.subscribe([&](
auto...) {
99 ++global_counter<
"text_widget:cursor_state:redraw">;
105 _blink_cursor = blink_cursor();
107 this->delegate->init(*
this);
132 void layout() noexcept
override
138 _shaped_text = text_shaper{_text_cache, theme<prefix>.text_theme(
this), resolved_alignment, os_settings::left_to_right()};
139 _cell.set_constraints(_shaped_text.constraints());
142 void draw(widget_draw_context& context)
noexcept override
144 using namespace std::literals::chrono_literals;
147 if (std::exchange(_request_scroll,
false)) {
148 scroll_to_show_selection();
151 if (_last_drag_mouse_event) {
152 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
153 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
155 }
else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
156 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
160 auto new_mouse_event = _last_drag_mouse_event;
164 text_widget::handle_event(new_mouse_event);
166 scroll_to_show_selection();
167 ++global_counter<
"text_widget:mouse_drag:redraw">;
172 context.draw_text(layout, _shaped_text);
174 context.draw_text_selection(layout, _shaped_text, _selection, theme<prefix>.selection_color(
this));
176 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
177 context.draw_text_cursors(
182 to_bool(_has_dead_character),
183 theme<prefix>.caret_primary_color(
this),
184 theme<prefix>.caret_secondary_color(
this),
185 theme<prefix>.caret_overwrite_color(
this),
186 theme<prefix>.caret_compose_color(
this));
191 bool handle_event(gui_event
const& event)
noexcept override
195 switch (event.type()) {
199 case gui_widget_next:
200 case gui_widget_prev:
205 process_event(gui_event_type::gui_activate);
208 case keyboard_grapheme:
211 add_character(event.grapheme(), add_type::append);
216 case keyboard_partial_grapheme:
219 add_character(event.grapheme(), add_type::dead);
224 case text_mode_insert:
227 _overwrite_mode = not _overwrite_mode;
228 fix_cursor_position();
233 case text_edit_paste:
237 replace_selection(event.clipboard_data());
244 auto new_text =
event.clipboard_data();
245 for (
auto& c : new_text) {
246 if (c == unicode_PS) {
250 replace_selection(new_text);
258 if (
hilet selected_text_ = selected_text(); not selected_text_.
empty()) {
270 replace_selection(text{});
292 case text_insert_line:
295 add_character(
grapheme{unicode_PS}, add_type::append);
300 case text_insert_line_up:
303 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
304 add_character(
grapheme{unicode_PS}, add_type::insert);
309 case text_insert_line_down:
312 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
313 add_character(
grapheme{unicode_PS}, add_type::insert);
318 case text_delete_char_next:
321 delete_character_next();
326 case text_delete_char_prev:
329 delete_character_prev();
334 case text_delete_word_next:
342 case text_delete_word_prev:
350 case text_cursor_left_char:
353 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
359 case text_cursor_right_char:
362 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
368 case text_cursor_down_char:
371 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
377 case text_cursor_up_char:
380 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
386 case text_cursor_left_word:
389 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
395 case text_cursor_right_word:
398 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
404 case text_cursor_begin_line:
407 _selection = _shaped_text.move_begin_line(_selection.cursor());
413 case text_cursor_end_line:
416 _selection = _shaped_text.move_end_line(_selection.cursor());
422 case text_cursor_begin_sentence:
425 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
431 case text_cursor_end_sentence:
434 _selection = _shaped_text.move_end_sentence(_selection.cursor());
440 case text_cursor_begin_document:
443 _selection = _shaped_text.move_begin_document(_selection.cursor());
449 case text_cursor_end_document:
452 _selection = _shaped_text.move_end_document(_selection.cursor());
461 _selection.clear_selection(_shaped_text.size());
466 case text_select_left_char:
469 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(),
false));
475 case text_select_right_char:
478 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(),
false));
484 case text_select_down_char:
487 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
493 case text_select_up_char:
496 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
502 case text_select_left_word:
505 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(),
false));
511 case text_select_right_word:
514 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(),
false));
520 case text_select_begin_line:
523 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
529 case text_select_end_line:
532 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
538 case text_select_begin_sentence:
541 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
547 case text_select_end_sentence:
550 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
556 case text_select_begin_document:
559 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
565 case text_select_end_document:
568 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
574 case text_select_document:
577 _selection = _shaped_text.move_begin_document(_selection.cursor());
578 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
589 _last_drag_mouse_event = {};
590 _last_drag_mouse_event_next_repeat = {};
597 hilet cursor = _shaped_text.get_nearest_cursor(narrow_cast<point2>(event.mouse().position));
598 switch (event.mouse().click_count) {
605 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
609 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
613 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
617 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
622 ++global_counter<
"text_widget:mouse_down:relayout">;
631 hilet cursor = _shaped_text.get_nearest_cursor(narrow_cast<point2>(event.mouse().position));
632 switch (event.mouse().click_count) {
635 _selection.drag_selection(cursor);
639 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
643 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
647 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
655 _last_drag_mouse_event = event;
656 _last_drag_mouse_event.
mouse().
position = layout.to_window *
event.mouse().position;
657 ++global_counter<
"text_widget:mouse_drag:redraw">;
669 hitbox hitbox_test(point2 position)
const noexcept override
673 if (layout.contains(position)) {
675 return hitbox{
id, layout.elevation, hitbox_type::text_edit};
678 return hitbox{
id, layout.elevation, hitbox_type::_default};
688 [[nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
691 return to_bool(group & keyboard_focus_group::normal);
693 return to_bool(group & keyboard_focus_group::mouse);
700 enum class add_type { append, insert, dead };
704 text_selection selection;
707 enum class cursor_state_type {
off,
on, busy, none };
710 text_shaper _shaped_text;
712 mutable box_constraints _constraints_cache;
714 delegate_type::callback_token _delegate_cbt;
716 text_selection _selection;
718 scoped_task<> _blink_cursor;
720 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
721 typename decltype(_cursor_state)::callback_token _cursor_state_cbt;
725 bool _request_scroll =
false;
733 gui_event _last_drag_mouse_event = {};
737 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
743 bool _overwrite_mode =
false;
750 std::optional<character> _has_dead_character = std::nullopt;
752 undo_stack<undo_type> _undo_stack = {1000};
754 void set_attributes() noexcept {}
755 void set_attributes(text_widget_attribute
auto&& first, text_widget_attribute
auto&&...rest)
noexcept
757 if constexpr (forward_of<
decltype(first), observer<hi::alignment>>) {
768 void scroll_to_show_selection() noexcept
771 hilet cursor = _selection.cursor();
772 hilet char_it = _shaped_text.begin() + cursor.index();
773 if (char_it < _shaped_text.end()) {
779 void request_scroll() noexcept
783 _request_scroll =
true;
784 ++global_counter<
"text_widget:request_scroll:redraw">;
797 void reset_state(
char const *states)
noexcept
801 while (*states != 0) {
804 delete_dead_character();
810 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
811 _cursor_state = cursor_state_type::busy;
821 [[nodiscard]] text selected_text() const noexcept
823 hilet[first, last] = _selection.selection_indices();
825 return _text_cache.
substr(first, last - first);
828 void undo_push() noexcept
830 _undo_stack.emplace(_text_cache, _selection);
835 if (_undo_stack.can_undo()) {
836 hilet & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
838 delegate->write(*
this, text);
839 _selection = selection;
845 if (_undo_stack.can_redo()) {
846 hilet & [ text, selection ] = _undo_stack.redo();
848 delegate->write(*
this, text);
849 _selection = selection;
853 scoped_task<> blink_cursor() noexcept
857 switch (*_cursor_state) {
858 case cursor_state_type::busy:
859 _cursor_state = cursor_state_type::on;
860 co_await when_any(os_settings::cursor_blink_delay(),
mode,
focus);
863 case cursor_state_type::on:
864 _cursor_state = cursor_state_type::off;
865 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
868 case cursor_state_type::off:
869 _cursor_state = cursor_state_type::on;
870 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
874 _cursor_state = cursor_state_type::busy;
878 _cursor_state = cursor_state_type::none;
886 void fix_cursor_position() noexcept
889 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
890 _selection = _selection.cursor().before_neighbor(size);
892 _selection.resize(size);
897 void replace_selection(text
const& replacement)
noexcept
901 hilet[first, last] = _selection.selection_indices();
903 auto text = _text_cache;
904 text.
replace(first, last - first, replacement);
905 delegate->write(*
this, text);
907 _selection = text_cursor{first + replacement.size() - 1,
true};
908 fix_cursor_position();
916 void add_character(
grapheme c, add_type add_mode)
noexcept
918 hilet original_cursor = _selection.cursor();
919 auto original_character = character{};
921 if (_selection.empty() and _overwrite_mode and original_cursor.before()) {
922 original_character = _text_cache[original_cursor.index()];
924 hilet[first, last] = _shaped_text.select_char(original_cursor);
925 _selection.drag_selection(last);
927 replace_selection(text{c});
929 if (add_mode == add_type::insert) {
931 _selection = original_cursor;
933 }
else if (add_mode == add_type::dead) {
934 _selection = original_cursor.before_neighbor(_text_cache.
size());
935 _has_dead_character = original_character;
939 void delete_dead_character() noexcept
941 if (_has_dead_character) {
944 if (_overwrite_mode) {
945 auto text = _text_cache;
946 text[_selection.cursor().index()] = *_has_dead_character;
947 delegate->write(*
this, text);
949 auto text = _text_cache;
950 text.
erase(_selection.cursor().index(), 1);
951 delegate->write(*
this, text);
954 _has_dead_character = std::nullopt;
957 void delete_character_next() noexcept
959 if (_selection.empty()) {
960 auto cursor = _selection.cursor();
961 cursor = cursor.before_neighbor(_shaped_text.size());
963 hilet[first, last] = _shaped_text.select_char(cursor);
964 _selection.drag_selection(last);
967 return replace_selection(text{});
970 void delete_character_prev() noexcept
972 if (_selection.empty()) {
973 auto cursor = _selection.cursor();
974 cursor = cursor.after_neighbor(_shaped_text.size());
976 hilet[first, last] = _shaped_text.select_char(cursor);
977 _selection.drag_selection(first);
980 return replace_selection(
hi::text{});
983 void delete_word_next() noexcept
985 if (_selection.empty()) {
986 auto cursor = _selection.cursor();
987 cursor = cursor.before_neighbor(_shaped_text.size());
989 hilet[first, last] = _shaped_text.select_word(cursor);
990 _selection.drag_selection(last);
993 return replace_selection(
hi::text{});
996 void delete_word_prev() noexcept
998 if (_selection.empty()) {
999 auto cursor = _selection.cursor();
1000 cursor = cursor.after_neighbor(_shaped_text.size());
1002 hilet[first, last] = _shaped_text.select_word(cursor);
1003 _selection.drag_selection(first);
1006 return replace_selection(text{});