mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-09 04:22:20 +00:00
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:
3 files changed
+118
-7
No files matched your search
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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();
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user