ElyPDF: text under a covering picture is aimed; a shaky click no longer moves it (P-03)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 13:05:48 -04:00
1 parent 04040a2a03
commit 4d8d5a6ef5
6 files changed
+95 -10

No files matched your search

+24 -7
View File
@@ -76,6 +76,10 @@ pub struct Drag {
pub from: (f32, f32),
pub now: (f32, f32),
pub scale: bool,
/// Where the press landed, in window pixels.
pub pressed: (f32, f32),
/// The pointer went past a click's slack.
pub moved: bool,
}
/// The corner handle's reach, in page points.
@@ -107,6 +111,11 @@ fn inside(rect: &[f32; 4], (x, y): (f32, f32)) -> bool {
x >= rect[0] && x <= rect[2] && y >= rect[1] && y <= rect[3]
}
/// Whether `outer` holds all of `inner`.
fn covers(outer: &[f32; 4], inner: &[f32; 4]) -> bool {
outer[0] <= inner[0] && outer[1] <= inner[1] && outer[2] >= inner[2] && outer[3] >= inner[3]
}
impl PdfView {
/// A drag or drawing is under way.
pub fn gesturing(&self) -> bool {
@@ -174,16 +183,21 @@ impl PdfView {
Some(Aim::Object { page: on, index }) if on == page => layer.drawn.get(index).filter(|d| corner(&d.rect)).map(|_| (index, true)),
_ => None,
};
let hit = held.or_else(|| layer.drawn.iter().rposition(|d| inside(&d.rect, point)).map(|index| (index, false)));
let text = layer.blocks.iter().position(|b| b.editable && inside(&b.rect, point));
// Text wins over an object drawn all around it.
let around = |rect: &[f32; 4]| text.is_some_and(|t| covers(rect, &layer.blocks[t].rect));
let hit = held.or_else(|| {
let object = layer.drawn.iter().rposition(|d| inside(&d.rect, point) && !around(&d.rect));
object.map(|index| (index, false))
});
self.aim = match hit {
Some((index, scale)) => {
let rect = layer.drawn[index].rect;
self.drag = Some(Drag { page, index, rect, from: point, now: point, scale });
let pressed = (f32::from(at.x), f32::from(at.y));
self.drag = Some(Drag { page, index, rect, from: point, now: point, scale, pressed, moved: false });
Some(Aim::Object { page, index })
}
None => (layer.blocks.iter())
.position(|b| b.editable && inside(&b.rect, point))
.map(|index| Aim::Text { page, index }),
None => text.map(|index| Aim::Text { page, index }),
};
cx.notify();
}
@@ -193,6 +207,8 @@ impl PdfView {
return;
};
if let Some(drag) = self.drag.as_mut().filter(|drag| drag.page == page) {
let far = (f32::from(at.x) - drag.pressed.0).abs() + (f32::from(at.y) - drag.pressed.1).abs();
drag.moved |= far >= crate::annotate::CLICK;
drag.now = point;
cx.notify();
}
@@ -203,7 +219,8 @@ impl PdfView {
let Some(drag) = self.drag.take() else {
return;
};
if (drag.now.0 - drag.from.0).abs() < 1.0 && (drag.now.1 - drag.from.1).abs() < 1.0 {
// A hand that shakes a little still clicks.
if !drag.moved {
return cx.notify();
}
// A queued edit may renumber what was dragged.
@@ -264,7 +281,7 @@ mod tests {
#[test]
fn a_drag_moves_or_scales_from_the_top_left() {
let mut drag = Drag { page: 0, index: 0, rect: [10.0, 20.0, 110.0, 70.0], from: (50.0, 40.0), now: (60.0, 45.0), scale: false };
let mut drag = Drag { page: 0, index: 0, rect: [10.0, 20.0, 110.0, 70.0], from: (50.0, 40.0), now: (60.0, 45.0), scale: false, pressed: (0.0, 0.0), moved: true };
assert_eq!(drag.change(), [1.0, 0.0, 0.0, 1.0, 10.0, 5.0]);
assert_eq!(drag.shown(), [20.0, 25.0, 120.0, 75.0]);
drag = Drag { from: (110.0, 70.0), now: (210.0, 80.0), scale: true, ..drag };
+59 -1
View File
@@ -1,6 +1,6 @@
//! Editing text and objects from the app (C11.3).
use ely_pdf_engine::sample::{Extras, pdf_with};
use ely_pdf_engine::sample::{Extras, one_page, pdf_with};
use gpui::{Entity, Modifiers, MouseButton, TestAppContext, VisualTestContext};
use super::input::over;
@@ -339,3 +339,61 @@ fn a_drag_follows_the_hand_when_the_page_shifts_under_it() {
let rect = cx.update(|_, app| view.read(app).layers[&0].drawn[1].rect);
assert!((rect[0] - 122.0).abs() < 0.5, "moved as far as the hand: {rect:?}");
}
#[test]
fn text_on_a_full_page_picture_aims_at_the_text_and_a_shake_stays_a_click() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let path = dir.path().join("scan.pdf");
let extras = Extras { picture: true, backdrop: true, ..Extras::default() };
std::fs::write(&path, pdf_with(&[(612.0, 792.0, &["Hello world"])], &[], &extras)).expect("writes");
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
let view = shown(cx, &viewer);
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
cx.update(|_, app| view.update(app, |view, cx| {
view.zoom = crate::layout::Zoom::Scale(1.0);
view.set_tool(Tool::Edit, cx)
}));
settle(cx, |app| view.read(app).layers.contains_key(&0));
click(cx, &view, (80.0, 66.0));
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Text { .. })), "the text, not the picture under it");
// A two-pixel wobble on the picture moves nothing.
let from = over(cx, &view, 0, (300.0, 400.0));
cx.simulate_mouse_down(from, MouseButton::Left, Modifiers::none());
let to = from + gpui::point(gpui::px(2.0), gpui::px(0.0));
cx.simulate_mouse_move(to, MouseButton::Left, Modifiers::none());
cx.simulate_mouse_up(to, MouseButton::Left, Modifiers::none());
frames(cx);
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Object { .. })), "the picture is aimed");
assert_eq!(cx.update(|_, app| view.read(app).edited), 0, "and not moved");
}
#[test]
fn a_small_picture_on_text_under_a_page_form_still_aims() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let path = dir.path().join("layered.pdf");
// The page-size form comes last, on top of both.
let content = "q 10 0 0 10 80 722 cm /Im1 Do Q BT /F1 18 Tf 72 720 Td (Hello world) Tj ET q /Fm1 Do Q";
let form = "0 0 1 rg 500 50 20 20 re f";
let objects = [
"<< /Type /Font /Subtype /Type1 /BaseFont /Helvetica >>".to_string(),
format!("<< /Type /XObject /Subtype /Form /BBox [0 0 612 792] /Length {} >>\nstream\n{form}\nendstream", form.len()),
"<< /Type /XObject /Subtype /Image /Width 1 /Height 1 /ColorSpace /DeviceGray /BitsPerComponent 8 /Length 1 >>\nstream\nA\nendstream".to_string(),
];
let page = " /Resources << /Font << /F1 5 0 R >> /XObject << /Fm1 6 0 R /Im1 7 0 R >> >>";
std::fs::write(&path, one_page("", page, content, &objects)).expect("writes");
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
let view = shown(cx, &viewer);
settle(cx, |app| view.read(app).room.size.width > gpui::px(100.0));
cx.update(|_, app| view.update(app, |view, cx| {
view.zoom = crate::layout::Zoom::Scale(1.0);
view.set_tool(Tool::Edit, cx)
}));
settle(cx, |app| view.read(app).layers.contains_key(&0));
// The picture sits at 80..90 across, 60..70 down.
click(cx, &view, (85.0, 65.0));
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Object { index: 0, .. })), "the small picture");
click(cx, &view, (140.0, 66.0));
assert!(matches!(cx.update(|_, app| view.read(app).aim), Some(Aim::Text { .. })), "the text, not the form around it");
}