Add triple-click paragraph selection to RichTextLabel

This commit is contained in:
Ismail Ivanov 2026-02-28 00:19:17 +01:00 committed by Ismail Ivanov
parent 634220e9fc
commit 8dabd60033
2 changed files with 85 additions and 28 deletions

View file

@ -2816,38 +2816,82 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) {
int c_index = 0;
bool outside;
selection.double_click = false;
selection.selection_mode = Selection::SINGLE_CLICK;
selection.drag_attempt = false;
_find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside, false);
if (c_item != nullptr) {
if (selection.enabled) {
// Detect triple-click.
const int triple_click_timeout = 600;
const int triple_click_tolerance = 5;
bool is_triple_click = (selection.enabled && (OS::get_singleton()->get_ticks_msec() - last_double_click) < (uint64_t)triple_click_timeout && b->get_position().distance_to(last_double_click_pos) < triple_click_tolerance);
if (is_triple_click) {
// Triple-click: select paragraph.
last_double_click = 0;
_find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside, false);
if (c_frame) {
const Line &l = c_frame->lines[c_line];
selection.from_frame = c_frame;
selection.from_line = c_line;
selection.from_item = c_item;
selection.from_char = 0;
selection.to_frame = c_frame;
selection.to_line = c_line;
selection.to_item = c_item;
selection.to_char = l.char_count;
selection.click_frame = c_frame;
selection.click_item = c_item;
selection.click_line = c_line;
selection.click_char = c_index;
// Erase previous selection.
if (selection.active) {
if (drag_and_drop_selection_enabled && _is_click_inside_selection()) {
selection.drag_attempt = true;
selection.click_item = nullptr;
} else {
selection.from_frame = nullptr;
selection.from_line = 0;
selection.from_item = nullptr;
selection.from_char = 0;
selection.to_frame = nullptr;
selection.to_line = 0;
selection.to_item = nullptr;
selection.to_char = 0;
deselect();
}
selection.active = true;
selection.selection_mode = Selection::TRIPLE_CLICK;
if (DisplayServer::get_singleton()->has_feature(DisplayServerEnums::FEATURE_CLIPBOARD_PRIMARY)) {
DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text());
}
if (!selection.drag_attempt) {
is_selecting_text = true;
click_select_held->start();
is_selecting_text = true;
click_select_held->start();
queue_accessibility_update();
queue_redraw();
}
} else {
// Regular single click.
_find_click(main, b->get_position(), &c_frame, &c_line, &c_item, &c_index, &outside, false);
if (c_item != nullptr) {
if (selection.enabled) {
selection.click_frame = c_frame;
selection.click_item = c_item;
selection.click_line = c_line;
selection.click_char = c_index;
// Erase previous selection.
if (selection.active) {
if (drag_and_drop_selection_enabled && _is_click_inside_selection()) {
selection.drag_attempt = true;
selection.click_item = nullptr;
} else {
selection.from_frame = nullptr;
selection.from_line = 0;
selection.from_item = nullptr;
selection.from_char = 0;
selection.to_frame = nullptr;
selection.to_line = 0;
selection.to_item = nullptr;
selection.to_char = 0;
deselect();
}
}
if (!selection.drag_attempt) {
is_selecting_text = true;
click_select_held->start();
}
}
}
}
@ -2895,10 +2939,12 @@ void RichTextLabel::gui_input(const Ref<InputEvent> &p_event) {
selection.click_line = c_line;
selection.click_char = c_index;
selection.double_click = true;
selection.selection_mode = Selection::DOUBLE_CLICK;
last_double_click = OS::get_singleton()->get_ticks_msec();
last_double_click_pos = b->get_position();
}
} else if (!b->is_pressed()) {
if (selection.enabled && DisplayServer::get_singleton()->has_feature(DisplayServer::FEATURE_CLIPBOARD_PRIMARY)) {
if (selection.enabled && DisplayServer::get_singleton()->has_feature(DisplayServerEnums::FEATURE_CLIPBOARD_PRIMARY)) {
DisplayServer::get_singleton()->clipboard_set_primary(get_selected_text());
}
selection.click_item = nullptr;
@ -3198,7 +3244,7 @@ void RichTextLabel::_update_selection() {
const Line &l2 = selection.click_frame->lines[selection.click_line];
if (l1.char_offset + c_index < l2.char_offset + selection.click_char) {
swap = true;
} else if (l1.char_offset + c_index == l2.char_offset + selection.click_char && !selection.double_click) {
} else if (l1.char_offset + c_index == l2.char_offset + selection.click_char && selection.selection_mode == Selection::SINGLE_CLICK) {
deselect();
return;
}
@ -3211,7 +3257,11 @@ void RichTextLabel::_update_selection() {
SWAP(selection.from_char, selection.to_char);
}
if (selection.double_click && c_frame) {
if (selection.selection_mode == Selection::TRIPLE_CLICK && c_frame) {
// Expand the selection to paragraph edges.
selection.from_char = 0;
selection.to_char = selection.to_frame->lines[selection.to_line].char_count;
} else if (selection.selection_mode == Selection::DOUBLE_CLICK && c_frame) {
// Expand the selection to word edges.
Line *l = &selection.from_frame->lines[selection.from_line];

View file

@ -632,13 +632,20 @@ private:
int to_char = 0;
mutable bool to_line_found = false;
bool double_click = false; // Selecting whole words?
enum SelectionMode {
SINGLE_CLICK,
DOUBLE_CLICK,
TRIPLE_CLICK,
};
SelectionMode selection_mode = SINGLE_CLICK;
bool active = false; // anything selected? i.e. from, to, etc. valid?
bool enabled = false; // allow selections?
bool drag_attempt = false;
};
Selection selection;
uint64_t last_double_click = 0;
Vector2 last_double_click_pos;
Callable selection_modifier;
bool deselect_on_focus_loss_enabled = true;
bool drag_and_drop_selection_enabled = true;