79 hi_assert_not_null(delegate);
80 delegate->deinit(*
this);
93 hi_assert_not_null(this->delegate);
94 _delegate_cbt = this->delegate->subscribe([&] {
98 auto new_layout = _layout;
99 auto const old_constraints = _constraints_cache;
102 auto const new_constraints = update_constraints();
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);
110 if (new_constraints != old_constraints) {
112 ++global_counter<
"text_widget:delegate:constrain">;
118 ++global_counter<
"text_widget:delegate:constrain">;
125 ++global_counter<
"text_widget:text_style:constrain">;
130 _cursor_state_cbt = _cursor_state.
subscribe([&](
auto...) {
131 ++global_counter<
"text_widget:cursor_state:redraw">;
137 _blink_cursor = blink_cursor();
139 this->delegate->init(*
this);
146 Attributes&&... attributes) noexcept :
149 set_attributes(std::forward<Attributes>(attributes)...);
158 template<incompatible_with<std::shared_ptr<delegate_type>> Text, text_w
idget_attribute... Attributes>
173 hi_assert_not_null(delegate);
174 _text_cache = delegate->read(*
this);
177 _selection.resize(_text_cache.size());
179 auto const actual_text_style = theme().text_style(*
text_style);
184 _shaped_text = text_shaper{_text_cache, actual_text_style, theme().pixel_density, alignment_, os_settings::left_to_right()};
187 auto const shaped_text_size = shaped_text_rectangle.size();
191 return _constraints_cache = {
192 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), theme().margin()};
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();
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(),
209 void set_layout(widget_layout
const& context)
noexcept override
212 hi_assert(context.shape.baseline);
214 _shaped_text.layout(context.rectangle(), *context.shape.baseline, context.sub_pixel_size);
218 void draw(draw_context
const& context)
noexcept override
220 using namespace std::literals::chrono_literals;
223 if (std::exchange(_request_scroll,
false)) {
224 scroll_to_show_selection();
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();
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();
236 auto new_mouse_event = _last_drag_mouse_event;
240 text_widget::handle_event(new_mouse_event);
242 scroll_to_show_selection();
243 ++global_counter<
"text_widget:mouse_drag:redraw">;
248 context.draw_text(
layout(), _shaped_text);
250 context.draw_text_selection(
layout(), _shaped_text, _selection, theme().color(semantic_color::text_select));
252 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
253 context.draw_text_cursors(
258 to_bool(_has_dead_character),
259 theme().color(semantic_color::primary_cursor),
260 theme().color(semantic_color::secondary_cursor));
265 bool handle_event(gui_event
const& event)
noexcept override
267 hi_axiom(loop::main().on_thread());
269 switch (event.type()) {
273 case gui_widget_next:
274 case gui_widget_prev:
282 case keyboard_grapheme:
285 add_character(event.grapheme(), add_type::append);
290 case keyboard_partial_grapheme:
293 add_character(event.grapheme(), add_type::dead);
298 case text_mode_insert:
301 _overwrite_mode = not _overwrite_mode;
302 fix_cursor_position();
307 case text_edit_paste:
310 auto tmp =
event.clipboard_data();
313 replace_selection(tmp);
316 }
else if (mode() >=
enabled) {
318 replace_selection(event.clipboard_data());
326 if (
auto const selected_text_ = selected_text(); not selected_text_.empty()) {
338 replace_selection(gstring{});
360 case text_insert_line:
363 add_character(
grapheme{unicode_PS}, add_type::append);
368 case text_insert_line_up:
371 _selection = _shaped_text.move_begin_paragraph(_selection.cursor());
372 add_character(
grapheme{unicode_PS}, add_type::insert);
377 case text_insert_line_down:
380 _selection = _shaped_text.move_end_paragraph(_selection.cursor());
381 add_character(
grapheme{unicode_PS}, add_type::insert);
386 case text_delete_char_next:
389 delete_character_next();
394 case text_delete_char_prev:
397 delete_character_prev();
402 case text_delete_word_next:
410 case text_delete_word_prev:
418 case text_cursor_left_char:
421 _selection = _shaped_text.move_left_char(_selection.cursor(), _overwrite_mode);
427 case text_cursor_right_char:
430 _selection = _shaped_text.move_right_char(_selection.cursor(), _overwrite_mode);
436 case text_cursor_down_char:
439 _selection = _shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x);
445 case text_cursor_up_char:
448 _selection = _shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x);
454 case text_cursor_left_word:
457 _selection = _shaped_text.move_left_word(_selection.cursor(), _overwrite_mode);
463 case text_cursor_right_word:
466 _selection = _shaped_text.move_right_word(_selection.cursor(), _overwrite_mode);
472 case text_cursor_begin_line:
475 _selection = _shaped_text.move_begin_line(_selection.cursor());
481 case text_cursor_end_line:
484 _selection = _shaped_text.move_end_line(_selection.cursor());
490 case text_cursor_begin_sentence:
493 _selection = _shaped_text.move_begin_sentence(_selection.cursor());
499 case text_cursor_end_sentence:
502 _selection = _shaped_text.move_end_sentence(_selection.cursor());
508 case text_cursor_begin_document:
511 _selection = _shaped_text.move_begin_document(_selection.cursor());
517 case text_cursor_end_document:
520 _selection = _shaped_text.move_end_document(_selection.cursor());
529 _selection.clear_selection(_shaped_text.size());
534 case text_select_left_char:
537 _selection.drag_selection(_shaped_text.move_left_char(_selection.cursor(),
false));
543 case text_select_right_char:
546 _selection.drag_selection(_shaped_text.move_right_char(_selection.cursor(),
false));
552 case text_select_down_char:
555 _selection.drag_selection(_shaped_text.move_down_char(_selection.cursor(), _vertical_movement_x));
561 case text_select_up_char:
564 _selection.drag_selection(_shaped_text.move_up_char(_selection.cursor(), _vertical_movement_x));
570 case text_select_left_word:
573 _selection.drag_selection(_shaped_text.move_left_word(_selection.cursor(),
false));
579 case text_select_right_word:
582 _selection.drag_selection(_shaped_text.move_right_word(_selection.cursor(),
false));
588 case text_select_begin_line:
591 _selection.drag_selection(_shaped_text.move_begin_line(_selection.cursor()));
597 case text_select_end_line:
600 _selection.drag_selection(_shaped_text.move_end_line(_selection.cursor()));
606 case text_select_begin_sentence:
609 _selection.drag_selection(_shaped_text.move_begin_sentence(_selection.cursor()));
615 case text_select_end_sentence:
618 _selection.drag_selection(_shaped_text.move_end_sentence(_selection.cursor()));
624 case text_select_begin_document:
627 _selection.drag_selection(_shaped_text.move_begin_document(_selection.cursor()));
633 case text_select_end_document:
636 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
642 case text_select_document:
645 _selection = _shaped_text.move_begin_document(_selection.cursor());
646 _selection.drag_selection(_shaped_text.move_end_document(_selection.cursor()));
657 _last_drag_mouse_event = {};
658 _last_drag_mouse_event_next_repeat = {};
665 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
666 switch (event.mouse().click_count) {
673 _selection.start_selection(cursor, _shaped_text.select_word(cursor));
677 _selection.start_selection(cursor, _shaped_text.select_sentence(cursor));
681 _selection.start_selection(cursor, _shaped_text.select_paragraph(cursor));
685 _selection.start_selection(cursor, _shaped_text.select_document(cursor));
690 ++global_counter<
"text_widget:mouse_down:relayout">;
699 auto const cursor = _shaped_text.get_nearest_cursor(event.mouse().position);
700 switch (event.mouse().click_count) {
703 _selection.drag_selection(cursor);
707 _selection.drag_selection(cursor, _shaped_text.select_word(cursor));
711 _selection.drag_selection(cursor, _shaped_text.select_sentence(cursor));
715 _selection.drag_selection(cursor, _shaped_text.select_paragraph(cursor));
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">;
737 hitbox hitbox_test(point2 position)
const noexcept override
739 hi_axiom(loop::main().on_thread());
741 if (
layout().contains(position)) {
743 return hitbox{
id, _layout.elevation, hitbox_type::text_edit};
746 return hitbox{
id, _layout.elevation, hitbox_type::_default};
756 [[nodiscard]]
bool accepts_keyboard_focus(keyboard_focus_group group)
const noexcept override
759 return to_bool(group & keyboard_focus_group::normal);
761 return to_bool(group & keyboard_focus_group::mouse);
768 enum class add_type { append, insert, dead };
772 text_selection selection;
775 enum class cursor_state_type { off,
on, busy, none };
778 text_shaper _shaped_text;
780 mutable box_constraints _constraints_cache;
782 text_selection _selection;
784 scoped_task<> _blink_cursor;
786 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
790 bool _request_scroll =
false;
798 gui_event _last_drag_mouse_event = {};
802 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
808 bool _overwrite_mode =
false;
819 std::optional<grapheme> _has_dead_character = std::nullopt;
821 undo_stack<undo_type> _undo_stack = {1000};
823 callback<void()> _delegate_cbt;
824 callback<void(semantic_text_style)> _text_style_cbt;
825 callback<void(cursor_state_type)> _cursor_state_cbt;
827 void set_attributes() noexcept {}
829 template<text_widget_attribute First, text_widget_attribute... Rest>
830 void set_attributes(First&& first, Rest&&... rest)
noexcept
832 if constexpr (forward_of<First, observer<hi::alignment>>) {
834 }
else if constexpr (forward_of<First, observer<hi::semantic_text_style>>) {
837 hi_static_no_default();
840 set_attributes(std::forward<Rest>(rest)...);
845 void scroll_to_show_selection() noexcept
848 auto const cursor = _selection.cursor();
849 auto const char_it = _shaped_text.begin() + cursor.index();
850 if (char_it < _shaped_text.end()) {
856 void request_scroll() noexcept
860 _request_scroll =
true;
861 ++global_counter<
"text_widget:request_scroll:redraw">;
874 void reset_state(
char const* states)
noexcept
876 hi_assert_not_null(states);
878 while (*states != 0) {
881 delete_dead_character();
887 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
888 _cursor_state = cursor_state_type::busy;
898 [[nodiscard]] gstring_view selected_text() const noexcept
900 auto const[first, last] = _selection.selection_indices();
902 return gstring_view{_text_cache}.substr(first, last - first);
905 void undo_push() noexcept
907 _undo_stack.emplace(_text_cache, _selection);
912 if (_undo_stack.can_undo()) {
913 auto const & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
915 delegate->write(*
this, text);
916 _selection = selection;
922 if (_undo_stack.can_redo()) {
923 auto const & [ text, selection ] = _undo_stack.redo();
925 delegate->write(*
this, text);
926 _selection = selection;
930 scoped_task<> blink_cursor() noexcept
934 switch (*_cursor_state) {
935 case cursor_state_type::busy:
936 _cursor_state = cursor_state_type::on;
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);
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);
951 _cursor_state = cursor_state_type::busy;
955 _cursor_state = cursor_state_type::none;
963 void fix_cursor_position() noexcept
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);
969 _selection.resize(size);
974 void replace_selection(gstring
const& replacement)
noexcept
978 auto const[first, last] = _selection.selection_indices();
980 auto text = _text_cache;
981 text.replace(first, last - first, replacement);
982 delegate->write(*
this, text);
984 _selection = text_cursor{first + replacement.size() - 1,
true};
985 fix_cursor_position();
993 void add_character(
grapheme c, add_type keyboard_mode)
noexcept
995 auto const[start_selection, end_selection] = _selection.selection(_text_cache.size());
996 auto original_grapheme =
grapheme{
char32_t{0xffff}};
998 if (_selection.empty() and _overwrite_mode and start_selection.before()) {
999 original_grapheme = _text_cache[start_selection.index()];
1001 auto const[first, last] = _shaped_text.select_char(start_selection);
1002 _selection.drag_selection(last);
1004 replace_selection(gstring{c});
1006 if (keyboard_mode == add_type::insert) {
1008 _selection = start_selection;
1010 }
else if (keyboard_mode == add_type::dead) {
1011 _selection = start_selection.before_neighbor(_text_cache.size());
1012 _has_dead_character = original_grapheme;
1016 void delete_dead_character() noexcept
1018 if (_has_dead_character) {
1019 hi_assert(_selection.cursor().before());
1020 hi_assert_bounds(_selection.cursor().index(), _text_cache);
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);
1027 auto text = _text_cache;
1028 text.erase(_selection.cursor().index(), 1);
1029 delegate->write(*
this, text);
1032 _has_dead_character = std::nullopt;
1035 void delete_character_next() noexcept
1037 if (_selection.empty()) {
1038 auto cursor = _selection.cursor();
1039 cursor = cursor.before_neighbor(_shaped_text.size());
1041 auto const[first, last] = _shaped_text.select_char(cursor);
1042 _selection.drag_selection(last);
1045 return replace_selection(gstring{});
1048 void delete_character_prev() noexcept
1050 if (_selection.empty()) {
1051 auto cursor = _selection.cursor();
1052 cursor = cursor.after_neighbor(_shaped_text.size());
1054 auto const[first, last] = _shaped_text.select_char(cursor);
1055 _selection.drag_selection(first);
1058 return replace_selection(gstring{});
1061 void delete_word_next() noexcept
1063 if (_selection.empty()) {
1064 auto cursor = _selection.cursor();
1065 cursor = cursor.before_neighbor(_shaped_text.size());
1067 auto const[first, last] = _shaped_text.select_word(cursor);
1068 _selection.drag_selection(last);
1071 return replace_selection(gstring{});
1074 void delete_word_prev() noexcept
1076 if (_selection.empty()) {
1077 auto cursor = _selection.cursor();
1078 cursor = cursor.after_neighbor(_shaped_text.size());
1080 auto const[first, last] = _shaped_text.select_word(cursor);
1081 _selection.drag_selection(first);
1084 return replace_selection(gstring{});