69 observer<alignment>
alignment = hi::alignment::top_flush();
73 observer<semantic_text_style>
text_style = semantic_text_style::label;
77 hi_assert_not_null(delegate);
78 delegate->deinit(*
this);
90 hi_assert_not_null(this->delegate);
91 _delegate_cbt = this->delegate->subscribe([&] {
95 auto new_layout = _layout;
96 hilet old_constraints = _constraints_cache;
101 new_layout.shape.x(),
102 new_layout.shape.y(),
103 std::max(new_layout.shape.width(), new_constraints.minimum.width()),
104 std::max(new_layout.shape.height(), new_constraints.minimum.height())};
105 set_layout(new_layout);
107 if (new_constraints != old_constraints) {
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);
142 set_attributes(hi_forward(attributes)...);
166 hi_assert_not_null(delegate);
167 _text_cache = delegate->read(*
this);
170 _selection.resize(_text_cache.size());
172 hilet actual_text_style = theme().text_style(*
text_style);
177 _shaped_text = text_shaper{_text_cache, actual_text_style, theme().scale, alignment_, os_settings::left_to_right()};
180 hilet shaped_text_size = shaped_text_rectangle.size();
184 return _constraints_cache = {
185 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), theme().margin()};
189 hilet preferred_shaped_text_rectangle =
ceil(_shaped_text.bounding_rectangle(550.0f));
190 hilet preferred_shaped_text_size = preferred_shaped_text_rectangle.size();
192 hilet height =
std::max(shaped_text_size.height(), preferred_shaped_text_size.height());
193 return _constraints_cache = {
194 extent2{preferred_shaped_text_size.width(), height},
195 extent2{preferred_shaped_text_size.width(), height},
196 extent2{shaped_text_size.width(), height},
197 _shaped_text.resolved_alignment(),
202 void set_layout(widget_layout
const& context)
noexcept override
205 hi_assert(context.shape.baseline);
207 _shaped_text.layout(context.rectangle(), *context.shape.baseline, context.sub_pixel_size);
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();
229 auto new_mouse_event = _last_drag_mouse_event;
230 new_mouse_event.mouse().position = _layout.from_window * _last_drag_mouse_event.mouse().position;
233 text_widget::handle_event(new_mouse_event);
235 scroll_to_show_selection();
236 ++global_counter<
"text_widget:mouse_drag:redraw">;
241 context.draw_text(
layout(), _shaped_text);
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) {
246 context.draw_text_cursors(
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());
319 if (hilet selected_text_ = selected_text(); not selected_text_.empty()) {
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;
781 scoped_task<> _blink_cursor;
783 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
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;
819 undo_stack<undo_type> _undo_stack = {1000};
821 void set_attributes() noexcept {}
822 void set_attributes(text_widget_attribute
auto&& first, text_widget_attribute
auto&&...rest)
noexcept
824 if constexpr (forward_of<
decltype(first), observer<hi::alignment>>) {
826 }
else if constexpr (forward_of<
decltype(first), observer<hi::semantic_text_style>>) {
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()) {
848 void request_scroll() noexcept
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);
870 while (*states != 0) {
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;
890 [[nodiscard]] gstring_view selected_text() const noexcept
892 hilet[first, last] = _selection.selection_indices();
894 return gstring_view{_text_cache}.substr(first, last - first);
897 void undo_push() noexcept
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;
922 scoped_task<> blink_cursor() noexcept
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;
955 void fix_cursor_position() noexcept
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;
974 text.replace(first, last - first, replacement);
975 delegate->write(*
this, text);
977 _selection = text_cursor{first + replacement.size() - 1,
true};
978 fix_cursor_position();
986 void add_character(
grapheme c, add_type keyboard_mode)
noexcept
988 hilet[start_selection, end_selection] = _selection.selection(_text_cache.size());
989 auto original_grapheme =
grapheme{
char32_t{0xffff}};
991 if (_selection.empty() and _overwrite_mode and start_selection.before()) {
992 original_grapheme = _text_cache[start_selection.index()];
994 hilet[first, last] = _shaped_text.select_char(start_selection);
995 _selection.drag_selection(last);
997 replace_selection(gstring{c});
999 if (keyboard_mode == add_type::insert) {
1001 _selection = start_selection;
1003 }
else if (keyboard_mode == add_type::dead) {
1004 _selection = start_selection.before_neighbor(_text_cache.size());
1005 _has_dead_character = original_grapheme;
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{});
1054 void delete_word_next() noexcept
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{});
1067 void delete_word_prev() noexcept
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{});