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([&] {
91 auto new_layout = layout;
92 hilet old_constraints = _constraints_cache;
95 hilet new_constraints = update_constraints();
99 std::max(new_layout.shape.width(), new_constraints.minimum.width()),
100 std::max(new_layout.shape.height(), new_constraints.minimum.height())};
101 set_layout(new_layout);
103 if (new_constraints != old_constraints) {
105 ++global_counter<
"text_widget:delegate:constrain">;
111 ++global_counter<
"text_widget:delegate:constrain">;
117 _cursor_state_cbt = _cursor_state.subscribe([&](
auto...) {
118 ++global_counter<
"text_widget:cursor_state:redraw">;
124 _blink_cursor = blink_cursor();
126 this->delegate->init(*
this);
155 _text_cache = delegate->read(*
this);
158 _selection.resize(_text_cache.
size());
163 auto &debug = theme<prefix>;
164 _shaped_text = text_shaper{_text_cache, theme<prefix>.text_theme(
this), alignment_, os_settings::left_to_right()};
166 hilet shaped_text_rectangle =
168 hilet shaped_text_size = shaped_text_rectangle.size();
170 hilet margins = theme<prefix>.margin(
this);
173 return _constraints_cache = box_constraints{
174 shaped_text_size, shaped_text_size, shaped_text_size, _shaped_text.resolved_alignment(), margins};
178 hilet preferred_shaped_text_rectangle = narrow_cast<aarectanglei>(
ceil(_shaped_text.bounding_rectangle(550.0f)));
179 hilet preferred_shaped_text_size = preferred_shaped_text_rectangle.size();
181 hilet height =
std::max(shaped_text_size.height(), preferred_shaped_text_size.height());
182 return _constraints_cache = box_constraints{
186 _shaped_text.resolved_alignment(),
191 void set_layout(widget_layout
const& context)
noexcept override
197 narrow_cast<aarectangle>(context.rectangle()),
198 narrow_cast<float>(*context.shape.baseline),
199 context.sub_pixel_size);
203 void draw(widget_draw_context& context)
noexcept override
205 using namespace std::literals::chrono_literals;
208 if (std::exchange(_request_scroll,
false)) {
209 scroll_to_show_selection();
212 if (_last_drag_mouse_event) {
213 if (_last_drag_mouse_event_next_repeat == utc_nanoseconds{}) {
214 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_delay();
216 }
else if (context.display_time_point >= _last_drag_mouse_event_next_repeat) {
217 _last_drag_mouse_event_next_repeat = context.display_time_point + os_settings::keyboard_repeat_interval();
221 auto new_mouse_event = _last_drag_mouse_event;
225 text_widget::handle_event(new_mouse_event);
227 scroll_to_show_selection();
228 ++global_counter<
"text_widget:mouse_drag:redraw">;
233 context.draw_text(layout, _shaped_text);
235 context.draw_text_selection(layout, _shaped_text, _selection, theme<prefix>.selection_color(
this));
237 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::busy) {
238 context.draw_text_cursors(
243 to_bool(_has_dead_character),
244 theme<prefix>.caret_primary_color(
this),
245 theme<prefix>.caret_secondary_color(
this),
246 theme<prefix>.caret_overwrite_color(
this),
247 theme<prefix>.caret_compose_color(
this));
252 bool handle_event(gui_event
const& event)
noexcept override
256 switch (event.type()) {
260 case gui_widget_next:
261 case gui_widget_prev:
266 process_event(gui_event_type::gui_activate);
269 case keyboard_grapheme:
272 add_character(event.grapheme(), add_type::append);
277 case keyboard_partial_grapheme:
280 add_character(event.grapheme(), add_type::dead);
285 case text_mode_insert:
288 _overwrite_mode = not _overwrite_mode;
289 fix_cursor_position();
294 case text_edit_paste:
298 replace_selection(event.clipboard_data());
305 auto new_text =
event.clipboard_data();
306 for (
auto& c : new_text) {
307 if (c == unicode_PS) {
311 replace_selection(new_text);
319 if (
hilet selected_text_ = selected_text(); not selected_text_.
empty()) {
331 replace_selection(text{});
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(narrow_cast<point2>(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(narrow_cast<point2>(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;
718 ++global_counter<
"text_widget:mouse_drag:redraw">;
730 hitbox hitbox_test(point2i position)
const noexcept override
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 text_selection _selection;
779 scoped_task<> _blink_cursor;
781 observer<cursor_state_type> _cursor_state = cursor_state_type::none;
782 typename decltype(_cursor_state)::callback_token _cursor_state_cbt;
786 bool _request_scroll =
false;
794 gui_event _last_drag_mouse_event = {};
798 utc_nanoseconds _last_drag_mouse_event_next_repeat = {};
804 bool _overwrite_mode =
false;
811 std::optional<character> _has_dead_character = std::nullopt;
813 undo_stack<undo_type> _undo_stack = {1000};
815 void set_attributes() noexcept {}
816 void set_attributes(text_widget_attribute
auto&& first, text_widget_attribute
auto&&...rest)
noexcept
818 if constexpr (forward_of<
decltype(first), observer<hi::alignment>>) {
829 void scroll_to_show_selection() noexcept
832 hilet cursor = _selection.cursor();
833 hilet char_it = _shaped_text.begin() + cursor.index();
834 if (char_it < _shaped_text.end()) {
840 void request_scroll() noexcept
844 _request_scroll =
true;
845 ++global_counter<
"text_widget:request_scroll:redraw">;
858 void reset_state(
char const *states)
noexcept
862 while (*states != 0) {
865 delete_dead_character();
871 if (*_cursor_state == cursor_state_type::on or *_cursor_state == cursor_state_type::off) {
872 _cursor_state = cursor_state_type::busy;
882 [[nodiscard]] text selected_text() const noexcept
884 hilet[first, last] = _selection.selection_indices();
886 return _text_cache.
substr(first, last - first);
889 void undo_push() noexcept
891 _undo_stack.emplace(_text_cache, _selection);
896 if (_undo_stack.can_undo()) {
897 hilet & [ text, selection ] = _undo_stack.undo(_text_cache, _selection);
899 delegate->write(*
this, text);
900 _selection = selection;
906 if (_undo_stack.can_redo()) {
907 hilet & [ text, selection ] = _undo_stack.redo();
909 delegate->write(*
this, text);
910 _selection = selection;
914 scoped_task<> blink_cursor() noexcept
918 switch (*_cursor_state) {
919 case cursor_state_type::busy:
920 _cursor_state = cursor_state_type::on;
921 co_await when_any(os_settings::cursor_blink_delay(),
mode,
focus);
924 case cursor_state_type::on:
925 _cursor_state = cursor_state_type::off;
926 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
929 case cursor_state_type::off:
930 _cursor_state = cursor_state_type::on;
931 co_await when_any(os_settings::cursor_blink_interval() / 2,
mode,
focus);
935 _cursor_state = cursor_state_type::busy;
939 _cursor_state = cursor_state_type::none;
947 void fix_cursor_position() noexcept
950 if (_overwrite_mode and _selection.empty() and _selection.cursor().after()) {
951 _selection = _selection.cursor().before_neighbor(size);
953 _selection.resize(size);
958 void replace_selection(text
const& replacement)
noexcept
962 hilet[first, last] = _selection.selection_indices();
964 auto text = _text_cache;
965 text.
replace(first, last - first, replacement);
966 delegate->write(*
this, text);
968 _selection = text_cursor{first + replacement.size() - 1,
true};
969 fix_cursor_position();
977 void add_character(
grapheme c, add_type add_mode)
noexcept
979 hilet original_cursor = _selection.cursor();
980 auto original_character = character{};
982 if (_selection.empty() and _overwrite_mode and original_cursor.before()) {
983 original_character = _text_cache[original_cursor.index()];
985 hilet[first, last] = _shaped_text.select_char(original_cursor);
986 _selection.drag_selection(last);
988 replace_selection(text{c});
990 if (add_mode == add_type::insert) {
992 _selection = original_cursor;
994 }
else if (add_mode == add_type::dead) {
995 _selection = original_cursor.before_neighbor(_text_cache.
size());
996 _has_dead_character = original_character;
1000 void delete_dead_character() noexcept
1002 if (_has_dead_character) {
1003 hi_assert(_selection.cursor().before());
1005 if (_overwrite_mode) {
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 hilet[first, last] = _shaped_text.select_char(cursor);
1025 _selection.drag_selection(last);
1028 return replace_selection(text{});
1031 void delete_character_prev() noexcept
1033 if (_selection.empty()) {
1034 auto cursor = _selection.cursor();
1035 cursor = cursor.after_neighbor(_shaped_text.size());
1037 hilet[first, last] = _shaped_text.select_char(cursor);
1038 _selection.drag_selection(first);
1041 return replace_selection(
hi::text{});
1044 void delete_word_next() noexcept
1046 if (_selection.empty()) {
1047 auto cursor = _selection.cursor();
1048 cursor = cursor.before_neighbor(_shaped_text.size());
1050 hilet[first, last] = _shaped_text.select_word(cursor);
1051 _selection.drag_selection(last);
1054 return replace_selection(
hi::text{});
1057 void delete_word_prev() noexcept
1059 if (_selection.empty()) {
1060 auto cursor = _selection.cursor();
1061 cursor = cursor.after_neighbor(_shaped_text.size());
1063 hilet[first, last] = _shaped_text.select_word(cursor);
1064 _selection.drag_selection(first);
1067 return replace_selection(text{});