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