diff --git a/doc/classes/Label.xml b/doc/classes/Label.xml
index ae5a62753f1..7657cf31214 100644
--- a/doc/classes/Label.xml
+++ b/doc/classes/Label.xml
@@ -74,6 +74,9 @@
Limits the lines of text the node shows on screen.
+
+ String used as a paragraph separator. Each paragraph is processed independently, in its own BiDi context.
+
Set BiDi algorithm override for the structured text.
@@ -129,6 +132,9 @@
[b]Note:[/b] If using a font with [member FontFile.multichannel_signed_distance_field] enabled, its [member FontFile.msdf_pixel_range] must be set to at least [i]twice[/i] the value of [theme_item outline_size] for outline rendering to look correct. Otherwise, the outline may appear to be cut off earlier than intended.
[b]Note:[/b] Using a value that is larger than half the font size is not recommended, as the font outline may fail to be fully closed in this case.
+
+ Vertical space between paragraphs. Added on top of [theme_item line_spacing].
+
The horizontal offset of the text's shadow.
diff --git a/doc/classes/LabelSettings.xml b/doc/classes/LabelSettings.xml
index ff7b8e7b0e4..610f10574b5 100644
--- a/doc/classes/LabelSettings.xml
+++ b/doc/classes/LabelSettings.xml
@@ -27,6 +27,9 @@
Text outline size.
+
+ Vertical space between paragraphs. Added on top of [member line_spacing].
+
Color of the shadow effect. If alpha is [code]0[/code], no shadow will be drawn.
diff --git a/scene/gui/label.cpp b/scene/gui/label.cpp
index 7a0e5b8867b..7dd24cc14d3 100644
--- a/scene/gui/label.cpp
+++ b/scene/gui/label.cpp
@@ -43,7 +43,9 @@ void Label::set_autowrap_mode(TextServer::AutowrapMode p_mode) {
}
autowrap_mode = p_mode;
- lines_dirty = true;
+ for (Paragraph ¶ : paragraphs) {
+ para.lines_dirty = true;
+ }
queue_redraw();
update_configuration_warnings();
@@ -62,7 +64,9 @@ void Label::set_justification_flags(BitField p_fl
}
jst_flags = p_flags;
- lines_dirty = true;
+ for (Paragraph ¶ : paragraphs) {
+ para.lines_dirty = true;
+ }
queue_redraw();
}
@@ -76,7 +80,7 @@ void Label::set_uppercase(bool p_uppercase) {
}
uppercase = p_uppercase;
- dirty = true;
+ text_dirty = true;
queue_redraw();
}
@@ -87,12 +91,14 @@ bool Label::is_uppercase() const {
int Label::get_line_height(int p_line) const {
Ref font = (settings.is_valid() && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font;
- if (p_line >= 0 && p_line < lines_rid.size()) {
- return TS->shaped_text_get_size(lines_rid[p_line]).y;
- } else if (lines_rid.size() > 0) {
+ if (p_line >= 0 && p_line < total_line_count) {
+ return TS->shaped_text_get_size(get_line_rid(p_line)).y;
+ } else if (total_line_count > 0) {
int h = 0;
- for (int i = 0; i < lines_rid.size(); i++) {
- h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y);
+ for (const Paragraph ¶ : paragraphs) {
+ for (const RID &line_rid : para.lines_rid) {
+ h = MAX(h, TS->shaped_text_get_size(line_rid).y);
+ }
}
return h;
} else {
@@ -105,181 +111,216 @@ void Label::_shape() {
Ref style = theme_cache.normal_style;
int width = (get_size().width - style->get_minimum_size().width);
- if (dirty || font_dirty) {
- if (dirty) {
- TS->shaped_text_clear(text_rid);
+ if (text_dirty) {
+ for (Paragraph ¶ : paragraphs) {
+ for (const RID &line_rid : para.lines_rid) {
+ TS->free_rid(line_rid);
+ }
+ para.lines_rid.clear();
+ TS->free_rid(para.text_rid);
}
- if (text_direction == Control::TEXT_DIRECTION_INHERITED) {
- TS->shaped_text_set_direction(text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
- } else {
- TS->shaped_text_set_direction(text_rid, (TextServer::Direction)text_direction);
- }
- const Ref &font = (settings.is_valid() && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font;
- int font_size = settings.is_valid() ? settings->get_font_size() : theme_cache.font_size;
- ERR_FAIL_COND(font.is_null());
+ paragraphs.clear();
+
String txt = (uppercase) ? TS->string_to_upper(xl_text, language) : xl_text;
if (visible_chars >= 0 && visible_chars_behavior == TextServer::VC_CHARS_BEFORE_SHAPING) {
txt = txt.substr(0, visible_chars);
}
- if (dirty) {
- TS->shaped_text_add_string(text_rid, txt, font->get_rids(), font_size, font->get_opentype_features(), language);
- } else {
- int spans = TS->shaped_get_span_count(text_rid);
- for (int i = 0; i < spans; i++) {
- TS->shaped_set_span_update_font(text_rid, i, font->get_rids(), font_size, font->get_opentype_features());
- }
+ String ps = paragraph_separator.c_unescape();
+ Vector para_text = txt.split(ps);
+ int start = 0;
+ for (const String &str : para_text) {
+ Paragraph para;
+ para.text_rid = TS->create_shaped_text();
+ para.text = str;
+ para.start = start;
+ start += str.length() + ps.length();
+ paragraphs.push_back(para);
}
- TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, txt));
- if (!tab_stops.is_empty()) {
- TS->shaped_text_tab_align(text_rid, tab_stops);
- }
- dirty = false;
- font_dirty = false;
- lines_dirty = true;
+ text_dirty = false;
}
- if (lines_dirty) {
- for (int i = 0; i < lines_rid.size(); i++) {
- TS->free_rid(lines_rid[i]);
- }
- lines_rid.clear();
+ total_line_count = 0;
+ for (Paragraph ¶ : paragraphs) {
+ if (para.dirty || font_dirty) {
+ if (para.dirty) {
+ TS->shaped_text_clear(para.text_rid);
+ }
+ if (text_direction == Control::TEXT_DIRECTION_INHERITED) {
+ TS->shaped_text_set_direction(para.text_rid, is_layout_rtl() ? TextServer::DIRECTION_RTL : TextServer::DIRECTION_LTR);
+ } else {
+ TS->shaped_text_set_direction(para.text_rid, (TextServer::Direction)text_direction);
+ }
+ const Ref &font = (settings.is_valid() && settings->get_font().is_valid()) ? settings->get_font() : theme_cache.font;
+ int font_size = settings.is_valid() ? settings->get_font_size() : theme_cache.font_size;
+ ERR_FAIL_COND(font.is_null());
- BitField autowrap_flags = TextServer::BREAK_MANDATORY;
- switch (autowrap_mode) {
- case TextServer::AUTOWRAP_WORD_SMART:
- autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_ADAPTIVE | TextServer::BREAK_MANDATORY;
- break;
- case TextServer::AUTOWRAP_WORD:
- autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY;
- break;
- case TextServer::AUTOWRAP_ARBITRARY:
- autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY;
- break;
- case TextServer::AUTOWRAP_OFF:
- break;
- }
- autowrap_flags = autowrap_flags | TextServer::BREAK_TRIM_EDGE_SPACES;
-
- PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(text_rid, width, 0, autowrap_flags);
- for (int i = 0; i < line_breaks.size(); i = i + 2) {
- RID line = TS->shaped_text_substr(text_rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]);
+ if (para.dirty) {
+ TS->shaped_text_add_string(para.text_rid, para.text, font->get_rids(), font_size, font->get_opentype_features(), language);
+ } else {
+ int spans = TS->shaped_get_span_count(para.text_rid);
+ for (int i = 0; i < spans; i++) {
+ TS->shaped_set_span_update_font(para.text_rid, i, font->get_rids(), font_size, font->get_opentype_features());
+ }
+ }
+ TS->shaped_text_set_bidi_override(para.text_rid, structured_text_parser(st_parser, st_args, para.text));
if (!tab_stops.is_empty()) {
- TS->shaped_text_tab_align(line, tab_stops);
+ TS->shaped_text_tab_align(para.text_rid, tab_stops);
}
- lines_rid.push_back(line);
+ para.dirty = false;
+ para.lines_dirty = true;
}
+
+ if (para.lines_dirty) {
+ for (const RID &line_rid : para.lines_rid) {
+ TS->free_rid(line_rid);
+ }
+ para.lines_rid.clear();
+
+ BitField autowrap_flags = TextServer::BREAK_MANDATORY;
+ switch (autowrap_mode) {
+ case TextServer::AUTOWRAP_WORD_SMART:
+ autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_ADAPTIVE | TextServer::BREAK_MANDATORY;
+ break;
+ case TextServer::AUTOWRAP_WORD:
+ autowrap_flags = TextServer::BREAK_WORD_BOUND | TextServer::BREAK_MANDATORY;
+ break;
+ case TextServer::AUTOWRAP_ARBITRARY:
+ autowrap_flags = TextServer::BREAK_GRAPHEME_BOUND | TextServer::BREAK_MANDATORY;
+ break;
+ case TextServer::AUTOWRAP_OFF:
+ break;
+ }
+ autowrap_flags = autowrap_flags | TextServer::BREAK_TRIM_EDGE_SPACES;
+
+ PackedInt32Array line_breaks = TS->shaped_text_get_line_breaks(para.text_rid, width, 0, autowrap_flags);
+ for (int i = 0; i < line_breaks.size(); i = i + 2) {
+ RID line = TS->shaped_text_substr(para.text_rid, line_breaks[i], line_breaks[i + 1] - line_breaks[i]);
+ if (!tab_stops.is_empty()) {
+ TS->shaped_text_tab_align(line, tab_stops);
+ }
+ para.lines_rid.push_back(line);
+ }
+ }
+ total_line_count += para.lines_rid.size();
}
+ dirty = false;
+ font_dirty = false;
if (xl_text.length() == 0) {
minsize = Size2(1, get_line_height());
return;
}
- if (autowrap_mode == TextServer::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) {
- minsize.width = TS->shaped_text_get_size(lines_rid[i]).x;
+ int visible_lines = get_visible_line_count();
+ bool lines_hidden = visible_lines > 0 && visible_lines < total_line_count;
+
+ int line_index = 0;
+ for (Paragraph ¶ : paragraphs) {
+ if (autowrap_mode == TextServer::AUTOWRAP_OFF) {
+ minsize.width = 0.0f;
+ for (const RID &line_rid : para.lines_rid) {
+ if (minsize.width < TS->shaped_text_get_size(line_rid).x) {
+ minsize.width = TS->shaped_text_get_size(line_rid).x;
+ }
}
}
- }
- if (lines_dirty) {
- BitField overrun_flags = TextServer::OVERRUN_NO_TRIM;
- switch (overrun_behavior) {
- case TextServer::OVERRUN_TRIM_WORD_ELLIPSIS:
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
- overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
- break;
- case TextServer::OVERRUN_TRIM_ELLIPSIS:
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
- overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
- break;
- case TextServer::OVERRUN_TRIM_WORD:
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
- break;
- case TextServer::OVERRUN_TRIM_CHAR:
- overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
- break;
- case TextServer::OVERRUN_NO_TRIMMING:
- break;
- }
-
- // Fill after min_size calculation.
-
- BitField line_jst_flags = jst_flags;
- if (!tab_stops.is_empty()) {
- line_jst_flags.set_flag(TextServer::JUSTIFICATION_AFTER_LAST_TAB);
- }
- if (autowrap_mode != TextServer::AUTOWRAP_OFF) {
- int visible_lines = get_visible_line_count();
- bool lines_hidden = visible_lines > 0 && visible_lines < lines_rid.size();
- if (lines_hidden) {
- overrun_flags.set_flag(TextServer::OVERRUN_ENFORCE_ELLIPSIS);
+ if (para.lines_dirty) {
+ BitField overrun_flags = TextServer::OVERRUN_NO_TRIM;
+ switch (overrun_behavior) {
+ case TextServer::OVERRUN_TRIM_WORD_ELLIPSIS:
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
+ overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
+ break;
+ case TextServer::OVERRUN_TRIM_ELLIPSIS:
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
+ overrun_flags.set_flag(TextServer::OVERRUN_ADD_ELLIPSIS);
+ break;
+ case TextServer::OVERRUN_TRIM_WORD:
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM_WORD_ONLY);
+ break;
+ case TextServer::OVERRUN_TRIM_CHAR:
+ overrun_flags.set_flag(TextServer::OVERRUN_TRIM);
+ break;
+ case TextServer::OVERRUN_NO_TRIMMING:
+ break;
}
- if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) {
- int jst_to_line = visible_lines;
- if (lines_rid.size() == 1 && line_jst_flags.has_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE)) {
- jst_to_line = lines_rid.size();
+
+ // Fill after min_size calculation.
+
+ BitField line_jst_flags = jst_flags;
+ if (!tab_stops.is_empty()) {
+ line_jst_flags.set_flag(TextServer::JUSTIFICATION_AFTER_LAST_TAB);
+ }
+ if (autowrap_mode != TextServer::AUTOWRAP_OFF) {
+ if (lines_hidden) {
+ overrun_flags.set_flag(TextServer::OVERRUN_ENFORCE_ELLIPSIS);
+ }
+ if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) {
+ int jst_to_line = para.lines_rid.size();
+ if (para.lines_rid.size() == 1 && line_jst_flags.has_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE)) {
+ jst_to_line = para.lines_rid.size();
+ } else {
+ if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE)) {
+ jst_to_line = para.lines_rid.size() - 1;
+ }
+ if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS)) {
+ for (int i = para.lines_rid.size() - 1; i >= 0; i--) {
+ if (TS->shaped_text_has_visible_chars(para.lines_rid[i])) {
+ jst_to_line = i;
+ break;
+ }
+ }
+ }
+ }
+ for (int i = 0; i < para.lines_rid.size(); i++) {
+ if (i < jst_to_line) {
+ TS->shaped_text_fit_to_width(para.lines_rid[i], width, line_jst_flags);
+ } else if (i == (visible_lines - line_index - 1)) {
+ TS->shaped_text_set_custom_ellipsis(para.lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
+ TS->shaped_text_overrun_trim_to_width(para.lines_rid[i], width, overrun_flags);
+ }
+ }
+ } else if (lines_hidden && (visible_lines - line_index - 1 >= 0) && (visible_lines - line_index - 1) < para.lines_rid.size()) {
+ TS->shaped_text_set_custom_ellipsis(para.lines_rid[visible_lines - line_index - 1], (el_char.length() > 0) ? el_char[0] : 0x2026);
+ TS->shaped_text_overrun_trim_to_width(para.lines_rid[visible_lines - line_index - 1], width, overrun_flags);
+ }
+ } else {
+ // Autowrap disabled.
+ int jst_to_line = para.lines_rid.size();
+ if (para.lines_rid.size() == 1 && line_jst_flags.has_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE)) {
+ jst_to_line = para.lines_rid.size();
} else {
if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE)) {
- jst_to_line = visible_lines - 1;
+ jst_to_line = para.lines_rid.size() - 1;
}
if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS)) {
- for (int i = visible_lines - 1; i >= 0; i--) {
- if (TS->shaped_text_has_visible_chars(lines_rid[i])) {
+ for (int i = para.lines_rid.size() - 1; i >= 0; i--) {
+ if (TS->shaped_text_has_visible_chars(para.lines_rid[i])) {
jst_to_line = i;
break;
}
}
}
}
- for (int i = 0; i < lines_rid.size(); i++) {
- 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 {
- // Autowrap disabled.
- int jst_to_line = lines_rid.size();
- if (lines_rid.size() == 1 && line_jst_flags.has_flag(TextServer::JUSTIFICATION_DO_NOT_SKIP_SINGLE_LINE)) {
- jst_to_line = lines_rid.size();
- } else {
- if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE)) {
- jst_to_line = lines_rid.size() - 1;
- }
- if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE_WITH_VISIBLE_CHARS)) {
- for (int i = lines_rid.size() - 1; i >= 0; i--) {
- if (TS->shaped_text_has_visible_chars(lines_rid[i])) {
- jst_to_line = i;
- break;
- }
+ for (int i = 0; i < para.lines_rid.size(); i++) {
+ if (i < jst_to_line && horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) {
+ TS->shaped_text_fit_to_width(para.lines_rid[i], width, line_jst_flags);
+ overrun_flags.set_flag(TextServer::OVERRUN_JUSTIFICATION_AWARE);
+ TS->shaped_text_set_custom_ellipsis(para.lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
+ TS->shaped_text_overrun_trim_to_width(para.lines_rid[i], width, overrun_flags);
+ TS->shaped_text_fit_to_width(para.lines_rid[i], width, line_jst_flags | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS);
+ } else {
+ TS->shaped_text_set_custom_ellipsis(para.lines_rid[i], (el_char.length() > 0) ? el_char[0] : 0x2026);
+ TS->shaped_text_overrun_trim_to_width(para.lines_rid[i], width, overrun_flags);
}
}
}
- for (int i = 0; i < lines_rid.size(); i++) {
- 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);
- }
- }
+ para.lines_dirty = false;
}
- lines_dirty = false;
+ line_index += para.lines_rid.size();
}
_update_visible();
@@ -291,20 +332,37 @@ void Label::_shape() {
void Label::_update_visible() {
int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing;
+ int paragraph_spacing = settings.is_valid() ? settings->get_paragraph_spacing() : theme_cache.paragraph_spacing;
Ref style = theme_cache.normal_style;
- int lines_visible = lines_rid.size();
+ int lines_visible = total_line_count;
if (max_lines_visible >= 0 && lines_visible > max_lines_visible) {
lines_visible = max_lines_visible;
}
minsize.height = 0;
- int last_line = MIN(lines_rid.size(), lines_visible + lines_skipped);
- for (int64_t i = lines_skipped; i < last_line; i++) {
- minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing;
+ int last_line = MIN(total_line_count, lines_visible + lines_skipped);
+
+ int line_index = 0;
+ for (const Paragraph ¶ : paragraphs) {
+ if (line_index + para.lines_rid.size() <= lines_skipped) {
+ line_index += para.lines_rid.size();
+ } else {
+ int start = (line_index < lines_skipped) ? lines_skipped - line_index : 0;
+ int end = (line_index + para.lines_rid.size() < last_line) ? para.lines_rid.size() : last_line - line_index;
+ if (end <= 0) {
+ break;
+ }
+ for (int i = start; i < end; i++) {
+ minsize.height += TS->shaped_text_get_size(para.lines_rid[i]).y + line_spacing;
+ }
+ minsize.height += paragraph_spacing;
+ line_index += para.lines_rid.size();
+ }
}
+
if (minsize.height > 0) {
- minsize.height -= line_spacing;
+ minsize.height -= (line_spacing + paragraph_spacing);
}
}
@@ -336,25 +394,61 @@ inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Col
}
void Label::_ensure_shaped() const {
- if (dirty || font_dirty || lines_dirty) {
+ if (dirty || font_dirty || text_dirty) {
const_cast