fix(ui): a drag from the selected picture moves it under another picture

Found in review. The selected picture's frame is its own on `main`: a
drag from it moves it, though another picture lies over it there. The
routed press was the top picture's, and the drag only started for the
selected one's, so nothing moved. `PicturePart::OverSelected` says so: a
click there selects the picture on top, as before, and a drag moves the
selected one.

Test, seen failing with the part planted away:
`a_drag_from_the_selected_picture_under_another_moves_the_selected_one`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
alehelandClaude Opus 5.5 committed 2026-11-04 19:37:47 +01:00
1 parent 1ee332ae55
commit fd091beaab
3 files changed
+74 -11

No files matched your search

+3
View File
@@ -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.
+13 -11
View File
@@ -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(_) => {}
+58
View File
@@ -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]