75 hi_assert_not_null(delegate);
76 delegate->deinit(*
this);
89 hi_assert_not_null(this->delegate);
90 _delegate_cbt = this->delegate->subscribe([&] {
94 auto new_layout = _layout;
95 auto const old_constraints = _constraints_cache;
100 new_layout.shape.x(),
101 new_layout.shape.y(),
102 std::max(new_layout.shape.width(), new_constraints.minimum.width()),
103 std::max(new_layout.shape.height(), new_constraints.minimum.height())};
104 set_layout(new_layout);
106 if (new_constraints != old_constraints) {
108 ++global_counter<
"text_widget:delegate:constrain">;
114 ++global_counter<
"text_widget:delegate:constrain">;
120 _cursor_state_cbt = _cursor_state.subscribe([&](
auto...) {
121 ++global_counter<
"text_widget:cursor_state:redraw">;
127 _blink_cursor = blink_cursor();
129 this->delegate->init(*
this);
135 Attributes&&... attributes) noexcept :
147 template<incompatible_with<std::shared_ptr<delegate_type>> Text, text_w
idget_attribute... Attributes>
161 hi_assert_not_null(delegate);
162 _text_cache = delegate->read(*
this);
165 _selection.resize(_text_cache.size());
170 _shaped_text = text_shaper{_text_cache, theme().text_style_set(),
style.pixel_density(), alignment_, os_settings::left_to_right()};
173 auto const shaped_text_size = shaped_text_rectangle.size();
177 return _constraints_cache = {
178 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), theme().margin()};
182 auto const preferred_shaped_text_rectangle =
ceil(_shaped_text.bounding_rectangle(550.0f));
183 auto const preferred_shaped_text_size = preferred_shaped_text_rectangle.size();
185 auto const height =
std::max(shaped_text_size.height(), preferred_shaped_text_size.height());
186 return _constraints_cache = {
187 extent2{preferred_shaped_text_size.width(), height},
188 extent2{preferred_shaped_text_size.width(), height},
189 extent2{shaped_text_size.width(), height},
190 _shaped_text.resolved_alignment(),
195 void set_layout(widget_layout
const& context)
noexcept override
198 hi_assert(context.shape.baseline);
200 _shaped_text.layout(context.rectangle(), *context.shape.baseline, context.sub_pixel_size);
204 void draw(draw_context
const& context)
noexcept override
206 using namespace std::literals::chrono_literals;
209 if (std::exchange(_request_scroll,
false)) {
210 scroll_to_show_selection();
213 if (_last_drag_mouse_event) {
214 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
215 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
217 }
else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
218 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
222 auto new_mouse_event = _last_drag_mouse_event;
223 new_mouse_event.mouse().position = _layout.from_window * _last_drag_mouse_event.mouse().position;
226 text_widget::handle_event(new_mouse_event);
228 scroll_to_show_selection();
229 ++global_counter<
"text_widget:mouse_drag:redraw">;
234 context.draw_text(
layout(), _shaped_text);
236 context.draw_text_selection(
layout(), _shaped_text, _selection, theme().text_select_color());
238 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
239 context.draw_text_cursors(
244 to_bool(_has_dead_character),
245 theme().primary_cursor_color(),
246 theme().secondary_cursor_color());
251 bool handle_event(gui_event
const& event)
noexcept override
253 hi_axiom(loop::main().on_thread());
255 switch (event.type()) {
259 case gui_widget_next:
260 case gui_widget_prev:
268 case keyboard_grapheme:
271 add_character(event.grapheme(), add_type::append);
276 case keyboard_partial_grapheme:
279 add_character(event.grapheme(), add_type::dead);
284 case text_mode_insert:
287 _overwrite_mode = not _overwrite_mode;
288 fix_cursor_position();
293 case text_edit_paste:
296 auto tmp =
event.clipboard_data();
299 replace_selection(tmp);
302 }
else if (mode() >=
enabled) {
304 replace_selection(event.clipboard_data());
312 if (
auto const selected_text_ = selected_text(); not selected_text_.empty()) {
324 replace_selection(gstring{});
346 case text_insert_line:
349 add_character(
grapheme{unicode_PS}, add_type::append);
354 case text_insert_line_up:
357 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
358 add_character(
grapheme{unicode_PS}, add_type::insert);
363 case text_insert_line_down:
366 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
367 add_character(
grapheme{unicode_PS}, add_type::insert);
372 case text_delete_char_next:
375 delete_character_next();
380 case text_delete_char_prev:
383 delete_character_prev();
388 case text_delete_word_next:
396 case text_delete_word_prev:
404 case text_cursor_left_char:
407 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
413 case text_cursor_right_char:
416 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
422 case text_cursor_down_char:
425 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
431 case text_cursor_up_char:
434 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
440 case text_cursor_left_word:
443 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
449 case text_cursor_right_word:
452 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
458 case text_cursor_begin_line:
461 _selection = _shaped_text.move_begin_line(_selection.cursor());
467 case text_cursor_end_line:
470 _selection = _shaped_text.move_end_line(_selection.cursor());
476 case text_cursor_begin_sentence:
479 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
485 case text_cursor_end_sentence:
488 _selection = _shaped_text.move_end_sentence(_selection.cursor());
494 case text_cursor_begin_document:
497 _selection = _shaped_text.move_begin_document(_selection.cursor());
503 case text_cursor_end_document:
506 _selection = _shaped_text.move_end_document(_selection.cursor());
515 _selection.clear_selection(_shaped_text.size());
520 case text_select_left_char:
523 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(),
false));
529 case text_select_right_char:
532 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(),
false));
538 case text_select_down_char:
541 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
547 case text_select_up_char:
550 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
556 case text_select_left_word:
559 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(),
false));
565 case text_select_right_word:
568 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(),
false));
574 case text_select_begin_line:
577 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
583 case text_select_end_line:
586 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
592 case text_select_begin_sentence:
595 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
601 case text_select_end_sentence:
604 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
610 case text_select_begin_document:
613 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
619 case text_select_end_document:
622 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
628 case text_select_document:
631 _selection = _shaped_text.move_begin_document(_selection.cursor());
632 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
643 _last_drag_mouse_event = {};
644 _last_drag_mouse_event_next_repeat = {};
651 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
652 switch (event.mouse().click_count) {
659 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
663 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
667 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
671 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
676 ++global_counter<
"text_widget:mouse_down:relayout">;
685 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
686 switch (event.mouse().click_count) {
689 _selection.drag_selection(cursor);
693 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
697 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
701 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
709 _last_drag_mouse_event = event;
710 _last_drag_mouse_event.mouse().position = _layout.to_window *
event.mouse().position;
711 ++global_counter<
"text_widget:mouse_drag:redraw">;
723 hitbox hitbox_test(point2 position)
const noexcept override
725 hi_axiom(loop::main().on_thread());
727 if (
layout().contains(position)) {
729 return hitbox{
id, _layout.elevation, hitbox_type::text_edit};
732 return hitbox{
id, _layout.elevation, hitbox_type::_default};
742 [[nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
745 return to_bool(group & keyboard_focus_group::normal);
747 return to_bool(group & keyboard_focus_group::mouse);
754 enum class add_type { append, insert, dead };
758 text_selection selection;
761 enum class cursor_state_type { off, on, busy, none };
764 text_shaper _shaped_text;
766 mutable box_constraints _constraints_cache;
768 text_selection _selection;
770 scoped_task<> _blink_cursor;
772 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
776 bool _request_scroll =
false;
784 gui_event _last_drag_mouse_event = {};
788 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
794 bool _overwrite_mode =
false;
805 std::optional<grapheme> _has_dead_character = std::nullopt;
807 undo_stack<undo_type> _undo_stack = {1000};
809 callback<void()> _delegate_cbt;
810 callback<void(cursor_state_type)> _cursor_state_cbt;
812 void set_attributes() noexcept {}
814 template<text_widget_attribute First, text_widget_attribute... Rest>
815 void set_attributes(First&& first, Rest&&... rest)
noexcept
817 if constexpr (forward_of<First, observer<hi::alignment>>) {
820 hi_static_no_default();
828 void scroll_to_show_selection() noexcept
831 auto const cursor = _selection.cursor();
832 auto const char_it = _shaped_text.begin() + cursor.index();
833 if (char_it < _shaped_text.end()) {
839 void request_scroll() noexcept
843 _request_scroll =
true;
844 ++global_counter<
"text_widget:request_scroll:redraw">;
857 void reset_state(
char const* states)
noexcept
859 hi_assert_not_null(states);
861 while (*states != 0) {
864 delete_dead_character();
870 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
871 _cursor_state = cursor_state_type::busy;
881 [[nodiscard]] gstring_view selected_text() const noexcept
883 auto const[first, last] = _selection.selection_indices();
885 return gstring_view{_text_cache}.substr(first, last - first);
888 void undo_push() noexcept
890 _undo_stack.emplace(_text_cache, _selection);
895 if (_undo_stack.can_undo()) {
896 auto const & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
898 delegate->write(*
this, text);
899 _selection = selection;
905 if (_undo_stack.can_redo()) {
906 auto const & [ text, selection ] = _undo_stack.redo();
908 delegate->write(*
this, text);
909 _selection = selection;
913 scoped_task<> blink_cursor() noexcept
917 switch (*_cursor_state) {
918 case cursor_state_type::busy:
919 _cursor_state = cursor_state_type::on;
923 case cursor_state_type::on:
924 _cursor_state = cursor_state_type::off;
925 co_await when_any(os_settings::cursor_blink_interval() / 2,
state);
928 case cursor_state_type::off:
929 _cursor_state = cursor_state_type::on;
930 co_await when_any(os_settings::cursor_blink_interval() / 2,
state);
934 _cursor_state = cursor_state_type::busy;
938 _cursor_state = cursor_state_type::none;
946 void fix_cursor_position() noexcept
948 auto const size = _text_cache.size();
949 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
950 _selection = _selection.cursor().before_neighbor(size);
952 _selection.resize(size);
957 void replace_selection(gstring
const& replacement)
noexcept
961 auto const[first, last] = _selection.selection_indices();
963 auto text = _text_cache;
964 text.replace(first, last - first, replacement);
965 delegate->write(*
this, text);
967 _selection = text_cursor{first + replacement.size() - 1,
true};
968 fix_cursor_position();
976 void add_character(
grapheme c, add_type keyboard_mode)
noexcept
978 auto const[start_selection, end_selection] = _selection.selection(_text_cache.size());
979 auto original_grapheme =
grapheme{
char32_t{0xffff}};
981 if (_selection.empty() and _overwrite_mode and start_selection.before()) {
982 original_grapheme = _text_cache[start_selection.index()];
984 auto const[first, last] = _shaped_text.select_char(start_selection);
985 _selection.drag_selection(last);
987 replace_selection(gstring{c});
989 if (keyboard_mode == add_type::insert) {
991 _selection = start_selection;
993 }
else if (keyboard_mode == add_type::dead) {
994 _selection = start_selection.before_neighbor(_text_cache.size());
995 _has_dead_character = original_grapheme;
999 void delete_dead_character() noexcept
1001 if (_has_dead_character) {
1002 hi_assert(_selection.cursor().before());
1003 hi_assert_bounds(_selection.cursor().index(), _text_cache);
1005 if (_has_dead_character != U
'\uffff') {
1006 auto text = _text_cache;
1007 text[_selection.cursor().index()] = *_has_dead_character;
1008 delegate->write(*
this, text);
1010 auto text = _text_cache;
1011 text.erase(_selection.cursor().index(), 1);
1012 delegate->write(*
this, text);
1015 _has_dead_character = std::nullopt;
1018 void delete_character_next() noexcept
1020 if (_selection.empty()) {
1021 auto cursor = _selection.cursor();
1022 cursor = cursor.before_neighbor(_shaped_text.size());
1024 auto const[first, last] = _shaped_text.select_char(cursor);
1025 _selection.drag_selection(last);
1028 return replace_selection(gstring{});
1031 void delete_character_prev() noexcept
1033 if (_selection.empty()) {
1034 auto cursor = _selection.cursor();
1035 cursor = cursor.after_neighbor(_shaped_text.size());
1037 auto const[first, last] = _shaped_text.select_char(cursor);
1038 _selection.drag_selection(first);
1041 return replace_selection(gstring{});
1044 void delete_word_next() noexcept
1046 if (_selection.empty()) {
1047 auto cursor = _selection.cursor();
1048 cursor = cursor.before_neighbor(_shaped_text.size());
1050 auto const[first, last] = _shaped_text.select_word(cursor);
1051 _selection.drag_selection(last);
1054 return replace_selection(gstring{});
1057 void delete_word_prev() noexcept
1059 if (_selection.empty()) {
1060 auto cursor = _selection.cursor();
1061 cursor = cursor.after_neighbor(_shaped_text.size());
1063 auto const[first, last] = _shaped_text.select_word(cursor);
1064 _selection.drag_selection(first);
1067 return replace_selection(gstring{});