From f9c31d34836d4738839dd1be3342fed6ecf23120 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=A3=8E=E9=9D=92=E5=B1=B1?= Date: Wed, 11 Feb 2026 00:41:39 +0800 Subject: [PATCH] Fix copying text across tables in `RichTextLabel` Previously, using `\r` instead of `\n` for line breaks in tables (e.g., in class references) could cause issues when copying text across tables. --- scene/gui/rich_text_label.cpp | 63 +++++++++++++++++++++++------------ scene/gui/rich_text_label.h | 4 ++- 2 files changed, 45 insertions(+), 22 deletions(-) diff --git a/scene/gui/rich_text_label.cpp b/scene/gui/rich_text_label.cpp index 7f051e89b1..464e1d2fbc 100644 --- a/scene/gui/rich_text_label.cpp +++ b/scene/gui/rich_text_label.cpp @@ -6915,38 +6915,41 @@ bool RichTextLabel::search(const String &p_string, bool p_from_selection, bool p } } -String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p_selection) const { +String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line) const { String txt; ERR_FAIL_NULL_V(p_frame, txt); ERR_FAIL_COND_V(p_line < 0 || p_line >= (int)p_frame->lines.size(), txt); + int start = 0; // Unless the current line is the `from` line. + if (!selection.from_line_found && p_frame == selection.from_frame) { + if (p_line < selection.from_line) { + return txt; // Skip the lines with smaller line numbers in the same `from` frame. + } + selection.from_line_found = true; + start = selection.from_char; + } + const bool from_this = selection.from_line_found; // Used to detect cases starting from a sub-frame. + Line &l = p_frame->lines[p_line]; Item *it_to = (p_line + 1 < (int)p_frame->lines.size()) ? p_frame->lines[p_line + 1].from : nullptr; - int end_idx = 0; - if (it_to != nullptr) { - end_idx = it_to->index; - } else { - for (Item *it = l.from; it; it = _get_next_item(it)) { - end_idx = it->index + 1; - } - } - for (Item *it = l.from; it && it != it_to; it = _get_next_item(it)) { + for (Item *it = l.from; !selection.to_line_found && it && it != it_to; it = _get_next_item(it)) { if (it->type == ITEM_TABLE) { ItemTable *table = static_cast(it); for (Item *E : table->subitems) { ERR_CONTINUE(E->type != ITEM_FRAME); // Children should all be frames. ItemFrame *frame = static_cast(E); - for (int i = 0; i < (int)frame->lines.size(); i++) { - txt += _get_line_text(frame, i, p_selection); + for (int i = 0; !selection.to_line_found && i < (int)frame->lines.size(); i++) { + txt += _get_line_text(frame, i); + } + if (selection.to_line_found) { + break; } } - } - if ((p_selection.to_item != nullptr) && (p_selection.to_item->index < l.from->index)) { continue; } - if ((p_selection.from_item != nullptr) && (p_selection.from_item->index >= end_idx)) { + if (!selection.from_line_found) { continue; } if (it->type == ITEM_DROPCAP) { @@ -6961,12 +6964,26 @@ String RichTextLabel::_get_line_text(ItemFrame *p_frame, int p_line, Selection p txt += " "; } } - if ((l.from != nullptr) && (p_frame == p_selection.to_frame) && (p_selection.to_item != nullptr) && (p_selection.to_item->index >= l.from->index) && (p_selection.to_item->index < end_idx)) { - txt = txt.substr(0, p_selection.to_char); + + if (!selection.from_line_found) { + return txt; // Empty. } - if ((l.from != nullptr) && (p_frame == p_selection.from_frame) && (p_selection.from_item != nullptr) && (p_selection.from_item->index >= l.from->index) && (p_selection.from_item->index < end_idx)) { - txt = txt.substr(p_selection.from_char); + + int chars = -1; // Unless the current line is the `to` line. + if (p_frame == selection.to_frame && p_line == selection.to_line) { + selection.to_line_found = true; + + if (from_this ^ selection.from_line_found) { + // For cases text from child frame to parent frame is considered to be on the same line, + // even if the line numbers are different. `start` has been reset to 0. + chars = selection.to_char - selection.from_char; + } else { + chars = selection.to_char - start; + } } + + txt = txt.substr(start, chars); + return txt; } @@ -7005,8 +7022,12 @@ String RichTextLabel::get_selected_text() const { String txt; int to_line = main->first_invalid_line.load(); - for (int i = 0; i < to_line; i++) { - txt += _get_line_text(main, i, selection); + + selection.from_line_found = false; + selection.to_line_found = false; + + for (int i = 0; !selection.to_line_found && i < to_line; i++) { + txt += _get_line_text(main, i); } if (selection_modifier.is_valid()) { diff --git a/scene/gui/rich_text_label.h b/scene/gui/rich_text_label.h index 021e677ddd..1a13dcf705 100644 --- a/scene/gui/rich_text_label.h +++ b/scene/gui/rich_text_label.h @@ -625,11 +625,13 @@ private: int from_line = 0; Item *from_item = nullptr; int from_char = 0; + mutable bool from_line_found = false; ItemFrame *to_frame = nullptr; int to_line = 0; Item *to_item = nullptr; int to_char = 0; + mutable bool to_line_found = false; bool double_click = false; // Selecting whole words? bool active = false; // anything selected? i.e. from, to, etc. valid? @@ -663,7 +665,7 @@ private: bool _is_click_inside_selection() const; void _find_click(ItemFrame *p_frame, const Point2i &p_click, ItemFrame **r_click_frame = nullptr, int *r_click_line = nullptr, Item **r_click_item = nullptr, int *r_click_char = nullptr, bool *r_outside = nullptr, bool p_meta = false); - String _get_line_text(ItemFrame *p_frame, int p_line, Selection p_sel) const; + String _get_line_text(ItemFrame *p_frame, int p_line) const; bool _search_table_cell(ItemTable *p_table, List::Element *p_cell, const String &p_string, bool p_reverse_search, int p_from_line); bool _search_table(ItemTable *p_table, List::Element *p_from, const String &p_string, bool p_reverse_search); bool _search_line(ItemFrame *p_frame, int p_line, const String &p_string, int p_char_idx, bool p_reverse_search);