diff --git a/crates/ui-egui/src/canvas/page_router.rs b/crates/ui-egui/src/canvas/page_router.rs index 2479e9860b..a8a07e6ad7 100644 --- a/crates/ui-egui/src/canvas/page_router.rs +++ b/crates/ui-egui/src/canvas/page_router.rs @@ -48,6 +48,9 @@ pub(crate) enum Owner { pub(crate) enum PicturePart { /// The picture: a click selects it, a drag moves the selected one. Body, + /// A picture lying over the selected one, within the selected one's frame: a click selects the + /// one on top, a drag moves the selected one. + OverSelected, /// A resize grip in `ImageObb::handles` order (four corners and four edge midpoints). Corner(usize), /// The selected picture's rotate knob: a drag turns it. diff --git a/crates/ui-egui/src/tools/edit_text_ui/image.rs b/crates/ui-egui/src/tools/edit_text_ui/image.rs index f39c644487..7a7ad701a2 100644 --- a/crates/ui-egui/src/tools/edit_text_ui/image.rs +++ b/crates/ui-egui/src/tools/edit_text_ui/image.rs @@ -611,17 +611,19 @@ pub(crate) fn body_hit(xf: &PageXform, page: usize, info: &DocInfo, images: &[pd } let (x, y) = xf.screen_to_view(p); let user = info.pages.get(page)?.view_to_user(x, y).map(f64::from); - let index = images - .iter() - .rposition(|im| im.visible_rect.is_some_and(|r| xf.user_rect(info, page, r.map(|v| v as f32)).contains(p)) && im.contains(user)) - .or_else(|| { - let s = view.image_selection.as_ref().filter(|s| s.page == page && s.crop.is_none())?; - let image = images.get(s.index)?; - quad_contains(ImageObb::from_image(image, xf, info, page).corners(), p).then_some(s.index) - })?; + let on_picture = |im: &pdfcraft_engine::PageImage| { + im.visible_rect.is_some_and(|r| xf.user_rect(info, page, r.map(|v| v as f32)).contains(p)) && im.contains(user) + }; + // The selected picture's frame is its own, a hole in it and a picture over it included. + let framed = + view.image_selection.as_ref().filter(|s| s.page == page && s.crop.is_none()).map(|s| s.index).filter(|i| { + images.get(*i).is_some_and(|image| on_picture(image) || quad_contains(ImageObb::from_image(image, xf, info, page).corners(), p)) + }); + let index = images.iter().rposition(on_picture).or(framed)?; let selected = view.image_selection.as_ref().is_some_and(|s| s.page == page && s.index == index); let icon = if selected { egui::CursorIcon::Move } else { egui::CursorIcon::PointingHand }; - Some(Hit::new(Owner::Picture { index, part: PicturePart::Body }, Layer::Text, icon)) + let part = if framed.is_some_and(|i| i != index) { PicturePart::OverSelected } else { PicturePart::Body }; + Some(Hit::new(Owner::Picture { index, part }, Layer::Text, icon)) } /// What a right-click on a selected image asks for. @@ -785,7 +787,7 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage && ptr.drag_started && let Some(o) = ptr.origin && let PagePress::Mine(Owner::Picture { index, part }) = press - && index == i + && (index == i || part == PicturePart::OverSelected) && let Some(s) = view.image_selection.as_mut() { match part { @@ -803,7 +805,7 @@ pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage }); } } - PicturePart::Body => { + PicturePart::Body | PicturePart::OverSelected => { s.drag = Some(ImageDrag { start: o, target: DragTarget::Move, initial_rect: b, initial_matrix: image.matrix }); } PicturePart::Crop(_) => {} diff --git a/crates/ui-egui/tests/it/editing.rs b/crates/ui-egui/tests/it/editing.rs index 06ff943bb5..cd82d95aec 100644 --- a/crates/ui-egui/tests/it/editing.rs +++ b/crates/ui-egui/tests/it/editing.rs @@ -7377,6 +7377,64 @@ fn a_right_click_on_an_open_paragraphs_frame_over_a_picture_selects_the_picture( assert!(h.query_by_label("Flip horizontal").is_some(), "its menu opens"); } +/// A drag from the selected picture moves it, though another picture lies over it there; a click +/// there selects the one on top. +#[test] +fn a_drag_from_the_selected_picture_under_another_moves_the_selected_one() { + let mut h = Harness::builder().with_size(egui::vec2(1400.0, 1400.0)).build_eframe(|_cc| { + let mut app = PdfCraftApp::new(); + app.set_option("language", "en").unwrap(); + app.open_bytes("two.pdf", None, two_image_fixture()).unwrap(); + app + }); + h.run_steps(4); + // The second picture lies over the right half of the first. + h.state_mut().apply_edit(pdfcraft_engine::Edit::EditPageImage { + page: 0, + index: 1, + change: pdfcraft_engine::ImageEdit::Move([110.0, 100.0, 130.0, 120.0]), + }); + assert!(h.state_mut().execute("edit.edit_text")); + h.run_steps(4); + h.state_mut().views[0].image_selection = Some(pdfcraft_ui_egui::ImageSelection::at(0, 0)); + h.run_steps(2); + let id = h.state().views[0].id; + let rects = |h: &Harness<'static, PdfCraftApp>| { + let images = h.state().session.get(id).unwrap().page_images(0); + (images[0].rect, images[1].rect) + }; + let before = rects(&h); + let xf = h.state().views[0].page_xform(0).unwrap(); + let doc = h.state().session.get(id).unwrap(); + let images = doc.page_images(0); + let frame = |i: usize| pdfcraft_ui_egui::canvas::image_screen_rect(&images[i], &xf, &doc.info, 0); + let overlap = frame(0).intersect(frame(1)).center(); + h.hover_at(overlap); + h.run_steps(1); + h.drag_at(overlap); + h.run_steps(1); + for k in 1..=4 { + h.hover_at(overlap + egui::vec2(8.0 * k as f32, 0.0)); + h.run_steps(1); + } + h.drop_at(overlap + egui::vec2(32.0, 0.0)); + h.run_steps(3); + let after = rects(&h); + assert_ne!(after.0, before.0, "the selected picture moves"); + assert_eq!(after.1, before.1, "the one over it stays"); + let selected = h.state().views[0].image_selection.as_ref().map(|s| s.index); + assert_eq!(selected, Some(0), "and stays selected"); + // After the move the two still overlap, a little further along. + let xf = h.state().views[0].page_xform(0).unwrap(); + let doc = h.state().session.get(id).unwrap(); + let images = doc.page_images(0); + let frame = |i: usize| pdfcraft_ui_egui::canvas::image_screen_rect(&images[i], &xf, &doc.info, 0); + let overlap = frame(0).intersect(frame(1)).center(); + slipping_click(&mut h, overlap, overlap); + let selected = h.state().views[0].image_selection.as_ref().map(|s| s.index); + assert_eq!(selected, Some(1), "a click selects the one on top"); +} + /// In the page grid, Delete deletes the page chosen there. The picture selected before the grid /// opened is not on screen, and stays. #[test]