Merge pull request #111258 from Koyper/fix_rich_text_label_bullet_list_issues

[RichTextLabel] Fix bullet list font color and formatting issues
This commit is contained in:
Thaddeus Crews 2025-10-07 11:54:43 -05:00
commit ccccb9d3cb
No known key found for this signature in database
GPG key ID: 8C6E5FEB5FC03CCC
2 changed files with 145 additions and 98 deletions

View file

@ -266,6 +266,110 @@ String RichTextLabel::_get_prefix(Item *p_item, const Vector<int> &p_list_index,
return prefix + " ";
}
void RichTextLabel::_add_list_prefixes(ItemFrame *p_frame, int p_line, Line &r_l) {
Vector<int> list_index;
Vector<int> list_count;
Vector<ItemList *> list_items;
_find_list(r_l.from, list_index, list_count, list_items);
if (list_items.size() > 0) {
ItemList *this_list = list_items[0];
if (list_index[0] == 1) {
// List level start, shape all prefixes for this level and compute max. prefix width.
list_items[0]->max_width = 0;
int index = 0;
for (int i = p_line; i < (int)p_frame->lines.size(); i++) { // For all the list rows in all lists in this frame.
Line &list_row_line = p_frame->lines[i];
if (_find_list_item(list_row_line.from) == this_list) { // Is a row inside this list.
index++;
Ref<Font> font = theme_cache.normal_font;
int font_size = theme_cache.normal_font_size;
int list_row_char_ofs = list_row_line.from->char_ofs;
int item_font_size = -1;
ItemFont *found_font_item = nullptr;
Vector<Item *> formatting_items_info;
ItemText *this_row_text_item = nullptr;
Item *it = _get_next_item(this_list);
while (it && (this_row_text_item != nullptr || it->char_ofs <= list_row_char_ofs)) { // Find the ItemText for this list row. There is only one per row or none.
if (it->type == ITEM_TEXT && it->char_ofs == list_row_char_ofs) {
ItemText *text_item = static_cast<ItemText *>(it);
this_row_text_item = text_item;
// `parent` is the enclosing item tag, if any, which itself can be further enclosed by another tag and so on,
// all of which will be applied to the text item. The `parent` is an interval predecessor, not a hierarchical parent.
Item *parent = text_item->parent;
while (parent && parent != main) {
// `formatting_items` is an Array of all ITEM types affecting glyph appearance, like ITEM_FONT, ITEM_COLOR, etc.
if (formatting_items.has(parent->type)) {
formatting_items_info.push_back(parent);
}
parent = parent->parent;
}
}
it = _get_next_item(it);
}
if (this_row_text_item == nullptr) { // If the row doesn't have any text yet.
it = _get_next_item(this_list);
// All format items at the same char location should be applied to the prefix.
// This won't add any earlier tags.
while (it && it->char_ofs <= list_row_char_ofs) {
if (formatting_items.has(it->type) && it->char_ofs == list_row_char_ofs) {
formatting_items_info.push_back(it);
}
it = _get_next_item(it);
}
}
for (Item *format_item : formatting_items_info) {
switch (format_item->type) {
case ITEM_FONT: {
ItemFont *font_item = static_cast<ItemFont *>(format_item);
if (font_item->def_font != RTL_CUSTOM_FONT) {
font_item = _find_font(format_item); // Sets `def_font` based on font type.
}
if (font_item->font.is_valid()) {
if (font_item->def_font == RTL_BOLD_ITALICS_FONT) { // Always set bold italic.
found_font_item = font_item;
} else if (found_font_item == nullptr || found_font_item->def_font != RTL_BOLD_ITALICS_FONT) { // Don't overwrite BOLD_ITALIC with BOLD or ITALIC.
found_font_item = font_item;
}
}
if (found_font_item->font_size > 0) {
font_size = found_font_item->font_size;
}
} break;
case ITEM_FONT_SIZE: {
ItemFontSize *font_size_item = static_cast<ItemFontSize *>(format_item);
item_font_size = font_size_item->font_size;
} break;
case ITEM_COLOR: {
ItemColor *color_item = static_cast<ItemColor *>(format_item);
list_row_line.prefix_color = color_item->color;
} break;
case ITEM_OUTLINE_SIZE: {
ItemOutlineSize *outline_size_item = static_cast<ItemOutlineSize *>(format_item);
list_row_line.prefix_outline_size = outline_size_item->outline_size;
} break;
case ITEM_OUTLINE_COLOR: {
ItemOutlineColor *outline_color_item = static_cast<ItemOutlineColor *>(format_item);
list_row_line.prefix_outline_color = outline_color_item->color;
} break;
default: {
} break;
}
}
font = found_font_item != nullptr ? found_font_item->font : font;
font_size = item_font_size != -1 ? item_font_size : font_size;
list_index.write[0] = index;
String prefix = _get_prefix(list_row_line.from, list_index, list_items);
list_row_line.text_prefix.instantiate();
list_row_line.text_prefix->set_direction(_find_direction(list_row_line.from));
list_row_line.text_prefix->add_string(prefix, font, font_size);
list_items.write[0]->max_width = MAX(this_list->max_width, list_row_line.text_prefix->get_size().x);
}
}
}
r_l.prefix_width = this_list->max_width;
}
}
void RichTextLabel::_update_line_font(ItemFrame *p_frame, int p_line, const Ref<Font> &p_base_font, int p_base_font_size) {
ERR_FAIL_NULL(p_frame);
ERR_FAIL_COND(p_line < 0 || p_line >= (int)p_frame->lines.size());
@ -273,50 +377,8 @@ void RichTextLabel::_update_line_font(ItemFrame *p_frame, int p_line, const Ref<
Line &l = p_frame->lines[p_line];
MutexLock lock(l.text_buf->get_mutex());
// Prefix.
Vector<int> list_index;
Vector<int> list_count;
Vector<ItemList *> list_items;
_find_list(l.from, list_index, list_count, list_items);
if (list_items.size() > 0) {
if (list_index[0] == 1) {
// List level start, shape all prefixes for this level and compute max. prefix width.
list_items[0]->max_width = 0;
int index = 0;
for (int i = p_line; i < (int)p_frame->lines.size(); i++) {
Line &list_l = p_frame->lines[i];
if (_find_list_item(list_l.from) == list_items[0]) {
index++;
Ref<Font> font = theme_cache.normal_font;
int font_size = theme_cache.normal_font_size;
ItemFont *font_it = _find_font(list_l.from);
if (font_it) {
if (font_it->font.is_valid()) {
font = font_it->font;
}
if (font_it->font_size > 0) {
font_size = font_it->font_size;
}
}
ItemFontSize *font_size_it = _find_font_size(list_l.from);
if (font_size_it && font_size_it->font_size > 0) {
font_size = font_size_it->font_size;
}
list_index.write[0] = index;
String prefix = _get_prefix(list_l.from, list_index, list_items);
list_l.text_prefix.instantiate();
list_l.text_prefix->set_direction(_find_direction(list_l.from));
list_l.text_prefix->add_string(prefix, font, font_size);
list_items.write[0]->max_width = MAX(list_items[0]->max_width, list_l.text_prefix->get_size().x);
}
}
}
l.prefix_width = list_items[0]->max_width;
}
// List.
_add_list_prefixes(p_frame, p_line, l);
RID t = l.text_buf->get_rid();
int spans = TS->shaped_get_span_count(t);
@ -471,50 +533,8 @@ float RichTextLabel::_shape_line(ItemFrame *p_frame, int p_line, const Ref<Font>
l.char_offset = *r_char_offset;
l.char_count = 0;
// List prefix.
Vector<int> list_index;
Vector<int> list_count;
Vector<ItemList *> list_items;
_find_list(l.from, list_index, list_count, list_items);
if (list_items.size() > 0) {
if (list_index[0] == 1) {
// List level start, shape all prefixes for this level and compute max. prefix width.
list_items[0]->max_width = 0;
int index = 0;
for (int i = p_line; i < (int)p_frame->lines.size(); i++) {
Line &list_l = p_frame->lines[i];
if (_find_list_item(list_l.from) == list_items[0]) {
index++;
Ref<Font> font = theme_cache.normal_font;
int font_size = theme_cache.normal_font_size;
ItemFont *font_it = _find_font(list_l.from);
if (font_it) {
if (font_it->font.is_valid()) {
font = font_it->font;
}
if (font_it->font_size > 0) {
font_size = font_it->font_size;
}
}
ItemFontSize *font_size_it = _find_font_size(list_l.from);
if (font_size_it && font_size_it->font_size > 0) {
font_size = font_size_it->font_size;
}
list_index.write[0] = index;
String prefix = _get_prefix(list_l.from, list_index, list_items);
list_l.text_prefix.instantiate();
list_l.text_prefix->set_direction(_find_direction(list_l.from));
list_l.text_prefix->add_string(prefix, font, font_size);
list_items.write[0]->max_width = MAX(list_items[0]->max_width, list_l.text_prefix->get_size().x);
}
}
}
l.prefix_width = list_items[0]->max_width;
}
// List.
_add_list_prefixes(p_frame, p_line, l);
// Add indent.
l.indent = _find_margin(l.from, p_base_font, p_base_font_size) + l.prefix_width;
@ -934,9 +954,9 @@ int RichTextLabel::_draw_line(ItemFrame *p_frame, int p_line, const Vector2 &p_o
bool skip_prefix = (trim_chars && l.char_offset > visible_characters) || (trim_glyphs_ltr && (r_processed_glyphs >= visible_glyphs)) || (trim_glyphs_rtl && (r_processed_glyphs < total_glyphs - visible_glyphs));
if (l.text_prefix.is_valid() && line == 0 && !skip_prefix) {
Color font_color = _find_color(l.from, p_base_color);
int outline_size = _find_outline_size(l.from, p_outline_size);
Color font_outline_color = _find_outline_color(l.from, p_outline_color);
Color font_color = l.prefix_color == Color(0, 0, 0, 0) ? _find_color(l.from, p_base_color) : l.prefix_color;
int outline_size = l.prefix_outline_size == -1 ? _find_outline_size(l.from, p_outline_size) : l.prefix_outline_size;
Color font_outline_color = l.prefix_outline_color == Color(0, 0, 0, 0) ? _find_outline_color(l.from, p_base_color) : l.prefix_outline_color;
Color font_shadow_color = p_font_shadow_color * Color(1, 1, 1, font_color.a);
if (rtl) {
if (p_shadow_outline_size > 0 && font_shadow_color.a != 0.0) {
@ -3951,13 +3971,13 @@ void RichTextLabel::add_text(const String &p_text) {
}
if (eol) {
ItemNewline *item = memnew(ItemNewline);
ItemNewline *item = memnew(ItemNewline); // Sets item->type to ITEM_NEWLINE.
item->owner = get_instance_id();
item->rid = items.make_rid(item);
item->line = current_frame->lines.size();
_add_item(item, false);
current_frame->lines.resize(current_frame->lines.size() + 1);
if (item->type != ITEM_NEWLINE) {
if (item->type != ITEM_NEWLINE) { // item IS an ITEM_NEWLINE so this will never get called?
current_frame->lines[current_frame->lines.size() - 1].from = item;
}
_invalidate_current_line(current_frame);

View file

@ -157,10 +157,13 @@ protected:
private:
struct Item;
struct Line {
Item *from = nullptr;
struct Line { // Line is a paragraph.
Item *from = nullptr; // `from` is main if this Line is the first Line in the doc, otherwise `from` is the previous Item in the doc of any type.
Ref<TextLine> text_prefix;
Color prefix_color = Color(0, 0, 0, 0);
int prefix_outline_size = -1;
Color prefix_outline_color = Color(0, 0, 0, 0);
float prefix_width = 0;
Ref<TextParagraph> text_buf;
@ -196,17 +199,17 @@ private:
struct Item {
int index = 0;
int char_ofs = 0;
Item *parent = nullptr;
Item *parent = nullptr; // "parent" means "enclosing item tag", if any. It is an interval predecessor, not a hierarchical parent.
ItemType type = ITEM_FRAME;
List<Item *> subitems;
List<Item *>::Element *E = nullptr;
ObjectID owner;
int line = 0;
int line = 0; // `line` is the index number of the paragraph (Line) this item is inside of (zero if the first paragraph).
RID rid;
RID accessibility_item_element;
void _clear_children() {
void _clear_children() { // Only ever called on main or a paragraph (Line).
RichTextLabel *owner_rtl = ObjectDB::get_instance<RichTextLabel>(owner);
while (subitems.size()) {
Item *subitem = subitems.front()->get();
@ -507,6 +510,29 @@ private:
struct ItemContext : public Item {
ItemContext() { type = ITEM_CONTEXT; }
};
const Array formatting_items = {
// all ITEM types affecting glyph appearance.
ITEM_FONT,
ITEM_FONT_SIZE,
ITEM_FONT_FEATURES,
ITEM_COLOR,
ITEM_OUTLINE_SIZE,
ITEM_OUTLINE_COLOR,
ITEM_UNDERLINE,
ITEM_STRIKETHROUGH,
ITEM_FADE,
ITEM_SHAKE,
ITEM_WAVE,
ITEM_TORNADO,
ITEM_RAINBOW,
ITEM_BGCOLOR,
ITEM_FGCOLOR,
ITEM_META,
ITEM_HINT,
ITEM_CUSTOMFX,
ITEM_LANGUAGE,
ITEM_PULSE,
};
ItemFrame *main = nullptr;
Item *current = nullptr;
@ -711,6 +737,7 @@ private:
Size2 _get_image_size(const Ref<Texture2D> &p_image, int p_width = 0, int p_height = 0, const Rect2 &p_region = Rect2());
String _get_prefix(Item *p_item, const Vector<int> &p_list_index, const Vector<ItemList *> &p_list_items);
void _add_list_prefixes(ItemFrame *p_frame, int p_line, Line &r_l);
static int _find_unquoted(const String &p_src, char32_t p_chr, int p_from);
static Vector<String> _split_unquoted(const String &p_src, char32_t p_splitter);