ElyPDF: quick strokes and clicks queue behind the last edit instead of being refused (P-05)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 22:02:50 -04:00
1 parent 4cec71703b
commit d7a4c37c1f
7 files changed
+82 -10

No files matched your search

+2 -2
View File
@@ -254,11 +254,11 @@ impl PdfView {
return;
}
let author = ely_ui::settings::current(cx).author.clone();
self.settled_edit(what, cx, move |worker, doc| {
self.edit_at(what, move |worker, doc| {
let name = ely_platform::user::named(&author)?;
annots.iter_mut().for_each(|annot| annot.author = name.clone());
worker.annotate(doc, &annots)
});
}, cx);
}
/// A press with a drawing or form tool.
+25 -4
View File
@@ -19,6 +19,8 @@ pub struct Queued {
doc: Doc,
/// The edit may add, drop, move or reshape pages.
pages: bool,
/// Aimed at a point: runs on the state it meets.
follow: bool,
}
/// The document as an edit left it.
@@ -41,6 +43,23 @@ impl PdfView {
self.queue_edit(what, None, false, op, cx);
}
/// Queues a point-aimed edit; only moving pages refuse it.
pub fn edit_at(
&mut self,
what: &'static str,
op: impl FnOnce(&mut Worker, Doc) -> io::Result<String> + Send + 'static,
cx: &mut Context<Self>,
) {
if self.moving || self.queue.iter().any(|queued| queued.pages) {
let text = crate::viewer::Viewer::text(cx, "pdf.pages.changed", &[]);
cx.emit(crate::view::Failed(text.to_string()));
return cx.notify();
}
log::info!("pdf: queues {what}");
self.queue.push_back(Queued { what, op: Box::new(op), pick: None, doc: self.doc, pages: false, follow: true });
self.next_edit(cx);
}
/// Queues an edit to the pages themselves.
pub fn edit_pages(
&mut self,
@@ -62,7 +81,7 @@ impl PdfView {
) {
log::info!("pdf: queues {what}");
// Aimed at what is shown now; later states refuse it.
self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc, pages });
self.queue.push_back(Queued { what, op: Box::new(op), pick, doc: self.doc, pages, follow: false });
self.next_edit(cx);
}
@@ -70,10 +89,12 @@ impl PdfView {
if self.editing {
return;
}
let Some(Queued { what, op, pick, doc, pages }) = self.queue.pop_front() else {
let Some(Queued { what, op, pick, doc, pages, follow }) = self.queue.pop_front() else {
return;
};
self.editing = true;
// Earlier edits have landed; a point still means the same.
let doc = if follow { self.doc } else { doc };
(self.editing, self.moving) = (true, pages);
self.off(
move |worker| {
let mark = op(worker, doc)?;
@@ -83,7 +104,7 @@ impl PdfView {
Ok(Fresh { doc, mark, pages, first, marks: worker.outline(doc)? })
},
move |view, fresh, cx| {
view.editing = false;
(view.editing, view.moving) = (false, false);
match fresh {
Ok(fresh) => {
view.refresh(fresh, pages, cx);
+45
View File
@@ -315,3 +315,48 @@ fn a_default_name_avoids_a_field_with_no_widget() {
send(cx, &viewer, Change::Design(typed, Vec::new()));
assert_eq!(notes(cx, &view).fields.len(), 1, "the field lands");
}
#[test]
fn quick_strokes_queue_behind_each_other() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let path = dir.path().join("one.pdf");
let page: (f32, f32, &[&str]) = (612.0, 792.0, &["Hello"]);
std::fs::write(&path, pdf_with(&[page], &[], &Extras::default())).expect("writes");
let (viewer, cx) = windowed(&mut cx, dir.path(), &[path], true);
let view = shown(cx, &viewer);
let note = |y: f32| ely_pdf_engine::NewAnnot { page: 0, shape: ely_pdf_engine::Shape::Note((100.0, y)), rgb: None, contents: "n".into(), author: String::new() };
// The second lands while the first still runs.
cx.update(|_, app| view.update(app, |view, cx| {
view.annotate("a first note", vec![note(500.0)], cx);
view.annotate("a second note", vec![note(400.0)], cx);
}));
settle(cx, |app| view.read(app).settled());
cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.side.update(cx, |side, cx| {
side.pane = crate::side::Pane::Notes;
cx.notify();
})));
let count = |cx: &mut gpui::VisualTestContext| {
settle(cx, |app| view.read(app).settled() && view.read(app).notes.contains_key(&0));
cx.update(|_, app| view.read(app).notes[&0].annots.len())
};
assert_eq!(count(cx), 2, "both notes landed");
let rotate = |view: &mut PdfView, cx: &mut gpui::Context<PdfView>| {
view.edit_pages("rotating", |worker, doc| worker.rotate_pages(doc, &[0], 90), cx)
};
// A running rotation would move the point: refused.
cx.update(|_, app| view.update(app, |view, cx| {
rotate(view, cx);
view.annotate("a note while turning", vec![note(300.0)], cx);
}));
assert_eq!(count(cx), 2, "refused while pages move");
// A rotation waiting behind another edit: refused too.
cx.update(|_, app| view.update(app, |view, cx| {
view.annotate("a third note", vec![note(250.0)], cx);
rotate(view, cx);
view.annotate("a note behind a turn", vec![note(200.0)], cx);
}));
assert_eq!(count(cx), 3, "only the note before the turn");
cx.update(|_, app| view.update(app, |view, cx| view.annotate("a settled note", vec![note(150.0)], cx)));
assert_eq!(count(cx), 4, "taken once pages are still");
}
+3
View File
@@ -65,6 +65,8 @@ pub struct PdfView {
/// Edits waiting their turn, and whether one runs.
pub queue: VecDeque<crate::edits::Queued>,
pub editing: bool,
/// The running edit may move pages.
pub moving: bool,
/// History marks: now, and at the last load or save.
pub mark: String,
pub saved: String,
@@ -186,6 +188,7 @@ impl PdfView {
edited: 0,
queue: VecDeque::new(),
editing: false,
moving: false,
saved: loaded.mark.clone(),
mark: loaded.mark,
thumbs: HashMap::new(),
+4 -3
View File
@@ -92,15 +92,16 @@ impl PdfView {
rgb: 0,
bx: [at[0], at[1], self.sizes[page].0 - at[0]],
};
self.settled_edit("placing a typed signature", cx, move |worker, doc| match worker.put_text(doc, page, &writing, 0.0)? {
let write = move |worker: &mut ely_pdf_engine::Worker, doc| match worker.put_text(doc, page, &writing, 0.0)? {
Wrote::Done(mark) => Ok(mark),
Wrote::Missing(c) => Err(io::Error::new(io::ErrorKind::InvalidInput, format!("the font has no “{c}”"))),
Wrote::Preview(..) => Err(io::Error::other("a preview where a write was asked")),
});
};
self.edit_at("placing a typed signature", write, cx);
}
Stamp::Picture { path, aspect } => {
let at = placed(aspect, rect, click);
self.settled_edit("placing a signature picture", cx, move |worker, doc| worker.add_image(doc, page, &path, at));
self.edit_at("placing a signature picture", move |worker, doc| worker.add_image(doc, page, &path, at), cx);
}
}
}
+2
View File
@@ -73,6 +73,7 @@ pub(super) const EN: &[(&str, &str)] = &[
"pdf.object.changed",
"The document changed meanwhile; nothing was done.",
),
("pdf.pages.changed", "Pages are moving; nothing was done. Try again."),
("pdf.tool.edit", "Edit Text and Images"),
("pdf.tool.text", "Add Text"),
("pdf.tool.image", "Add Image…"),
@@ -290,6 +291,7 @@ pub(super) const ZH_CN: &[(&str, &str)] = &[
("pdf.object.replace", "替换图片…"),
("pdf.object.delete", "删除"),
("pdf.object.changed", "文档已改动,未执行。"),
("pdf.pages.changed", "页面正在变动,未执行,请再试。"),
("pdf.tool.edit", "编辑文字和图片"),
("pdf.tool.text", "添加文字"),
("pdf.tool.image", "添加图片…"),
+1 -1
View File
@@ -37,7 +37,7 @@
- [x] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失 — 图块随 R2.6;缩略图在编辑后留到重画;R1、R2 测试未证实际绘制;R3 PASS
- [x] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图 — 包住文字块的对象让位于文字,移动须过 3 个窗口像素;R1 漏掉同点的小图;R2 PASS
- [x] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应 — 整页一遍干跑判定全部文字块(1000 块 1.5 s→0.1 s);R1 转向文字后错位、共用原点只计一块;R2 PASS
- [ ] P-05 连续快画或快点被拒:“文档在此间已改动,未执行”
- [x] P-05 连续快画或快点被拒:“文档在此间已改动,未执行” — 按坐标的编辑排队、跑在出队时的状态上,只在页面变动时拒绝;R2 PASS
- [ ] P-06 默认字体“原字体”在 Type 1/Type 3 字体的文件上失败,只给引擎原话
- [ ] P-07 含连字的文字块永远不可编辑
- [ ] P-08 页面读取失败后每帧重试、每帧弹提示