From 7a12a28b67319db454e6b9819e92175d0a3681d0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Fri, 9 Oct 2026 18:11:39 -0400 Subject: [PATCH] ElyTable: the sheet's size follows every scale change; its end stays reachable after a zoom (T-05) --- crates/ely-lok/src/client/doc.rs | 9 ++ crates/ely-lok/src/events.rs | 19 +++ crates/ely-lok/src/lib.rs | 7 +- crates/ely-lok/src/view.rs | 15 +- crates/ely-lok/src/view/scroll.rs | 7 +- crates/ely-lok/src/view/sheet.rs | 36 ++++- crates/ely-lok/src/view/tests/large.rs | 190 ++++++++++++++++++++++++- crates/ely-lok/src/view/zoom.rs | 2 +- crates/ely-lok/src/worker.rs | 15 +- tasks/BUGS.md | 2 +- tasks/reviews/T-05-r1.md | 3 + tasks/reviews/T-05-r2.md | 3 + tasks/reviews/T-05-r3.md | 5 + tasks/reviews/T-05-r4.md | 5 + tasks/reviews/T-05-r5.md | 1 + 15 files changed, 294 insertions(+), 25 deletions(-) create mode 100644 tasks/reviews/T-05-r1.md create mode 100644 tasks/reviews/T-05-r2.md create mode 100644 tasks/reviews/T-05-r3.md create mode 100644 tasks/reviews/T-05-r4.md create mode 100644 tasks/reviews/T-05-r5.md diff --git a/crates/ely-lok/src/client/doc.rs b/crates/ely-lok/src/client/doc.rs index 86bd56b..417b846 100644 --- a/crates/ely-lok/src/client/doc.rs +++ b/crates/ely-lok/src/client/doc.rs @@ -98,6 +98,15 @@ impl Doc { Ok((unpack.i64()?, unpack.i64()?)) } + /// The size and the client zoom it was measured at. + pub fn size_at(&self, worker: &mut Worker) -> io::Result<((i64, i64), (i32, i32))> { + let payload = worker.request(tag::DOC_SIZE, &Pack::new().u64(worker.doc_id(self)?).bytes())?; + let mut unpack = Unpack::new(&payload); + let size = (unpack.i64()?, unpack.i64()?); + let number = |value: i64| i32::try_from(value).map_err(io::Error::other); + Ok((size, (number(unpack.i64()?)?, number(unpack.i64()?)?))) + } + /// Page rectangles in twips; empty without pages. pub fn pages(&self, worker: &mut Worker) -> io::Result> { let payload = worker.request(tag::PAGES, &Pack::new().u64(worker.doc_id(self)?).bytes())?; diff --git a/crates/ely-lok/src/events.rs b/crates/ely-lok/src/events.rs index 00ae3c3..1f88296 100644 --- a/crates/ely-lok/src/events.rs +++ b/crates/ely-lok/src/events.rs @@ -113,6 +113,8 @@ struct Slot { result: Option<(String, bool)>, /// The seq last pushed to the client. pushed: u64, + /// The client zoom last set: tile pixels, tile twips. + zoom: (i32, i32), } impl Slot { @@ -447,6 +449,23 @@ pub fn wait_cropping(id: u64, timeout: Duration) -> bool { } } +/// The client zoom last set on `id`, in tile units. +pub fn zoom(id: u64) -> (i32, i32) { + slots().lock().expect("the events lock").get(&id).map_or((0, 0), |slot| slot.zoom) +} + +/// A new client zoom resizes the document unannounced (Calc). +pub fn rescaled(id: u64, zoom: (i32, i32)) { + if let Some(slot) = slots().lock().expect("the events lock").get_mut(&id) + && slot.zoom != zoom + { + slot.zoom = zoom; + slot.batch.resized += 1; + slot.batch.seq += 1; + } + signal().notify_all(); +} + /// Seeds the a11y text from the kernel's snapshot (§7 tail). pub fn seed_a11y(id: u64, paragraph: &str) { if let Some(slot) = slots().lock().expect("the events lock").get_mut(&id) { diff --git a/crates/ely-lok/src/lib.rs b/crates/ely-lok/src/lib.rs index a1167f2..1c6c3eb 100644 --- a/crates/ely-lok/src/lib.rs +++ b/crates/ely-lok/src/lib.rs @@ -18,11 +18,16 @@ pub use client::{ }; #[cfg(feature = "view")] pub use view::{ - CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, TILE_TWIPS, ZOOM_STEPS, + CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, ZOOM_STEPS, Zoomed, init, layout, paper, read_off, scheme, }; #[cfg(feature = "view")] mod view; + +/// One tile edge in twips. +pub const TILE_TWIPS: i32 = 3840; +/// Twips per pixel at zoom 1.0 and DPI 1.0. +pub(crate) const TWIPS_PER_PIXEL: f32 = 15.0; pub use ipc::STALE; pub use ipc::file_url_of as file_url; pub use ipc::path_of_file_url as file_path; diff --git a/crates/ely-lok/src/view.rs b/crates/ely-lok/src/view.rs index 6a63846..f2a962c 100644 --- a/crates/ely-lok/src/view.rs +++ b/crates/ely-lok/src/view.rs @@ -33,10 +33,7 @@ pub use input::{CONTEXT, Copy, Cut, Paste, init}; pub use off::read_off; pub use render::scheme; -/// One tile edge in twips. -pub const TILE_TWIPS: i32 = 3840; -/// Twips per pixel at zoom 1.0 and DPI 1.0. -pub(crate) const TWIPS_PER_PIXEL: f32 = 15.0; +pub(crate) use crate::{TILE_TWIPS, TWIPS_PER_PIXEL}; /// Document/view conversion under one zoom and DPI (LOK-04). #[derive(Clone, Copy, Debug, PartialEq)] @@ -147,7 +144,7 @@ pub struct LokView { /// A document's size and page frames, in twips. pub type Layout = ((i64, i64), Vec); -type Measured = io::Result; +use sheet::{Measured, measure}; /// A page's colour; the kernel's Ely schemes share it. pub fn paper(cx: &gpui::App) -> gpui::Hsla { @@ -165,10 +162,6 @@ pub fn layout(worker: &mut Worker, doc: &Doc) -> io::Result { Ok((doc.size(worker)?, doc.pages(worker)?)) } -/// Reads the layout under the worker lock. -fn measure(worker: &Mutex, doc: &Doc) -> Measured { - layout(&mut worker.lock().expect("the worker lock"), doc) -} impl EventEmitter for LokView {} @@ -362,8 +355,8 @@ impl LokView { // What the view shows is the document at this revision. self.seen_revision(batch.revision); match measured { - Some(Ok(((width, height), pages))) => { - self.doc_size = (width as i32, height as i32); + Some(Ok((((width, height), pages), zoom))) => { + self.doc_size = self.at_scale((width as i32, height as i32), zoom); if pages.len() != self.pages.len() { log::info!("lok: {} pages", pages.len()); } diff --git a/crates/ely-lok/src/view/scroll.rs b/crates/ely-lok/src/view/scroll.rs index 67af759..89d7eee 100644 --- a/crates/ely-lok/src/view/scroll.rs +++ b/crates/ely-lok/src/view/scroll.rs @@ -111,10 +111,9 @@ impl LokView { if self.fit { return self.doc_size; } - self.tile_of(( - self.doc_size.0.max(self.reach.0), - self.doc_size.1.max(self.reach.1), - )) + // The kernel sizes a sheet in tile twips already. + let reach = self.tile_of(self.reach); + (self.doc_size.0.max(reach.0), self.doc_size.1.max(reach.1)) } /// The print-twips document point under a view position (LOK-04). diff --git a/crates/ely-lok/src/view/sheet.rs b/crates/ely-lok/src/view/sheet.rs index f6ca18e..885279d 100644 --- a/crates/ely-lok/src/view/sheet.rs +++ b/crates/ely-lok/src/view/sheet.rs @@ -1,9 +1,21 @@ //! Calc reports print twips; tiles and clicks take tile twips. +use std::{io, sync::Mutex}; + use gpui::Context; -use super::{LokView, TILE_TWIPS}; -use crate::{Rect, sheet::Geometry}; +use super::{Layout, LokView, TILE_TWIPS}; +use crate::{Doc, Rect, Worker, sheet::Geometry}; + +/// A layout and the client zoom it was measured at. +pub(super) type Measured = io::Result<(Layout, (i32, i32))>; + +/// Reads the layout under the worker lock. +pub(super) fn measure(worker: &Mutex, doc: &Doc) -> Measured { + let mut worker = worker.lock().expect("the worker lock"); + let (size, zoom) = doc.size_at(&mut worker)?; + Ok(((size, doc.pages(&mut worker)?), zoom)) +} impl LokView { /// Lays the sheet out the kernel's way. @@ -51,11 +63,29 @@ impl LokView { /// Rescales, keeping the scrolling pane's start in place. pub(super) fn keep_scroll(&mut self, change: impl FnOnce(&mut Self)) { let start = self.scrolled_start(); - change(self); + self.carry_size(change); let ((x, y), (fx, fy)) = (self.tile_of(start), self.frozen_tiles()); self.scroll = (x - fx, y - fy); } + /// Rescales; the measured size moves to the new scale too. + pub(super) fn carry_size(&mut self, change: impl FnOnce(&mut Self)) { + // Until measured again, scrolls clamp against this. + let print = self.print_of(self.doc_size); + change(self); + self.doc_size = self.tile_of(print); + } + + /// A sheet size the kernel measured at `zoom`, at ours. + pub(super) fn at_scale(&self, size: (i32, i32), (pixels, twips): (i32, i32)) -> (i32, i32) { + let Some(sheet) = self.sheet.as_ref().filter(|_| pixels > 0 && twips > 0) else { + return size; + }; + let theirs = f64::from(pixels) / f64::from(twips); + let print = (sheet.columns.print(size.0, theirs), sheet.rows.print(size.1, theirs)); + self.tile_of(print) + } + /// Where the scrolling pane starts, in print twips. pub fn scrolled_start(&self) -> (i32, i32) { let (fx, fy) = self.frozen_tiles(); diff --git a/crates/ely-lok/src/view/tests/large.rs b/crates/ely-lok/src/view/tests/large.rs index 22aeb31..53c2c33 100644 --- a/crates/ely-lok/src/view/tests/large.rs +++ b/crates/ely-lok/src/view/tests/large.rs @@ -82,14 +82,22 @@ fn far_down(cx: &mut gpui::TestAppContext, install: &std::path::Path) -> (Entity }); let geometry = crate::sheet::geometry(&json).expect("parsed"); let row = cell(&geometry, 1, 4997); + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(800.0), px(600.0)) }; cx.update(|app| { view.update(app, |view, cx| { view.geom.dpi = 2.0; view.set_geometry(geometry.clone(), cx); - view.prepare(Bounds { origin: point(px(0.0), px(0.0)), size: size(px(800.0), px(600.0)) }); - view.show(row, cx); + view.prepare(bounds); }) }); + // The first scale's report and size land before the move. + for _ in 0..100 { + cx.executor().tick(); + } + cx.update(|app| view.update(app, |view, cx| { + view.show(row, cx); + view.prepare(bounds); + })); (view, geometry) } @@ -192,6 +200,10 @@ fn a_sheet_brings_far_cut_and_frozen_cells_into_view() { view.set_geometry(crate::sheet::geometry(&json).expect("parsed"), cx); view.prepare(wide); assert_eq!(view.spare, (0.0, 0.0), "and stays left after it"); + let size = view.doc_size; + view.doc_size = view.tile_of((view.print_of(size).0 + 20 * 1280, size.1)); + assert_eq!(view.extent(), view.doc_size, "the kernel's size is the extent, uncut"); + view.doc_size = size; view.viewport = (800.0, 200.0); let (width, height) = (view.geom.twips_of(view.viewport.0), view.geom.twips_of(view.viewport.1)); let shows = |view: &LokView, rect: crate::Rect| { @@ -254,3 +266,177 @@ fn a_newly_selected_object_comes_into_view_a_moved_one_stays() { }); }); } + +/// Data out to the last cell sets the size. +const FULL_SHEET: &str = r#" + + + + + + 1 + +"#; + +#[test] +fn a_zoom_measures_the_sheet_again() { + let Some(install) = crate::client::tests::install() else { + eprintln!("SKIP: no pinned LOK build"); + return; + }; + let mut cx = gpui::TestAppContext::single(); + let (view, _) = cx.update(|app| live_view_of(app, &install, "full.fods", FULL_SHEET)); + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(400.0), px(200.0)) }; + cx.update(|app| view.update(app, |view, _| view.prepare(bounds))); + for _ in 0..100 { + cx.executor().tick(); + } + let before = cx.update(|app| view.read(app).doc_size); + cx.update(|app| view.update(app, |view, cx| { + view.set_zoom(1.25, cx); + view.prepare(bounds); + })); + // The kernel's size at the new zoom, read fresh. + let kernel = |cx: &mut gpui::TestAppContext| { + cx.update(|app| { + let read = view.read(app); + let (w, h) = read.doc.size(&mut read.worker.lock().expect("the worker lock")).expect("a size"); + (w as i32, h as i32) + }) + }; + // The kernel takes the zoom, then the view its size. + let (mut seen, mut now) = (before, before); + for _ in 0..2000 { + cx.executor().tick(); + (seen, now) = (cx.update(|app| view.read(app).doc_size), kernel(&mut cx)); + if now != before && seen == now { + break; + } + } + assert_ne!(now, before, "the zoom changes the kernel's size here"); + assert_eq!(seen, now, "the view takes the size the kernel has at the new zoom"); +} + +#[test] +fn a_zoom_at_the_end_of_a_sheet_keeps_the_row_in_the_middle() { + let Some(install) = crate::client::tests::install() else { + eprintln!("SKIP: no pinned LOK build"); + return; + }; + let mut cx = gpui::TestAppContext::single(); + let (view, worker) = cx.update(|app| live_view_of(app, &install, "full.fods", FULL_SHEET)); + let json = cx.update(|app| worker.lock().expect("the worker lock").values(&view.read(app).doc, crate::sheet::GEOMETRY).expect("geometry")); + let geometry = crate::sheet::geometry(&json).expect("parsed"); + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(400.0), px(200.0)) }; + cx.update(|app| view.update(app, |view, cx| { + view.set_geometry(geometry.clone(), cx); + view.prepare(bounds); + })); + for _ in 0..100 { + cx.executor().tick(); + } + // Scrolled to the end, as by the wheel. + cx.update(|app| view.update(app, |view, cx| { + view.scroll_by(0.0, -1.0e7, cx); + view.prepare(bounds); + })); + for _ in 0..100 { + cx.executor().tick(); + } + let middle = |cx: &mut gpui::TestAppContext| { + cx.update(|app| { + let read = view.read(app); + let at = read.origin() + point(px(200.0), px(100.0)); + geometry.rows.at(read.doc_point(at).1) + }) + }; + let before = middle(&mut cx); + cx.update(|app| view.update(app, |view, cx| { + view.set_zoom(1.25, cx); + view.prepare(bounds); + })); + let right_after = middle(&mut cx); + for _ in 0..400 { + cx.executor().tick(); + } + let measured = middle(&mut cx); + assert!(before.abs_diff(right_after) <= 1, "row {before} became {right_after} at the zoom"); + assert!(before.abs_diff(measured) <= 1, "row {before} became {measured} once measured"); +} + +#[test] +fn a_late_size_from_the_last_zoom_keeps_the_row() { + let Some(install) = crate::client::tests::install() else { + eprintln!("SKIP: no pinned LOK build"); + return; + }; + let mut cx = gpui::TestAppContext::single(); + let (view, worker) = cx.update(|app| live_view_of(app, &install, "full.fods", FULL_SHEET)); + let json = cx.update(|app| worker.lock().expect("the worker lock").values(&view.read(app).doc, crate::sheet::GEOMETRY).expect("geometry")); + let geometry = crate::sheet::geometry(&json).expect("parsed"); + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(400.0), px(200.0)) }; + let settle = |cx: &mut gpui::TestAppContext| { + for _ in 0..200 { + cx.executor().tick(); + } + }; + cx.update(|app| view.update(app, |view, cx| { + view.set_geometry(geometry.clone(), cx); + view.set_zoom(1.25, cx); + view.prepare(bounds); + })); + settle(&mut cx); + cx.update(|app| view.update(app, |view, cx| { + view.scroll_by(0.0, -1.0e7, cx); + view.prepare(bounds); + })); + settle(&mut cx); + // Measured at 125%, landing after the zoom to 150%. + let late = cx.update(|app| super::super::measure(&view.read(app).worker, &view.read(app).doc)); + let middle = |cx: &mut gpui::TestAppContext| { + cx.update(|app| { + let read = view.read(app); + geometry.rows.at(read.doc_point(read.origin() + point(px(200.0), px(100.0))).1) + }) + }; + cx.update(|app| view.update(app, |view, cx| view.set_zoom(1.5, cx))); + let before = middle(&mut cx); + cx.update(|app| view.update(app, |view, cx| view.apply_batch(crate::Events::default(), Some(late), cx))); + let after = middle(&mut cx); + assert!(before.abs_diff(after) <= 1, "row {before} became {after} when the old size landed"); +} + +#[test] +fn the_first_size_has_a_scale_and_keeps_the_row() { + let Some(install) = crate::client::tests::install() else { + eprintln!("SKIP: no pinned LOK build"); + return; + }; + let mut cx = gpui::TestAppContext::single(); + let (view, worker) = cx.update(|app| live_view_of(app, &install, "full.fods", FULL_SHEET)); + // Read before any visible area: the load set a scale. + let first = cx.update(|app| super::super::measure(&view.read(app).worker, &view.read(app).doc)); + assert_eq!(first.as_ref().map(|(_, zoom)| *zoom).ok(), Some((256, 3840)), "a known scale"); + let json = cx.update(|app| worker.lock().expect("the worker lock").values(&view.read(app).doc, crate::sheet::GEOMETRY).expect("geometry")); + let geometry = crate::sheet::geometry(&json).expect("parsed"); + let bounds = Bounds { origin: point(px(0.0), px(0.0)), size: size(px(400.0), px(200.0)) }; + cx.update(|app| view.update(app, |view, cx| { + view.set_geometry(geometry.clone(), cx); + view.prepare(bounds); + view.scroll_by(0.0, -1.0e7, cx); + })); + let middle = |cx: &mut gpui::TestAppContext| { + cx.update(|app| { + let read = view.read(app); + geometry.rows.at(read.doc_point(read.origin() + point(px(200.0), px(100.0))).1) + }) + }; + cx.update(|app| view.update(app, |view, cx| view.set_zoom(1.25, cx))); + let before = middle(&mut cx); + cx.update(|app| view.update(app, |view, cx| view.apply_batch(crate::Events::default(), Some(first), cx))); + let after = middle(&mut cx); + assert!(before.abs_diff(after) <= 1, "row {before} became {after} when the first size landed"); +} diff --git a/crates/ely-lok/src/view/zoom.rs b/crates/ely-lok/src/view/zoom.rs index bf96a65..b80ca07 100644 --- a/crates/ely-lok/src/view/zoom.rs +++ b/crates/ely-lok/src/view/zoom.rs @@ -54,7 +54,7 @@ impl LokView { let under = self.print_of((self.scroll.0 + self.geom.twips_of(sx), self.scroll.1 + self.geom.twips_of(sy))); self.fit = false; if (zoom - self.geom.zoom).abs() > 0.0001 { - self.geom.zoom = zoom; + self.carry_size(|view| view.geom.zoom = zoom); self.retire_tiles(); self.failed.clear(); } diff --git a/crates/ely-lok/src/worker.rs b/crates/ely-lok/src/worker.rs index 4b39fe9..0df225f 100644 --- a/crates/ely-lok/src/worker.rs +++ b/crates/ely-lok/src/worker.rs @@ -192,8 +192,12 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) { tag::EXPORT_TEXT => text::export(unpack).map(|()| Vec::new()), tag::DOC_SIZE => unpack .u64() - .and_then(|id| with_doc(id, |doc| doc.size())) - .map(|(width, height)| Pack::new().i64(width).i64(height).bytes()), + .and_then(|id| Ok((with_doc(id, |doc| doc.size())?, crate::events::zoom(id)))) + .map(|((width, height), (pixels, twips))| { + // The scale the kernel measured at; 0 if never set. + let pack = Pack::new().i64(width).i64(height); + pack.i64(i64::from(pixels)).i64(i64::from(twips)).bytes() + }), tag::SAVE_AS => save(unpack).map(|()| Vec::new()), tag::EXPORT_PDF => export_pdf(unpack).map(|()| Vec::new()), tag::CLOSE_DOC => unpack.u64().map(|id| { @@ -255,6 +259,7 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) { doc.set_client_zoom(pixels, twips); doc.set_visible_area(x, y, w, h) }) + .inspect(|_| crate::events::rescaled(id, (pixels, twips))) }) .map(|()| Vec::new()), tag::SELECTION_TEXT => unpack @@ -331,6 +336,12 @@ fn load(lok: &Lok, url: &str, password: String, filter: &str) -> io::Result crate::events::seed_a11y(id, &doc.a11y_paragraph()?); // Settle the initial layout now; later paints see it (LOK-03). doc.post_uno(".uno:Repaginate", None)?; + // A sheet's size has a known scale from the start. + if doc.kind() == 1 { + let zoom = (crate::TILE_TWIPS / crate::TWIPS_PER_PIXEL as i32, crate::TILE_TWIPS); + doc.set_client_zoom(zoom.0, zoom.1); + crate::events::rescaled(id, zoom); + } let mut docs = docs().lock().expect("the docs lock"); docs.insert(id, doc); log::info!("lok-worker: {} documents open", docs.len()); diff --git a/tasks/BUGS.md b/tasks/BUGS.md index b6f3901..7e68afe 100644 --- a/tasks/BUGS.md +++ b/tasks/BUGS.md @@ -12,7 +12,7 @@ - [x] T-02 单元格光标露出边缘即把视图滚到中央,点击与拖选时视图跳动 — R1 拖选中仍滚、超宽格跳回起点;R2 PASS - [x] T-03 视口比工作表宽时网格先居中再左移一次 — 载入回复带文档种类,表格不居中;R1 测试漏几何到达前的首帧;R2 PASS - [x] T-04 从不调用 `setClientZoom`,内核命中与几何按默认缩放算 — R2.16 已修(worker 每次报可见区前 setClientZoom) -- [ ] T-05 文档尺寸按内核像素取整,表尾被裁 +- [x] T-05 文档尺寸按内核像素取整,表尾被裁 — 内核尺寸按其测量时的比例换到当前比例(加载即设比例、缩放即改比例并重测、迟到的测量也换算);R1–R4 缩放后未重测、表尾锚点被截、连续缩放迟到、首次测量无比例与文件超行;R5 PASS - [ ] T-06 滚轮向下被内核平铺区卡住 - [ ] T-07 表头点选、行高列宽拖动是“按编号的命令”,而每次点击与拖选都推进修订,紧接着的表头操作被当作过期拒绝 - [ ] T-08 自动筛选的列菜单对光标所在的表而不是按钮所在的表筛选、排序 diff --git a/tasks/reviews/T-05-r1.md b/tasks/reviews/T-05-r1.md new file mode 100644 index 0000000..e9be9d2 --- /dev/null +++ b/tasks/reviews/T-05-r1.md @@ -0,0 +1,3 @@ +FAIL + +crates/ely-lok/src/view/scroll.rs:116(回归测试:crates/ely-lok/src/view/tests/large.rs:197):`extent()` 直接沿用旧缩放比例下测得的 `doc_size`,缩放后的表尾仍被裁切。具体复现:1× DPI 下打开 2048 列、6000 行的 FODS,列宽 0.4 cm、行高固定 0.5 cm,最后一格有值;等待初始状态落定,将缩放从 100% 调至 125%,等待输入队列与事件处理完成,再向下滚到底。视图保留高度 1,620,000 twips,内核实际高度与几何计算的末行底边均为 1,656,000 twips,可见区底边被夹在 1,620,000,末尾约 130 行无法通过滚动显示。`setClientZoom` 改变了内核尺寸,尺寸变更计数仍为 0,事件监听因此继续使用旧尺寸。新增测试手动设置 `doc_size` 后立即断言相等,漏掉了这条真实缩放路径。修复方向:在缩放或 DPI 改变后同步重测尺寸,或依据当前几何与比例重算滚动范围;补充真实内核下缩放后滚到末行的回归测试。已自行构建 worker,35 项视图测试通过;隔离探针及增加等待后的独立复测均复现上述裁切(日志:`/tmp/ely-t05-zoom-rerun.log`)。 diff --git a/tasks/reviews/T-05-r2.md b/tasks/reviews/T-05-r2.md new file mode 100644 index 0000000..b3b4b03 --- /dev/null +++ b/tasks/reviews/T-05-r2.md @@ -0,0 +1,3 @@ +FAIL + +crates/ely-lok/src/view/scroll.rs:116(调用:crates/ely-lok/src/view/zoom.rs:67;回归覆盖:crates/ely-lok/src/view/tests/large.rs:319):缩放瞬间的 `extent()` 仍取旧比例下的 `doc_size`,`clamp_scroll()` 因而截断已经按新比例计算的缩放锚点;异步重测更新尺寸后,丢失的滚动位置保持原状,违反 PRD LOK-04。复现:1× DPI 打开新增 `FULL_SHEET` 样本(2048 列、6000 行,列宽 0.4 cm、行高 0.5 cm,最后一格有值),载入真实几何,视口设为 400×200,等待启动事件落定后滚到数据表尾,再从 100% 缩放到 125%。视口中心从第 5995 行跳到第 5891 行;夹取时使用旧高度 1,626,885 twips,把纵向位置压到 1,624,485,随后高度重测为 1,662,804,中心仍在第 5891 行,实际内核点击确认选中 N5891。独立复测一致,日志为 `/tmp/ely-t05-r2-bottom-rerun.log`、`/tmp/ely-t05-r2-bottom-click.log`。上一轮的尺寸重测缺陷已修复,移除通知的变异 worker 能使新增测试失败;当前 36 项视图测试通过,但缩放回归从表头开始且只核对最终尺寸,覆盖不到上述边界。修复方向:让滚动夹取使用当前比例的尺寸,并保留重测期间的缩放锚点;补充真实几何下表尾缩放前后可见行保持一致的回归测试。 diff --git a/tasks/reviews/T-05-r3.md b/tasks/reviews/T-05-r3.md new file mode 100644 index 0000000..7b577e4 --- /dev/null +++ b/tasks/reviews/T-05-r3.md @@ -0,0 +1,5 @@ +FAIL + +crates/ely-lok/src/view/sheet.rs:64(异步覆盖:crates/ely-lok/src/view.rs:366、371;回归覆盖:crates/ely-lok/src/view/tests/large.rs:324):连续缩放时,`carry_size` 换算出的当前尺寸会被旧比例的异步测量覆盖,随后的 `clamp_scroll()` 截断缩放锚点;当前比例的尺寸随后到达,滚动位置仍保持截断后的值,违反 PRD LOK-04。真实内核复现:1× DPI 载入 `FULL_SHEET` 与真实几何,视口 400×200;反复回到 100%、等待落定并以 `scroll_by` 滚到底,再连续设为 125%、150%。隔离探针依次使用 0、1、2、3 次 executor tick 作为两次缩放之间的间隔,第 4 次出现旧测量晚到:150% 的预换算高度为 1,707,000,125% 的测量高度 1,683,216 将滚动从 1,697,778 压到 1,681,216;之后重测恢复高度 1,707,000,中心行零基索引仍从 6075 变为 6008,真实内核点击选中 N6009。初次探针与两次独立重跑均复现,日志为 `/tmp/ely-t05-r3-rapid.log`、`/tmp/ely-t05-r3-rapid-trace.log`、`/tmp/ely-t05-r3-rapid-click.log`,探针代码留在 `/tmp/ely-t05-r3-reproducer.log`。第 2 轮的单次缩放场景已修复:本轮 37 项选中测试通过,移除 `carry_size` 的变异使新增测试按预期报 `5994 -> 5890`;现有测试只做一次缩放,漏掉旧测量跨越下一次缩放的时序。修复方向:让尺寸测量携带其实际内核比例,应用时核对比例并转换或丢弃过期结果,保住当前锚点;补充上述连续缩放时旧测量晚到的回归。 + +crates/ely-lok/src/view/tests/large.rs:341:新增注释 `Scrolled as far as the sheet goes, as by the wheel.` 有 11 个单词,违反 agents.md 规则 5 的单行十词上限。运行 `python3 tests/scripts/comments.py` 即失败并指向该行。修复方向:将该注释缩至十词以内。 diff --git a/tasks/reviews/T-05-r4.md b/tasks/reviews/T-05-r4.md new file mode 100644 index 0000000..fc6b856 --- /dev/null +++ b/tasks/reviews/T-05-r4.md @@ -0,0 +1,5 @@ +FAIL + +crates/ely-lok/src/view/sheet.rs:69(比例来源:crates/ely-lok/src/events.rs:453、crates/ely-lok/src/worker.rs:195):首次设置可见区之前,DOC_SIZE 的比例仍为 `(0, 0)`;`at_scale` 对这份尺寸直接返回原值。该测量跨越首次缩放才到达时,仍会覆盖已换算的尺寸并截断滚动,违反 PRD LOK-04。真实内核探针:载入 FULL_SHEET 与真实几何,保留首次测量 `((442789, 1620000), (0, 0))`;视口 400×200,首次 `prepare` 排队报告可见区,滚到底后从 100% 缩放到 125%,再交付保留的测量。高度从 1,656,000 退为 1,620,000,纵向滚动从 1,653,265 被压到 1,617,600,中心行从 5995 变为 5866;后续重测仍停在 5866,真实点击选中 N5866。两次独立复跑一致,日志 `/tmp/ely-t05-r4-initial-rerun1.log`、`/tmp/ely-t05-r4-initial-rerun2.log`,复现代码 `/tmp/ely-t05-r4-reproducer.log`。隔离副本按正确初始比例换算后保持第 5995 行、点击 N5995(`/tmp/ely-t05-r4-initial-scale-probe.log`)。上轮已登记比例的连续缩放场景通过;移除 `at_scale` 的变异使新增回归按预期报出 `5995 -> 5935`。现有 240 项测试全部通过,新增测试遗漏了首次未登记比例的测量。修复方向:在首次尺寸读取前显式设置并记录内核比例,让每份尺寸均能正确换算;补充初始测量跨越首次缩放的回归。 + +crates/ely-lok/src/view.rs:503:本轮新增 Measured 注释与测量代码使该文件从 HEAD 的 500 行增至 503 行,违反 agents.md 规则 5。`wc -l crates/ely-lok/src/view.rs` 与逐行计数均为 503,末行 `mod tests;` 位于第 503 行;规则检查日志 `/tmp/ely-t05-r4-rules.log`。上轮的长注释已修复,`python3 tests/scripts/comments.py` 通过。修复方向:将布局测量辅助代码移入合适的现有模块,使每个文件保持在 500 行以内。 diff --git a/tasks/reviews/T-05-r5.md b/tasks/reviews/T-05-r5.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/T-05-r5.md @@ -0,0 +1 @@ +PASS