/**************************************************************************/ /* label.cpp */ /**************************************************************************/ /* This file is part of: */ /* GODOT ENGINE */ /* https://godotengine.org */ /**************************************************************************/ /* Copyright (c) 2014-present Godot Engine contributors (see AUTHORS.md). */ /* Copyright (c) 2007-2014 Juan Linietsky, Ariel Manzur. */ /* */ /* Permission is hereby granted, free of charge, to any person obtaining */ /* a copy of this software and associated documentation files (the */ /* "Software"), to deal in the Software without restriction, including */ /* without limitation the rights to use, copy, modify, merge, publish, */ /* distribute, sublicense, and/or sell copies of the Software, and to */ /* permit persons to whom the Software is furnished to do so, subject to */ /* the following conditions: */ /* */ /* The above copyright notice and this permission notice shall be */ /* included in all copies or substantial portions of the Software. */ /* */ /* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, */ /* EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF */ /* MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. */ /* IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY */ /* CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, */ /* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE */ /* SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. */ /**************************************************************************/ #include "label.h" #include "core/config/project_settings.h" #include "core/string/print_string.h" #include "core/string/translation.h" #include "scene/gui/container.h" #include "scene/theme/theme_db.h" #include "servers/text_server.h" void Label::set_autowrap_mode(TextServer::AutowrapMode p_mode) { if (autowrap_mode == p_mode) { return; } autowrap_mode = p_mode; lines_dirty = true; queue_redraw(); update_configuration_warnings(); if (clip || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) { update_minimum_size(); } } TextServer::AutowrapMode Label::get_autowrap_mode() const { return autowrap_mode; } void Label::set_justification_flags(BitField p_flags) { if (jst_flags == p_flags) { return; } jst_flags = p_flags; lines_dirty = true; queue_redraw(); } BitField Label::get_justification_flags() const { return jst_flags; } void Label::set_uppercase(bool p_uppercase) { if (uppercase == p_uppercase) { return; } uppercase = p_uppercase; dirty = true; queue_redraw(); } bool Label::is_uppercase() const { return uppercase; } 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) { int h = 0; for (int i = 0; i < lines_rid.size(); i++) { h = MAX(h, TS->shaped_text_get_size(lines_rid[i]).y); } return h; } else { int font_size = settings.is_valid() ? settings->get_font_size() : theme_cache.font_size; return font->get_height(font_size); } } 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_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()); 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()); } } 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; } if (lines_dirty) { for (int i = 0; i < lines_rid.size(); i++) { TS->free_rid(lines_rid[i]); } 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(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 (!tab_stops.is_empty()) { TS->shaped_text_tab_align(line, tab_stops); } lines_rid.push_back(line); } } 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; } } } 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 (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(); } else { if (line_jst_flags.has_flag(TextServer::JUSTIFICATION_SKIP_LAST_LINE)) { jst_to_line = visible_lines - 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])) { 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_overrun_trim_to_width(lines_rid[i], width, overrun_flags); } } } else if (lines_hidden) { 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 < 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_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_overrun_trim_to_width(lines_rid[i], width, overrun_flags); } } } lines_dirty = false; } _update_visible(); if (autowrap_mode == TextServer::AUTOWRAP_OFF || !clip || overrun_behavior == TextServer::OVERRUN_NO_TRIMMING) { update_minimum_size(); } } void Label::_update_visible() { int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing; Ref style = theme_cache.normal_style; int lines_visible = lines_rid.size(); 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; } if (minsize.height > 0) { minsize.height -= line_spacing; } } inline void draw_glyph(const Glyph &p_gl, const RID &p_canvas, const Color &p_font_color, const Vector2 &p_ofs) { if (p_gl.font_rid != RID()) { TS->font_draw_glyph(p_gl.font_rid, p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_color); } else { TS->draw_hex_code_box(p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_color); } } inline void draw_glyph_outline(const Glyph &p_gl, const RID &p_canvas, const Color &p_font_color, const Color &p_font_shadow_color, const Color &p_font_outline_color, const int &p_shadow_outline_size, const int &p_outline_size, const Vector2 &p_ofs, const Vector2 &shadow_ofs) { if (p_gl.font_rid != RID()) { if (p_font_shadow_color.a > 0) { TS->font_draw_glyph(p_gl.font_rid, p_canvas, p_gl.font_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + shadow_ofs, p_gl.index, p_font_shadow_color); } if (p_font_shadow_color.a > 0 && p_shadow_outline_size > 0) { TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_shadow_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off) + shadow_ofs, p_gl.index, p_font_shadow_color); } if (p_font_outline_color.a != 0.0 && p_outline_size > 0) { TS->font_draw_glyph_outline(p_gl.font_rid, p_canvas, p_gl.font_size, p_outline_size, p_ofs + Vector2(p_gl.x_off, p_gl.y_off), p_gl.index, p_font_outline_color); } } } PackedStringArray Label::get_configuration_warnings() const { PackedStringArray warnings = Control::get_configuration_warnings(); // FIXME: This is not ideal and the sizing model should be fixed, // but for now we have to warn about this impossible to resolve combination. // See GH-83546. if (is_inside_tree() && get_tree()->get_edited_scene_root() != this) { // If the Label happens to be the root node of the edited scene, we don't need // to check what its parent is. It's going to be some node from the editor tree // and it can be a container, but that makes no difference to the user. Container *parent_container = Object::cast_to(get_parent_control()); if (parent_container && autowrap_mode != TextServer::AUTOWRAP_OFF && get_custom_minimum_size() == Size2()) { warnings.push_back(RTR("Labels with autowrapping enabled must have a custom minimum size configured to work correctly inside a container.")); } } // Ensure that the font can render all of the required glyphs. Ref font; if (settings.is_valid()) { font = settings->get_font(); } if (font.is_null()) { font = theme_cache.font; } if (font.is_valid()) { if (dirty || font_dirty || lines_dirty) { const_cast