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);
+ }
+ }
}
}
}