diff options
author | Hendrik Brucker <hendrik.brucker@mail.de> | 2021-07-04 16:43:55 +0200 |
---|---|---|
committer | Hendrik Brucker <hendrik.brucker@mail.de> | 2021-07-04 16:43:55 +0200 |
commit | 56a8d3f30c09fdc03b19ef48a97d344ffe2df974 (patch) | |
tree | f3be955afcbb70de9d3afcd8a462ff7cecc980ce | |
parent | cb4e42155dd3a0e513106d7a219366dd355e9483 (diff) |
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
24 files changed, 564 insertions, 43 deletions
diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml index 0789ac9010..ca22567ad5 100644 --- a/doc/classes/Label.xml +++ b/doc/classes/Label.xml @@ -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"> diff --git a/doc/classes/TextServer.xml b/doc/classes/TextServer.xml index c943946ab3..0f9318fda1 100644 --- a/doc/classes/TextServer.xml +++ b/doc/classes/TextServer.xml @@ -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> diff --git a/editor/animation_track_editor.cpp b/editor/animation_track_editor.cpp index f61fb6bab3..7090572b0f 100644 --- a/editor/animation_track_editor.cpp +++ b/editor/animation_track_editor.cpp @@ -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); diff --git a/editor/debugger/editor_performance_profiler.cpp b/editor/debugger/editor_performance_profiler.cpp index fc0104c07a..08609080c5 100644 --- a/editor/debugger/editor_performance_profiler.cpp +++ b/editor/debugger/editor_performance_profiler.cpp @@ -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); diff --git a/editor/debugger/script_editor_debugger.cpp b/editor/debugger/script_editor_debugger.cpp index 0d3fd8c7f6..e283043a4f 100644 --- a/editor/debugger/script_editor_debugger.cpp +++ b/editor/debugger/script_editor_debugger.cpp @@ -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); diff --git a/editor/editor_about.cpp b/editor/editor_about.cpp index b8504ad02a..e28ba9a184 100644 --- a/editor/editor_about.cpp +++ b/editor/editor_about.cpp @@ -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); diff --git a/editor/groups_editor.cpp b/editor/groups_editor.cpp index 1b11ec4451..1be7a5336a 100644 --- a/editor/groups_editor.cpp +++ b/editor/groups_editor.cpp @@ -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); diff --git a/editor/node_dock.cpp b/editor/node_dock.cpp index 43c9cabe01..95a2d81e00 100644 --- a/editor/node_dock.cpp +++ b/editor/node_dock.cpp @@ -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); } diff --git a/editor/plugins/theme_editor_plugin.cpp b/editor/plugins/theme_editor_plugin.cpp index 99d7267eac..b8f1c0197e 100644 --- a/editor/plugins/theme_editor_plugin.cpp +++ b/editor/plugins/theme_editor_plugin.cpp @@ -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); } diff --git a/editor/plugins/visual_shader_editor_plugin.cpp b/editor/plugins/visual_shader_editor_plugin.cpp index 1183da4d04..bf6cff6d8b 100644 --- a/editor/plugins/visual_shader_editor_plugin.cpp +++ b/editor/plugins/visual_shader_editor_plugin.cpp @@ -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); diff --git a/editor/script_create_dialog.cpp b/editor/script_create_dialog.cpp index fdbde8dc5c..50f05345da 100644 --- a/editor/script_create_dialog.cpp +++ b/editor/script_create_dialog.cpp @@ -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); diff --git a/modules/gdnative/include/text/godot_text.h b/modules/gdnative/include/text/godot_text.h index f3c50e6f87..5c59de7c06 100644 --- a/modules/gdnative/include/text/godot_text.h +++ b/modules/gdnative/include/text/godot_text.h @@ -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 *); diff --git a/modules/gdnative/text/text_server_gdnative.cpp b/modules/gdnative/text/text_server_gdnative.cpp index bc4b1ac134..392121c3a9 100644 --- a/modules/gdnative/text/text_server_gdnative.cpp +++ b/modules/gdnative/text/text_server_gdnative.cpp @@ -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); diff --git a/modules/gdnative/text/text_server_gdnative.h b/modules/gdnative/text/text_server_gdnative.h index 7e42b16fe1..d613a7ec00 100644 --- a/modules/gdnative/text/text_server_gdnative.h +++ b/modules/gdnative/text/text_server_gdnative.h @@ -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; diff --git a/modules/gridmap/grid_map_editor_plugin.cpp b/modules/gridmap/grid_map_editor_plugin.cpp index a2f570e6a5..d894425ce8 100644 --- a/modules/gridmap/grid_map_editor_plugin.cpp +++ b/modules/gridmap/grid_map_editor_plugin.cpp @@ -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); diff --git a/modules/text_server_adv/text_server_adv.cpp b/modules/text_server_adv/text_server_adv.cpp index 906ebe4993..72c5ccc699 100644 --- a/modules/text_server_adv/text_server_adv.cpp +++ b/modules/text_server_adv/text_server_adv.cpp @@ -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); diff --git a/modules/text_server_adv/text_server_adv.h b/modules/text_server_adv/text_server_adv.h index 4ad23ca059..3c4f840bfd 100644 --- a/modules/text_server_adv/text_server_adv.h +++ b/modules/text_server_adv/text_server_adv.h @@ -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; diff --git a/modules/text_server_fb/text_server_fb.cpp b/modules/text_server_fb/text_server_fb.cpp index a22559efdd..576d130cc0 100644 --- a/modules/text_server_fb/text_server_fb.cpp +++ b/modules/text_server_fb/text_server_fb.cpp @@ -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); diff --git a/modules/text_server_fb/text_server_fb.h b/modules/text_server_fb/text_server_fb.h index 8f5eb1d315..b70c8f4ec0 100644 --- a/modules/text_server_fb/text_server_fb.h +++ b/modules/text_server_fb/text_server_fb.h @@ -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; diff --git a/scene/gui/dialogs.cpp b/scene/gui/dialogs.cpp index f63ae7569f..8ea0532625 100644 --- a/scene/gui/dialogs.cpp +++ b/scene/gui/dialogs.cpp @@ -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) { diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp index 6580d794d1..78b9ad2569 100644 --- a/scene/gui/label.cpp +++ b/scene/gui/label.cpp @@ -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"); diff --git a/scene/gui/label.h b/scene/gui/label.h index 032b4112e1..8b48eb9670 100644 --- a/scene/gui/label.h +++ b/scene/gui/label.h @@ -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 diff --git a/servers/text_server.cpp b/servers/text_server.cpp index 6f48148cab..97cfe828f0 100644 --- a/servers/text_server.cpp +++ b/servers/text_server.cpp @@ -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; } diff --git a/servers/text_server.h b/servers/text_server.h index 138ceb9356..06020d3ffd 100644 --- a/servers/text_server.h +++ b/servers/text_server.h @@ -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); |