Browse Source

Merge pull request #80650 from bruvzg/comp_char_fix

[TextServer] Fix system font fallback and caret/selection behavior for composite characters.
Rémi Verschelde 2 years ago
parent
commit
b51ee8b029

+ 1 - 1
doc/classes/LineEdit.xml

@@ -197,7 +197,7 @@
 		<member name="caret_force_displayed" type="bool" setter="set_caret_force_displayed" getter="is_caret_force_displayed" default="false">
 			If [code]true[/code], the [LineEdit] will always show the caret, even if focus is lost.
 		</member>
-		<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="true">
+		<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="false">
 			Allow moving caret, selecting and removing the individual composite character components.
 			[b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components.
 		</member>

+ 1 - 1
doc/classes/TextEdit.xml

@@ -1100,7 +1100,7 @@
 		<member name="caret_draw_when_editable_disabled" type="bool" setter="set_draw_caret_when_editable_disabled" getter="is_drawing_caret_when_editable_disabled" default="false">
 			If [code]true[/code], caret will be visible when [member editable] is disabled.
 		</member>
-		<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="true">
+		<member name="caret_mid_grapheme" type="bool" setter="set_caret_mid_grapheme_enabled" getter="is_caret_mid_grapheme_enabled" default="false">
 			Allow moving caret, selecting and removing the individual composite character components.
 			[b]Note:[/b] [kbd]Backspace[/kbd] is always removing individual composite character components.
 		</member>

+ 45 - 2
doc/classes/TextServer.xml

@@ -1139,6 +1139,14 @@
 				Clears text buffer (removes text and inline objects).
 			</description>
 		</method>
+		<method name="shaped_text_closest_character_pos" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+				Returns composite character position closest to the [param pos].
+			</description>
+		</method>
 		<method name="shaped_text_draw" qualifiers="const">
 			<return type="void" />
 			<param index="0" name="shaped" type="RID" />
@@ -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.
 			</description>
 		</method>
+		<method name="shaped_text_get_character_breaks" qualifiers="const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="shaped" type="RID" />
+			<description>
+				Returns array of the composite character boundaries.
+			</description>
+		</method>
 		<method name="shaped_text_get_custom_punctuation" qualifiers="const">
 			<return type="String" />
 			<param index="0" name="shaped" type="RID" />
@@ -1432,7 +1447,7 @@
 				Returns [code]true[/code] if buffer is successfully shaped.
 			</description>
 		</method>
-		<method name="shaped_text_next_grapheme_pos" qualifiers="const">
+		<method name="shaped_text_next_character_pos" qualifiers="const">
 			<return type="int" />
 			<param index="0" name="shaped" type="RID" />
 			<param index="1" name="pos" type="int" />
@@ -1440,6 +1455,14 @@
 				Returns composite character end position closest to the [param pos].
 			</description>
 		</method>
+		<method name="shaped_text_next_grapheme_pos" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+				Returns grapheme end position closest to the [param pos].
+			</description>
+		</method>
 		<method name="shaped_text_overrun_trim_to_width">
 			<return type="void" />
 			<param index="0" name="shaped" type="RID" />
@@ -1449,7 +1472,7 @@
 				Trims text if it exceeds the given width.
 			</description>
 		</method>
-		<method name="shaped_text_prev_grapheme_pos" qualifiers="const">
+		<method name="shaped_text_prev_character_pos" qualifiers="const">
 			<return type="int" />
 			<param index="0" name="shaped" type="RID" />
 			<param index="1" name="pos" type="int" />
@@ -1457,6 +1480,14 @@
 				Returns composite character start position closest to the [param pos].
 			</description>
 		</method>
+		<method name="shaped_text_prev_grapheme_pos" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+				Returns grapheme start position closest to the [param pos].
+			</description>
+		</method>
 		<method name="shaped_text_resize_object">
 			<return type="bool" />
 			<param index="0" name="shaped" type="RID" />
@@ -1568,6 +1599,18 @@
 				[b]Note:[/b] Always returns [code]false[/code] if the server does not support the [constant FEATURE_UNICODE_SECURITY] feature.
 			</description>
 		</method>
+		<method name="string_get_character_breaks" qualifiers="const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="string" type="String" />
+			<param index="1" name="language" type="String" default="&quot;&quot;" />
+			<description>
+				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]
+			</description>
+		</method>
 		<method name="string_get_word_breaks" qualifiers="const">
 			<return type="PackedInt32Array" />
 			<param index="0" name="string" type="String" />

+ 34 - 0
doc/classes/TextServerExtension.xml

@@ -981,6 +981,13 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_closest_character_pos" qualifiers="virtual const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+			</description>
+		</method>
 		<method name="_shaped_text_draw" qualifiers="virtual const">
 			<return type="void" />
 			<param index="0" name="shaped" type="RID" />
@@ -1026,6 +1033,12 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_get_character_breaks" qualifiers="virtual const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="shaped" type="RID" />
+			<description>
+			</description>
+		</method>
 		<method name="_shaped_text_get_custom_punctuation" qualifiers="virtual const">
 			<return type="String" />
 			<param index="0" name="shaped" type="RID" />
@@ -1229,6 +1242,13 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_next_character_pos" qualifiers="virtual const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+			</description>
+		</method>
 		<method name="_shaped_text_next_grapheme_pos" qualifiers="virtual const">
 			<return type="int" />
 			<param index="0" name="shaped" type="RID" />
@@ -1244,6 +1264,13 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_prev_character_pos" qualifiers="virtual const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="pos" type="int" />
+			<description>
+			</description>
+		</method>
 		<method name="_shaped_text_prev_grapheme_pos" qualifiers="virtual const">
 			<return type="int" />
 			<param index="0" name="shaped" type="RID" />
@@ -1356,6 +1383,13 @@
 			<description>
 			</description>
 		</method>
+		<method name="_string_get_character_breaks" qualifiers="virtual const">
+			<return type="PackedInt32Array" />
+			<param index="0" name="string" type="String" />
+			<param index="1" name="language" type="String" />
+			<description>
+			</description>
+		</method>
 		<method name="_string_get_word_breaks" qualifiers="virtual const">
 			<return type="PackedInt32Array" />
 			<param index="0" name="string" type="String" />

+ 106 - 1
modules/text_server_adv/text_server_adv.cpp

@@ -3780,6 +3780,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;
 	}
 }
@@ -4833,6 +4834,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<ShapedTextDataAdvanced::Span> &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<TextServerAdvanced *>(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);
@@ -5336,7 +5407,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<FontStyle> font_style = _font_get_style(fdef);
 			int font_weight = _font_get_weight(fdef);
@@ -6600,6 +6681,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) {

+ 6 - 0
modules/text_server_adv/text_server_adv.h

@@ -509,9 +509,11 @@ class TextServerAdvanced : public TextServerExtension {
 
 		HashMap<int, bool> jstops;
 		HashMap<int, bool> 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<SystemFontKey, SystemFontCache, SystemFontKeyHasher> system_fonts;
 	mutable HashMap<String, PackedByteArray> 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 &);

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

+ 2 - 0
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 &);

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

+ 1 - 1
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;

+ 1 - 0
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;

+ 11 - 4
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 */

+ 1 - 1
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;
 

+ 46 - 0
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)) {

+ 12 - 0
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 &);
 

+ 57 - 0
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);

+ 6 - 0
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; };

+ 0 - 40
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.");

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