From caf189522fac9d239f4897e4884032a4f2c81f87 Mon Sep 17 00:00:00 2001 From: Hitesh Date: Thu, 22 Oct 2026 23:18:01 +0530 Subject: [PATCH] fix(edit): a refused delete keeps the object selection Review on #4111: Delete cleared the selection before the edit ran, so a delete the engine refuses (text used as a clipping path) left the document as it was but dropped what the user had selected. The selection now stays until the edit applies; `ObjectSelection::prepare` already clears it when the document changes and keeps it when an edit is refused. Co-Authored-By: Claude Opus 5.5 --- crates/ui-egui/src/object_ui.rs | 3 ++- crates/ui-egui/tests/it/editing.rs | 25 +++++++++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/ui-egui/src/object_ui.rs b/crates/ui-egui/src/object_ui.rs index 7f81f76a2..04e37feee 100644 --- a/crates/ui-egui/src/object_ui.rs +++ b/crates/ui-egui/src/object_ui.rs @@ -396,8 +396,9 @@ pub(crate) fn delete_keys(ctx: &egui::Context, view: &mut DocView) { if !ctx.input_mut(|i| i.consume_key(Modifiers::NONE, Key::Delete) || i.consume_key(Modifiers::NONE, Key::Backspace)) { return; } + // The selection stays until the edit applies: `prepare` clears it when the document changes, + // and a refused delete (text used as a clipping path) leaves it selected. view.pending_edit = delete_edit(page, view.objects.selected.iter().map(|o| o.target).collect()); - view.objects.clear(); } /// The edit that deletes `targets` (indexes into one inventory of `page`) as one undo step. diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 38c4ddbdb..4a35390c9 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -4219,6 +4219,30 @@ fn an_ordinary_paragraph_drag_refusal_never_rewrites_the_text() { assert!(h.state().toast.as_ref().unwrap().0.contains("Text used as a clipping path cannot be moved with this selection.")); } +/// A delete the engine refuses (text used as a clipping path) leaves the document as it was and +/// keeps the selection, so the user can still move or nudge what they had selected. +#[test] +fn a_refused_delete_keeps_the_object_selection() { + let content = "q BT 4 Tr /F1 12 Tf 60 320 Td (Clipping text) Tj ET Q"; + let mut h = paragraph_drag_harness(paragraph_drag_fixture(content, 0, 1), 0); + let id = h.state().views[0].id; + let (generation, bytes) = { + let doc = h.state().session.get(id).unwrap(); + (doc.edit_generation(), doc.bytes.clone()) + }; + // An area selection from empty page space around the paragraph. + let (center, _) = paragraph_drag_points(&h, "Clipping text", [0.0, 0.0]); + drag(&mut h, center - egui::vec2(60.0, 25.0), center + egui::vec2(60.0, 25.0)); + h.get_by_label("1 object selected"); + h.key_press(Key::Delete); + h.run_steps(4); + let doc = h.state().session.get(id).unwrap(); + assert_eq!((doc.edit_generation(), &doc.bytes), (generation, &bytes), "the refused delete changed nothing"); + assert!(doc.can_undo().is_none()); + assert!(h.state().toast.is_some(), "the refusal is reported"); + h.get_by_label("1 object selected"); +} + #[test] fn an_ordinary_paragraph_drag_cannot_reuse_an_index_after_an_edit() { let content = "BT /F1 12 Tf 60 120 Td (Discard) Tj ET BT /F1 12 Tf 60 320 Td (Moving alpha) Tj ET BT /F1 12 Tf 60 220 Td (Keep here) Tj ET"; @@ -6002,6 +6026,7 @@ fn a_paragraph_selected_by_its_frame_deletes_with_one_key() { press(&mut h, Modifiers::NONE, key); assert!(blocks(&h).is_empty(), "{key:?} deletes the whole paragraph: {:?}", blocks(&h)); assert!(h.state().views[0].line_editor.is_none(), "no empty editor is left behind"); + assert!(h.query_all_by_label_contains("object selected").next().is_none(), "the deleted paragraph is no longer selected"); assert_eq!(h.state().session.get(id).unwrap().text_blocks(1)[0].text, "Page 2", "the next page keeps its text"); assert_eq!(h.state().session.get(id).unwrap().info.pages.len(), 2, "Delete removed the paragraph, not a page"); assert!(h.state_mut().execute("edit.undo"));