/**************************************************************************/ /* 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/core_string_names.h" #include "core/string/print_string.h" #include "core/string/translation.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(); if (clip || overrun_behavior != TextServer::OVERRUN_NO_TRIMMING) { update_minimum_size(); } } TextServer::AutowrapMode Label::get_autowrap_mode() const { return autowrap_mode; } 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); } } bool 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_V(font.is_null(), true); 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()); } } for (int i = 0; i < TextServer::SPACING_MAX; i++) { TS->shaped_text_set_spacing(text_rid, TextServer::SpacingType(i), font->get_spacing(TextServer::SpacingType(i))); } TS->shaped_text_set_bidi_override(text_rid, structured_text_parser(st_parser, st_args, txt)); dirty = false; font_dirty = false; lines_dirty = true; // Note for future maintainers: forgetting stable width here (e.g., setting it to -1) may fix still undiscovered bugs. } if (lines_dirty) { for (int i = 0; i < lines_rid.size(); i++) { TS->free_rid(lines_rid[i]); } lines_rid.clear(); } Size2i prev_minsize = minsize; minsize = Size2(); bool can_process_lines = false; if (xl_text.length() == 0) { can_process_lines = true; lines_dirty = false; } else { // With autowrap on or off with trimming enabled, we won't compute the minimum size until width is stable // (two shape requests in a row with the same width.) This avoids situations in which the initial width is // very narrow and the label would break text into many very short lines, causing a very tall label that can // leave a deformed container. In the remaining case (namely, autowrap off and no trimming), the label is // free to dictate its own width, something that will be taken advtantage of. bool can_dictate_width = autowrap_mode == TextServer::AUTOWRAP_OFF && overrun_behavior == TextServer::OVERRUN_NO_TRIMMING; bool is_width_stable = get_size().width == stable_width; can_process_lines = can_dictate_width || is_width_stable; stable_width = get_size().width; if (can_process_lines) { if (lines_dirty) { 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]); lines_rid.push_back(line); } } if (can_dictate_width) { 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; } } width = (minsize.width - style->get_minimum_size().width); } 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. int visible_lines = lines_rid.size(); if (max_lines_visible >= 0 && visible_lines > max_lines_visible) { visible_lines = max_lines_visible; } if (autowrap_mode != TextServer::AUTOWRAP_OFF) { 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) { for (int i = 0; i < lines_rid.size(); i++) { if (i < visible_lines - 1 || lines_rid.size() == 1) { TS->shaped_text_fit_to_width(lines_rid[i], width); } else if (i == (visible_lines - 1)) { TS->shaped_text_overrun_trim_to_width(lines_rid[visible_lines - 1], 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. for (int i = 0; i < lines_rid.size(); i++) { if (horizontal_alignment == HORIZONTAL_ALIGNMENT_FILL) { TS->shaped_text_fit_to_width(lines_rid[i], width); 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, TextServer::JUSTIFICATION_WORD_BOUND | TextServer::JUSTIFICATION_KASHIDA | TextServer::JUSTIFICATION_CONSTRAIN_ELLIPSIS); } else { TS->shaped_text_overrun_trim_to_width(lines_rid[i], width, overrun_flags); } } } int last_line = MIN(lines_rid.size(), visible_lines + lines_skipped); int line_spacing = settings.is_valid() ? settings->get_line_spacing() : theme_cache.line_spacing; for (int64_t i = lines_skipped; i < last_line; i++) { minsize.height += TS->shaped_text_get_size(lines_rid[i]).y + line_spacing; } lines_dirty = false; } } else { callable_mp(this, &Label::_shape).call_deferred(); } } if (draw_pending) { queue_redraw(); draw_pending = false; } if (minsize != prev_minsize) { update_minimum_size(); } return can_process_lines; } 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); } } } void Label::_update_theme_item_cache() { Control::_update_theme_item_cache(); theme_cache.normal_style = get_theme_stylebox(SNAME("normal")); theme_cache.font = get_theme_font(SNAME("font")); theme_cache.font_size = get_theme_font_size(SNAME("font_size")); theme_cache.line_spacing = get_theme_constant(SNAME("line_spacing")); theme_cache.font_color = get_theme_color(SNAME("font_color")); theme_cache.font_shadow_color = get_theme_color(SNAME("font_shadow_color")); theme_cache.font_shadow_offset = Point2(get_theme_constant(SNAME("shadow_offset_x")), get_theme_constant(SNAME("shadow_offset_y"))); theme_cache.font_outline_color = get_theme_color(SNAME("font_outline_color")); theme_cache.font_outline_size = get_theme_constant(SNAME("outline_size")); theme_cache.font_shadow_outline_size = get_theme_constant(SNAME("shadow_outline_size")); } PackedStringArray Label::get_configuration_warnings() const { PackedStringArray warnings = Control::get_configuration_warnings(); // 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