Pārlūkot izejas kodu

[Text Overrun] Add option to set custom ellipsis character, add support for system font fallback.

bruvzg 1 gadu atpakaļ
vecāks
revīzija
56579f397d

+ 3 - 0
doc/classes/Label.xml

@@ -45,6 +45,9 @@
 		<member name="clip_text" type="bool" setter="set_clip_text" getter="is_clipping_text" default="false">
 			If [code]true[/code], the Label only shows the text that fits inside its bounding rectangle and will clip text horizontally.
 		</member>
+		<member name="ellipsis_char" type="String" setter="set_ellipsis_char" getter="get_ellipsis_char" default="&quot;…&quot;">
+			Ellipsis character used for text clipping.
+		</member>
 		<member name="horizontal_alignment" type="int" setter="set_horizontal_alignment" getter="get_horizontal_alignment" enum="HorizontalAlignment" default="0">
 			Controls the text's horizontal alignment. Supports left, center, right, and fill, or justify. Set it to one of the [enum HorizontalAlignment] constants.
 		</member>

+ 3 - 0
doc/classes/TextLine.xml

@@ -151,6 +151,9 @@
 		<member name="direction" type="int" setter="set_direction" getter="get_direction" enum="TextServer.Direction" default="0">
 			Text writing direction.
 		</member>
+		<member name="ellipsis_char" type="String" setter="set_ellipsis_char" getter="get_ellipsis_char" default="&quot;…&quot;">
+			Ellipsis character used for text clipping.
+		</member>
 		<member name="flags" type="int" setter="set_flags" getter="get_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="3">
 			Line alignment rules. For more info see [TextServer].
 		</member>

+ 3 - 0
doc/classes/TextParagraph.xml

@@ -274,6 +274,9 @@
 		<member name="direction" type="int" setter="set_direction" getter="get_direction" enum="TextServer.Direction" default="0">
 			Text writing direction.
 		</member>
+		<member name="ellipsis_char" type="String" setter="set_ellipsis_char" getter="get_ellipsis_char" default="&quot;…&quot;">
+			Ellipsis character used for text clipping.
+		</member>
 		<member name="justification_flags" type="int" setter="set_justification_flags" getter="get_justification_flags" enum="TextServer.JustificationFlag" is_bitfield="true" default="163">
 			Line fill alignment rules. For more info see [enum TextServer.JustificationFlag].
 		</member>

+ 15 - 0
doc/classes/TextServer.xml

@@ -1243,6 +1243,13 @@
 				Returns array of the composite character boundaries.
 			</description>
 		</method>
+		<method name="shaped_text_get_custom_ellipsis" qualifiers="const">
+			<return type="int" />
+			<param index="0" name="shaped" type="RID" />
+			<description>
+				Returns ellipsis character used for text clipping.
+			</description>
+		</method>
 		<method name="shaped_text_get_custom_punctuation" qualifiers="const">
 			<return type="String" />
 			<param index="0" name="shaped" type="RID" />
@@ -1547,6 +1554,14 @@
 				Override ranges should cover full source text without overlaps. BiDi algorithm will be used on each range separately.
 			</description>
 		</method>
+		<method name="shaped_text_set_custom_ellipsis">
+			<return type="void" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="char" type="int" />
+			<description>
+				Sets ellipsis character used for text clipping.
+			</description>
+		</method>
 		<method name="shaped_text_set_custom_punctuation">
 			<return type="void" />
 			<param index="0" name="shaped" type="RID" />

+ 13 - 0
doc/classes/TextServerExtension.xml

@@ -1073,6 +1073,12 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_get_custom_ellipsis" qualifiers="virtual const">
+			<return type="int" />
+			<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" />
@@ -1329,6 +1335,13 @@
 			<description>
 			</description>
 		</method>
+		<method name="_shaped_text_set_custom_ellipsis" qualifiers="virtual">
+			<return type="void" />
+			<param index="0" name="shaped" type="RID" />
+			<param index="1" name="char" type="int" />
+			<description>
+			</description>
+		</method>
 		<method name="_shaped_text_set_custom_punctuation" qualifiers="virtual">
 			<return type="void" />
 			<param index="0" name="shaped" type="RID" />

+ 215 - 163
modules/text_server_adv/text_server_adv.cpp

@@ -4048,6 +4048,20 @@ String TextServerAdvanced::_shaped_text_get_custom_punctuation(const RID &p_shap
 	return sd->custom_punct;
 }
 
+void TextServerAdvanced::_shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) {
+	_THREAD_SAFE_METHOD_
+	ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped);
+	ERR_FAIL_NULL(sd);
+	sd->el_char = p_char;
+}
+
+int64_t TextServerAdvanced::_shaped_text_get_custom_ellipsis(const RID &p_shaped) const {
+	_THREAD_SAFE_METHOD_
+	const ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped);
+	ERR_FAIL_NULL_V(sd, 0);
+	return sd->el_char;
+}
+
 void TextServerAdvanced::_shaped_text_set_bidi_override(const RID &p_shaped, const Array &p_override) {
 	ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped);
 	ERR_FAIL_NULL(sd);
@@ -4800,6 +4814,166 @@ double TextServerAdvanced::_shaped_text_tab_align(const RID &p_shaped, const Pac
 	return 0.0;
 }
 
+RID TextServerAdvanced::_find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text) {
+	RID f;
+	// Try system fallback.
+	String font_name = _font_get_name(p_fdef);
+	BitField<FontStyle> font_style = _font_get_style(p_fdef);
+	int font_weight = _font_get_weight(p_fdef);
+	int font_stretch = _font_get_stretch(p_fdef);
+	Dictionary dvar = _font_get_variation_coordinates(p_fdef);
+	static int64_t wgth_tag = _name_to_tag("weight");
+	static int64_t wdth_tag = _name_to_tag("width");
+	static int64_t ital_tag = _name_to_tag("italic");
+	if (dvar.has(wgth_tag)) {
+		font_weight = dvar[wgth_tag].operator int();
+	}
+	if (dvar.has(wdth_tag)) {
+		font_stretch = dvar[wdth_tag].operator int();
+	}
+	if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) {
+		font_style.set_flag(TextServer::FONT_ITALIC);
+	}
+
+	String locale = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language;
+	PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, p_text, locale, p_script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC);
+#ifdef GDEXTENSION
+	for (int fb = 0; fb < fallback_font_name.size(); fb++) {
+		const String &E = fallback_font_name[fb];
+#else
+	for (const String &E : fallback_font_name) {
+#endif
+		SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, p_fdef, this);
+		if (system_fonts.has(key)) {
+			const SystemFontCache &sysf_cache = system_fonts[key];
+			int best_score = 0;
+			int best_match = -1;
+			for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) {
+				const SystemFontCacheRec &F = sysf_cache.var[face_idx];
+				if (unlikely(!_font_has_char(F.rid, p_text[0]))) {
+					continue;
+				}
+				BitField<FontStyle> style = _font_get_style(F.rid);
+				int weight = _font_get_weight(F.rid);
+				int stretch = _font_get_stretch(F.rid);
+				int score = (20 - Math::abs(weight - font_weight) / 50);
+				score += (20 - Math::abs(stretch - font_stretch) / 10);
+				if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
+					score += 30;
+				}
+				if (score >= best_score) {
+					best_score = score;
+					best_match = face_idx;
+				}
+				if (best_score == 70) {
+					break;
+				}
+			}
+			if (best_match != -1) {
+				f = sysf_cache.var[best_match].rid;
+			}
+		}
+		if (!f.is_valid()) {
+			if (system_fonts.has(key)) {
+				const SystemFontCache &sysf_cache = system_fonts[key];
+				if (sysf_cache.max_var == sysf_cache.var.size()) {
+					// All subfonts already tested, skip.
+					continue;
+				}
+			}
+
+			if (!system_font_data.has(E)) {
+				system_font_data[E] = FileAccess::get_file_as_bytes(E);
+			}
+
+			const PackedByteArray &font_data = system_font_data[E];
+
+			SystemFontCacheRec sysf;
+			sysf.rid = _create_font();
+			_font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size());
+
+			Dictionary var = dvar;
+			// Select matching style from collection.
+			int best_score = 0;
+			int best_match = -1;
+			for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) {
+				_font_set_face_index(sysf.rid, face_idx);
+				if (unlikely(!_font_has_char(sysf.rid, p_text[0]))) {
+					continue;
+				}
+				BitField<FontStyle> style = _font_get_style(sysf.rid);
+				int weight = _font_get_weight(sysf.rid);
+				int stretch = _font_get_stretch(sysf.rid);
+				int score = (20 - Math::abs(weight - font_weight) / 50);
+				score += (20 - Math::abs(stretch - font_stretch) / 10);
+				if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
+					score += 30;
+				}
+				if (score >= best_score) {
+					best_score = score;
+					best_match = face_idx;
+				}
+				if (best_score == 70) {
+					break;
+				}
+			}
+			if (best_match == -1) {
+				_free_rid(sysf.rid);
+				continue;
+			} else {
+				_font_set_face_index(sysf.rid, best_match);
+			}
+			sysf.index = best_match;
+
+			// If it's a variable font, apply weight, stretch and italic coordinates to match requested style.
+			if (best_score != 70) {
+				Dictionary ftr = _font_supported_variation_list(sysf.rid);
+				if (ftr.has(wdth_tag)) {
+					var[wdth_tag] = font_stretch;
+					_font_set_stretch(sysf.rid, font_stretch);
+				}
+				if (ftr.has(wgth_tag)) {
+					var[wgth_tag] = font_weight;
+					_font_set_weight(sysf.rid, font_weight);
+				}
+				if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) {
+					var[ital_tag] = 1;
+					_font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC);
+				}
+			}
+
+			_font_set_antialiasing(sysf.rid, key.antialiasing);
+			_font_set_generate_mipmaps(sysf.rid, key.mipmaps);
+			_font_set_multichannel_signed_distance_field(sysf.rid, key.msdf);
+			_font_set_msdf_pixel_range(sysf.rid, key.msdf_range);
+			_font_set_msdf_size(sysf.rid, key.msdf_source_size);
+			_font_set_fixed_size(sysf.rid, key.fixed_size);
+			_font_set_force_autohinter(sysf.rid, key.force_autohinter);
+			_font_set_hinting(sysf.rid, key.hinting);
+			_font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning);
+			_font_set_variation_coordinates(sysf.rid, var);
+			_font_set_oversampling(sysf.rid, key.oversampling);
+			_font_set_embolden(sysf.rid, key.embolden);
+			_font_set_transform(sysf.rid, key.transform);
+			_font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]);
+			_font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]);
+			_font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]);
+			_font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]);
+
+			if (system_fonts.has(key)) {
+				system_fonts[key].var.push_back(sysf);
+			} else {
+				SystemFontCache &sysf_cache = system_fonts[key];
+				sysf_cache.max_var = _font_get_face_count(sysf.rid);
+				sysf_cache.var.push_back(sysf);
+			}
+			f = sysf.rid;
+		}
+		break;
+	}
+	return f;
+}
+
 void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_line, double p_width, BitField<TextServer::TextOverrunFlag> p_trim_flags) {
 	ShapedTextDataAdvanced *sd = shaped_owner.get_or_null(p_shaped_line);
 	ERR_FAIL_NULL_MSG(sd, "ShapedTextDataAdvanced invalid.");
@@ -4842,20 +5016,52 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 
 	int sd_size = sd->glyphs.size();
 	int last_gl_font_size = sd_glyphs[sd_size - 1].font_size;
+	bool found_el_char = false;
 
 	// Find usable fonts, if fonts from the last glyph do not have required chars.
 	RID dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
-	if (!_font_has_char(dot_gl_font_rid, '.')) {
+	if (!_font_has_char(dot_gl_font_rid, sd->el_char)) {
 		const Array &fonts = spans[spans.size() - 1].fonts;
 		for (int i = 0; i < fonts.size(); i++) {
-			if (_font_has_char(fonts[i], '.')) {
+			if (_font_has_char(fonts[i], sd->el_char)) {
 				dot_gl_font_rid = fonts[i];
+				found_el_char = true;
 				break;
 			}
 		}
+		if (!found_el_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) {
+			const char32_t u32str[] = { sd->el_char, 0 };
+			RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, u32str);
+			if (rid.is_valid()) {
+				dot_gl_font_rid = rid;
+				found_el_char = true;
+			}
+		}
+	} else {
+		found_el_char = true;
+	}
+	if (!found_el_char) {
+		bool found_dot_char = false;
+		dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
+		if (!_font_has_char(dot_gl_font_rid, '.')) {
+			const Array &fonts = spans[spans.size() - 1].fonts;
+			for (int i = 0; i < fonts.size(); i++) {
+				if (_font_has_char(fonts[i], '.')) {
+					dot_gl_font_rid = fonts[i];
+					found_dot_char = true;
+					break;
+				}
+			}
+			if (!found_dot_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) {
+				RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, ".");
+				if (rid.is_valid()) {
+					dot_gl_font_rid = rid;
+				}
+			}
+		}
 	}
 	RID whitespace_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
-	if (!_font_has_char(whitespace_gl_font_rid, '.')) {
+	if (!_font_has_char(whitespace_gl_font_rid, ' ')) {
 		const Array &fonts = spans[spans.size() - 1].fonts;
 		for (int i = 0; i < fonts.size(); i++) {
 			if (_font_has_char(fonts[i], ' ')) {
@@ -4865,14 +5071,14 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 		}
 	}
 
-	int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, '.', 0) : -10;
+	int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, (found_el_char ? sd->el_char : '.'), 0) : -1;
 	Vector2 dot_adv = dot_gl_font_rid.is_valid() ? _font_get_glyph_advance(dot_gl_font_rid, last_gl_font_size, dot_gl_idx) : Vector2();
-	int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -10;
+	int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -1;
 	Vector2 whitespace_adv = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_advance(whitespace_gl_font_rid, last_gl_font_size, whitespace_gl_idx) : Vector2();
 
 	int ellipsis_width = 0;
 	if (add_ellipsis && whitespace_gl_font_rid.is_valid()) {
-		ellipsis_width = 3 * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0);
+		ellipsis_width = (found_el_char ? 1 : 3) * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0);
 	}
 
 	int ell_min_characters = 6;
@@ -4951,7 +5157,7 @@ void TextServerAdvanced::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 			if (dot_gl_idx != 0) {
 				Glyph gl;
 				gl.count = 1;
-				gl.repeat = 3;
+				gl.repeat = (found_el_char ? 1 : 3);
 				gl.advance = dot_adv.x;
 				gl.index = dot_gl_idx;
 				gl.font_rid = dot_gl_font_rid;
@@ -5580,166 +5786,12 @@ void TextServerAdvanced::_shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_star
 					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);
-			int font_stretch = _font_get_stretch(fdef);
-			Dictionary dvar = _font_get_variation_coordinates(fdef);
-			static int64_t wgth_tag = _name_to_tag("weight");
-			static int64_t wdth_tag = _name_to_tag("width");
-			static int64_t ital_tag = _name_to_tag("italic");
-			if (dvar.has(wgth_tag)) {
-				font_weight = dvar[wgth_tag].operator int();
-			}
-			if (dvar.has(wdth_tag)) {
-				font_stretch = dvar[wdth_tag].operator int();
-			}
-			if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) {
-				font_style.set_flag(TextServer::FONT_ITALIC);
-			}
-
 			char scr_buffer[5] = { 0, 0, 0, 0, 0 };
 			hb_tag_to_string(hb_script_to_iso15924_tag(p_script), scr_buffer);
 			String script_code = String(scr_buffer);
-			String locale = (p_sd->spans[p_span].language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_sd->spans[p_span].language;
-
-			PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, text, locale, script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC);
-#ifdef GDEXTENSION
-			for (int fb = 0; fb < fallback_font_name.size(); fb++) {
-				const String &E = fallback_font_name[fb];
-#else
-			for (const String &E : fallback_font_name) {
-#endif
-				SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, fdef, this);
-				if (system_fonts.has(key)) {
-					const SystemFontCache &sysf_cache = system_fonts[key];
-					int best_score = 0;
-					int best_match = -1;
-					for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) {
-						const SystemFontCacheRec &F = sysf_cache.var[face_idx];
-						if (unlikely(!_font_has_char(F.rid, text[0]))) {
-							continue;
-						}
-						BitField<FontStyle> style = _font_get_style(F.rid);
-						int weight = _font_get_weight(F.rid);
-						int stretch = _font_get_stretch(F.rid);
-						int score = (20 - Math::abs(weight - font_weight) / 50);
-						score += (20 - Math::abs(stretch - font_stretch) / 10);
-						if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
-							score += 30;
-						}
-						if (score >= best_score) {
-							best_score = score;
-							best_match = face_idx;
-						}
-						if (best_score == 70) {
-							break;
-						}
-					}
-					if (best_match != -1) {
-						f = sysf_cache.var[best_match].rid;
-					}
-				}
-				if (!f.is_valid()) {
-					if (system_fonts.has(key)) {
-						const SystemFontCache &sysf_cache = system_fonts[key];
-						if (sysf_cache.max_var == sysf_cache.var.size()) {
-							// All subfonts already tested, skip.
-							continue;
-						}
-					}
 
-					if (!system_font_data.has(E)) {
-						system_font_data[E] = FileAccess::get_file_as_bytes(E);
-					}
-
-					const PackedByteArray &font_data = system_font_data[E];
-
-					SystemFontCacheRec sysf;
-					sysf.rid = _create_font();
-					_font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size());
-
-					Dictionary var = dvar;
-					// Select matching style from collection.
-					int best_score = 0;
-					int best_match = -1;
-					for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) {
-						_font_set_face_index(sysf.rid, face_idx);
-						if (unlikely(!_font_has_char(sysf.rid, text[0]))) {
-							continue;
-						}
-						BitField<FontStyle> style = _font_get_style(sysf.rid);
-						int weight = _font_get_weight(sysf.rid);
-						int stretch = _font_get_stretch(sysf.rid);
-						int score = (20 - Math::abs(weight - font_weight) / 50);
-						score += (20 - Math::abs(stretch - font_stretch) / 10);
-						if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
-							score += 30;
-						}
-						if (score >= best_score) {
-							best_score = score;
-							best_match = face_idx;
-						}
-						if (best_score == 70) {
-							break;
-						}
-					}
-					if (best_match == -1) {
-						_free_rid(sysf.rid);
-						continue;
-					} else {
-						_font_set_face_index(sysf.rid, best_match);
-					}
-					sysf.index = best_match;
-
-					// If it's a variable font, apply weight, stretch and italic coordinates to match requested style.
-					if (best_score != 70) {
-						Dictionary ftr = _font_supported_variation_list(sysf.rid);
-						if (ftr.has(wdth_tag)) {
-							var[wdth_tag] = font_stretch;
-							_font_set_stretch(sysf.rid, font_stretch);
-						}
-						if (ftr.has(wgth_tag)) {
-							var[wgth_tag] = font_weight;
-							_font_set_weight(sysf.rid, font_weight);
-						}
-						if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) {
-							var[ital_tag] = 1;
-							_font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC);
-						}
-					}
-
-					_font_set_antialiasing(sysf.rid, key.antialiasing);
-					_font_set_generate_mipmaps(sysf.rid, key.mipmaps);
-					_font_set_multichannel_signed_distance_field(sysf.rid, key.msdf);
-					_font_set_msdf_pixel_range(sysf.rid, key.msdf_range);
-					_font_set_msdf_size(sysf.rid, key.msdf_source_size);
-					_font_set_fixed_size(sysf.rid, key.fixed_size);
-					_font_set_force_autohinter(sysf.rid, key.force_autohinter);
-					_font_set_hinting(sysf.rid, key.hinting);
-					_font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning);
-					_font_set_variation_coordinates(sysf.rid, var);
-					_font_set_oversampling(sysf.rid, key.oversampling);
-					_font_set_embolden(sysf.rid, key.embolden);
-					_font_set_transform(sysf.rid, key.transform);
-					_font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]);
-					_font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]);
-					_font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]);
-					_font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]);
-
-					if (system_fonts.has(key)) {
-						system_fonts[key].var.push_back(sysf);
-					} else {
-						SystemFontCache &sysf_cache = system_fonts[key];
-						sysf_cache.max_var = _font_get_face_count(sysf.rid);
-						sysf_cache.var.push_back(sysf);
-					}
-					f = sysf.rid;
-				}
-				break;
-			}
+			String text = p_sd->text.substr(p_start, next - p_start);
+			f = _find_sys_font_for_text(fdef, script_code, p_sd->spans[p_span].language, text);
 		}
 	}
 

+ 5 - 0
modules/text_server_adv/text_server_adv.h

@@ -502,6 +502,7 @@ class TextServerAdvanced : public TextServerExtension {
 		double upos = 0.0;
 		double uthk = 0.0;
 
+		char32_t el_char = 0x2026;
 		TrimData overrun_trim_data;
 		bool fit_width_minimum_reached = false;
 
@@ -647,6 +648,7 @@ class TextServerAdvanced : public TextServerExtension {
 	bool _shape_substr(ShapedTextDataAdvanced *p_new_sd, const ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_length) const;
 	void _shape_run(ShapedTextDataAdvanced *p_sd, int64_t p_start, int64_t p_end, hb_script_t p_script, hb_direction_t p_direction, TypedArray<RID> p_fonts, int64_t p_span, int64_t p_fb_index, int64_t p_prev_start, int64_t p_prev_end);
 	Glyph _shape_single_glyph(ShapedTextDataAdvanced *p_sd, char32_t p_char, hb_script_t p_script, hb_direction_t p_direction, const RID &p_font, int64_t p_font_size);
+	_FORCE_INLINE_ RID _find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text);
 
 	_FORCE_INLINE_ void _add_featuers(const Dictionary &p_source, Vector<hb_feature_t> &r_ftrs);
 
@@ -899,6 +901,9 @@ public:
 	MODBIND2(shaped_text_set_custom_punctuation, const RID &, const String &);
 	MODBIND1RC(String, shaped_text_get_custom_punctuation, const RID &);
 
+	MODBIND2(shaped_text_set_custom_ellipsis, const RID &, int64_t);
+	MODBIND1RC(int64_t, shaped_text_get_custom_ellipsis, const RID &);
+
 	MODBIND2(shaped_text_set_orientation, const RID &, Orientation);
 	MODBIND1RC(Orientation, shaped_text_get_orientation, const RID &);
 

+ 216 - 162
modules/text_server_fb/text_server_fb.cpp

@@ -2905,6 +2905,20 @@ String TextServerFallback::_shaped_text_get_custom_punctuation(const RID &p_shap
 	return sd->custom_punct;
 }
 
+void TextServerFallback::_shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) {
+	_THREAD_SAFE_METHOD_
+	ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped);
+	ERR_FAIL_NULL(sd);
+	sd->el_char = p_char;
+}
+
+int64_t TextServerFallback::_shaped_text_get_custom_ellipsis(const RID &p_shaped) const {
+	_THREAD_SAFE_METHOD_
+	const ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped);
+	ERR_FAIL_NULL_V(sd, 0);
+	return sd->el_char;
+}
+
 void TextServerFallback::_shaped_text_set_orientation(const RID &p_shaped, TextServer::Orientation p_orientation) {
 	ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped);
 	ERR_FAIL_NULL(sd);
@@ -3601,6 +3615,168 @@ bool TextServerFallback::_shaped_text_update_justification_ops(const RID &p_shap
 	return true;
 }
 
+RID TextServerFallback::_find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text) {
+	RID f;
+	// Try system fallback.
+	if (_font_is_allow_system_fallback(p_fdef)) {
+		String font_name = _font_get_name(p_fdef);
+		BitField<FontStyle> font_style = _font_get_style(p_fdef);
+		int font_weight = _font_get_weight(p_fdef);
+		int font_stretch = _font_get_stretch(p_fdef);
+		Dictionary dvar = _font_get_variation_coordinates(p_fdef);
+		static int64_t wgth_tag = _name_to_tag("weight");
+		static int64_t wdth_tag = _name_to_tag("width");
+		static int64_t ital_tag = _name_to_tag("italic");
+		if (dvar.has(wgth_tag)) {
+			font_weight = dvar[wgth_tag].operator int();
+		}
+		if (dvar.has(wdth_tag)) {
+			font_stretch = dvar[wdth_tag].operator int();
+		}
+		if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) {
+			font_style.set_flag(TextServer::FONT_ITALIC);
+		}
+
+		String locale = (p_language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : p_language;
+		PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, p_text, locale, p_script_code, font_weight, font_stretch, font_style & TextServer::FONT_ITALIC);
+#ifdef GDEXTENSION
+		for (int fb = 0; fb < fallback_font_name.size(); fb++) {
+			const String &E = fallback_font_name[fb];
+#else
+		for (const String &E : fallback_font_name) {
+#endif
+			SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, p_fdef, this);
+			if (system_fonts.has(key)) {
+				const SystemFontCache &sysf_cache = system_fonts[key];
+				int best_score = 0;
+				int best_match = -1;
+				for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) {
+					const SystemFontCacheRec &F = sysf_cache.var[face_idx];
+					if (unlikely(!_font_has_char(F.rid, p_text[0]))) {
+						continue;
+					}
+					BitField<FontStyle> style = _font_get_style(F.rid);
+					int weight = _font_get_weight(F.rid);
+					int stretch = _font_get_stretch(F.rid);
+					int score = (20 - Math::abs(weight - font_weight) / 50);
+					score += (20 - Math::abs(stretch - font_stretch) / 10);
+					if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
+						score += 30;
+					}
+					if (score >= best_score) {
+						best_score = score;
+						best_match = face_idx;
+					}
+					if (best_score == 70) {
+						break;
+					}
+				}
+				if (best_match != -1) {
+					f = sysf_cache.var[best_match].rid;
+				}
+			}
+			if (!f.is_valid()) {
+				if (system_fonts.has(key)) {
+					const SystemFontCache &sysf_cache = system_fonts[key];
+					if (sysf_cache.max_var == sysf_cache.var.size()) {
+						// All subfonts already tested, skip.
+						continue;
+					}
+				}
+
+				if (!system_font_data.has(E)) {
+					system_font_data[E] = FileAccess::get_file_as_bytes(E);
+				}
+
+				const PackedByteArray &font_data = system_font_data[E];
+
+				SystemFontCacheRec sysf;
+				sysf.rid = _create_font();
+				_font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size());
+
+				Dictionary var = dvar;
+				// Select matching style from collection.
+				int best_score = 0;
+				int best_match = -1;
+				for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) {
+					_font_set_face_index(sysf.rid, face_idx);
+					if (unlikely(!_font_has_char(sysf.rid, p_text[0]))) {
+						continue;
+					}
+					BitField<FontStyle> style = _font_get_style(sysf.rid);
+					int weight = _font_get_weight(sysf.rid);
+					int stretch = _font_get_stretch(sysf.rid);
+					int score = (20 - Math::abs(weight - font_weight) / 50);
+					score += (20 - Math::abs(stretch - font_stretch) / 10);
+					if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
+						score += 30;
+					}
+					if (score >= best_score) {
+						best_score = score;
+						best_match = face_idx;
+					}
+					if (best_score == 70) {
+						break;
+					}
+				}
+				if (best_match == -1) {
+					_free_rid(sysf.rid);
+					continue;
+				} else {
+					_font_set_face_index(sysf.rid, best_match);
+				}
+				sysf.index = best_match;
+
+				// If it's a variable font, apply weight, stretch and italic coordinates to match requested style.
+				if (best_score != 70) {
+					Dictionary ftr = _font_supported_variation_list(sysf.rid);
+					if (ftr.has(wdth_tag)) {
+						var[wdth_tag] = font_stretch;
+						_font_set_stretch(sysf.rid, font_stretch);
+					}
+					if (ftr.has(wgth_tag)) {
+						var[wgth_tag] = font_weight;
+						_font_set_weight(sysf.rid, font_weight);
+					}
+					if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) {
+						var[ital_tag] = 1;
+						_font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC);
+					}
+				}
+
+				_font_set_antialiasing(sysf.rid, key.antialiasing);
+				_font_set_generate_mipmaps(sysf.rid, key.mipmaps);
+				_font_set_multichannel_signed_distance_field(sysf.rid, key.msdf);
+				_font_set_msdf_pixel_range(sysf.rid, key.msdf_range);
+				_font_set_msdf_size(sysf.rid, key.msdf_source_size);
+				_font_set_fixed_size(sysf.rid, key.fixed_size);
+				_font_set_force_autohinter(sysf.rid, key.force_autohinter);
+				_font_set_hinting(sysf.rid, key.hinting);
+				_font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning);
+				_font_set_variation_coordinates(sysf.rid, var);
+				_font_set_oversampling(sysf.rid, key.oversampling);
+				_font_set_embolden(sysf.rid, key.embolden);
+				_font_set_transform(sysf.rid, key.transform);
+				_font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]);
+				_font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]);
+				_font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]);
+				_font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]);
+
+				if (system_fonts.has(key)) {
+					system_fonts[key].var.push_back(sysf);
+				} else {
+					SystemFontCache &sysf_cache = system_fonts[key];
+					sysf_cache.max_var = _font_get_face_count(sysf.rid);
+					sysf_cache.var.push_back(sysf);
+				}
+				f = sysf.rid;
+			}
+			break;
+		}
+	}
+	return f;
+}
+
 void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_line, double p_width, BitField<TextServer::TextOverrunFlag> p_trim_flags) {
 	ShapedTextDataFallback *sd = shaped_owner.get_or_null(p_shaped_line);
 	ERR_FAIL_NULL_MSG(sd, "ShapedTextDataFallback invalid.");
@@ -3643,20 +3819,52 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 
 	int sd_size = sd->glyphs.size();
 	int last_gl_font_size = sd_glyphs[sd_size - 1].font_size;
+	bool found_el_char = false;
 
 	// Find usable fonts, if fonts from the last glyph do not have required chars.
 	RID dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
-	if (!_font_has_char(dot_gl_font_rid, '.')) {
+	if (!_font_has_char(dot_gl_font_rid, sd->el_char)) {
 		const Array &fonts = spans[spans.size() - 1].fonts;
 		for (int i = 0; i < fonts.size(); i++) {
-			if (_font_has_char(fonts[i], '.')) {
+			if (_font_has_char(fonts[i], sd->el_char)) {
 				dot_gl_font_rid = fonts[i];
+				found_el_char = true;
 				break;
 			}
 		}
+		if (!found_el_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) {
+			const char32_t u32str[] = { sd->el_char, 0 };
+			RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, u32str);
+			if (rid.is_valid()) {
+				dot_gl_font_rid = rid;
+				found_el_char = true;
+			}
+		}
+	} else {
+		found_el_char = true;
+	}
+	if (!found_el_char) {
+		bool found_dot_char = false;
+		dot_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
+		if (!_font_has_char(dot_gl_font_rid, '.')) {
+			const Array &fonts = spans[spans.size() - 1].fonts;
+			for (int i = 0; i < fonts.size(); i++) {
+				if (_font_has_char(fonts[i], '.')) {
+					dot_gl_font_rid = fonts[i];
+					found_dot_char = true;
+					break;
+				}
+			}
+			if (!found_dot_char && OS::get_singleton()->has_feature("system_fonts") && fonts.size() > 0 && _font_is_allow_system_fallback(fonts[0])) {
+				RID rid = _find_sys_font_for_text(fonts[0], String(), spans[spans.size() - 1].language, ".");
+				if (rid.is_valid()) {
+					dot_gl_font_rid = rid;
+				}
+			}
+		}
 	}
 	RID whitespace_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
-	if (!_font_has_char(whitespace_gl_font_rid, '.')) {
+	if (!_font_has_char(whitespace_gl_font_rid, ' ')) {
 		const Array &fonts = spans[spans.size() - 1].fonts;
 		for (int i = 0; i < fonts.size(); i++) {
 			if (_font_has_char(fonts[i], ' ')) {
@@ -3666,14 +3874,14 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 		}
 	}
 
-	int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, '.', 0) : -10;
+	int32_t dot_gl_idx = dot_gl_font_rid.is_valid() ? _font_get_glyph_index(dot_gl_font_rid, last_gl_font_size, (found_el_char ? sd->el_char : '.'), 0) : -1;
 	Vector2 dot_adv = dot_gl_font_rid.is_valid() ? _font_get_glyph_advance(dot_gl_font_rid, last_gl_font_size, dot_gl_idx) : Vector2();
-	int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -10;
+	int32_t whitespace_gl_idx = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_index(whitespace_gl_font_rid, last_gl_font_size, ' ', 0) : -1;
 	Vector2 whitespace_adv = whitespace_gl_font_rid.is_valid() ? _font_get_glyph_advance(whitespace_gl_font_rid, last_gl_font_size, whitespace_gl_idx) : Vector2();
 
 	int ellipsis_width = 0;
 	if (add_ellipsis && whitespace_gl_font_rid.is_valid()) {
-		ellipsis_width = 3 * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0);
+		ellipsis_width = (found_el_char ? 1 : 3) * dot_adv.x + sd->extra_spacing[SPACING_GLYPH] + _font_get_spacing(dot_gl_font_rid, SPACING_GLYPH) + (cut_per_word ? whitespace_adv.x : 0);
 	}
 
 	int ell_min_characters = 6;
@@ -3742,7 +3950,7 @@ void TextServerFallback::_shaped_text_overrun_trim_to_width(const RID &p_shaped_
 			if (dot_gl_idx != 0) {
 				Glyph gl;
 				gl.count = 1;
-				gl.repeat = 3;
+				gl.repeat = (found_el_char ? 1 : 3);
 				gl.advance = dot_adv.x;
 				gl.index = dot_gl_idx;
 				gl.font_rid = dot_gl_font_rid;
@@ -3873,161 +4081,7 @@ bool TextServerFallback::_shaped_text_shape(const RID &p_shaped) {
 					RID fdef = span.fonts[0];
 					if (_font_is_allow_system_fallback(fdef)) {
 						String text = sd->text.substr(j, 1);
-						String font_name = _font_get_name(fdef);
-						BitField<FontStyle> font_style = _font_get_style(fdef);
-						int font_weight = _font_get_weight(fdef);
-						int font_stretch = _font_get_stretch(fdef);
-						Dictionary dvar = _font_get_variation_coordinates(fdef);
-						static int64_t wgth_tag = _name_to_tag("weight");
-						static int64_t wdth_tag = _name_to_tag("width");
-						static int64_t ital_tag = _name_to_tag("italic");
-						if (dvar.has(wgth_tag)) {
-							font_weight = dvar[wgth_tag].operator int();
-						}
-						if (dvar.has(wdth_tag)) {
-							font_stretch = dvar[wdth_tag].operator int();
-						}
-						if (dvar.has(ital_tag) && dvar[ital_tag].operator int() == 1) {
-							font_style.set_flag(TextServer::FONT_ITALIC);
-						}
-
-						String locale = (span.language.is_empty()) ? TranslationServer::get_singleton()->get_tool_locale() : span.language;
-
-						PackedStringArray fallback_font_name = OS::get_singleton()->get_system_font_path_for_text(font_name, text, locale, String(), font_weight, font_stretch, font_style & TextServer::FONT_ITALIC);
-#ifdef GDEXTENSION
-						for (int fb = 0; fb < fallback_font_name.size(); fb++) {
-							const String &E = fallback_font_name[fb];
-#else
-						for (const String &E : fallback_font_name) {
-#endif
-							SystemFontKey key = SystemFontKey(E, font_style & TextServer::FONT_ITALIC, font_weight, font_stretch, fdef, this);
-							if (system_fonts.has(key)) {
-								const SystemFontCache &sysf_cache = system_fonts[key];
-								int best_score = 0;
-								int best_match = -1;
-								for (int face_idx = 0; face_idx < sysf_cache.var.size(); face_idx++) {
-									const SystemFontCacheRec &F = sysf_cache.var[face_idx];
-									if (unlikely(!_font_has_char(F.rid, text[0]))) {
-										continue;
-									}
-									BitField<FontStyle> style = _font_get_style(F.rid);
-									int weight = _font_get_weight(F.rid);
-									int stretch = _font_get_stretch(F.rid);
-									int score = (20 - Math::abs(weight - font_weight) / 50);
-									score += (20 - Math::abs(stretch - font_stretch) / 10);
-									if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
-										score += 30;
-									}
-									if (score >= best_score) {
-										best_score = score;
-										best_match = face_idx;
-									}
-									if (best_score == 70) {
-										break;
-									}
-								}
-								if (best_match != -1) {
-									gl.font_rid = sysf_cache.var[best_match].rid;
-								}
-							}
-							if (!gl.font_rid.is_valid()) {
-								if (system_fonts.has(key)) {
-									const SystemFontCache &sysf_cache = system_fonts[key];
-									if (sysf_cache.max_var == sysf_cache.var.size()) {
-										// All subfonts already tested, skip.
-										continue;
-									}
-								}
-
-								if (!system_font_data.has(E)) {
-									system_font_data[E] = FileAccess::get_file_as_bytes(E);
-								}
-
-								const PackedByteArray &font_data = system_font_data[E];
-
-								SystemFontCacheRec sysf;
-								sysf.rid = _create_font();
-								_font_set_data_ptr(sysf.rid, font_data.ptr(), font_data.size());
-
-								Dictionary var = dvar;
-								// Select matching style from collection.
-								int best_score = 0;
-								int best_match = -1;
-								for (int face_idx = 0; face_idx < _font_get_face_count(sysf.rid); face_idx++) {
-									_font_set_face_index(sysf.rid, face_idx);
-									if (unlikely(!_font_has_char(sysf.rid, text[0]))) {
-										continue;
-									}
-									BitField<FontStyle> style = _font_get_style(sysf.rid);
-									int weight = _font_get_weight(sysf.rid);
-									int stretch = _font_get_stretch(sysf.rid);
-									int score = (20 - Math::abs(weight - font_weight) / 50);
-									score += (20 - Math::abs(stretch - font_stretch) / 10);
-									if (bool(style & TextServer::FONT_ITALIC) == bool(font_style & TextServer::FONT_ITALIC)) {
-										score += 30;
-									}
-									if (score >= best_score) {
-										best_score = score;
-										best_match = face_idx;
-									}
-									if (best_score == 70) {
-										break;
-									}
-								}
-								if (best_match == -1) {
-									_free_rid(sysf.rid);
-									continue;
-								} else {
-									_font_set_face_index(sysf.rid, best_match);
-								}
-								sysf.index = best_match;
-
-								// If it's a variable font, apply weight, stretch and italic coordinates to match requested style.
-								if (best_score != 70) {
-									Dictionary ftr = _font_supported_variation_list(sysf.rid);
-									if (ftr.has(wdth_tag)) {
-										var[wdth_tag] = font_stretch;
-										_font_set_stretch(sysf.rid, font_stretch);
-									}
-									if (ftr.has(wgth_tag)) {
-										var[wgth_tag] = font_weight;
-										_font_set_weight(sysf.rid, font_weight);
-									}
-									if ((font_style & TextServer::FONT_ITALIC) && ftr.has(ital_tag)) {
-										var[ital_tag] = 1;
-										_font_set_style(sysf.rid, _font_get_style(sysf.rid) | TextServer::FONT_ITALIC);
-									}
-								}
-
-								_font_set_antialiasing(sysf.rid, key.antialiasing);
-								_font_set_generate_mipmaps(sysf.rid, key.mipmaps);
-								_font_set_multichannel_signed_distance_field(sysf.rid, key.msdf);
-								_font_set_msdf_pixel_range(sysf.rid, key.msdf_range);
-								_font_set_msdf_size(sysf.rid, key.msdf_source_size);
-								_font_set_fixed_size(sysf.rid, key.fixed_size);
-								_font_set_force_autohinter(sysf.rid, key.force_autohinter);
-								_font_set_hinting(sysf.rid, key.hinting);
-								_font_set_subpixel_positioning(sysf.rid, key.subpixel_positioning);
-								_font_set_variation_coordinates(sysf.rid, var);
-								_font_set_oversampling(sysf.rid, key.oversampling);
-								_font_set_embolden(sysf.rid, key.embolden);
-								_font_set_transform(sysf.rid, key.transform);
-								_font_set_spacing(sysf.rid, SPACING_TOP, key.extra_spacing[SPACING_TOP]);
-								_font_set_spacing(sysf.rid, SPACING_BOTTOM, key.extra_spacing[SPACING_BOTTOM]);
-								_font_set_spacing(sysf.rid, SPACING_SPACE, key.extra_spacing[SPACING_SPACE]);
-								_font_set_spacing(sysf.rid, SPACING_GLYPH, key.extra_spacing[SPACING_GLYPH]);
-
-								if (system_fonts.has(key)) {
-									system_fonts[key].var.push_back(sysf);
-								} else {
-									SystemFontCache &sysf_cache = system_fonts[key];
-									sysf_cache.max_var = _font_get_face_count(sysf.rid);
-									sysf_cache.var.push_back(sysf);
-								}
-								gl.font_rid = sysf.rid;
-							}
-							break;
-						}
+						gl.font_rid = _find_sys_font_for_text(fdef, String(), span.language, text);
 					}
 				}
 				prev_font = gl.font_rid;

+ 5 - 0
modules/text_server_fb/text_server_fb.h

@@ -447,6 +447,7 @@ class TextServerFallback : public TextServerExtension {
 		double upos = 0.0;
 		double uthk = 0.0;
 
+		char32_t el_char = 0x2026;
 		TrimData overrun_trim_data;
 		bool fit_width_minimum_reached = false;
 
@@ -555,6 +556,7 @@ class TextServerFallback : public TextServerExtension {
 	mutable HashMap<String, PackedByteArray> system_font_data;
 
 	void _realign(ShapedTextDataFallback *p_sd) const;
+	_FORCE_INLINE_ RID _find_sys_font_for_text(const RID &p_fdef, const String &p_script_code, const String &p_language, const String &p_text);
 
 	Mutex ft_mutex;
 
@@ -766,6 +768,9 @@ public:
 	MODBIND2(shaped_text_set_custom_punctuation, const RID &, const String &);
 	MODBIND1RC(String, shaped_text_get_custom_punctuation, const RID &);
 
+	MODBIND2(shaped_text_set_custom_ellipsis, const RID &, int64_t);
+	MODBIND1RC(int64_t, shaped_text_get_custom_ellipsis, const RID &);
+
 	MODBIND2(shaped_text_set_orientation, const RID &, Orientation);
 	MODBIND1RC(Orientation, shaped_text_get_orientation, const RID &);
 

+ 28 - 0
scene/gui/label.cpp

@@ -240,10 +240,12 @@ void Label::_shape() {
 					if (i < jst_to_line) {
 						TS->shaped_text_fit_to_width(lines_rid[i], width, line_jst_flags);
 					} else if (i == (visible_lines - 1)) {
+						TS->shaped_text_set_custom_ellipsis(lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
 						TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags);
 					}
 				}
 			} else if (lines_hidden) {
+				TS->shaped_text_set_custom_ellipsis(lines_rid[visible_lines - 1], (el_char.length() > 0) ? el_char[0] : 0x2026);
 				TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags);
 			}
 		} else {
@@ -268,9 +270,11 @@ void Label::_shape() {
 				if (i < jst_to_line && horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) {
 					TS->shaped_text_fit_to_width(lines_rid[i], width, line_jst_flags);
 					overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE);
+					TS->shaped_text_set_custom_ellipsis(lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
 					TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags);
 					TS->shaped_text_fit_to_width(lines_rid[i], width, line_jst_flags | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS);
 				} else {
+					TS->shaped_text_set_custom_ellipsis(lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
 					TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags);
 				}
 			}
@@ -887,6 +891,27 @@ TextServer::OverrunBehavior Label::get_text_overrun_behavior() const {
 	return overrun_behavior;
 }
 
+void Label::set_ellipsis_char(const String &p_char) {
+	String c = p_char;
+	if (c.length() > 1) {
+		WARN_PRINT("Ellipsis must be exactly one character long (" + itos(c.length()) + " characters given).");
+		c = c.left(1);
+	}
+	if (el_char == c) {
+		return;
+	}
+	el_char = c;
+	lines_dirty = true;
+	queue_redraw();
+	if (clip || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) {
+		update_minimum_size();
+	}
+}
+
+String Label::get_ellipsis_char() const {
+	return el_char;
+}
+
 String Label::get_text() const {
 	return text;
 }
@@ -1007,6 +1032,8 @@ void Label::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("get_tab_stops"), &Label::get_tab_stops);
 	ClassDB::bind_method(D_METHOD("set_text_overrun_behavior", "overrun_behavior"), &Label::set_text_overrun_behavior);
 	ClassDB::bind_method(D_METHOD("get_text_overrun_behavior"), &Label::get_text_overrun_behavior);
+	ClassDB::bind_method(D_METHOD("set_ellipsis_char", "char"), &Label::set_ellipsis_char);
+	ClassDB::bind_method(D_METHOD("get_ellipsis_char"), &Label::get_ellipsis_char);
 	ClassDB::bind_method(D_METHOD("set_uppercase", "enable"), &Label::set_uppercase);
 	ClassDB::bind_method(D_METHOD("is_uppercase"), &Label::is_uppercase);
 	ClassDB::bind_method(D_METHOD("get_line_height", "line"), &Label::get_line_height, DEFVAL(-1));
@@ -1037,6 +1064,7 @@ void Label::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "clip_text"), "set_clip_text", "is_clipping_text");
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior");
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "ellipsis_char"), "set_ellipsis_char", "get_ellipsis_char");
 	ADD_PROPERTY(PropertyInfo(Variant::BOOL, "uppercase"), "set_uppercase", "is_uppercase");
 	ADD_PROPERTY(PropertyInfo(Variant::PACKED_FLOAT32_ARRAY, "tab_stops"), "set_tab_stops", "get_tab_stops");
 

+ 4 - 0
scene/gui/label.h

@@ -45,6 +45,7 @@ private:
 	TextServer::AutowrapMode autowrap_mode = TextServer::AUTOWRAP_OFF;
 	BitField<TextServer::JustificationFlag> jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;
 	bool clip = false;
+	String el_char = U"…";
 	TextServer::OverrunBehavior overrun_behavior = TextServer::OVERRUN_NO_TRIMMING;
 	Size2 minsize;
 	bool uppercase = false;
@@ -147,6 +148,9 @@ public:
 	void set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior);
 	TextServer::OverrunBehavior get_text_overrun_behavior() const;
 
+	void set_ellipsis_char(const String &p_char);
+	String get_ellipsis_char() const;
+
 	void set_lines_skipped(int p_lines);
 	int get_lines_skipped() const;
 

+ 9 - 12
scene/gui/line_edit.cpp

@@ -1914,12 +1914,15 @@ bool LineEdit::is_secret() const {
 }
 
 void LineEdit::set_secret_character(const String &p_string) {
-	if (secret_character == p_string) {
+	String c = p_string;
+	if (c.length() > 1) {
+		WARN_PRINT("Secret character must be exactly one character long (" + itos(c.length()) + " characters given).");
+		c = c.left(1);
+	}
+	if (secret_character == c) {
 		return;
 	}
-
-	secret_character = p_string;
-	update_configuration_warnings();
+	secret_character = c;
 	_shape();
 	queue_redraw();
 }
@@ -2285,14 +2288,8 @@ void LineEdit::_shape() {
 	if (text.length() == 0 && ime_text.length() == 0) {
 		t = placeholder_translated;
 	} else if (pass) {
-		// TODO: Integrate with text server to add support for non-latin scripts.
-		// Allow secret_character as empty strings, act like if a space was used as a secret character.
-		String secret = " ";
-		// Allow values longer than 1 character in the property, but trim characters after the first one.
-		if (!secret_character.is_empty()) {
-			secret = secret_character.left(1);
-		}
-		t = secret.repeat(text.length() + ime_text.length());
+		String s = (secret_character.length() > 0) ? secret_character.left(1) : U"•";
+		t = s.repeat(text.length() + ime_text.length());
 	} else {
 		if (ime_text.length() > 0) {
 			t = text.substr(0, caret_column) + ime_text + text.substr(caret_column, text.length());

+ 23 - 0
scene/resources/text_line.cpp

@@ -81,6 +81,10 @@ void TextLine::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior");
 
+	ClassDB::bind_method(D_METHOD("set_ellipsis_char", "char"), &TextLine::set_ellipsis_char);
+	ClassDB::bind_method(D_METHOD("get_ellipsis_char"), &TextLine::get_ellipsis_char);
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "ellipsis_char"), "set_ellipsis_char", "get_ellipsis_char");
+
 	ClassDB::bind_method(D_METHOD("get_objects"), &TextLine::get_objects);
 	ClassDB::bind_method(D_METHOD("get_object_rect", "key"), &TextLine::get_object_rect);
 
@@ -137,8 +141,10 @@ void TextLine::_shape() {
 			if (alignment == HORIZONTAL_ALIGNMENT_FILL) {
 				TS->shaped_text_fit_to_width(rid, width, flags);
 				overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE);
+				TS->shaped_text_set_custom_ellipsis(rid, (el_char.length() > 0) ? el_char[0] : 0x2026);
 				TS->shaped_text_overrun_trim_to_width(rid, width, overrun_flags);
 			} else {
+				TS->shaped_text_set_custom_ellipsis(rid, (el_char.length() > 0) ? el_char[0] : 0x2026);
 				TS->shaped_text_overrun_trim_to_width(rid, width, overrun_flags);
 			}
 		} else if (alignment == HORIZONTAL_ALIGNMENT_FILL) {
@@ -306,6 +312,23 @@ TextServer::OverrunBehavior TextLine::get_text_overrun_behavior() const {
 	return overrun_behavior;
 }
 
+void TextLine::set_ellipsis_char(const String &p_char) {
+	String c = p_char;
+	if (c.length() > 1) {
+		WARN_PRINT("Ellipsis must be exactly one character long (" + itos(c.length()) + " characters given).");
+		c = c.left(1);
+	}
+	if (el_char == c) {
+		return;
+	}
+	el_char = c;
+	dirty = true;
+}
+
+String TextLine::get_ellipsis_char() const {
+	return el_char;
+}
+
 void TextLine::set_width(float p_width) {
 	width = p_width;
 	if (alignment == HORIZONTAL_ALIGNMENT_FILL || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) {

+ 4 - 0
scene/resources/text_line.h

@@ -47,6 +47,7 @@ private:
 	float width = -1.0;
 	BitField<TextServer::JustificationFlag> flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA;
 	HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT;
+	String el_char = U"…";
 	TextServer::OverrunBehavior overrun_behavior = TextServer::OVERRUN_TRIM_ELLIPSIS;
 
 	Vector<float> tab_stops;
@@ -90,6 +91,9 @@ public:
 	void set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior);
 	TextServer::OverrunBehavior get_text_overrun_behavior() const;
 
+	void set_ellipsis_char(const String &p_char);
+	String get_ellipsis_char() const;
+
 	void set_width(float p_width);
 	float get_width() const;
 

+ 25 - 0
scene/resources/text_paragraph.cpp

@@ -89,6 +89,10 @@ void TextParagraph::_bind_methods() {
 
 	ADD_PROPERTY(PropertyInfo(Variant::INT, "text_overrun_behavior", PROPERTY_HINT_ENUM, "Trim Nothing,Trim Characters,Trim Words,Ellipsis,Word Ellipsis"), "set_text_overrun_behavior", "get_text_overrun_behavior");
 
+	ClassDB::bind_method(D_METHOD("set_ellipsis_char", "char"), &TextParagraph::set_ellipsis_char);
+	ClassDB::bind_method(D_METHOD("get_ellipsis_char"), &TextParagraph::get_ellipsis_char);
+	ADD_PROPERTY(PropertyInfo(Variant::STRING, "ellipsis_char"), "set_ellipsis_char", "get_ellipsis_char");
+
 	ClassDB::bind_method(D_METHOD("set_width", "width"), &TextParagraph::set_width);
 	ClassDB::bind_method(D_METHOD("get_width"), &TextParagraph::get_width);
 
@@ -252,10 +256,12 @@ void TextParagraph::_shape_lines() {
 					if (i < jst_to_line) {
 						TS->shaped_text_fit_to_width(lines_rid[i], line_w, jst_flags);
 					} else if (i == (visible_lines - 1)) {
+						TS->shaped_text_set_custom_ellipsis(lines_rid[visible_lines - 1], (el_char.length() > 0) ? el_char[0] : 0x2026);
 						TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], line_w, overrun_flags);
 					}
 				}
 			} else if (lines_hidden) {
+				TS->shaped_text_set_custom_ellipsis(lines_rid[visible_lines - 1], (el_char.length() > 0) ? el_char[0] : 0x2026);
 				TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], (visible_lines - 1 <= dropcap_lines) ? (width - h_offset) : width, overrun_flags);
 			}
 		} else {
@@ -281,9 +287,11 @@ void TextParagraph::_shape_lines() {
 				if (i < jst_to_line && alignment == HORIZONTAL_ALIGNMENT_FILL) {
 					TS->shaped_text_fit_to_width(lines_rid[i], line_w, jst_flags);
 					overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE);
+					TS->shaped_text_set_custom_ellipsis(lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
 					TS->shaped_text_overrun_trim_to_width(lines_rid[i], line_w, overrun_flags);
 					TS->shaped_text_fit_to_width(lines_rid[i], line_w, jst_flags | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS);
 				} else {
+					TS->shaped_text_set_custom_ellipsis(lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
 					TS->shaped_text_overrun_trim_to_width(lines_rid[i], line_w, overrun_flags);
 				}
 			}
@@ -501,6 +509,23 @@ TextServer::OverrunBehavior TextParagraph::get_text_overrun_behavior() const {
 	return overrun_behavior;
 }
 
+void TextParagraph::set_ellipsis_char(const String &p_char) {
+	String c = p_char;
+	if (c.length() > 1) {
+		WARN_PRINT("Ellipsis must be exactly one character long (" + itos(c.length()) + " characters given).");
+		c = c.left(1);
+	}
+	if (el_char == c) {
+		return;
+	}
+	el_char = c;
+	lines_dirty = true;
+}
+
+String TextParagraph::get_ellipsis_char() const {
+	return el_char;
+}
+
 void TextParagraph::set_width(float p_width) {
 	_THREAD_SAFE_METHOD_
 

+ 4 - 0
scene/resources/text_paragraph.h

@@ -56,6 +56,7 @@ private:
 
 	BitField<TextServer::LineBreakFlag> brk_flags = TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND;
 	BitField<TextServer::JustificationFlag> jst_flags = TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_SKIP_LAST_LINE | TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE;
+	String el_char = U"…";
 	TextServer::OverrunBehavior overrun_behavior = TextServer::OVERRUN_NO_TRIMMING;
 
 	HorizontalAlignment alignment = HORIZONTAL_ALIGNMENT_LEFT;
@@ -112,6 +113,9 @@ public:
 	void set_text_overrun_behavior(TextServer::OverrunBehavior p_behavior);
 	TextServer::OverrunBehavior get_text_overrun_behavior() const;
 
+	void set_ellipsis_char(const String &p_char);
+	String get_ellipsis_char() const;
+
 	void set_width(float p_width);
 	float get_width() const;
 

+ 13 - 0
servers/text/text_server_extension.cpp

@@ -236,6 +236,9 @@ void TextServerExtension::_bind_methods() {
 	GDVIRTUAL_BIND(_shaped_text_set_custom_punctuation, "shaped", "punct");
 	GDVIRTUAL_BIND(_shaped_text_get_custom_punctuation, "shaped");
 
+	GDVIRTUAL_BIND(_shaped_text_set_custom_ellipsis, "shaped", "char");
+	GDVIRTUAL_BIND(_shaped_text_get_custom_ellipsis, "shaped");
+
 	GDVIRTUAL_BIND(_shaped_text_set_orientation, "shaped", "orientation");
 	GDVIRTUAL_BIND(_shaped_text_get_orientation, "shaped");
 
@@ -1058,6 +1061,16 @@ String TextServerExtension::shaped_text_get_custom_punctuation(const RID &p_shap
 	return ret;
 }
 
+void TextServerExtension::shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) {
+	GDVIRTUAL_CALL(_shaped_text_set_custom_ellipsis, p_shaped, p_char);
+}
+
+int64_t TextServerExtension::shaped_text_get_custom_ellipsis(const RID &p_shaped) const {
+	int64_t ret = 0;
+	GDVIRTUAL_CALL(_shaped_text_get_custom_ellipsis, p_shaped, ret);
+	return ret;
+}
+
 void TextServerExtension::shaped_text_set_preserve_invalid(const RID &p_shaped, bool p_enabled) {
 	GDVIRTUAL_CALL(_shaped_text_set_preserve_invalid, p_shaped, p_enabled);
 }

+ 5 - 0
servers/text/text_server_extension.h

@@ -391,6 +391,11 @@ public:
 	GDVIRTUAL2(_shaped_text_set_custom_punctuation, RID, String);
 	GDVIRTUAL1RC(String, _shaped_text_get_custom_punctuation, RID);
 
+	virtual void shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) override;
+	virtual int64_t shaped_text_get_custom_ellipsis(const RID &p_shaped) const override;
+	GDVIRTUAL2(_shaped_text_set_custom_ellipsis, RID, int64_t);
+	GDVIRTUAL1RC(int64_t, _shaped_text_get_custom_ellipsis, RID);
+
 	virtual void shaped_text_set_orientation(const RID &p_shaped, Orientation p_orientation = ORIENTATION_HORIZONTAL) override;
 	virtual Orientation shaped_text_get_orientation(const RID &p_shaped) const override;
 	GDVIRTUAL2(_shaped_text_set_orientation, RID, Orientation);

+ 3 - 0
servers/text_server.cpp

@@ -390,6 +390,9 @@ void TextServer::_bind_methods() {
 	ClassDB::bind_method(D_METHOD("shaped_text_set_custom_punctuation", "shaped", "punct"), &TextServer::shaped_text_set_custom_punctuation);
 	ClassDB::bind_method(D_METHOD("shaped_text_get_custom_punctuation", "shaped"), &TextServer::shaped_text_get_custom_punctuation);
 
+	ClassDB::bind_method(D_METHOD("shaped_text_set_custom_ellipsis", "shaped", "char"), &TextServer::shaped_text_set_custom_ellipsis);
+	ClassDB::bind_method(D_METHOD("shaped_text_get_custom_ellipsis", "shaped"), &TextServer::shaped_text_get_custom_ellipsis);
+
 	ClassDB::bind_method(D_METHOD("shaped_text_set_orientation", "shaped", "orientation"), &TextServer::shaped_text_set_orientation, DEFVAL(ORIENTATION_HORIZONTAL));
 	ClassDB::bind_method(D_METHOD("shaped_text_get_orientation", "shaped"), &TextServer::shaped_text_get_orientation);
 

+ 3 - 0
servers/text_server.h

@@ -427,6 +427,9 @@ public:
 	virtual void shaped_text_set_custom_punctuation(const RID &p_shaped, const String &p_punct) = 0;
 	virtual String shaped_text_get_custom_punctuation(const RID &p_shaped) const = 0;
 
+	virtual void shaped_text_set_custom_ellipsis(const RID &p_shaped, int64_t p_char) = 0;
+	virtual int64_t shaped_text_get_custom_ellipsis(const RID &p_shaped) const = 0;
+
 	virtual void shaped_text_set_orientation(const RID &p_shaped, Orientation p_orientation = ORIENTATION_HORIZONTAL) = 0;
 	virtual Orientation shaped_text_get_orientation(const RID &p_shaped) const = 0;