Improvements to Label's layout options

- Added options to trim the text in case it overruns
- Added more autowrap modes
- Improved line breaking, which ignores trailing spaces
This commit is contained in:
Hendrik Brucker 2021-07-04 16:43:55 +02:00
parent cb4e42155d
commit 56a8d3f30c
24 changed files with 564 additions and 43 deletions

View file

@ -75,8 +75,8 @@
<member name="align" type="int" setter="set_align" getter="get_align" enum="Label.Align" default="0">
Controls the text's horizontal align. Supports left, center, right, and fill, or justify. Set it to one of the [enum Align] constants.
</member>
<member name="autowrap" type="bool" setter="set_autowrap" getter="has_autowrap" default="false">
If [code]true[/code], wraps the text inside the node's bounding rectangle. If you resize the node, it will change its height automatically to show all the text.
<member name="autowrap_mode" type="int" setter="set_autowrap_mode" getter="get_autowrap_mode" enum="Label.AutowrapMode" default="0">
If set to something other than [constant AUTOWRAP_OFF], the text gets wrapped inside the node's bounding rectangle. If you resize the node, it will change its height automatically to show all the text. To see how each mode behaves, see [enum AutowrapMode].
</member>
<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.
@ -107,6 +107,9 @@
<member name="text_direction" type="int" setter="set_text_direction" getter="get_text_direction" enum="Control.TextDirection" default="0">
Base text writing direction.
</member>
<member name="text_overrun_behavior" type="int" setter="set_text_overrun_behavior" getter="get_text_overrun_behavior" enum="Label.OverrunBehavior" default="0">
Sets the clipping behavior when the text exceeds the node's bounding rectangle. See [enum OverrunBehavior] for a description of all modes.
</member>
<member name="uppercase" type="bool" setter="set_uppercase" getter="is_uppercase" default="false">
If [code]true[/code], all the text displays as UPPERCASE.
</member>
@ -142,6 +145,33 @@
<constant name="VALIGN_FILL" value="3" enum="VAlign">
Align the whole text by spreading the rows.
</constant>
<constant name="AUTOWRAP_OFF" value="0" enum="AutowrapMode">
Autowrap is disabled.
</constant>
<constant name="AUTOWRAP_ARBITRARY" value="1" enum="AutowrapMode">
Wraps the text inside the node's bounding rectangle by allowing to break lines at arbitrary positions, which is useful when very limited space is available.
</constant>
<constant name="AUTOWRAP_WORD" value="2" enum="AutowrapMode">
Wraps the text inside the node's bounding rectangle by soft-breaking between words.
</constant>
<constant name="AUTOWRAP_WORD_SMART" value="3" enum="AutowrapMode">
Behaves similarly to [constant AUTOWRAP_WORD], but force-breaks a word if that single word does not fit in one line.
</constant>
<constant name="OVERRUN_NO_TRIMMING" value="0" enum="OverrunBehavior">
No text trimming is performed.
</constant>
<constant name="OVERRUN_TRIM_CHAR" value="1" enum="OverrunBehavior">
Trims the text per character.
</constant>
<constant name="OVERRUN_TRIM_WORD" value="2" enum="OverrunBehavior">
Trims the text per word.
</constant>
<constant name="OVERRUN_TRIM_ELLIPSIS" value="3" enum="OverrunBehavior">
Trims the text per character and adds an ellipsis to indicate that parts are hidden.
</constant>
<constant name="OVERRUN_TRIM_WORD_ELLIPSIS" value="4" enum="OverrunBehavior">
Trims the text per word and adds an ellipsis to indicate that parts are hidden.
</constant>
</constants>
<theme_items>
<theme_item name="font" type="Font">

View file

@ -1088,6 +1088,19 @@
Returns composite character end position closest to the [code]pos[/code].
</description>
</method>
<method name="shaped_text_overrun_trim_to_width">
<return type="void">
</return>
<argument index="0" name="shaped" type="RID">
</argument>
<argument index="1" name="width" type="float" default="0">
</argument>
<argument index="2" name="overrun_trim_flags" type="int" default="0">
</argument>
<description>
Trims text if it exceeds the given width.
</description>
</method>
<method name="shaped_text_prev_grapheme_pos">
<return type="int">
</return>
@ -1260,6 +1273,21 @@
<constant name="BREAK_GRAPHEME_BOUND" value="64" enum="LineBreakFlag">
Break the line between any unconnected graphemes.
</constant>
<constant name="OVERRUN_NO_TRIMMING" value="0" enum="TextOverrunFlag">
No trimming is performed.
</constant>
<constant name="OVERRUN_TRIM" value="1" enum="TextOverrunFlag">
Trims the text when it exceeds the given width.
</constant>
<constant name="OVERRUN_TRIM_WORD_ONLY" value="2" enum="TextOverrunFlag">
Trims the text per word instead of per grapheme.
</constant>
<constant name="OVERRUN_ADD_ELLIPSIS" value="4" enum="TextOverrunFlag">
Determines whether an ellipsis should be added at the end of the text.
</constant>
<constant name="OVERRUN_ENFORCE_ELLIPSIS" value="8" enum="TextOverrunFlag">
Determines whether the ellipsis at the end of the text is enforced and may not be hidden.
</constant>
<constant name="GRAPHEME_IS_RTL" value="2" enum="GraphemeFlag">
Grapheme is part of right-to-left or bottom-to-top run.
</constant>

View file

@ -5784,7 +5784,7 @@ AnimationTrackEditor::AnimationTrackEditor() {
info_message->set_text(TTR("Select an AnimationPlayer node to create and edit animations."));
info_message->set_valign(Label::VALIGN_CENTER);
info_message->set_align(Label::ALIGN_CENTER);
info_message->set_autowrap(true);
info_message->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
info_message->set_anchors_and_offsets_preset(PRESET_WIDE, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);
main_panel->add_child(info_message);

View file

@ -380,7 +380,7 @@ EditorPerformanceProfiler::EditorPerformanceProfiler() {
info_message->set_text(TTR("Pick one or more items from the list to display the graph."));
info_message->set_valign(Label::VALIGN_CENTER);
info_message->set_align(Label::ALIGN_CENTER);
info_message->set_autowrap(true);
info_message->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
info_message->set_anchors_and_offsets_preset(PRESET_WIDE, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);
monitor_draw->add_child(info_message);

View file

@ -1535,7 +1535,7 @@ ScriptEditorDebugger::ScriptEditorDebugger(EditorNode *p_editor) {
reason->set_text("");
hbc->add_child(reason);
reason->set_h_size_flags(SIZE_EXPAND_FILL);
reason->set_autowrap(true);
reason->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
reason->set_max_lines_visible(3);
reason->set_mouse_filter(Control::MOUSE_FILTER_PASS);

View file

@ -212,7 +212,7 @@ EditorAbout::EditorAbout() {
Label *tpl_label = memnew(Label);
tpl_label->set_h_size_flags(Control::SIZE_EXPAND_FILL);
tpl_label->set_autowrap(true);
tpl_label->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
tpl_label->set_text(TTR("Godot Engine relies on a number of third-party free and open source libraries, all compatible with the terms of its MIT license. The following is an exhaustive list of all such third-party components with their respective copyright statements and license terms."));
tpl_label->set_size(Size2(630, 1) * EDSCALE);
license_thirdparty->add_child(tpl_label);

View file

@ -531,7 +531,7 @@ GroupDialog::GroupDialog() {
group_empty->set_text(TTR("Empty groups will be automatically removed."));
group_empty->set_valign(Label::VALIGN_CENTER);
group_empty->set_align(Label::ALIGN_CENTER);
group_empty->set_autowrap(true);
group_empty->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
group_empty->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
nodes_to_remove->add_child(group_empty);
group_empty->set_anchors_and_offsets_preset(Control::PRESET_WIDE, Control::PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);

View file

@ -130,6 +130,6 @@ NodeDock::NodeDock() {
select_a_node->set_v_size_flags(SIZE_EXPAND_FILL);
select_a_node->set_valign(Label::VALIGN_CENTER);
select_a_node->set_align(Label::ALIGN_CENTER);
select_a_node->set_autowrap(true);
select_a_node->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
add_child(select_a_node);
}

View file

@ -1141,7 +1141,7 @@ ThemeItemImportTree::ThemeItemImportTree() {
select_icons_warning = memnew(Label);
select_icons_warning->set_text(TTR("Caution: Adding icon data may considerably increase the size of your Theme resource."));
select_icons_warning->set_autowrap(true);
select_icons_warning->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
select_icons_warning->set_h_size_flags(Control::SIZE_EXPAND_FILL);
select_icons_warning_hb->add_child(select_icons_warning);
}

View file

@ -3855,7 +3855,7 @@ VisualShaderEditor::VisualShaderEditor() {
error_label = memnew(Label);
error_panel->add_child(error_label);
error_label->set_autowrap(true);
error_label->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
///////////////////////////////////////
// POPUP MENU
@ -3941,7 +3941,7 @@ VisualShaderEditor::VisualShaderEditor() {
add_child(members_dialog);
alert = memnew(AcceptDialog);
alert->get_label()->set_autowrap(true);
alert->get_label()->set_autowrap_mode(Label::AUTOWRAP_WORD);
alert->get_label()->set_align(Label::ALIGN_CENTER);
alert->get_label()->set_valign(Label::VALIGN_CENTER);
alert->get_label()->set_custom_minimum_size(Size2(400, 60) * EDSCALE);

View file

@ -771,7 +771,7 @@ ScriptCreateDialog::ScriptCreateDialog() {
builtin_warning_label->set_text(
TTR("Note: Built-in scripts have some limitations and can't be edited using an external editor."));
vb->add_child(builtin_warning_label);
builtin_warning_label->set_autowrap(true);
builtin_warning_label->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
builtin_warning_label->hide();
script_name_warning_label = memnew(Label);
@ -779,7 +779,7 @@ ScriptCreateDialog::ScriptCreateDialog() {
TTR("Warning: Having the script name be the same as a built-in type is usually not desired."));
vb->add_child(script_name_warning_label);
script_name_warning_label->add_theme_color_override("font_color", Color(1, 0.85, 0.4));
script_name_warning_label->set_autowrap(true);
script_name_warning_label->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
script_name_warning_label->hide();
status_panel = memnew(PanelContainer);
@ -892,7 +892,7 @@ ScriptCreateDialog::ScriptCreateDialog() {
add_child(file_browse);
get_ok_button()->set_text(TTR("Create"));
alert = memnew(AcceptDialog);
alert->get_label()->set_autowrap(true);
alert->get_label()->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
alert->get_label()->set_align(Label::ALIGN_CENTER);
alert->get_label()->set_valign(Label::VALIGN_CENTER);
alert->get_label()->set_custom_minimum_size(Size2(325, 60) * EDSCALE);

View file

@ -143,6 +143,7 @@ typedef struct {
bool (*shaped_text_shape)(void *, godot_rid *);
bool (*shaped_text_update_breaks)(void *, godot_rid *);
bool (*shaped_text_update_justification_ops)(void *, godot_rid *);
void (*shaped_text_overrun_trim_to_width)(void *, godot_rid *, float, uint8_t);
bool (*shaped_text_is_ready)(void *, godot_rid *);
godot_packed_glyph_array (*shaped_text_get_glyphs)(void *, godot_rid *);
godot_vector2i (*shaped_text_get_range)(void *, godot_rid *);

View file

@ -498,6 +498,11 @@ bool TextServerGDNative::shaped_text_update_justification_ops(RID p_shaped) {
return interface->shaped_text_update_justification_ops(data, (godot_rid *)&p_shaped);
}
void TextServerGDNative::shaped_text_overrun_trim_to_width(RID p_shaped_line, float p_width, uint8_t p_clip_flags) {
ERR_FAIL_COND(interface == nullptr);
interface->shaped_text_overrun_trim_to_width(data, (godot_rid *)&p_shaped_line, p_width, p_clip_flags);
};
bool TextServerGDNative::shaped_text_is_ready(RID p_shaped) const {
ERR_FAIL_COND_V(interface == nullptr, false);
return interface->shaped_text_is_ready(data, (godot_rid *)&p_shaped);

View file

@ -167,6 +167,8 @@ public:
virtual bool shaped_text_update_breaks(RID p_shaped) override;
virtual bool shaped_text_update_justification_ops(RID p_shaped) override;
virtual void shaped_text_overrun_trim_to_width(RID p_shaped, float p_width, uint8_t p_clip_flags) override;
virtual bool shaped_text_is_ready(RID p_shaped) const override;
virtual Vector<Glyph> shaped_text_get_glyphs(RID p_shaped) const override;

View file

@ -1271,7 +1271,7 @@ GridMapEditor::GridMapEditor(EditorNode *p_editor) {
info_message->set_text(TTR("Give a MeshLibrary resource to this GridMap to use its meshes."));
info_message->set_valign(Label::VALIGN_CENTER);
info_message->set_align(Label::ALIGN_CENTER);
info_message->set_autowrap(true);
info_message->set_autowrap_mode(Label::AUTOWRAP_WORD_SMART);
info_message->set_custom_minimum_size(Size2(100 * EDSCALE, 0));
info_message->set_anchors_and_offsets_preset(PRESET_WIDE, PRESET_MODE_KEEP_SIZE, 8 * EDSCALE);
mesh_library_palette->add_child(info_message);

View file

@ -1658,6 +1658,161 @@ float TextServerAdvanced::shaped_text_tab_align(RID p_shaped, const Vector<float
return 0.f;
}
void TextServerAdvanced::shaped_text_overrun_trim_to_width(RID p_shaped_line, float p_width, uint8_t p_clip_flags) {
_THREAD_SAFE_METHOD_
ShapedTextDataAdvanced *sd = shaped_owner.getornull(p_shaped_line);
ERR_FAIL_COND_MSG(!sd, "ShapedTextDataAdvanced invalid.");
if (!sd->valid) {
shaped_text_shape(p_shaped_line);
}
bool add_ellipsis = (p_clip_flags & OVERRUN_ADD_ELLIPSIS) == OVERRUN_ADD_ELLIPSIS;
bool cut_per_word = (p_clip_flags & OVERRUN_TRIM_WORD_ONLY) == OVERRUN_TRIM_WORD_ONLY;
bool enforce_ellipsis = (p_clip_flags & OVERRUN_ENFORCE_ELLIPSIS) == OVERRUN_ENFORCE_ELLIPSIS;
Glyph *sd_glyphs = sd->glyphs.ptrw();
if ((p_clip_flags & OVERRUN_TRIM) == OVERRUN_NO_TRIMMING || sd_glyphs == nullptr || p_width <= 0 || !(sd->width > p_width || enforce_ellipsis)) {
return;
}
int sd_size = sd->glyphs.size();
RID last_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
int last_gl_font_size = sd_glyphs[sd_size - 1].font_size;
uint32_t dot_gl_idx = font_get_glyph_index(last_gl_font_rid, '.');
Vector2 dot_adv = font_get_glyph_advance(last_gl_font_rid, dot_gl_idx, last_gl_font_size);
uint32_t whitespace_gl_idx = font_get_glyph_index(last_gl_font_rid, ' ');
Vector2 whitespace_adv = font_get_glyph_advance(last_gl_font_rid, whitespace_gl_idx, last_gl_font_size);
int ellipsis_advance = 0;
if (add_ellipsis) {
ellipsis_advance = 3 * dot_adv.x + font_get_spacing_glyph(last_gl_font_rid) + (cut_per_word ? whitespace_adv.x : 0);
}
int ell_min_characters = 6;
float width = sd->width;
bool is_rtl = sd->direction == DIRECTION_RTL || (sd->direction == DIRECTION_AUTO && sd->para_direction == DIRECTION_RTL);
int trim_pos = (is_rtl) ? sd_size : 0;
int ellipsis_pos = (enforce_ellipsis) ? 0 : -1;
int last_valid_cut = 0;
bool found = false;
int glyphs_from = (is_rtl) ? 0 : sd_size - 1;
int glyphs_to = (is_rtl) ? sd_size - 1 : -1;
int glyphs_delta = (is_rtl) ? +1 : -1;
for (int i = glyphs_from; i != glyphs_to; i += glyphs_delta) {
if (!is_rtl) {
width -= sd_glyphs[i].advance;
}
if (sd_glyphs[i].count > 0) {
bool above_min_char_treshold = ((is_rtl) ? sd_size - 1 - i : i) >= ell_min_characters;
if (width + (((above_min_char_treshold && add_ellipsis) || enforce_ellipsis) ? ellipsis_advance : 0) <= p_width) {
if (cut_per_word && above_min_char_treshold) {
if ((sd_glyphs[i].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT) {
last_valid_cut = i;
found = true;
}
} else {
last_valid_cut = i;
found = true;
}
if (found) {
trim_pos = last_valid_cut;
if (above_min_char_treshold && width - ellipsis_advance <= p_width) {
ellipsis_pos = trim_pos;
}
break;
}
}
}
if (is_rtl) {
width -= sd_glyphs[i].advance;
}
}
if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) {
int added_glyphs = 0;
if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) {
// Insert an additional space when cutting word bound for aesthetics.
if (cut_per_word && (ellipsis_pos > 0)) {
TextServer::Glyph gl;
gl.start = sd_glyphs[ellipsis_pos].start;
gl.end = sd_glyphs[ellipsis_pos].end;
gl.count = 1;
gl.advance = whitespace_adv.x;
gl.index = whitespace_gl_idx;
gl.font_rid = last_gl_font_rid;
gl.font_size = last_gl_font_size;
gl.flags = GRAPHEME_IS_SPACE | GRAPHEME_IS_BREAK_SOFT | GRAPHEME_IS_VIRTUAL | (is_rtl ? GRAPHEME_IS_RTL : 0);
// Optimized glyph insertion by replacing a glyph whenever possible.
int glyph_idx = trim_pos + ((is_rtl) ? (-added_glyphs - 1) : added_glyphs);
if (is_rtl) {
if (glyph_idx < 0) {
sd->glyphs.insert(0, gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
} else {
if (glyph_idx > (sd_size - 1)) {
sd->glyphs.append(gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
}
added_glyphs++;
}
// Add ellipsis dots.
for (int d = 0; d < 3; d++) {
TextServer::Glyph gl;
gl.start = sd_glyphs[ellipsis_pos].start;
gl.end = sd_glyphs[ellipsis_pos].end;
gl.count = 1;
gl.advance = dot_adv.x;
gl.index = dot_gl_idx;
gl.font_rid = last_gl_font_rid;
gl.font_size = last_gl_font_size;
gl.flags = GRAPHEME_IS_PUNCTUATION | GRAPHEME_IS_VIRTUAL | (is_rtl ? GRAPHEME_IS_RTL : 0);
// Optimized glyph insertion by replacing a glyph whenever possible.
int glyph_idx = trim_pos + ((is_rtl) ? (-added_glyphs - 1) : added_glyphs);
if (is_rtl) {
if (glyph_idx < 0) {
sd->glyphs.insert(0, gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
} else {
if (glyph_idx > (sd_size - 1)) {
sd->glyphs.append(gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
}
added_glyphs++;
}
}
// Cut the remaining glyphs off.
if (!is_rtl) {
sd->glyphs.resize(trim_pos + added_glyphs);
} else {
if (trim_pos - added_glyphs >= 0) {
sd->glyphs = sd->glyphs.subarray(trim_pos - added_glyphs, sd->glyphs.size() - 1);
}
}
// Update to correct width.
sd->width = width + ((ellipsis_pos != -1) ? ellipsis_advance : 0);
}
}
bool TextServerAdvanced::shaped_text_update_breaks(RID p_shaped) {
_THREAD_SAFE_METHOD_
ShapedTextDataAdvanced *sd = shaped_owner.getornull(p_shaped);

View file

@ -229,6 +229,8 @@ public:
virtual bool shaped_text_update_breaks(RID p_shaped) override;
virtual bool shaped_text_update_justification_ops(RID p_shaped) override;
virtual void shaped_text_overrun_trim_to_width(RID p_shaped, float p_width, uint8_t p_clip_flags) override;
virtual bool shaped_text_is_ready(RID p_shaped) const override;
virtual Vector<Glyph> shaped_text_get_glyphs(RID p_shaped) const override;

View file

@ -1141,6 +1141,161 @@ bool TextServerFallback::shaped_text_update_justification_ops(RID p_shaped) {
return true;
}
void TextServerFallback::shaped_text_overrun_trim_to_width(RID p_shaped_line, float p_width, uint8_t p_clip_flags) {
_THREAD_SAFE_METHOD_
ShapedTextData *sd = shaped_owner.getornull(p_shaped_line);
ERR_FAIL_COND_MSG(!sd, "ShapedTextDataAdvanced invalid.");
if (!sd->valid) {
shaped_text_shape(p_shaped_line);
}
bool add_ellipsis = (p_clip_flags & OVERRUN_ADD_ELLIPSIS) == OVERRUN_ADD_ELLIPSIS;
bool cut_per_word = (p_clip_flags & OVERRUN_TRIM_WORD_ONLY) == OVERRUN_TRIM_WORD_ONLY;
bool enforce_ellipsis = (p_clip_flags & OVERRUN_ENFORCE_ELLIPSIS) == OVERRUN_ENFORCE_ELLIPSIS;
Glyph *sd_glyphs = sd->glyphs.ptrw();
if ((p_clip_flags & OVERRUN_TRIM) == OVERRUN_NO_TRIMMING || sd_glyphs == nullptr || p_width <= 0 || !(sd->width > p_width || enforce_ellipsis)) {
return;
}
int sd_size = sd->glyphs.size();
RID last_gl_font_rid = sd_glyphs[sd_size - 1].font_rid;
int last_gl_font_size = sd_glyphs[sd_size - 1].font_size;
uint32_t dot_gl_idx = font_get_glyph_index(last_gl_font_rid, '.');
Vector2 dot_adv = font_get_glyph_advance(last_gl_font_rid, dot_gl_idx, last_gl_font_size);
uint32_t whitespace_gl_idx = font_get_glyph_index(last_gl_font_rid, ' ');
Vector2 whitespace_adv = font_get_glyph_advance(last_gl_font_rid, whitespace_gl_idx, last_gl_font_size);
int ellipsis_advance = 0;
if (add_ellipsis) {
ellipsis_advance = 3 * dot_adv.x + font_get_spacing_glyph(last_gl_font_rid) + (cut_per_word ? whitespace_adv.x : 0);
}
int ell_min_characters = 6;
float width = sd->width;
bool is_rtl = sd->direction == DIRECTION_RTL || (sd->direction == DIRECTION_AUTO && sd->para_direction == DIRECTION_RTL);
int trim_pos = (is_rtl) ? sd_size : 0;
int ellipsis_pos = (enforce_ellipsis) ? 0 : -1;
int last_valid_cut = 0;
bool found = false;
int glyphs_from = (is_rtl) ? 0 : sd_size - 1;
int glyphs_to = (is_rtl) ? sd_size - 1 : -1;
int glyphs_delta = (is_rtl) ? +1 : -1;
for (int i = glyphs_from; i != glyphs_to; i += glyphs_delta) {
if (!is_rtl) {
width -= sd_glyphs[i].advance;
}
if (sd_glyphs[i].count > 0) {
bool above_min_char_treshold = ((is_rtl) ? sd_size - 1 - i : i) >= ell_min_characters;
if (width + (((above_min_char_treshold && add_ellipsis) || enforce_ellipsis) ? ellipsis_advance : 0) <= p_width) {
if (cut_per_word && above_min_char_treshold) {
if ((sd_glyphs[i].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT) {
last_valid_cut = i;
found = true;
}
} else {
last_valid_cut = i;
found = true;
}
if (found) {
trim_pos = last_valid_cut;
if (above_min_char_treshold && width - ellipsis_advance <= p_width) {
ellipsis_pos = trim_pos;
}
break;
}
}
}
if (is_rtl) {
width -= sd_glyphs[i].advance;
}
}
if ((trim_pos >= 0 && sd->width > p_width) || enforce_ellipsis) {
int added_glyphs = 0;
if (add_ellipsis && (ellipsis_pos > 0 || enforce_ellipsis)) {
// Insert an additional space when cutting word bound for aesthetics.
if (cut_per_word && (ellipsis_pos > 0)) {
TextServer::Glyph gl;
gl.start = sd_glyphs[ellipsis_pos].start;
gl.end = sd_glyphs[ellipsis_pos].end;
gl.count = 1;
gl.advance = whitespace_adv.x;
gl.index = whitespace_gl_idx;
gl.font_rid = last_gl_font_rid;
gl.font_size = last_gl_font_size;
gl.flags = GRAPHEME_IS_SPACE | GRAPHEME_IS_BREAK_SOFT | GRAPHEME_IS_VIRTUAL | (is_rtl ? GRAPHEME_IS_RTL : 0);
// Optimized glyph insertion by replacing a glyph whenever possible.
int glyph_idx = trim_pos + ((is_rtl) ? -added_glyphs : added_glyphs);
if (is_rtl) {
if (glyph_idx < 0) {
sd->glyphs.insert(0, gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
} else {
if (glyph_idx > (sd_size - 1)) {
sd->glyphs.append(gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
}
added_glyphs++;
}
// Add ellipsis dots.
for (int d = 0; d < 3; d++) {
TextServer::Glyph gl;
gl.start = sd_glyphs[ellipsis_pos].start;
gl.end = sd_glyphs[ellipsis_pos].end;
gl.count = 1;
gl.advance = dot_adv.x;
gl.index = dot_gl_idx;
gl.font_rid = last_gl_font_rid;
gl.font_size = last_gl_font_size;
gl.flags = GRAPHEME_IS_PUNCTUATION | GRAPHEME_IS_VIRTUAL | (is_rtl ? GRAPHEME_IS_RTL : 0);
// Optimized glyph insertion by replacing a glyph whenever possible.
int glyph_idx = trim_pos + ((is_rtl) ? -added_glyphs : added_glyphs);
if (is_rtl) {
if (glyph_idx < 0) {
sd->glyphs.insert(0, gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
} else {
if (glyph_idx > (sd_size - 1)) {
sd->glyphs.append(gl);
} else {
sd->glyphs.set(glyph_idx, gl);
}
}
added_glyphs++;
}
}
// Cut the remaining glyphs off.
if (!is_rtl) {
sd->glyphs.resize(trim_pos + added_glyphs);
} else {
for (int ridx = 0; ridx <= trim_pos - added_glyphs; ridx++) {
sd->glyphs.remove(0);
}
}
// Update to correct width.
sd->width = width + ((ellipsis_pos != -1) ? ellipsis_advance : 0);
}
}
bool TextServerFallback::shaped_text_shape(RID p_shaped) {
_THREAD_SAFE_METHOD_
ShapedTextData *sd = shaped_owner.getornull(p_shaped);

View file

@ -178,6 +178,8 @@ public:
virtual bool shaped_text_update_breaks(RID p_shaped) override;
virtual bool shaped_text_update_justification_ops(RID p_shaped) override;
virtual void shaped_text_overrun_trim_to_width(RID p_shaped, float p_width, uint8_t p_clip_flags) override;
virtual bool shaped_text_is_ready(RID p_shaped) const override;
virtual Vector<Glyph> shaped_text_get_glyphs(RID p_shaped) const override;

View file

@ -148,11 +148,11 @@ bool AcceptDialog::get_hide_on_ok() const {
}
void AcceptDialog::set_autowrap(bool p_autowrap) {
label->set_autowrap(p_autowrap);
label->set_autowrap_mode(p_autowrap ? Label::AUTOWRAP_WORD : Label::AUTOWRAP_OFF);
}
bool AcceptDialog::has_autowrap() {
return label->has_autowrap();
return label->get_autowrap_mode() != Label::AUTOWRAP_OFF;
}
void AcceptDialog::register_text_enter(Control *p_line_edit) {

View file

@ -36,20 +36,20 @@
#include "servers/text_server.h"
void Label::set_autowrap(bool p_autowrap) {
if (autowrap != p_autowrap) {
autowrap = p_autowrap;
void Label::set_autowrap_mode(Label::AutowrapMode p_mode) {
if (autowrap_mode != p_mode) {
autowrap_mode = p_mode;
lines_dirty = true;
}
update();
if (clip) {
if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) {
minimum_size_changed();
}
}
bool Label::has_autowrap() const {
return autowrap;
Label::AutowrapMode Label::get_autowrap_mode() const {
return autowrap_mode;
}
void Label::set_uppercase(bool p_uppercase) {
@ -94,24 +94,76 @@ void Label::_shape() {
dirty = false;
lines_dirty = true;
}
uint8_t overrun_flags = TextServer::OVERRUN_NO_TRIMMING;
if (lines_dirty) {
for (int i = 0; i < lines_rid.size(); i++) {
TS->free(lines_rid[i]);
}
lines_rid.clear();
Vector<Vector2i> lines = TS->shaped_text_get_line_breaks(text_rid, width, 0, (autowrap) ? (TextServer::BREAK_MANDATORY | TextServer::BREAK_WORD_BOUND) : TextServer::BREAK_MANDATORY);
uint8_t autowrap_flags = TextServer::BREAK_MANDATORY;
switch (autowrap_mode) {
case AUTOWRAP_WORD_SMART:
autowrap_flags = TextServer::BREAK_WORD_BOUND_ADAPTIVE | TextServer::BREAK_MANDATORY;
break;
case AUTOWRAP_WORD:
autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY;
break;
case AUTOWRAP_ARBITRARY:
autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY;
break;
case AUTOWRAP_OFF:
break;
}
Vector<Vector2i> lines = TS->shaped_text_get_line_breaks(text_rid, width, 0, autowrap_flags);
for (int i = 0; i < lines.size(); i++) {
RID line = TS->shaped_text_substr(text_rid, lines[i].x, lines[i].y - lines[i].x);
switch (overrun_behavior) {
case OVERRUN_TRIM_WORD_ELLIPSIS:
overrun_flags |= TextServer::OVERRUN_TRIM;
overrun_flags |= TextServer::OVERRUN_TRIM_WORD_ONLY;
overrun_flags |= TextServer::OVERRUN_ADD_ELLIPSIS;
break;
case OVERRUN_TRIM_ELLIPSIS:
overrun_flags |= TextServer::OVERRUN_TRIM;
overrun_flags |= TextServer::OVERRUN_ADD_ELLIPSIS;
break;
case OVERRUN_TRIM_WORD:
overrun_flags |= TextServer::OVERRUN_TRIM;
overrun_flags |= TextServer::OVERRUN_TRIM_WORD_ONLY;
break;
case OVERRUN_TRIM_CHAR:
overrun_flags |= TextServer::OVERRUN_TRIM;
break;
case OVERRUN_NO_TRIMMING:
break;
}
if (autowrap_mode == AUTOWRAP_OFF && align != ALIGN_FILL && overrun_behavior != OVERRUN_NO_TRIMMING) {
TS->shaped_text_overrun_trim_to_width(line, width, overrun_flags);
}
lines_rid.push_back(line);
}
if (autowrap_mode != AUTOWRAP_OFF && overrun_behavior != OVERRUN_NO_TRIMMING) {
int visible_lines = get_visible_line_count();
if (visible_lines < lines_rid.size() && visible_lines > 0) {
overrun_flags |= TextServer::OVERRUN_ENFORCE_ELLIPSIS;
TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], width, overrun_flags);
}
}
}
if (xl_text.length() == 0) {
minsize = Size2(1, get_line_height());
return;
}
if (!autowrap) {
if (autowrap_mode == AUTOWRAP_OFF) {
minsize.width = 0.0f;
for (int i = 0; i < lines_rid.size(); i++) {
if (minsize.width < TS->shaped_text_get_size(lines_rid[i]).x) {
@ -120,10 +172,21 @@ void Label::_shape() {
}
}
if (lines_dirty) { // Fill after min_size calculation.
if (lines_dirty) {
// Fill after min_size calculation.
if (align == ALIGN_FILL) {
for (int i = 0; i < lines_rid.size(); i++) {
TS->shaped_text_fit_to_width(lines_rid.write[i], width);
if (overrun_behavior != OVERRUN_NO_TRIMMING && autowrap_mode == AUTOWRAP_OFF) {
float line_unaltered_width = TS->shaped_text_get_width(lines_rid[i]);
TS->shaped_text_fit_to_width(lines_rid[i], width);
float new_line_width = TS->shaped_text_get_width(lines_rid[i]);
// Begin trimming when there is no space between words available anymore.
if (new_line_width < line_unaltered_width) {
TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags);
}
} else {
TS->shaped_text_fit_to_width(lines_rid[i], width);
}
}
}
lines_dirty = false;
@ -131,7 +194,7 @@ void Label::_shape() {
_update_visible();
if (!autowrap || !clip) {
if (autowrap_mode == AUTOWRAP_OFF || !clip || overrun_behavior == OVERRUN_NO_TRIMMING) {
minimum_size_changed();
}
}
@ -370,13 +433,12 @@ Size2 Label::get_minimum_size() const {
min_size.height = MAX(min_size.height, font->get_height(get_theme_font_size("font_size")) + font->get_spacing(Font::SPACING_TOP) + font->get_spacing(Font::SPACING_BOTTOM));
Size2 min_style = get_theme_stylebox("normal")->get_minimum_size();
if (autowrap) {
return Size2(1, clip ? 1 : min_size.height) + min_style;
if (autowrap_mode != AUTOWRAP_OFF) {
return Size2(1, (clip || overrun_behavior != OVERRUN_NO_TRIMMING) ? 1 : min_size.height) + min_style;
} else {
if (clip) {
if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) {
min_size.width = 1;
}
return min_size + min_style;
}
}
@ -536,6 +598,21 @@ bool Label::is_clipping_text() const {
return clip;
}
void Label::set_text_overrun_behavior(Label::OverrunBehavior p_behavior) {
if (overrun_behavior != p_behavior) {
overrun_behavior = p_behavior;
lines_dirty = true;
}
update();
if (clip || overrun_behavior != OVERRUN_NO_TRIMMING) {
minimum_size_changed();
}
}
Label::OverrunBehavior Label::get_text_overrun_behavior() const {
return overrun_behavior;
}
String Label::get_text() const {
return text;
}
@ -663,10 +740,12 @@ void Label::_bind_methods() {
ClassDB::bind_method(D_METHOD("clear_opentype_features"), &Label::clear_opentype_features);
ClassDB::bind_method(D_METHOD("set_language", "language"), &Label::set_language);
ClassDB::bind_method(D_METHOD("get_language"), &Label::get_language);
ClassDB::bind_method(D_METHOD("set_autowrap", "enable"), &Label::set_autowrap);
ClassDB::bind_method(D_METHOD("has_autowrap"), &Label::has_autowrap);
ClassDB::bind_method(D_METHOD("set_autowrap_mode", "autowrap_mode"), &Label::set_autowrap_mode);
ClassDB::bind_method(D_METHOD("get_autowrap_mode"), &Label::get_autowrap_mode);
ClassDB::bind_method(D_METHOD("set_clip_text", "enable"), &Label::set_clip_text);
ClassDB::bind_method(D_METHOD("is_clipping_text"), &Label::is_clipping_text);
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_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));
@ -696,13 +775,25 @@ void Label::_bind_methods() {
BIND_ENUM_CONSTANT(VALIGN_BOTTOM);
BIND_ENUM_CONSTANT(VALIGN_FILL);
BIND_ENUM_CONSTANT(AUTOWRAP_OFF);
BIND_ENUM_CONSTANT(AUTOWRAP_ARBITRARY);
BIND_ENUM_CONSTANT(AUTOWRAP_WORD);
BIND_ENUM_CONSTANT(AUTOWRAP_WORD_SMART);
BIND_ENUM_CONSTANT(OVERRUN_NO_TRIMMING);
BIND_ENUM_CONSTANT(OVERRUN_TRIM_CHAR);
BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD);
BIND_ENUM_CONSTANT(OVERRUN_TRIM_ELLIPSIS);
BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD_ELLIPSIS);
ADD_PROPERTY(PropertyInfo(Variant::STRING, "text", PROPERTY_HINT_MULTILINE_TEXT, "", PROPERTY_USAGE_DEFAULT_INTL), "set_text", "get_text");
ADD_PROPERTY(PropertyInfo(Variant::INT, "text_direction", PROPERTY_HINT_ENUM, "Auto,Left-to-Right,Right-to-Left,Inherited"), "set_text_direction", "get_text_direction");
ADD_PROPERTY(PropertyInfo(Variant::STRING, "language"), "set_language", "get_language");
ADD_PROPERTY(PropertyInfo(Variant::INT, "align", PROPERTY_HINT_ENUM, "Left,Center,Right,Fill"), "set_align", "get_align");
ADD_PROPERTY(PropertyInfo(Variant::INT, "valign", PROPERTY_HINT_ENUM, "Top,Center,Bottom,Fill"), "set_valign", "get_valign");
ADD_PROPERTY(PropertyInfo(Variant::BOOL, "autowrap"), "set_autowrap", "has_autowrap");
ADD_PROPERTY(PropertyInfo(Variant::INT, "autowrap_mode", PROPERTY_HINT_ENUM, "Off,Arbitrary,Word,Word (Smart)"), "set_autowrap_mode", "get_autowrap_mode");
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::BOOL, "uppercase"), "set_uppercase", "is_uppercase");
ADD_PROPERTY(PropertyInfo(Variant::INT, "visible_characters", PROPERTY_HINT_RANGE, "-1,128000,1", PROPERTY_USAGE_EDITOR), "set_visible_characters", "get_visible_characters");
ADD_PROPERTY(PropertyInfo(Variant::FLOAT, "percent_visible", PROPERTY_HINT_RANGE, "0,1,0.001"), "set_percent_visible", "get_percent_visible");

View file

@ -51,13 +51,29 @@ public:
VALIGN_FILL
};
enum AutowrapMode {
AUTOWRAP_OFF,
AUTOWRAP_ARBITRARY,
AUTOWRAP_WORD,
AUTOWRAP_WORD_SMART
};
enum OverrunBehavior {
OVERRUN_NO_TRIMMING,
OVERRUN_TRIM_CHAR,
OVERRUN_TRIM_WORD,
OVERRUN_TRIM_ELLIPSIS,
OVERRUN_TRIM_WORD_ELLIPSIS,
};
private:
Align align = ALIGN_LEFT;
VAlign valign = VALIGN_TOP;
String text;
String xl_text;
bool autowrap = false;
AutowrapMode autowrap_mode = AUTOWRAP_OFF;
bool clip = false;
OverrunBehavior overrun_behavior = OVERRUN_NO_TRIMMING;
Size2 minsize;
bool uppercase = false;
@ -118,8 +134,8 @@ public:
void set_structured_text_bidi_override_options(Array p_args);
Array get_structured_text_bidi_override_options() const;
void set_autowrap(bool p_autowrap);
bool has_autowrap() const;
void set_autowrap_mode(AutowrapMode p_mode);
AutowrapMode get_autowrap_mode() const;
void set_uppercase(bool p_uppercase);
bool is_uppercase() const;
@ -131,6 +147,9 @@ public:
void set_clip_text(bool p_clip);
bool is_clipping_text() const;
void set_text_overrun_behavior(OverrunBehavior p_behavior);
OverrunBehavior get_text_overrun_behavior() const;
void set_percent_visible(float p_percent);
float get_percent_visible() const;
@ -150,5 +169,7 @@ public:
VARIANT_ENUM_CAST(Label::Align);
VARIANT_ENUM_CAST(Label::VAlign);
VARIANT_ENUM_CAST(Label::AutowrapMode);
VARIANT_ENUM_CAST(Label::OverrunBehavior);
#endif

View file

@ -331,6 +331,9 @@ void TextServer::_bind_methods() {
ClassDB::bind_method(D_METHOD("shaped_text_get_line_breaks_adv", "shaped", "width", "start", "once", "break_flags"), &TextServer::_shaped_text_get_line_breaks_adv, DEFVAL(0), DEFVAL(true), DEFVAL(BREAK_MANDATORY | BREAK_WORD_BOUND));
ClassDB::bind_method(D_METHOD("shaped_text_get_line_breaks", "shaped", "width", "start", "break_flags"), &TextServer::_shaped_text_get_line_breaks, DEFVAL(0), DEFVAL(BREAK_MANDATORY | BREAK_WORD_BOUND));
ClassDB::bind_method(D_METHOD("shaped_text_get_word_breaks", "shaped"), &TextServer::_shaped_text_get_word_breaks);
ClassDB::bind_method(D_METHOD("shaped_text_overrun_trim_to_width", "shaped", "width", "overrun_trim_flags"), &TextServer::shaped_text_overrun_trim_to_width, DEFVAL(0), DEFVAL(OVERRUN_NO_TRIMMING));
ClassDB::bind_method(D_METHOD("shaped_text_get_objects", "shaped"), &TextServer::shaped_text_get_objects);
ClassDB::bind_method(D_METHOD("shaped_text_get_object_rect", "shaped", "key"), &TextServer::shaped_text_get_object_rect);
@ -381,6 +384,13 @@ void TextServer::_bind_methods() {
BIND_ENUM_CONSTANT(BREAK_WORD_BOUND);
BIND_ENUM_CONSTANT(BREAK_GRAPHEME_BOUND);
/* TextOverrunFlag */
BIND_ENUM_CONSTANT(OVERRUN_NO_TRIMMING);
BIND_ENUM_CONSTANT(OVERRUN_TRIM);
BIND_ENUM_CONSTANT(OVERRUN_TRIM_WORD_ONLY);
BIND_ENUM_CONSTANT(OVERRUN_ADD_ELLIPSIS);
BIND_ENUM_CONSTANT(OVERRUN_ENFORCE_ELLIPSIS);
/* GraphemeFlag */
BIND_ENUM_CONSTANT(GRAPHEME_IS_RTL);
BIND_ENUM_CONSTANT(GRAPHEME_IS_VIRTUAL);
@ -646,7 +656,7 @@ Vector<Vector2i> TextServer::shaped_text_get_line_breaks(RID p_shaped, float p_w
float width = 0.f;
int line_start = MAX(p_start, range.x);
int last_safe_break = -1;
int word_count = 0;
int l_size = logical.size();
const Glyph *l_gl = logical.ptr();
@ -655,12 +665,15 @@ Vector<Vector2i> TextServer::shaped_text_get_line_breaks(RID p_shaped, float p_w
continue;
}
if (l_gl[i].count > 0) {
if ((p_width > 0) && (width + l_gl[i].advance > p_width) && (last_safe_break >= 0)) {
//Ignore trailing spaces.
bool is_space = (l_gl[i].flags & GRAPHEME_IS_SPACE) == GRAPHEME_IS_SPACE;
if ((p_width > 0) && (width + (is_space ? 0 : l_gl[i].advance) > p_width) && (last_safe_break >= 0)) {
lines.push_back(Vector2i(line_start, l_gl[last_safe_break].end));
line_start = l_gl[last_safe_break].end;
i = last_safe_break;
last_safe_break = -1;
width = 0;
word_count = 0;
continue;
}
if ((p_break_flags & BREAK_MANDATORY) == BREAK_MANDATORY) {
@ -675,8 +688,12 @@ Vector<Vector2i> TextServer::shaped_text_get_line_breaks(RID p_shaped, float p_w
if ((p_break_flags & BREAK_WORD_BOUND) == BREAK_WORD_BOUND) {
if ((l_gl[i].flags & GRAPHEME_IS_BREAK_SOFT) == GRAPHEME_IS_BREAK_SOFT) {
last_safe_break = i;
word_count++;
}
}
if (((p_break_flags & BREAK_WORD_BOUND_ADAPTIVE) == BREAK_WORD_BOUND_ADAPTIVE) && word_count == 0) {
last_safe_break = i;
}
if ((p_break_flags & BREAK_GRAPHEME_BOUND) == BREAK_GRAPHEME_BOUND) {
last_safe_break = i;
}

View file

@ -66,8 +66,16 @@ public:
BREAK_NONE = 0,
BREAK_MANDATORY = 1 << 4,
BREAK_WORD_BOUND = 1 << 5,
BREAK_GRAPHEME_BOUND = 1 << 6
//RESERVED = 1 << 7
BREAK_GRAPHEME_BOUND = 1 << 6,
BREAK_WORD_BOUND_ADAPTIVE = 1 << 5 | 1 << 7
};
enum TextOverrunFlag {
OVERRUN_NO_TRIMMING = 0,
OVERRUN_TRIM = 1 << 0,
OVERRUN_TRIM_WORD_ONLY = 1 << 1,
OVERRUN_ADD_ELLIPSIS = 1 << 2,
OVERRUN_ENFORCE_ELLIPSIS = 1 << 3
};
enum GraphemeFlag {
@ -138,7 +146,7 @@ public:
return true;
}
}
return l.count > r.count; // Sort first glyoh with count & flags, order of the rest are irrelevant.
return l.count > r.count; // Sort first glyph with count & flags, order of the rest are irrelevant.
} else {
return l.start < r.start;
}
@ -347,6 +355,9 @@ public:
virtual Vector<Vector2i> shaped_text_get_line_breaks_adv(RID p_shaped, const Vector<float> &p_width, int p_start = 0, bool p_once = true, uint8_t /*TextBreakFlag*/ p_break_flags = BREAK_MANDATORY | BREAK_WORD_BOUND) const;
virtual Vector<Vector2i> shaped_text_get_line_breaks(RID p_shaped, float p_width, int p_start = 0, uint8_t /*TextBreakFlag*/ p_break_flags = BREAK_MANDATORY | BREAK_WORD_BOUND) const;
virtual Vector<Vector2i> shaped_text_get_word_breaks(RID p_shaped) const;
virtual void shaped_text_overrun_trim_to_width(RID p_shaped, float p_width, uint8_t p_clip_flags) = 0;
virtual Array shaped_text_get_objects(RID p_shaped) const = 0;
virtual Rect2 shaped_text_get_object_rect(RID p_shaped, Variant p_key) const = 0;
@ -461,6 +472,7 @@ VARIANT_ENUM_CAST(TextServer::Direction);
VARIANT_ENUM_CAST(TextServer::Orientation);
VARIANT_ENUM_CAST(TextServer::JustificationFlag);
VARIANT_ENUM_CAST(TextServer::LineBreakFlag);
VARIANT_ENUM_CAST(TextServer::TextOverrunFlag);
VARIANT_ENUM_CAST(TextServer::GraphemeFlag);
VARIANT_ENUM_CAST(TextServer::Hinting);
VARIANT_ENUM_CAST(TextServer::Feature);