ElyPDF: page pictures stay through an edit until they repaint (P-02)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-09 12:50:11 -04:00
1 parent 139e8f6250
commit 04040a2a03
8 files changed
+60 -9

No files matched your search

+6 -4
View File
@@ -115,9 +115,14 @@ impl PdfView {
// A page may now stand where another stood.
self.retire_tiles();
self.sizes = measured;
self.thumbs_stale.clear();
for (_, image) in self.thumbs.drain() {
self.retired.push(image);
}
} else {
// Same pages: old tiles stay under them until repainted.
// Same pages: old pictures stay until repainted.
self.keep_all();
self.thumbs_stale.extend(self.thumbs.keys().copied());
let read = fresh.first.len();
self.sizes[..read].copy_from_slice(&measured[..read]);
}
@@ -125,9 +130,6 @@ impl PdfView {
self.failed.clear();
self.text.clear();
self.asking_text.clear();
for (_, image) in self.thumbs.drain() {
self.retired.push(image);
}
self.thumbs_failed.clear();
// Indices shift with any edit; aims start over.
(self.layers, self.aim, self.drag) = (Default::default(), None, None);
+13 -4
View File
@@ -1,6 +1,6 @@
//! Page pictures, and bookmarks if any (PDF-01, PDF-02).
use std::{collections::BTreeSet, ops::Range};
use std::{collections::BTreeSet, ops::Range, sync::Arc};
use ely_gpui_component::{
buttons::SegmentedControl,
@@ -9,7 +9,7 @@ use ely_gpui_component::{
theme::{ActiveTheme as _, ControlSize},
};
use gpui::{
Action, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, Role,
Action, App, Context, Entity, InteractiveElement as _, IntoElement, ParentElement as _, Render, RenderImage, Role,
StatefulInteractiveElement as _, Styled as _, WeakEntity, Window, div, img, prelude::FluentBuilder as _, px,
uniform_list,
};
@@ -104,7 +104,8 @@ impl PdfView {
if self.thumbing {
return;
}
let Some(page) = rows.clone().find(|page| !self.thumbs.contains_key(&(*page, dark)) && !self.thumbs_failed.contains(page)) else {
let due = |page: &usize| !self.thumbs.contains_key(&(*page, dark)) || self.thumbs_stale.contains(&(*page, dark));
let Some(page) = rows.clone().find(|page| due(page) && !self.thumbs_failed.contains(page)) else {
return;
};
let (w, h) = self.sizes[page];
@@ -127,6 +128,7 @@ impl PdfView {
});
match painted.map(|bgra| crate::pages::picture((tile[2] as u32, tile[3] as u32), bgra)) {
Ok(Some(image)) => {
view.thumbs_stale.remove(&(page, dark));
if let Some(old) = view.thumbs.insert((page, dark), image) {
view.retired.push(old);
}
@@ -145,13 +147,20 @@ impl PdfView {
}
}
impl PdfView {
/// The picture a row draws; an old one until repainted.
pub fn picture(&self, page: usize, dark: bool) -> Option<Arc<RenderImage>> {
self.thumbs.get(&(page, dark)).cloned()
}
}
/// One row: the page number and its picture.
fn row(view: &Entity<PdfView>, page: usize, dark: bool, cx: &App) -> gpui::AnyElement {
let read = view.read(cx);
let colors = &cx.theme().colors;
let (w, h) = read.sizes[page];
let picked = read.picked.contains(&page);
let picture = read.thumbs.get(&(page, dark)).cloned();
let picture = read.picture(page, dark);
let count = read.sizes.len();
let label = Viewer::text(cx, "pdf.page.label", &[("n", &(page + 1).to_string())]);
let (clicked, pressed, dropped) = (view.clone(), view.clone(), view.clone());
+28
View File
@@ -326,3 +326,31 @@ fn old_tiles_are_painted_and_freed_with_their_tab() {
cx.update(|_, app| view.update(app, |view, _| view.retire_all()));
assert!(cx.update(|_, app| view.read(app).backdrop.is_empty()), "closing hands every old tile over");
}
#[test]
fn an_edit_keeps_page_pictures_until_they_repaint() {
let dir = tempfile::tempdir().expect("a temp dir");
let mut cx = TestAppContext::single();
let (viewer, cx) = windowed(&mut cx, dir.path(), &[file(dir.path(), "one.pdf", &[(612.0, 792.0, &["Hello"])])], true);
let view = shown(cx, &viewer);
let dark = cx.update(|_, app| ely_gpui_component::theme::ActiveTheme::theme(app).is_dark());
settle(cx, |app| view.read(app).picture(0, dark).is_some() && repainted(view.read(app)));
let old = cx.update(|_, app| view.read(app).picture(0, dark).expect("a picture"));
// A picture on its way holds back the repaint.
cx.update(|_, app| view.update(app, |view, _| view.thumbing = true));
cx.update(|_, app| view.update(app, |view, cx| view.annotate("adding a note", vec![note()], cx)));
settle(cx, |app| view.read(app).edited == 1 && repainted(view.read(app)));
// Dropped from the atlas, it returns only if drawn.
cx.update(|window, _| window.drop_image(old.clone())).expect("drops");
super::frames(cx);
assert!(cx.update(|window, _| window.has_image_atlas_entry(&old)), "the old picture shows meanwhile");
cx.update(|_, app| view.update(app, |view, cx| {
view.thumbing = false;
cx.notify()
}));
settle(cx, |app| view.read(app).thumbs_stale.is_empty());
let new = cx.update(|_, app| view.read(app).picture(0, dark).expect("a picture"));
assert!(!std::sync::Arc::ptr_eq(&new, &old), "the new picture replaces it");
super::frames(cx);
assert!(cx.update(|window, _| window.has_image_atlas_entry(&new)), "and is drawn");
}
+3
View File
@@ -71,6 +71,8 @@ pub struct PdfView {
/// Page pictures for the side panel, by page and darkness.
pub thumbs: HashMap<(usize, bool), Arc<RenderImage>>,
pub thumbs_failed: HashSet<usize>,
/// Pictures from before an edit; shown until repainted.
pub thumbs_stale: HashSet<(usize, bool)>,
/// A picture is on its way from the worker.
pub thumbing: bool,
/// Pages picked in the side panel, and the anchor.
@@ -188,6 +190,7 @@ impl PdfView {
mark: loaded.mark,
thumbs: HashMap::new(),
thumbs_failed: HashSet::new(),
thumbs_stale: HashSet::new(),
thumbing: false,
picked: BTreeSet::new(),
anchor: None,
+1 -1
View File
@@ -34,7 +34,7 @@
- [ ] S-09 改尺寸失败后 `size_asked` 一直留着
- [ ] S-10 开始放映的两处拒绝无提示(规则 2)
- [x] P-01 文件系统的 `PermissionDenied` 被拼成不存在的文案键,应用崩溃 — worker 的拒绝改为自有错误类型 Refused;R1 测试不跨平台、不看提示;组件库 f834f55 加 Toaster::shown;R2 PASS
- [ ] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失
- [x] P-02 每次编辑落地清掉全部图块与缩略图,页面空白、刚画的墨迹消失 — 图块随 R2.6;缩略图在编辑后留到重画;R1、R2 测试未证实际绘制;R3 PASS
- [ ] P-03 “编辑文字和图片”先命中图片对象再命中文字;一点抖动就移动了背景图
- [ ] P-04 `TEXT_BLOCKS` 对每个文字块重跑一遍整页内容过滤,编辑工具长时间无反应
- [ ] P-05 连续快画或快点被拒:“文档在此间已改动,未执行”
+3
View File
@@ -0,0 +1,3 @@
FAIL
apps/ely-pdf/src/tests/input.rs:340 — 新增回归测试仅检查缩略图缓存与 Arc 身份,P-02 要求的等待期间持续绘制旧图缺少断言。具体复现:在隔离副本中将 side.rs:156 的图片读取加上 `.filter(|_| !read.thumbs_stale.contains(&(page, dark)))`,编辑后的过期缩略图随即停止绘制,新增测试 `an_edit_keeps_page_pictures_until_they_repaint` 仍通过。另一个探针暂停替换请求、清除旧图的图集项并重绘,原实现通过,该变异实现因旧图未进入绘制而失败;恢复原实现后再次通过。证据保存在 `/tmp/ely-p02-mutation-existing.log`、`/tmp/ely-p02-mutation-visual.log`、`/tmp/ely-p02-restored.log`。修复方向:给新增测试加入可控的替换等待阶段,验证旧缩略图在该阶段实际绘制,并验证新图到达后的替换。
+5
View File
@@ -0,0 +1,5 @@
FAIL
apps/ely-pdf/src/tests/input.rs:344 — 上一轮的实际绘制测试缺口仍在:`drawn` 调用的 `picture()` 返回 `thumbs` 缓存,Arc 身份断言验证的是缓存保留。具体复现:在隔离副本中将 `side.rs:163` 改成 `let picture = read.picture(page, dark).filter(|_| !read.thumbs_stale.contains(&(page, dark)));`,编辑后的过期缩略图停止绘制,`an_edit_keeps_page_pictures_until_they_repaint` 仍通过。暂停重画期间清除旧图图集项、重绘并检查 `window.has_image_atlas_entry(&old)` 的探针,在原实现上通过、该变异上失败、恢复后再次通过。worker 构建与 ElyPDF 全部 123 项测试通过;变异证据见 `/tmp/ely-p02-r2-mutation-existing.log`、`/tmp/ely-p02-r2-mutation-visual.log`、`/tmp/ely-p02-r2-restored.log`,探针说明见 `/tmp/ely-p02-r2-probe.log`。修复方向:在受控等待阶段及新图替换后断言缩略图实际进入绘制,覆盖行组件隐藏旧图的回归。
apps/ely-pdf/src/tests/input.rs:339 — 新增注释 `Hold the repaint: a picture on its way blocks the next.` 含 11 个英文单词,超过 agents.md 规则 5 的十词上限。具体复现:运行 `python3 tests/scripts/comments.py`,退出码为 1,唯一报告指向该行;证据见 `/tmp/ely-p02-r2-comments.log`。修复方向:将该单行注释缩短至十词以内。
+1
View File
@@ -0,0 +1 @@
PASS