From 5d3fcc57669c4104a85c79327f7c2662a0d191a3 Mon Sep 17 00:00:00 2001 From: bruvzg <7645683+bruvzg@users.noreply.github.com> Date: Tue, 15 Aug 2023 11:42:40 +0300 Subject: [PATCH] [TextServer] Fix system font fallback and caret/selection behavior for composite characters. --- doc/classes/LineEdit.xml | 2 +- doc/classes/TextEdit.xml | 2 +- doc/classes/TextServer.xml | 47 ++++++++- doc/classes/TextServerExtension.xml | 34 +++++++ modules/text_server_adv/text_server_adv.cpp | 107 +++++++++++++++++++- modules/text_server_adv/text_server_adv.h | 6 ++ modules/text_server_fb/text_server_fb.cpp | 14 +++ modules/text_server_fb/text_server_fb.h | 2 + scene/gui/line_edit.cpp | 9 +- scene/gui/line_edit.h | 2 +- scene/gui/rich_text_label.cpp | 1 + scene/gui/text_edit.cpp | 15 ++- scene/gui/text_edit.h | 2 +- servers/text/text_server_extension.cpp | 46 +++++++++ servers/text/text_server_extension.h | 12 +++ servers/text_server.cpp | 57 +++++++++++ servers/text_server.h | 6 ++ tests/scene/test_text_edit.h | 40 -------- tests/servers/test_text_server.h | 48 +++++++++ 19 files changed, 398 insertions(+), 54 deletions(-) diff --git a/doc/classes/LineEdit.xml b/doc/classes/LineEdit.xml index f5cb2e32a28..ef620da8471 100644 --- a/doc/classes/LineEdit.xml +++ b/doc/classes/LineEdit.xml @@ -197,7 +197,7 @@ If [code]true[/code], the [LineEdit] will always show the caret, even if focus is lost. - + Allow moving caret, selecting and removing the individual composite character components. [b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components. diff --git a/doc/classes/TextEdit.xml b/doc/classes/TextEdit.xml index e611b7e3fa1..d22e646dd0a 100644 --- a/doc/classes/TextEdit.xml +++ b/doc/classes/TextEdit.xml @@ -1100,7 +1100,7 @@ If [code]true[/code], caret will be visible when [member editable] is disabled. - + Allow moving caret, selecting and removing the individual composite character components. [b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components. diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index e3afe6f65d6..bf376fba629 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -1139,6 +1139,14 @@ Clears text buffer (removes text and inline objects). + + + + + + Returns composite character position closest to the [param pos]. + + @@ -1189,6 +1197,13 @@ Returns shapes of the carets corresponding to the character offset [param position] in the text. Returned caret shape is 1 pixel wide rectangle. + + + + + Returns array of the composite character boundaries. + + @@ -1432,7 +1447,7 @@ Returns [code]true[/code] if buffer is successfully shaped. - + @@ -1440,6 +1455,14 @@ Returns composite character end position closest to the [param pos]. + + + + + + Returns grapheme end position closest to the [param pos]. + + @@ -1449,7 +1472,7 @@ Trims text if it exceeds the given width. - + @@ -1457,6 +1480,14 @@ Returns composite character start position closest to the [param pos]. + + + + + + Returns grapheme start position closest to the [param pos]. + + @@ -1568,6 +1599,18 @@ [b]Note:[/b] Always returns [code]false[/code] if the server does not support the [constant FEATURE_UNICODE_SECURITY] feature. + + + + + + Returns array of the composite character boundaries. + [codeblock] + var ts = TextServerManager.get_primary_interface() + print(ts.string_get_word_breaks("Test ❤️‍🔥 Test")) # Prints [1, 2, 3, 4, 5, 9, 10, 11, 12, 13, 14] + [/codeblock] + + diff --git a/doc/classes/TextServerExtension.xml b/doc/classes/TextServerExtension.xml index fc0a4241877..c58e3b705f9 100644 --- a/doc/classes/TextServerExtension.xml +++ b/doc/classes/TextServerExtension.xml @@ -981,6 +981,13 @@ + + + + + + + @@ -1026,6 +1033,12 @@ + + + + + + @@ -1229,6 +1242,13 @@ + + + + + + + @@ -1244,6 +1264,13 @@ + + + + + + + @@ -1356,6 +1383,13 @@ + + + + + + + diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 043a33ab35d..09a0fe5e8ef 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -3782,6 +3782,7 @@ void TextServerAdvanced::invalidate(TextServerAdvanced::ShapedTextDataAdvanced * p_shaped->script_iter = nullptr; } p_shaped->break_ops_valid = false; + p_shaped->chars_valid = false; p_shaped->js_ops_valid = false; } } @@ -4835,6 +4836,76 @@ int64_t TextServerAdvanced::_shaped_text_get_ellipsis_glyph_count(const RID &p_s return sd->overrun_trim_data.ellipsis_glyph_buf.size(); } +void TextServerAdvanced::_update_chars(ShapedTextDataAdvanced *p_sd) const { + if (!p_sd->chars_valid) { + p_sd->chars.clear(); + + const UChar *data = p_sd->utf16.get_data(); + UErrorCode err = U_ZERO_ERROR; + int prev = -1; + int i = 0; + + Vector &spans = p_sd->spans; + if (p_sd->parent != RID()) { + ShapedTextDataAdvanced *parent_sd = shaped_owner.get_or_null(p_sd->parent); + ERR_FAIL_COND(!parent_sd->valid); + spans = parent_sd->spans; + } + + while (i < spans.size()) { + if (spans[i].start > p_sd->end) { + break; + } + if (spans[i].end < p_sd->start) { + i++; + continue; + } + + int r_start = MAX(0, spans[i].start - p_sd->start); + String language = spans[i].language; + while (i + 1 < spans.size() && language == spans[i + 1].language) { + i++; + } + int r_end = MIN(spans[i].end - p_sd->start, p_sd->text.size()); + + UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, (language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale().ascii().get_data() : language.ascii().get_data(), data + _convert_pos_inv(p_sd, r_start), _convert_pos_inv(p_sd, r_end - r_start), &err); + if (U_SUCCESS(err)) { + while (ubrk_next(bi) != UBRK_DONE) { + int pos = _convert_pos(p_sd, ubrk_current(bi)) + r_start + p_sd->start; + if (prev != pos) { + p_sd->chars.push_back(pos); + } + prev = pos; + } + ubrk_close(bi); + } else { + for (int j = r_start; j <= r_end; j++) { + if (prev != j) { + p_sd->chars.push_back(j + p_sd->start); + } + prev = j; + } + } + i++; + } + p_sd->chars_valid = true; + } +} + +PackedInt32Array TextServerAdvanced::_shaped_text_get_character_breaks(const RID &p_shaped) const { + ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_COND_V(!sd, PackedInt32Array()); + + MutexLock lock(sd->mutex); + if (!sd->valid) { + const_cast(this)->_shaped_text_shape(p_shaped); + } + + _update_chars(sd); + + return sd->chars; +} + bool TextServerAdvanced::_shaped_text_update_breaks(const RID &p_shaped) { ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped); ERR_FAIL_COND_V(!sd, false); @@ -5338,7 +5409,17 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star // Try system fallback. RID fdef = p_fonts[0]; if (_font_is_allow_system_fallback(fdef)) { - String text = p_sd->text.substr(p_start, 1); + _update_chars(p_sd); + + int64_t next = p_end; + for (const int32_t &E : p_sd->chars) { + if (E > p_start) { + next = E; + break; + } + } + String text = p_sd->text.substr(p_start, next - p_start); + String font_name = _font_get_name(fdef); BitField font_style = _font_get_style(fdef); int font_weight = _font_get_weight(fdef); @@ -6602,6 +6683,30 @@ PackedInt32Array TextServerAdvanced::_string_get_word_breaks(const String &p_str return ret; } +PackedInt32Array TextServerAdvanced::_string_get_character_breaks(const String &p_string, const String &p_language) const { + const String lang = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language; + // Convert to UTF-16. + Char16String utf16 = p_string.utf16(); + + PackedInt32Array ret; + + UErrorCode err = U_ZERO_ERROR; + UBreakIterator *bi = ubrk_open(UBRK_CHARACTER, lang.ascii().get_data(), (const UChar *)utf16.get_data(), utf16.length(), &err); + if (U_SUCCESS(err)) { + while (ubrk_next(bi) != UBRK_DONE) { + int pos = _convert_pos(p_string, utf16, ubrk_current(bi)); + ret.push_back(pos); + } + ubrk_close(bi); + } else { + for (int i = 0; i <= p_string.size(); i++) { + ret.push_back(i); + } + } + + return ret; +} + bool TextServerAdvanced::_is_valid_identifier(const String &p_string) const { #ifndef ICU_STATIC_DATA if (!icu_data_loaded) { diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index 44700e045b0..f27fa1dee95 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -509,9 +509,11 @@ class TextServerAdvanced : public TextServerExtension { HashMap jstops; HashMap breaks; + PackedInt32Array chars; int break_inserts = 0; bool break_ops_valid = false; bool js_ops_valid = false; + bool chars_valid = false; ~ShapedTextDataAdvanced() { for (int i = 0; i < bidi_iter.size(); i++) { @@ -609,6 +611,7 @@ class TextServerAdvanced : public TextServerExtension { mutable HashMap system_fonts; mutable HashMap system_font_data; + void _update_chars(ShapedTextDataAdvanced *p_sd) const; void _realign(ShapedTextDataAdvanced *p_sd) const; int64_t _convert_pos(const String &p_utf32, const Char16String &p_utf16, int64_t p_pos) const; int64_t _convert_pos(const ShapedTextDataAdvanced *p_sd, int64_t p_pos) const; @@ -920,11 +923,14 @@ public: MODBIND1RC(double, shaped_text_get_underline_position, const RID &); MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &); + MODBIND1RC(PackedInt32Array, shaped_text_get_character_breaks, const RID &); + MODBIND2RC(String, format_number, const String &, const String &); MODBIND2RC(String, parse_number, const String &, const String &); MODBIND1RC(String, percent_sign, const String &); MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t); + MODBIND2RC(PackedInt32Array, string_get_character_breaks, const String &, const String &); MODBIND2RC(int64_t, is_confusable, const String &, const PackedStringArray &); MODBIND1RC(bool, spoof_check, const String &); diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index 70da5e27827..bf7cc776c01 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -4079,6 +4079,20 @@ double TextServerFallback::_shaped_text_get_underline_thickness(const RID &p_sha return sd->uthk; } +PackedInt32Array TextServerFallback::_shaped_text_get_character_breaks(const RID &p_shaped) const { + const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped); + ERR_FAIL_COND_V(!sd, PackedInt32Array()); + + MutexLock lock(sd->mutex); + + PackedInt32Array ret; + ret.resize(sd->end - sd->start); + for (int i = sd->start; i < sd->end; i++) { + ret.write[i] = i; + } + return ret; +} + String TextServerFallback::_string_to_upper(const String &p_string, const String &p_language) const { return p_string.to_upper(); } diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 54311caaf95..457573ecb3d 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -788,6 +788,8 @@ public: MODBIND1RC(double, shaped_text_get_underline_position, const RID &); MODBIND1RC(double, shaped_text_get_underline_thickness, const RID &); + MODBIND1RC(PackedInt32Array, shaped_text_get_character_breaks, const RID &); + MODBIND3RC(PackedInt32Array, string_get_word_breaks, const String &, const String &, int64_t); MODBIND2RC(String, string_to_upper, const String &, const String &); diff --git a/scene/gui/line_edit.cpp b/scene/gui/line_edit.cpp index 42ee6cb0bcd..956e93466cb 100644 --- a/scene/gui/line_edit.cpp +++ b/scene/gui/line_edit.cpp @@ -79,7 +79,7 @@ void LineEdit::_move_caret_left(bool p_select, bool p_move_by_word) { if (caret_mid_grapheme_enabled) { set_caret_column(get_caret_column() - 1); } else { - set_caret_column(TS->shaped_text_prev_grapheme_pos(text_rid, get_caret_column())); + set_caret_column(TS->shaped_text_prev_character_pos(text_rid, get_caret_column())); } } @@ -112,7 +112,7 @@ void LineEdit::_move_caret_right(bool p_select, bool p_move_by_word) { if (caret_mid_grapheme_enabled) { set_caret_column(get_caret_column() + 1); } else { - set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, get_caret_column())); + set_caret_column(TS->shaped_text_next_character_pos(text_rid, get_caret_column())); } } @@ -211,7 +211,7 @@ void LineEdit::_delete(bool p_word, bool p_all_to_right) { delete_char(); } else { int cc = caret_column; - set_caret_column(TS->shaped_text_next_grapheme_pos(text_rid, caret_column)); + set_caret_column(TS->shaped_text_next_character_pos(text_rid, caret_column)); delete_text(cc, caret_column); } } @@ -1326,6 +1326,9 @@ void LineEdit::set_caret_at_pixel_pos(int p_x) { } int ofs = ceil(TS->shaped_text_hit_test_position(text_rid, p_x - x_ofs - scroll_offset)); + if (!caret_mid_grapheme_enabled) { + ofs = TS->shaped_text_closest_character_pos(text_rid, ofs); + } set_caret_column(ofs); } diff --git a/scene/gui/line_edit.h b/scene/gui/line_edit.h index dba81b7b502..734813f8a07 100644 --- a/scene/gui/line_edit.h +++ b/scene/gui/line_edit.h @@ -113,7 +113,7 @@ private: PopupMenu *menu_dir = nullptr; PopupMenu *menu_ctl = nullptr; - bool caret_mid_grapheme_enabled = true; + bool caret_mid_grapheme_enabled = false; int caret_column = 0; float scroll_offset = 0.0; diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 3a0fb6d89cd..2906e532019 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -1585,6 +1585,7 @@ float RichTextLabel::_find_click_in_line(ItemFrame *p_frame, int p_line, const V } } else { char_pos = TS->shaped_text_hit_test_position(rid, p_click.x - rect.position.x); + char_pos = TS->shaped_text_closest_character_pos(rid, char_pos); } } line_clicked = true; diff --git a/scene/gui/text_edit.cpp b/scene/gui/text_edit.cpp index 3b2013f7ecf..4a82f6e7259 100644 --- a/scene/gui/text_edit.cpp +++ b/scene/gui/text_edit.cpp @@ -2374,7 +2374,7 @@ void TextEdit::_move_caret_left(bool p_select, bool p_move_by_word) { if (caret_mid_grapheme_enabled) { set_caret_column(get_caret_column(i) - 1, i == 0, i); } else { - set_caret_column(TS->shaped_text_prev_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); + set_caret_column(TS->shaped_text_prev_character_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); } } } @@ -2433,7 +2433,7 @@ void TextEdit::_move_caret_right(bool p_select, bool p_move_by_word) { if (caret_mid_grapheme_enabled) { set_caret_column(get_caret_column(i) + 1, i == 0, i); } else { - set_caret_column(TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); + set_caret_column(TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(i))->get_rid(), get_caret_column(i)), i == 0, i); } } } @@ -2815,7 +2815,7 @@ void TextEdit::_delete(bool p_word, bool p_all_to_right) { if (caret_mid_grapheme_enabled) { next_column = get_caret_column(caret_idx) < curline_len ? (get_caret_column(caret_idx) + 1) : 0; } else { - next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_grapheme_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; + next_column = get_caret_column(caret_idx) < curline_len ? TS->shaped_text_next_character_pos(text.get_line_data(get_caret_line(caret_idx))->get_rid(), (get_caret_column(caret_idx))) : 0; } // Remove overlapping carets. @@ -4331,6 +4331,9 @@ Point2i TextEdit::get_line_column_at_pos(const Point2i &p_pos, bool p_allow_out_ colx = TS->shaped_text_get_size(text_rid).x - colx; } col = TS->shaped_text_hit_test_position(text_rid, colx); + if (!caret_mid_grapheme_enabled) { + col = TS->shaped_text_closest_character_pos(text_rid, col); + } return Point2i(col, row); } @@ -7023,7 +7026,11 @@ int TextEdit::_get_char_pos_for_line(int p_px, int p_line, int p_wrap_index) con if (is_layout_rtl()) { p_px = TS->shaped_text_get_size(text_rid).x - p_px; } - return TS->shaped_text_hit_test_position(text_rid, p_px); + int ofs = TS->shaped_text_hit_test_position(text_rid, p_px); + if (!caret_mid_grapheme_enabled) { + ofs = TS->shaped_text_closest_character_pos(text_rid, ofs); + } + return ofs; } /* Caret */ diff --git a/scene/gui/text_edit.h b/scene/gui/text_edit.h index 83f6d58bea1..b52fde93612 100644 --- a/scene/gui/text_edit.h +++ b/scene/gui/text_edit.h @@ -423,7 +423,7 @@ private: bool move_caret_on_right_click = true; - bool caret_mid_grapheme_enabled = true; + bool caret_mid_grapheme_enabled = false; bool multi_carets_enabled = true; diff --git a/servers/text/text_server_extension.cpp b/servers/text/text_server_extension.cpp index e3e86acb7ac..6fc13867cc1 100644 --- a/servers/text/text_server_extension.cpp +++ b/servers/text/text_server_extension.cpp @@ -303,6 +303,11 @@ void TextServerExtension::_bind_methods() { GDVIRTUAL_BIND(_shaped_text_next_grapheme_pos, "shaped", "pos"); GDVIRTUAL_BIND(_shaped_text_prev_grapheme_pos, "shaped", "pos"); + GDVIRTUAL_BIND(_shaped_text_get_character_breaks, "shaped"); + GDVIRTUAL_BIND(_shaped_text_next_character_pos, "shaped", "pos"); + GDVIRTUAL_BIND(_shaped_text_prev_character_pos, "shaped", "pos"); + GDVIRTUAL_BIND(_shaped_text_closest_character_pos, "shaped", "pos"); + GDVIRTUAL_BIND(_format_number, "string", "language"); GDVIRTUAL_BIND(_parse_number, "string", "language"); GDVIRTUAL_BIND(_percent_sign, "language"); @@ -311,6 +316,7 @@ void TextServerExtension::_bind_methods() { GDVIRTUAL_BIND(_is_valid_identifier, "string"); GDVIRTUAL_BIND(_string_get_word_breaks, "string", "language", "chars_per_line"); + GDVIRTUAL_BIND(_string_get_character_breaks, "string", "language"); GDVIRTUAL_BIND(_is_confusable, "string", "dict"); GDVIRTUAL_BIND(_spoof_check, "string"); @@ -1333,6 +1339,38 @@ int64_t TextServerExtension::shaped_text_prev_grapheme_pos(const RID &p_shaped, return TextServer::shaped_text_prev_grapheme_pos(p_shaped, p_pos); } +PackedInt32Array TextServerExtension::shaped_text_get_character_breaks(const RID &p_shaped) const { + PackedInt32Array ret; + if (GDVIRTUAL_CALL(_shaped_text_get_character_breaks, p_shaped, ret)) { + return ret; + } + return PackedInt32Array(); +} + +int64_t TextServerExtension::shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const { + int64_t ret; + if (GDVIRTUAL_CALL(_shaped_text_next_character_pos, p_shaped, p_pos, ret)) { + return ret; + } + return TextServer::shaped_text_next_character_pos(p_shaped, p_pos); +} + +int64_t TextServerExtension::shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const { + int64_t ret; + if (GDVIRTUAL_CALL(_shaped_text_prev_character_pos, p_shaped, p_pos, ret)) { + return ret; + } + return TextServer::shaped_text_prev_character_pos(p_shaped, p_pos); +} + +int64_t TextServerExtension::shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const { + int64_t ret; + if (GDVIRTUAL_CALL(_shaped_text_closest_character_pos, p_shaped, p_pos, ret)) { + return ret; + } + return TextServer::shaped_text_closest_character_pos(p_shaped, p_pos); +} + String TextServerExtension::format_number(const String &p_string, const String &p_language) const { String ret; if (GDVIRTUAL_CALL(_format_number, p_string, p_language, ret)) { @@ -1399,6 +1437,14 @@ PackedInt32Array TextServerExtension::string_get_word_breaks(const String &p_str return ret; } +PackedInt32Array TextServerExtension::string_get_character_breaks(const String &p_string, const String &p_language) const { + PackedInt32Array ret; + if (GDVIRTUAL_CALL(_string_get_character_breaks, p_string, p_language, ret)) { + return ret; + } + return TextServer::string_get_character_breaks(p_string, p_language); +} + int64_t TextServerExtension::is_confusable(const String &p_string, const PackedStringArray &p_dict) const { int64_t ret; if (GDVIRTUAL_CALL(_is_confusable, p_string, p_dict, ret)) { diff --git a/servers/text/text_server_extension.h b/servers/text/text_server_extension.h index cfaec7e629e..a0c47a70d65 100644 --- a/servers/text/text_server_extension.h +++ b/servers/text/text_server_extension.h @@ -505,6 +505,15 @@ public: GDVIRTUAL2RC(int64_t, _shaped_text_next_grapheme_pos, RID, int64_t); GDVIRTUAL2RC(int64_t, _shaped_text_prev_grapheme_pos, RID, int64_t); + virtual PackedInt32Array shaped_text_get_character_breaks(const RID &p_shaped) const override; + virtual int64_t shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const override; + virtual int64_t shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const override; + virtual int64_t shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const override; + GDVIRTUAL1RC(PackedInt32Array, _shaped_text_get_character_breaks, RID); + GDVIRTUAL2RC(int64_t, _shaped_text_next_character_pos, RID, int64_t); + GDVIRTUAL2RC(int64_t, _shaped_text_prev_character_pos, RID, int64_t); + GDVIRTUAL2RC(int64_t, _shaped_text_closest_character_pos, RID, int64_t); + virtual String format_number(const String &p_string, const String &p_language = "") const override; virtual String parse_number(const String &p_string, const String &p_language = "") const override; virtual String percent_sign(const String &p_language = "") const override; @@ -518,6 +527,9 @@ public: virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const override; GDVIRTUAL3RC(PackedInt32Array, _string_get_word_breaks, const String &, const String &, int64_t); + virtual PackedInt32Array string_get_character_breaks(const String &p_string, const String &p_language = "") const override; + GDVIRTUAL2RC(PackedInt32Array, _string_get_character_breaks, const String &, const String &); + virtual bool is_valid_identifier(const String &p_string) const override; GDVIRTUAL1RC(bool, _is_valid_identifier, const String &); diff --git a/servers/text_server.cpp b/servers/text_server.cpp index 07ad14f1209..470814ca59c 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -448,6 +448,11 @@ void TextServer::_bind_methods() { ClassDB::bind_method(D_METHOD("shaped_text_next_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_next_grapheme_pos); ClassDB::bind_method(D_METHOD("shaped_text_prev_grapheme_pos", "shaped", "pos"), &TextServer::shaped_text_prev_grapheme_pos); + ClassDB::bind_method(D_METHOD("shaped_text_get_character_breaks", "shaped"), &TextServer::shaped_text_get_character_breaks); + ClassDB::bind_method(D_METHOD("shaped_text_next_character_pos", "shaped", "pos"), &TextServer::shaped_text_next_character_pos); + ClassDB::bind_method(D_METHOD("shaped_text_prev_character_pos", "shaped", "pos"), &TextServer::shaped_text_prev_character_pos); + ClassDB::bind_method(D_METHOD("shaped_text_closest_character_pos", "shaped", "pos"), &TextServer::shaped_text_closest_character_pos); + ClassDB::bind_method(D_METHOD("shaped_text_draw", "shaped", "canvas", "pos", "clip_l", "clip_r", "color"), &TextServer::shaped_text_draw, DEFVAL(-1), DEFVAL(-1), DEFVAL(Color(1, 1, 1))); ClassDB::bind_method(D_METHOD("shaped_text_draw_outline", "shaped", "canvas", "pos", "clip_l", "clip_r", "outline_size", "color"), &TextServer::shaped_text_draw_outline, DEFVAL(-1), DEFVAL(-1), DEFVAL(1), DEFVAL(Color(1, 1, 1))); @@ -458,6 +463,7 @@ void TextServer::_bind_methods() { ClassDB::bind_method(D_METHOD("percent_sign", "language"), &TextServer::percent_sign, DEFVAL("")); ClassDB::bind_method(D_METHOD("string_get_word_breaks", "string", "language", "chars_per_line"), &TextServer::string_get_word_breaks, DEFVAL(""), DEFVAL(0)); + ClassDB::bind_method(D_METHOD("string_get_character_breaks", "string", "language"), &TextServer::string_get_character_breaks, DEFVAL("")); ClassDB::bind_method(D_METHOD("is_confusable", "string", "dict"), &TextServer::is_confusable); ClassDB::bind_method(D_METHOD("spoof_check", "string"), &TextServer::spoof_check); @@ -1424,6 +1430,57 @@ int64_t TextServer::shaped_text_prev_grapheme_pos(const RID &p_shaped, int64_t p return p_pos; } +int64_t TextServer::shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const { + const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped); + int64_t prev = 0; + for (const int32_t &E : chars) { + if (E >= p_pos) { + return prev; + } + prev = E; + } + return prev; +} + +int64_t TextServer::shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const { + const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped); + int64_t prev = 0; + for (const int32_t &E : chars) { + if (E > p_pos) { + return E; + } + prev = E; + } + return prev; +} + +int64_t TextServer::shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const { + const PackedInt32Array &chars = shaped_text_get_character_breaks(p_shaped); + int64_t prev = 0; + for (const int32_t &E : chars) { + if (E == p_pos) { + return E; + } else if (E > p_pos) { + if ((E - p_pos) < (p_pos - prev)) { + return E; + } else { + return prev; + } + } + prev = E; + } + return prev; +} + +PackedInt32Array TextServer::string_get_character_breaks(const String &p_string, const String &p_language) const { + PackedInt32Array ret; + ret.resize(p_string.size()); + for (int i = 0; i <= p_string.size(); i++) { + ret.write[i] = i; + } + return ret; +} + void TextServer::shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l, double p_clip_r, const Color &p_color) const { TextServer::Orientation orientation = shaped_text_get_orientation(p_shaped); bool hex_codes = shaped_text_get_preserve_control(p_shaped) || shaped_text_get_preserve_invalid(p_shaped); diff --git a/servers/text_server.h b/servers/text_server.h index a4ea1dfdc7c..7bc353cb8cb 100644 --- a/servers/text_server.h +++ b/servers/text_server.h @@ -491,6 +491,11 @@ public: virtual int64_t shaped_text_next_grapheme_pos(const RID &p_shaped, int64_t p_pos) const; virtual int64_t shaped_text_prev_grapheme_pos(const RID &p_shaped, int64_t p_pos) const; + virtual PackedInt32Array shaped_text_get_character_breaks(const RID &p_shaped) const = 0; + virtual int64_t shaped_text_next_character_pos(const RID &p_shaped, int64_t p_pos) const; + virtual int64_t shaped_text_prev_character_pos(const RID &p_shaped, int64_t p_pos) const; + virtual int64_t shaped_text_closest_character_pos(const RID &p_shaped, int64_t p_pos) const; + // The pen position is always placed on the baseline and moveing left to right. virtual void shaped_text_draw(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, const Color &p_color = Color(1, 1, 1)) const; virtual void shaped_text_draw_outline(const RID &p_shaped, const RID &p_canvas, const Vector2 &p_pos, double p_clip_l = -1.0, double p_clip_r = -1.0, int64_t p_outline_size = 1, const Color &p_color = Color(1, 1, 1)) const; @@ -502,6 +507,7 @@ public: // String functions. virtual PackedInt32Array string_get_word_breaks(const String &p_string, const String &p_language = "", int64_t p_chars_per_line = 0) const = 0; + virtual PackedInt32Array string_get_character_breaks(const String &p_string, const String &p_language = "") const; virtual int64_t is_confusable(const String &p_string, const PackedStringArray &p_dict) const { return -1; }; virtual bool spoof_check(const String &p_string) const { return false; }; diff --git a/tests/scene/test_text_edit.h b/tests/scene/test_text_edit.h index 8cfb1893709..79766cd9194 100644 --- a/tests/scene/test_text_edit.h +++ b/tests/scene/test_text_edit.h @@ -2297,34 +2297,6 @@ TEST_CASE("[SceneTree][TextEdit] text entry") { SIGNAL_CHECK("caret_changed", empty_signal_args); SIGNAL_CHECK("text_changed", empty_signal_args); SIGNAL_CHECK("lines_edited_from", lines_edited_args); - - text_edit->set_caret_mid_grapheme_enabled(false); - CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled()); - - text_edit->start_action(TextEdit::EditAction::ACTION_NONE); - - text_edit->undo(); - MessageQueue::get_singleton()->flush(); - CHECK(text_edit->get_text() == "ffi some test text.ffi some test text."); - - SIGNAL_DISCARD("text_set"); - SIGNAL_DISCARD("text_changed"); - SIGNAL_DISCARD("lines_edited_from"); - SIGNAL_DISCARD("caret_changed"); - - SEND_GUI_ACTION("ui_text_delete"); - CHECK(text_edit->get_viewport()->is_input_handled()); - CHECK(text_edit->get_text() == " some test text. some test text."); - CHECK(text_edit->get_caret_line() == 0); - CHECK(text_edit->get_caret_column() == 0); - CHECK_FALSE(text_edit->has_selection(0)); - - CHECK(text_edit->get_caret_line(1) == 0); - CHECK(text_edit->get_caret_column(1) == 16); - CHECK_FALSE(text_edit->has_selection(1)); - SIGNAL_CHECK("caret_changed", empty_signal_args); - SIGNAL_CHECK("text_changed", empty_signal_args); - SIGNAL_CHECK("lines_edited_from", lines_edited_args); } SUBCASE("[TextEdit] ui_text_caret_word_left") { @@ -3335,18 +3307,6 @@ TEST_CASE("[SceneTree][TextEdit] caret") { SEND_GUI_ACTION("ui_text_caret_left"); CHECK(text_edit->get_caret_column() == 2); - text_edit->set_caret_mid_grapheme_enabled(false); - CHECK_FALSE(text_edit->is_caret_mid_grapheme_enabled()); - - SEND_GUI_ACTION("ui_text_caret_left"); - CHECK(text_edit->get_caret_column() == 0); - - SEND_GUI_ACTION("ui_text_caret_right"); - CHECK(text_edit->get_caret_column() == 3); - - SEND_GUI_ACTION("ui_text_caret_left"); - CHECK(text_edit->get_caret_column() == 0); - text_edit->set_line(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); for (int i = 0; i < 3; i++) { text_edit->insert_line_at(0, "Lorem ipsum dolor sit amet, consectetur adipiscing elit. Donec vasius mattis leo, sed porta ex lacinia bibendum. Nunc bibendum pellentesque."); diff --git a/tests/servers/test_text_server.h b/tests/servers/test_text_server.h index 989d83d504e..75e97d03848 100644 --- a/tests/servers/test_text_server.h +++ b/tests/servers/test_text_server.h @@ -636,6 +636,54 @@ TEST_SUITE("[TextServer]") { CHECK(breaks[17] == 42); } } + + if (ts->has_feature(TextServer::FEATURE_BREAK_ITERATORS)) { + String text2 = U"U+2764 U+FE0F U+200D U+1F525 ; 13.1 # ❤️‍🔥"; + + PackedInt32Array breaks = ts->string_get_character_breaks(text2, "en"); + CHECK(breaks.size() == 39); + if (breaks.size() == 39) { + CHECK(breaks[0] == 1); + CHECK(breaks[1] == 2); + CHECK(breaks[2] == 3); + CHECK(breaks[3] == 4); + CHECK(breaks[4] == 5); + CHECK(breaks[5] == 6); + CHECK(breaks[6] == 7); + CHECK(breaks[7] == 8); + CHECK(breaks[8] == 9); + CHECK(breaks[9] == 10); + CHECK(breaks[10] == 11); + CHECK(breaks[11] == 12); + CHECK(breaks[12] == 13); + CHECK(breaks[13] == 14); + CHECK(breaks[14] == 15); + CHECK(breaks[15] == 16); + CHECK(breaks[16] == 17); + CHECK(breaks[17] == 18); + CHECK(breaks[18] == 19); + CHECK(breaks[19] == 20); + CHECK(breaks[20] == 21); + CHECK(breaks[21] == 22); + CHECK(breaks[22] == 23); + CHECK(breaks[23] == 24); + CHECK(breaks[24] == 25); + CHECK(breaks[25] == 26); + CHECK(breaks[26] == 27); + CHECK(breaks[27] == 28); + CHECK(breaks[28] == 29); + CHECK(breaks[29] == 30); + CHECK(breaks[30] == 31); + CHECK(breaks[31] == 32); + CHECK(breaks[32] == 33); + CHECK(breaks[33] == 34); + CHECK(breaks[34] == 35); + CHECK(breaks[35] == 36); + CHECK(breaks[36] == 37); + CHECK(breaks[37] == 38); + CHECK(breaks[38] == 42); + } + } } } }