Merge pull request #5562 from vangel76/fix/bookmark-rename-select-5529

Select the bookmark title when creating or renaming a bookmark (#5529)
This commit is contained in:
FredApps authored and GitHub committed 2026-11-08 13:19:34 +01:00
commit 04f3421c9f
3 files changed
+118 -7

No files matched your search

+82 -5
View File
@@ -99,6 +99,24 @@ pub(crate) fn ellipsized_prefix(text: &str, mut fits: impl FnMut(&str) -> bool)
}
}
/// The inline rename field (#5529). On the frame it takes focus it selects the whole title, so
/// typing replaces the old text at once; later frames leave the user's own caret and selection
/// alone. Same pattern as the zoom box in `chrome.rs`.
fn rename_field(ui: &mut egui::Ui, text: &mut String, id: egui::Id) -> egui::Response {
let first_frame = !ui.memory(|m| m.has_focus(id));
let resp = ui.add(crate::widgets::line(text).desired_width(ui.available_width() - 8.0).id(id));
if !resp.has_focus() && !resp.lost_focus() {
if first_frame {
let mut state = egui::TextEdit::load_state(ui.ctx(), id).unwrap_or_default();
let all = egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(text.chars().count()));
state.cursor.set_char_range(Some(all));
state.store(ui.ctx(), id);
}
resp.request_focus();
}
resp
}
/// How a bookmark's title is drawn: in its own style and colour (ISO 32000-2 §12.3.3, Tables 151
/// and 152), bold in the heavier face and italic slanted, its colour kept readable on the panel.
/// Top-level bookmarks without a style keep the medium face.
@@ -149,11 +167,7 @@ pub(super) fn outline_item(
ui.horizontal(|ui| {
ui.add_space(x_text);
crate::widgets::begin_dialog(ui);
let edit = crate::widgets::line(text).desired_width(ui.available_width() - 8.0).id(id.with("rename"));
let resp = ui.add(edit);
if !resp.has_focus() && !resp.lost_focus() {
resp.request_focus();
}
let resp = rename_field(ui, text, id.with("rename"));
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
cancel = true;
} else if resp.lost_focus() {
@@ -376,3 +390,66 @@ mod bookmark_filter {
assert!(outline_matches(&outline, &fold("izmir")).is_empty());
}
}
#[cfg(test)]
mod rename_field {
use super::rename_field as field;
fn cursor_range(ctx: &egui::Context, id: egui::Id) -> Option<(usize, usize)> {
egui::TextEdit::load_state(ctx, id)
.and_then(|s| s.cursor.char_range())
.map(|r| (r.primary.index.0.min(r.secondary.index.0), r.primary.index.0.max(r.secondary.index.0)))
}
/// #5529: the rename field opens with the whole title selected, so typing replaces it.
#[test]
fn first_focus_selects_the_whole_title() {
let ctx = egui::Context::default();
let id = egui::Id::unique("bm-rename-test");
let mut text = String::from("Untitled");
for _ in 0..2 {
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
field(ui, &mut text, id);
});
output.drop_without_applying_deltas();
}
assert_eq!(cursor_range(&ctx, id), Some((0, 8)));
assert!(ctx.memory(|m| m.has_focus(id)), "the field takes focus");
}
/// A Unicode title is counted in characters, not bytes.
#[test]
fn a_multibyte_title_is_selected_in_characters() {
let ctx = egui::Context::default();
let id = egui::Id::unique("bm-rename-unicode");
let mut text = String::from("図面(一階平面図)");
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
field(ui, &mut text, id);
});
output.drop_without_applying_deltas();
assert_eq!(cursor_range(&ctx, id), Some((0, 9)));
}
/// A later frame must not reset the selection the user made; a caret stays a caret.
#[test]
fn a_later_frame_keeps_the_users_selection() {
let ctx = egui::Context::default();
let id = egui::Id::unique("bm-rename-keep");
let mut text = String::from("Chapter");
for _ in 0..2 {
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
field(ui, &mut text, id);
});
output.drop_without_applying_deltas();
}
// The user moves the caret to 3 with no selection.
let mut state = egui::TextEdit::load_state(&ctx, id).unwrap_or_default();
state.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(3))));
state.store(&ctx, id);
let output = ctx.run_ui(egui::RawInput::default(), |ui| {
field(ui, &mut text, id);
});
output.drop_without_applying_deltas();
assert_eq!(cursor_range(&ctx, id), Some((3, 3)), "the user's caret is kept");
}
}
+34
View File
@@ -119,6 +119,40 @@ fn the_panel_keeps_its_width_while_the_pointer_moves() {
assert_eq!(panel_width(&h), before, "the Bookmarks panel drifted wider");
}
/// #5529: the title is selected when the rename editor opens, so typing replaces it at once,
/// with no Ctrl+A. New bookmarks and double-click rename both get this.
#[test]
fn typing_replaces_the_title_when_the_editor_opens() {
let mut h = harness();
h.key_press_modifiers(Modifiers::COMMAND, Key::B);
h.run_steps(3);
field(&h, "Untitled").type_text("Intro"); // the whole default is replaced
h.key_press(Key::Enter);
h.run_steps(3);
assert_eq!(outline(&h), [("Intro".into(), Some(0), 0)]);
menu(&mut h, "Intro", "Rename");
field(&h, "Intro").type_text("Introduction");
h.key_press(Key::Enter);
h.run_steps(3);
assert_eq!(outline(&h)[0].0, "Introduction");
}
/// The selection is one-shot: after the user collapses it to a caret, later frames must not
/// reselect, so typing at that caret inserts.
#[test]
fn a_rename_editor_keeps_the_users_caret_across_frames() {
let mut h = harness();
h.key_press_modifiers(Modifiers::COMMAND, Key::B);
h.run_steps(3);
h.key_press(Key::ArrowLeft); // collapses the opening selection to the caret at 0
h.run_steps(2);
field(&h, "Untitled").type_text("I"); // inserts at the caret, does not replace
h.key_press(Key::Enter);
h.run_steps(3);
assert_eq!(outline(&h)[0].0, "IUntitled");
}
#[test]
fn escape_cancels_a_rename() {
let mut h = harness();
+2 -2
View File
@@ -3322,8 +3322,8 @@ milestone = "M4"
status = "shipped"
commands = []
tools = ["bookmark_rename"]
evidence = ["crates/ui-egui/tests/it/bookmarks.rs::new_rename_reorder_indent_delete_and_undo", "crates/ui-egui/tests/it/bookmarks.rs::escape_cancels_a_rename", "crates/organize/src/tests.rs::bookmarks_add_rename_move_delete", "crates/organize/src/tests.rs::bookmark_edits_keep_unknown_keys_and_touch_few_objects"]
notes = ""
evidence = ["crates/ui-egui/tests/it/bookmarks.rs::new_rename_reorder_indent_delete_and_undo", "crates/ui-egui/tests/it/bookmarks.rs::escape_cancels_a_rename", "crates/ui-egui/tests/it/bookmarks.rs::typing_replaces_the_title_when_the_editor_opens", "crates/ui-egui/tests/it/bookmarks.rs::a_rename_editor_keeps_the_users_caret_across_frames", "crates/organize/src/tests.rs::bookmarks_add_rename_move_delete", "crates/organize/src/tests.rs::bookmark_edits_keep_unknown_keys_and_touch_few_objects"]
notes = "The inline editor opens with the whole title selected, so typing replaces it at once (#5529); a caret the user moves afterwards is kept by later frames."
[[feature]]
id = "organize.bookmark-delete"