ElyPDF: text under a covering picture is aimed; a shaky click no longer moves it (P-03)
This commit is contained in:
6 files changed
+95
-10
No files matched your search
@@ -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 };
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -26,6 +26,8 @@ pub struct Extras<'a> {
|
||||
pub stamp: Option<&'a str>,
|
||||
/// Page one: a 2x2 image, 100x50 at 72, 500.
|
||||
pub picture: bool,
|
||||
/// With `picture`: page one draws it full-page first.
|
||||
pub backdrop: bool,
|
||||
/// Fields hang under one parent of this name.
|
||||
pub parent: Option<&'a str>,
|
||||
/// A parent and child field with no widget.
|
||||
@@ -95,7 +97,11 @@ pub fn pdf_with(pages: &[(f32, f32, &[&str])], marks: &[Mark], extras: &Extras)
|
||||
_ => b"<< /Type /Outlines /Count 0 >>".to_vec(),
|
||||
});
|
||||
for (n, (w, h, lines)) in pages.iter().enumerate() {
|
||||
let mut stream = String::from("BT /F1 18 Tf 72 0 Td\n");
|
||||
let mut stream = match n == 0 && extras.backdrop {
|
||||
true => format!("q {w} 0 0 {h} 0 0 cm /Im1 Do Q\n"),
|
||||
false => String::new(),
|
||||
};
|
||||
stream.push_str("BT /F1 18 Tf 72 0 Td\n");
|
||||
for (at, line) in lines.iter().enumerate() {
|
||||
let y = h - 72.0 - at as f32 * 24.0;
|
||||
stream.push_str(&format!("1 0 0 1 72 {y} Tm ({line}) Tj\n"));
|
||||
|
||||
+1
-1
@@ -35,7 +35,7 @@
|
||||
- [ ] S-10 开始放映的两处拒绝无提示(规则 2)
|
||||
- [x] P-01 文件系统的 `PermissionDenied` 被拼成不存在的文案键,应用崩溃 — worker 的拒绝改为自有错误类型 Refused;R1 测试不跨平台、不看提示;组件库 f834f55 加 Toaster::shown;R2 PASS
|
||||
- [x] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失 — 图块随 R2.6;缩略图在编辑后留到重画;R1、R2 测试未证实际绘制;R3 PASS
|
||||
- [ ] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图
|
||||
- [x] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图 — 包住文字块的对象让位于文字,移动须过 3 个窗口像素;R1 漏掉同点的小图;R2 PASS
|
||||
- [ ] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应
|
||||
- [ ] P-05 连续快画或快点被拒:“文档在此间已改动,未执行”
|
||||
- [ ] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
FAIL
|
||||
|
||||
apps/ely-pdf/src/content.rs:190 — `rposition` 先选中最后一个对象,再过滤包围文字的对象,过滤后直接落到文字,漏掉同点命中的小图片,影响 PDF-04 的图片选择与编辑。实际 worker + GPUI 探针复现:文字框为 [72,52.65,161.01001,77.382],小图片为 [80,67,90,77],随后绘制一个 BBox 为 [0,0,612,792]、仅在远处画色块的 Form;点击可见小图片中心 (85,70),得到 `Aim::Text`,小图片整个范围均落在文字框内,无法选中来移动、替换或删除。把 `!around(&d.rect)` 放进 `rposition` 的谓词后,同一探针选中图片并通过。修复方向:反向查找时逐个跳过包围文字的对象,继续检查其余命中对象;补充文字、小图片和包围 Form 同时存在的回归测试。
|
||||
@@ -0,0 +1 @@
|
||||
PASS
|
||||
Reference in new issue
Block a user