77 hi_assert_not_null(delegate);
78 delegate->deinit(*
this);
90 hi_assert_not_null(this->delegate);
91 _delegate_cbt = this->delegate->subscribe([&] {
109 ++global_counter<
"text_widget:delegate:constrain">;
115 ++global_counter<
"text_widget:delegate:constrain">;
121 _text_style_cbt =
text_style.subscribe([&](
auto...) {
122 ++global_counter<
"text_widget:text_style:constrain">;
127 _cursor_state_cbt = _cursor_state.subscribe([&](
auto...) {
128 ++global_counter<
"text_widget:cursor_state:redraw">;
134 _blink_cursor = blink_cursor();
136 this->delegate->init(*
this);
166 hi_assert_not_null(delegate);
167 _text_cache = delegate->read(*
this);
170 _selection.resize(_text_cache.size());
184 return _constraints_cache = {
193 return _constraints_cache = {
197 _shaped_text.resolved_alignment(),
202 void set_layout(widget_layout
const&
context)
noexcept override
205 hi_assert(
context.shape.baseline);
211 void draw(draw_context
const&
context)
noexcept override
213 using namespace std::literals::chrono_literals;
216 if (std::exchange(_request_scroll,
false)) {
217 scroll_to_show_selection();
220 if (_last_drag_mouse_event) {
221 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
222 _last_drag_mouse_event_next_repeat =
context.display_time_point + os_settings::keyboard_repeat_delay();
224 }
else if (
context.display_time_point >= _last_drag_mouse_event_next_repeat) {
225 _last_drag_mouse_event_next_repeat =
context.display_time_point + os_settings::keyboard_repeat_interval();
235 scroll_to_show_selection();
236 ++global_counter<
"text_widget:mouse_drag:redraw">;
243 context.draw_text_selection(
layout(), _shaped_text, _selection, theme().color(semantic_color::text_select));
245 if (*_cursor_state == cursor_state_type::on
or *_cursor_state == cursor_state_type::busy) {
251 to_bool(_has_dead_character),
252 theme().color(semantic_color::primary_cursor),
253 theme().color(semantic_color::secondary_cursor));
258 bool handle_event(gui_event
const&
event)
noexcept override
260 hi_axiom(loop::main().on_thread());
262 switch (
event.type()) {
266 case gui_widget_next:
267 case gui_widget_prev:
275 case keyboard_grapheme:
278 add_character(
event.grapheme(), add_type::append);
283 case keyboard_partial_grapheme:
286 add_character(
event.grapheme(), add_type::dead);
291 case text_mode_insert:
294 _overwrite_mode =
not _overwrite_mode;
295 fix_cursor_position();
300 case text_edit_paste:
303 auto tmp =
event.clipboard_data();
306 replace_selection(
tmp);
311 replace_selection(
event.clipboard_data());
331 replace_selection(gstring{});
353 case text_insert_line:
356 add_character(
grapheme{unicode_PS}, add_type::append);
361 case text_insert_line_up:
364 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
365 add_character(
grapheme{unicode_PS}, add_type::insert);
370 case text_insert_line_down:
373 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
374 add_character(
grapheme{unicode_PS}, add_type::insert);
379 case text_delete_char_next:
382 delete_character_next();
387 case text_delete_char_prev:
390 delete_character_prev();
395 case text_delete_word_next:
403 case text_delete_word_prev:
411 case text_cursor_left_char:
414 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
420 case text_cursor_right_char:
423 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
429 case text_cursor_down_char:
432 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
438 case text_cursor_up_char:
441 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
447 case text_cursor_left_word:
450 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
456 case text_cursor_right_word:
459 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
465 case text_cursor_begin_line:
468 _selection = _shaped_text.move_begin_line(_selection.cursor());
474 case text_cursor_end_line:
477 _selection = _shaped_text.move_end_line(_selection.cursor());
483 case text_cursor_begin_sentence:
486 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
492 case text_cursor_end_sentence:
495 _selection = _shaped_text.move_end_sentence(_selection.cursor());
501 case text_cursor_begin_document:
504 _selection = _shaped_text.move_begin_document(_selection.cursor());
510 case text_cursor_end_document:
513 _selection = _shaped_text.move_end_document(_selection.cursor());
522 _selection.clear_selection(_shaped_text.size());
527 case text_select_left_char:
530 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(),
false));
536 case text_select_right_char:
539 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(),
false));
545 case text_select_down_char:
548 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
554 case text_select_up_char:
557 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
563 case text_select_left_word:
566 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(),
false));
572 case text_select_right_word:
575 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(),
false));
581 case text_select_begin_line:
584 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
590 case text_select_end_line:
593 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
599 case text_select_begin_sentence:
602 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
608 case text_select_end_sentence:
611 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
617 case text_select_begin_document:
620 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
626 case text_select_end_document:
629 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
635 case text_select_document:
638 _selection = _shaped_text.move_begin_document(_selection.cursor());
639 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
650 _last_drag_mouse_event = {};
651 _last_drag_mouse_event_next_repeat = {};
658 hilet cursor = _shaped_text.get_nearest_cursor(
event.mouse().position);
659 switch (
event.mouse().click_count) {
666 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
670 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
674 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
678 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
683 ++global_counter<
"text_widget:mouse_down:relayout">;
692 hilet cursor = _shaped_text.get_nearest_cursor(
event.mouse().position);
693 switch (
event.mouse().click_count) {
696 _selection.drag_selection(cursor);
700 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
704 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
708 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
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">;
730 hitbox hitbox_test(point2 position)
const noexcept override
732 hi_axiom(loop::main().on_thread());
734 if (
layout().contains(position)) {
736 return hitbox{
id, _layout.elevation, hitbox_type::text_edit};
739 return hitbox{
id, _layout.elevation, hitbox_type::_default};
749 [[
nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
752 return to_bool(group & keyboard_focus_group::normal);
754 return to_bool(group & keyboard_focus_group::mouse);
761 enum class add_type { append, insert, dead };
765 text_selection selection;
768 enum class cursor_state_type {
off,
on, busy, none };
771 text_shaper _shaped_text;
773 mutable box_constraints _constraints_cache;
775 delegate_type::callback_token _delegate_cbt;
777 decltype(
text_style)::callback_token _text_style_cbt;
779 text_selection _selection;
784 decltype(_cursor_state)::callback_token _cursor_state_cbt;
788 bool _request_scroll =
false;
796 gui_event _last_drag_mouse_event = {};
800 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
806 bool _overwrite_mode =
false;
817 std::optional<grapheme> _has_dead_character = std::nullopt;
822 void set_attributes(text_widget_attribute
auto&& first, text_widget_attribute
auto&&...
rest)
noexcept
829 hi_static_no_default();
832 set_attributes(hi_forward(
rest)...);
837 void scroll_to_show_selection()
noexcept
840 hilet cursor = _selection.cursor();
841 hilet
char_it = _shaped_text.begin() + cursor.index();
842 if (
char_it < _shaped_text.end()) {
852 _request_scroll =
true;
853 ++global_counter<
"text_widget:request_scroll:redraw">;
866 void reset_state(
char const *
states)
noexcept
868 hi_assert_not_null(
states);
873 delete_dead_character();
879 if (*_cursor_state == cursor_state_type::on
or *_cursor_state == cursor_state_type::off) {
880 _cursor_state = cursor_state_type::busy;
892 hilet[first, last] = _selection.selection_indices();
894 return gstring_view{_text_cache}.substr(first, last - first);
899 _undo_stack.emplace(_text_cache, _selection);
904 if (_undo_stack.can_undo()) {
905 hilet & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
907 delegate->write(*
this, text);
908 _selection = selection;
914 if (_undo_stack.can_redo()) {
915 hilet & [ text, selection ] = _undo_stack.redo();
917 delegate->write(*
this, text);
918 _selection = selection;
926 switch (*_cursor_state) {
927 case cursor_state_type::busy:
928 _cursor_state = cursor_state_type::on;
929 co_await when_any(os_settings::cursor_blink_delay(),
mode,
focus);
932 case cursor_state_type::on:
933 _cursor_state = cursor_state_type::off;
934 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
937 case cursor_state_type::off:
938 _cursor_state = cursor_state_type::on;
939 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
943 _cursor_state = cursor_state_type::busy;
947 _cursor_state = cursor_state_type::none;
957 hilet size = _text_cache.size();
958 if (_overwrite_mode
and _selection.empty()
and _selection.cursor().after()) {
959 _selection = _selection.cursor().before_neighbor(size);
961 _selection.resize(size);
967 void replace_selection(gstring
const&
replacement)
noexcept
971 hilet[first, last] = _selection.selection_indices();
973 auto text = _text_cache;
975 delegate->write(*
this, text);
977 _selection = text_cursor{first +
replacement.size() - 1,
true};
978 fix_cursor_position();
988 hilet[start_selection,
end_selection] = _selection.selection(_text_cache.size());
991 if (_selection.empty()
and _overwrite_mode
and start_selection.before()) {
994 hilet[first, last] = _shaped_text.select_char(start_selection);
995 _selection.drag_selection(last);
997 replace_selection(gstring{c});
1001 _selection = start_selection;
1004 _selection = start_selection.before_neighbor(_text_cache.size());
1009 void delete_dead_character()
noexcept
1011 if (_has_dead_character) {
1012 hi_assert(_selection.cursor().before());
1013 hi_assert_bounds(_selection.cursor().index(), _text_cache);
1015 if (_has_dead_character != U
'\uffff') {
1016 auto text = _text_cache;
1017 text[_selection.cursor().index()] = *_has_dead_character;
1018 delegate->write(*
this, text);
1020 auto text = _text_cache;
1021 text.erase(_selection.cursor().index(), 1);
1022 delegate->write(*
this, text);
1025 _has_dead_character = std::nullopt;
1028 void delete_character_next()
noexcept
1030 if (_selection.empty()) {
1031 auto cursor = _selection.cursor();
1032 cursor = cursor.before_neighbor(_shaped_text.size());
1034 hilet[first, last] = _shaped_text.select_char(cursor);
1035 _selection.drag_selection(last);
1038 return replace_selection(gstring{});
1041 void delete_character_prev()
noexcept
1043 if (_selection.empty()) {
1044 auto cursor = _selection.cursor();
1045 cursor = cursor.after_neighbor(_shaped_text.size());
1047 hilet[first, last] = _shaped_text.select_char(cursor);
1048 _selection.drag_selection(first);
1051 return replace_selection(gstring{});
1056 if (_selection.empty()) {
1057 auto cursor = _selection.cursor();
1058 cursor = cursor.before_neighbor(_shaped_text.size());
1060 hilet[first, last] = _shaped_text.select_word(cursor);
1061 _selection.drag_selection(last);
1064 return replace_selection(gstring{});
1069 if (_selection.empty()) {
1070 auto cursor = _selection.cursor();
1071 cursor = cursor.after_neighbor(_shaped_text.size());
1073 hilet[first, last] = _shaped_text.select_word(cursor);
1074 _selection.drag_selection(first);
1077 return replace_selection(gstring{});