diff --git a/agents.md b/agents.md index 723819f..b3676b9 100644 --- a/agents.md +++ b/agents.md @@ -261,3 +261,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-06:ElyPDF 页面组织(C11.2,PDF-02)。worker 打开即开 MuPDF journal;插入空白页、从文件插入(整份 PDF 嫁接)、删除、移动、旋转(90 度倍数,写页面的 `/Rotate`)、裁剪(按所见的上右下左边距设 `/CropBox`,全零即去掉)、撤销与重做都是一步,回复带历史标记(worker 按 MuPDF 的撤销位置记每步是编辑还是保存,标记是最后一个已做的编辑;撤销重做跳过保存步)。删除时指向已删页的书签保留但失去跳转,链接移除,表单字段去掉已删页上的控件;页码标签由 MuPDF 的删除与插入随之平移。坏的编辑(删光、页码乱序、非 90 度、边距超出页面)拒绝并回滚整步。保存(Save、Save As 走外壳)先 `pdf_save_snapshot` 写出(不动内存中的文档与撤销历史,靠补丁 0001),再打开该副本以 `garbage=2` 紧缩写到工作目录(已删页的对象不留在文件里),加密文件以用户输入的密码重开副本、照原加密写出(密码只在 worker 内存);外壳读回字节、原子写入。提取(选中页写新文件,经 `ely_files::export`,不覆盖打开着的文件,被别的标签持有即拒绝)在快照副本上删去其余页再紧缩写出,原文档的撤销与重做都不动;拆分按每份页数写到所选文件夹(`名称-序号.pdf`,先以 `ely_files::claim_all` 占下全部目标,任一已存在即一个不写;原 ElyShow 的同名函数移入 ely-files 共用)。合并即从文件插入到末尾再另存;插入的页带着自己的批注、链接与表单字段(指向源文件自身页的链接改指插入后的页,字段并入表单,重名的加序号,如 `field0 2`(名称按需分配,长名也终止),源表单的默认外观 `/DA` 与默认对齐 `/Q` 给到没有自己值的字段树根,源表单的字体资源并入目标 `/DR`,同名而不同的字体改名为 `名称_2` 并改写字段外观里的引用(按 PDF 名称语法解 `#xx` 转义,写出时照样转义);源文件的书签不并入)。删除时表单控件按被删页的 `/Annots` 认定,不只看可省略的 `/P`。裁剪边距以所见为准:先按视图旋转、再按页面 `/Rotate` 换到页面自身方向,并按 `/UserUnit` 换算。编辑失败也通知外壳队列已动,等着的保存、关闭不会挂住。结构编辑后区间点选的起点作废。应用:每个视图一条编辑队列,一次一条、按序执行,落地后重读页数、尺寸与书签,清掉图块、文字、缩略图、找到的词与选择,视图版本加一,此前发出的读取回来即丢;查找在编辑后重查。修改标记是当前历史与载入或上次保存时的历史不同,撤销回到保存点即为已保存(外壳新增 `Engine::saved` 与 `Session::reverted`:引擎回报未修改时 Dirty 回到 Saved,保存中、冲突、失败不变)。左栏是页面缩略图(`side.rs`;有书签时上方分段切换到书签):点选、Shift 连选、Cmd 增减,右键菜单,拖到某页之前即移动(移动后仍选中),Delete 删除;无选中时作用于当前页。“页面”菜单与“编辑”菜单的撤销(Cmd+Z)、重做(Shift+Cmd+Z)。裁剪对话框以毫米输入。MuPDF 的构建改为在 `engines/mupdf-src`(reference 只读)打 `patches/mupdf` 后编译。 - 2026-10-06:ElyPDF 阅读(C11.1,PDF-01)。MuPDF 只链进 `ely-pdf-worker`(`crates/ely-pdf-engine`,build.rs 把 C 垫片 `shim/ely_mupdf.c` 与 `libmupdf.a`、`libmupdf-third.a` 只链给这个二进制),应用不链 MuPDF。build.rs 以协议、垫片与 worker 源码的散列为版本戳,应用启动 worker 先握手(`HELLO`),戳不同即拒绝(单测不重建 worker,旧二进制曾被误用)。worker 在打开第一个文件时才启动(PERF-01),`tests/scripts/pdf-idle.sh` 查空白窗口无子进程。垫片每个调用都在 `fz_try`/`fz_catch` 里,返回码加最后一条错误信息,MuPDF 的跳转从不越过 Rust。worker 与应用之间是 `ely-ipc` 的帧(从 ely-lok 抽出,两者共用):打开(锁定即报)、解锁、页数、页框、渲染一块(设备像素、缩放、旋转、深色)、按阅读顺序的字符与外框、页内搜索(每处命中及其全部四边形,跨行短语算一处,数量不设上限)、书签(JSON)、关闭;文件读不出只报错,worker 照常工作。查看器:打开时只读页数、前 100 页尺寸与书签,其余尺寸后台每批 100 页补齐;视图只排屏上的页(连续、单页、双页;适合宽度、适合页面、实际大小与缩放档;视图旋转 90°),按 512 设备像素一块请求,一次一块、由近及远,只留屏上的块(横纵两向),后台回来的块不在屏上即弃;画不出的块记下不再请求,换文档或重开前不重试;深色外观下页面按亮度反相(MuPDF `fz_invert_pixmap_luminance`,色相保留)。文字选择按字符、可跨页,复制按行换行,未读的页先读;查找每批 20 页在后台进行,命中逐个跳转并标出,Esc 关闭(组件库 617a8d6);书签栏用 `lists::Outline`,点即跳到页与位置。切换视图、缩放、旋转后仍停在正在读的那页。加密文件打开时问密码(外壳的密码框,见上一条),错了再问,取消即不打开。没有页的文件报错并关闭,不开标签。双页时同一行在视口内的两页都可为当前页;当前页只在横纵两向都可见的页里定,跳转在目标页左缘不在视口时横向移过去。图块按视口半开区间取。首次按下时文字未读,读到后照原按下处起选;查找结果按查找实例与所属标签核对,只在该标签为当前时跳转。书签为空时不留左栏。启动参数里的文件直接打开。 - 2026-10-09(用户定:“打开方式”里不得出现 LibreOffice):系统会登记磁盘上能找到的 .app、挂载镜像里的 .app 与运行过的包,构建目录里的 `LibreOffice.app` 与测试挂载的官方镜像曾因此成为 xlsx 等的默认程序。现构建后由 `scripts/inert-lo.sh` 去掉其全部文件与 URL 声明、快速查看与聚焦插件,注销它,并在 `engines/` 放 `.metadata_never_index`;Ely 只取其 Frameworks 与 Resources。任何脚本与测试都不挂载 LibreOffice 镜像:LOK 探测用本构建的内核,单测所需的官方内核由 `scripts/extract-official-lo.sh` 以 7zz 解成无包的文件夹。`tests/scripts/lo-unregistered.sh` 以 NSWorkspace 核对 docx、xlsx、pptx、odt、csv 的“打开方式”里没有 LibreOffice。 +- 2026-10-09(用户报:ElyTable 点格跳到别处、拖不出选区):Calc 在 `sc_print_twips_msgs` 下以打印 twips(行高原值)报光标、选区与 `.uno:SheetGeometryData`,图块与鼠标却在图块 twips(每行按 `ScViewData::ToPixel` 截成整像素),两者随行号漂移(5000 行处约 140 行)。视图对外一律打印 twips;`LokView::set_geometry` 收下工作表几何后,`sheet::Axis::tile`、`print` 照内核的截断换算(每像素 twips 取图块的像素与 twips 之比),绘制、显示、跳转与冻结都先换成图块 twips,鼠标以图块 twips 发出(`tile_point`),拖动句柄仍为打印 twips;`.uno:` 读回与应用(表头、填充柄)照旧用打印 twips,表头的起点取 `LokView::scrolled_start`。内核读鼠标用的比例原取最后一块图块,worker 现于每次 `setClientVisibleArea` 前以 `setClientZoom` 告知图块比例,点击不再依赖先画过图块。 diff --git a/apps/ely-table/src/book.rs b/apps/ely-table/src/book.rs index 95e457a..cd434f2 100644 --- a/apps/ely-table/src/book.rs +++ b/apps/ely-table/src/book.rs @@ -229,7 +229,10 @@ impl Book { match read { Ok((sheets, geometry, at)) => { log::info!("table: tab {id} has {} sheets", sheets.list.len()); - view.update(cx, |view, _| view.set_part(sheets.current)); + view.update(cx, |view, cx| { + view.set_part(sheets.current); + view.set_geometry(geometry.clone(), cx); + }); let extra = &mut tab.extra; extra.sheets = sheets; extra.geometry = geometry; diff --git a/apps/ely-table/src/grid.rs b/apps/ely-table/src/grid.rs index fc87bc9..b616955 100644 --- a/apps/ely-table/src/grid.rs +++ b/apps/ely-table/src/grid.rs @@ -99,7 +99,7 @@ impl Book { }; let length = if column { width } else { height }; let pick = |pair: (i32, i32)| if column { pair.0 } else { pair.1 }; - let (frozen, scroll) = (pick(read.frozen()), pick(read.scroll())); + let (frozen, first) = (pick(read.frozen()), pick(read.scrolled_start())); let mut spans = Vec::new(); let mut take = |index: u32| { let (start, end) = (along(axis.start(index)), along(axis.start(index + 1))); @@ -114,7 +114,7 @@ impl Book { 0 }; (0..held).for_each(&mut take); - let mut index = axis.at(frozen + scroll).max(held); + let mut index = axis.at(first).max(held); while index < axis.count() && along(axis.start(index)) < length { take(index); index += 1; diff --git a/crates/ely-lok/src/client/doc.rs b/crates/ely-lok/src/client/doc.rs index eea5b08..51ed029 100644 --- a/crates/ely-lok/src/client/doc.rs +++ b/crates/ely-lok/src/client/doc.rs @@ -55,14 +55,16 @@ impl Doc { Ok(Sheets { current, list }) } - /// Tells the kernel the area the view shows, in twips. - pub fn visible_area(&self, worker: &mut Worker, (x, y, w, h): Rect) -> io::Result<()> { + /// Tells the kernel the shown area and the tile scale. + pub fn visible_area(&self, worker: &mut Worker, (x, y, w, h): Rect, (tile, tile_twips): (u32, i32)) -> io::Result<()> { let request = Pack::new() .u64(worker.doc_id(self)?) .i64(x.into()) .i64(y.into()) .i64(w.into()) .i64(h.into()) + .i64(tile.into()) + .i64(tile_twips.into()) .bytes(); worker.request(tag::VISIBLE_AREA, &request)?; Ok(()) diff --git a/crates/ely-lok/src/client/tests/rules.rs b/crates/ely-lok/src/client/tests/rules.rs index 5142284..24e010e 100644 --- a/crates/ely-lok/src/client/tests/rules.rs +++ b/crates/ely-lok/src/client/tests/rules.rs @@ -485,3 +485,29 @@ fn enter_and_tab_move_the_cell_cursor() { }); assert_eq!(right.cell.address, "G2", "Tab moves right"); } + +#[test] +fn a_click_far_down_selects_the_cell_under_it() { + if skip() { + return; + } + let (_dir, mut worker, doc, seen) = open(); + // One painted tile fixes the kernel's scale. + let (side, twips) = (512u32, 3840); + let ppt = f64::from(side) / f64::from(twips); + doc.paint_tile(&mut worker, (0, 0, twips, twips), (side, side)).expect("a tile"); + let geometry = sheet::geometry(&worker.values(&doc, sheet::GEOMETRY).expect("geometry")).expect("parsed"); + worker.run(&doc, "cell.goto", &["B4998"]).expect("B4998"); + let at = poll_until(&mut worker, &doc, &seen, SETTLE, |seen| seen.cell.address == "B4998"); + let (x, y, w, h) = at.cell.cursor.expect("the cursor"); + worker.run(&doc, "cell.goto", &["A1"]).expect("A1"); + let home = poll_until(&mut worker, &doc, &at, SETTLE, |seen| seen.cell.address == "A1"); + // The cursor is in print twips; clicks take tile twips. + let (x, y) = (geometry.columns.tile(x + w / 2, ppt), geometry.rows.tile(y + h / 2, ppt)); + for kind in [0, 1] { + let click = crate::Mouse { kind, x, y, count: 1, buttons: 1, modifiers: 0 }; + worker.post_mouse(&doc, click).expect("clicks"); + } + let after = poll_until(&mut worker, &doc, &home, SETTLE, |seen| seen.cell.address != "A1"); + assert_eq!(after.cell.address, "B4998"); +} diff --git a/crates/ely-lok/src/client/tests/sheet.rs b/crates/ely-lok/src/client/tests/sheet.rs index d47dc53..8f3b1ab 100644 --- a/crates/ely-lok/src/client/tests/sheet.rs +++ b/crates/ely-lok/src/client/tests/sheet.rs @@ -176,7 +176,7 @@ pub(super) fn open_book(text: &str) -> (tempfile::TempDir, Worker, Doc) { let doc = worker .load(&format!("file://{}", source.display())) .expect("the book loads"); - doc.visible_area(&mut worker, (0, 0, 15000, 10000)) + doc.visible_area(&mut worker, (0, 0, 15000, 10000), (256, 3840)) .expect("the area"); (dir, worker, doc) } diff --git a/crates/ely-lok/src/ffi/document.rs b/crates/ely-lok/src/ffi/document.rs index d855366..e73918d 100644 --- a/crates/ely-lok/src/ffi/document.rs +++ b/crates/ely-lok/src/ffi/document.rs @@ -247,6 +247,13 @@ impl Document { call!(class, 69, F, self.doc, view, true); } + /// Tells the kernel a tile's size in pixels and twips. + pub fn set_client_zoom(&self, pixels: i32, twips: i32) { + let class = unsafe { (*self.doc).class }; + type F = unsafe extern "C" fn(*mut Doc, i32, i32, i32, i32); + call!(class, 23, F, self.doc, pixels, pixels, twips, twips); + } + /// Tells the kernel the area the client shows, in twips. pub fn set_visible_area(&self, x: i32, y: i32, width: i32, height: i32) { let class = unsafe { (*self.doc).class }; diff --git a/crates/ely-lok/src/sheet.rs b/crates/ely-lok/src/sheet.rs index 93ad9bf..61d6e61 100644 --- a/crates/ely-lok/src/sheet.rs +++ b/crates/ely-lok/src/sheet.rs @@ -71,6 +71,66 @@ impl Axis { } } +impl Axis { + /// Where `index` starts in tile pixels. + fn tile_start(&self, index: u32, ppt: f64) -> i64 { + let (mut at, mut first) = (0i64, 0u32); + for &(last, size) in &self.runs { + let pixels = i64::from(to_pixels(size, ppt)); + if index <= last { + return at + i64::from(index - first) * pixels; + } + at += i64::from(last - first + 1) * pixels; + first = last + 1; + } + at + } + + /// A print-twips position in tile twips at `ppt`. + pub fn tile(&self, print: i32, ppt: f64) -> i32 { + let end = self.start(self.count()); + if print >= end { + return clamp(twips_of(self.tile_start(self.count(), ppt), ppt) + i64::from(print - end)); + } + let index = self.at(print); + let (from, size) = (self.start(index), self.size(index)); + let start = twips_of(self.tile_start(index, ppt), ppt); + let span = twips_of(self.tile_start(index + 1, ppt), ppt) - start; + clamp(start + i64::from(print - from) * span / i64::from(size)) + } + + /// The print-twips position under tile twips `tile`. + pub fn print(&self, tile: i32, ppt: f64) -> i32 { + let pixel = (f64::from(tile.max(0)) * ppt) as i64; + let (mut at, mut first) = (0i64, 0u32); + for &(last, size) in &self.runs { + let pixels = i64::from(to_pixels(size, ppt)); + let span = i64::from(last - first + 1) * pixels; + if pixels > 0 && pixel < at + span { + let index = first + u32::try_from((pixel - at) / pixels).expect("inside a run"); + let start = twips_of(self.tile_start(index, ppt), ppt); + let width = twips_of(self.tile_start(index + 1, ppt), ppt) - start; + let into = i64::from(tile) - start; + return clamp(i64::from(self.start(index)) + into * i64::from(size) / width.max(1)); + } + at += span; + first = last + 1; + } + let end = twips_of(self.tile_start(self.count(), ppt), ppt); + clamp(i64::from(self.start(self.count())) + i64::from(tile) - end) + } +} + +/// The kernel's `ScViewData::ToPixel`: truncates, never to 0. +fn to_pixels(twips: i32, ppt: f64) -> i32 { + let pixels = (f64::from(twips) * ppt) as i32; + if pixels == 0 && twips != 0 { 1 } else { pixels } +} + +fn twips_of(pixels: i64, ppt: f64) -> i64 { + (pixels as f64 / ppt).round() as i64 +} + fn clamp(twips: i64) -> i32 { i32::try_from(twips).unwrap_or(i32::MAX) } @@ -371,4 +431,29 @@ mod tests { assert_eq!(flags("1:2 5").expect("flags"), [(2, 1), (5, 0)]); assert!(merge(&[(3, 10)], &[(5, 0)]).is_err(), "lengths must agree"); } + + #[test] + fn tiles_round_each_row_to_whole_pixels() { + // 293 twips at 2/15 px per twip truncate to 39. + let rows = Axis { runs: vec![(9999, 293)] }; + let ppt = 2.0 / 15.0; + assert_eq!(to_pixels(293, ppt), 39); + let row = 4997u32; + let print = rows.start(row); + let tile = rows.tile(print, ppt); + assert_eq!(tile, (f64::from(39 * row) / ppt).round() as i32, "row tops line up"); + assert_eq!(rows.print(tile, ppt), print, "and come back"); + let mid = rows.tile(print + 146, ppt); + assert_eq!(rows.at(rows.print(mid, ppt)), row, "a point inside stays in its row"); + assert!(tile < print, "rounding down pulls far rows up"); + } + + #[test] + fn hidden_rows_take_no_tile_space_and_tiny_rows_take_a_pixel() { + let rows = Axis { runs: vec![(1, 300), (3, 0), (4, 3)] }; + let ppt = 1.0 / 15.0; + assert_eq!(rows.tile(rows.start(4), ppt), rows.tile(rows.start(2), ppt), "hidden rows fold"); + assert_eq!(to_pixels(3, ppt), 1, "never 0 for a shown row"); + } + } diff --git a/crates/ely-lok/src/view.rs b/crates/ely-lok/src/view.rs index 26073e4..5c8053e 100644 --- a/crates/ely-lok/src/view.rs +++ b/crates/ely-lok/src/view.rs @@ -28,6 +28,7 @@ mod render; mod zoom; pub use zoom::{ZOOM_STEPS, Zoomed}; mod scroll; +mod sheet; pub use input::{CONTEXT, Copy, Cut, Paste, init}; pub use off::read_off; pub use render::scheme; @@ -89,13 +90,15 @@ pub struct LokView { fetching: HashSet<(i32, i32)>, failed: HashSet<((i32, i32), u64)>, /// The area last told to the kernel. - shown: Option, + shown: Option<(Rect, (u32, i32))>, viewport: (f32, f32), origin: Point, /// Pixels a small document sits in: left, top. spare: (f32, f32), /// Frozen columns and rows, in twips from the top left. frozen: (i32, i32), + /// A spreadsheet's rows and columns, in print twips. + sheet: Option, /// The farthest point a jump needed; Calc grows to it. reach: (i32, i32), pages: Vec, @@ -210,6 +213,7 @@ impl LokView { origin: Point::default(), spare: (0.0, 0.0), frozen: (0, 0), + sheet: None, reach: (0, 0), pages, focus: cx.focus_handle().tab_stop(true), diff --git a/crates/ely-lok/src/view/input.rs b/crates/ely-lok/src/view/input.rs index 71a775c..ca17a96 100644 --- a/crates/ely-lok/src/view/input.rs +++ b/crates/ely-lok/src/view/input.rs @@ -60,8 +60,8 @@ pub(super) enum Input { from: (i32, i32), to: (i32, i32), }, - /// The area the view shows, in twips. - Visible(Rect), + /// The area shown in twips, and the tile scale. + Visible(Rect, (u32, i32)), /// Shows sheet or slide `part`, from 0. Part(usize), } @@ -74,7 +74,7 @@ impl Input { Input::Key { key_code, .. } => !matches!(key_code & 0x0FFF, 1024..=1031 | 1281), Input::Mouse(_) | Input::Copy { cut: false } - | Input::Visible(_) + | Input::Visible(..) | Input::Part(_) | Input::Theme(_) => false, Input::Run { commands, .. } => commands.iter().any(|(id, _)| *id != "edit.select_all"), @@ -171,7 +171,7 @@ fn lok_button(button: MouseButton) -> Option { /// Runs one input; gives a copy's mark and the revision. pub(super) fn run(worker: &Mutex, doc: &Doc, input: Input) -> (io::Result>, Option) { let mut worker = worker.lock().expect("the worker lock"); - let counts = !matches!(input, Input::Visible(_)); + let counts = !matches!(input, Input::Visible(..)); let copied = apply(&mut worker, doc, input); // A failed input may have moved the revision too. let now = match counts { @@ -189,7 +189,7 @@ fn apply(worker: &mut Worker, doc: &Doc, input: Input) -> io::Result worker.post_key(doc, char_code, key_code), Input::Mouse(event) => worker.post_mouse(doc, event), Input::Drag { from, to } => worker.drag_graphic(doc, from, to), - Input::Visible(area) => doc.visible_area(worker, area), + Input::Visible(area, scale) => doc.visible_area(worker, area, scale), Input::Part(part) => doc.show_sheet(worker, part), Input::Complete(name) => doc.complete_function(worker, &name), Input::Theme(scheme) => worker.run(doc, "view.theme", &[scheme]), @@ -280,7 +280,8 @@ impl LokView { count: usize, modifiers: &Modifiers, ) { - let (x, y) = self.doc_point(position); + // The kernel reads clicks in tile twips. + let (x, y) = self.tile_point(position); self.send(Input::Mouse(Mouse { kind, x, diff --git a/crates/ely-lok/src/view/render.rs b/crates/ely-lok/src/view/render.rs index 2f5d130..ea61a72 100644 --- a/crates/ely-lok/src/view/render.rs +++ b/crates/ely-lok/src/view/render.rs @@ -272,7 +272,7 @@ impl LokView { }, ); let pages = if self.pages.is_empty() { - vec![self.screen_rect((0, 0, extent.0, extent.1))] + vec![self.tile_screen_rect((0, 0, extent.0, extent.1))] } else { self.pages .iter() @@ -283,10 +283,11 @@ impl LokView { geom.twips_of(bounds.size.width.into()), geom.twips_of(bounds.size.height.into()), ); - if self.shown != Some((sx, sy, vw, vh)) { - self.shown = Some((sx, sy, vw, vh)); + let scale = (geom.tile_pixels(), TILE_TWIPS); + if self.shown != Some(((sx, sy, vw, vh), scale)) { + self.shown = Some(((sx, sy, vw, vh), scale)); self.failed.clear(); - self.send(input::Input::Visible((sx, sy, vw, vh))); + self.send(input::Input::Visible((sx, sy, vw, vh), scale)); } let (ax, ay) = self.axes(); let side = px(geom.px_of(TILE_TWIPS)); diff --git a/crates/ely-lok/src/view/scroll.rs b/crates/ely-lok/src/view/scroll.rs index 8b63ffd..e5a47ed 100644 --- a/crates/ely-lok/src/view/scroll.rs +++ b/crates/ely-lok/src/view/scroll.rs @@ -41,9 +41,10 @@ impl LokView { self.scroll_by(dx, dy, cx); } - /// Scrolls `rect` into view; frozen parts always show. - pub fn show(&mut self, (x, y, w, h): Rect, cx: &mut Context) { - let (fx, fy) = self.frozen; + /// Scrolls print-twips `rect` into view; frozen parts always show. + pub fn show(&mut self, rect: Rect, cx: &mut Context) { + let (x, y, w, h) = self.tile_rect(rect); + let (fx, fy) = self.frozen_tiles(); let (vw, vh) = ( self.geom.twips_of(self.viewport.0) - fx, self.geom.twips_of(self.viewport.1) - fy, @@ -63,7 +64,7 @@ impl LokView { cx.notify(); } - /// Freezes what lies before `frozen`, in twips. + /// Freezes what lies before `frozen`, in print twips. pub fn set_frozen(&mut self, frozen: (i32, i32), cx: &mut Context) { if self.frozen != frozen { log::info!("lok: frozen at {frozen:?}"); @@ -75,7 +76,8 @@ impl LokView { /// Scrolls `rect` into view and puts the cursor there. pub fn reveal(&mut self, rect: Rect, cx: &mut Context) { self.show(rect, cx); - let (x, y, _, h) = rect; + // Clicks land in tile twips. + let (x, y, _, h) = self.tile_rect(rect); log::info!("lok: reveal at {x},{y}"); for kind in [0, 1] { let (count, buttons, modifiers) = (1, 1, 0); @@ -108,8 +110,13 @@ impl LokView { ) } - /// The document point under a view position (LOK-04). + /// The print-twips document point under a view position (LOK-04). pub fn doc_point(&self, position: Point) -> (i32, i32) { + self.print_of(self.tile_point(position)) + } + + /// The tile-twips point under a view position. + pub(super) fn tile_point(&self, position: Point) -> (i32, i32) { let local = position - self.origin; let (ax, ay) = self.axes(); ( @@ -121,14 +128,17 @@ impl LokView { /// The panes of each axis. pub(super) fn axes(&self) -> (super::panes::Axis, super::panes::Axis) { let axis = |frozen, scroll| super::panes::Axis { frozen, scroll }; - ( - axis(self.frozen.0, self.scroll.0), - axis(self.frozen.1, self.scroll.1), - ) + let (fx, fy) = self.frozen_tiles(); + (axis(fx, self.scroll.0), axis(fy, self.scroll.1)) } - /// The window point of a document point in twips (LOK-04). - pub fn window_point(&self, (x, y): (i32, i32)) -> gpui::Point { + /// The window point of a print-twips document point (LOK-04). + pub fn window_point(&self, at: (i32, i32)) -> gpui::Point { + self.tile_window_point(self.tile_of(at)) + } + + /// The window point of a tile-twips point. + pub(super) fn tile_window_point(&self, (x, y): (i32, i32)) -> gpui::Point { let (ax, ay) = self.axes(); let (x, y) = (self.geom.px_of(ax.show(x)), self.geom.px_of(ay.show(y))); self.origin + point(px(self.spare.0 + x), px(self.spare.1 + y)) @@ -139,12 +149,19 @@ impl LokView { Bounds::from_corners(self.window_point((x, y)), self.window_point((x + w, y + h))) } + /// A tile-twips rect in window pixels. + pub(super) fn tile_screen_rect(&self, (x, y, w, h): crate::Rect) -> Bounds { + Bounds::from_corners(self.tile_window_point((x, y)), self.tile_window_point((x + w, y + h))) + } + /// The cursor rect in window pixels. pub fn cursor_bounds(&self) -> Option> { let (x, y, w, h) = self.cursor?; + let origin = self.window_point((x, y)); + let end = self.window_point((x + w, y + h)); Some(Bounds { - origin: self.window_point((x, y)), - size: size(px(self.geom.px_of(w.max(1))), px(self.geom.px_of(h))), + origin, + size: size((end.x - origin.x).max(px(self.geom.px_of(1))), end.y - origin.y), }) } } diff --git a/crates/ely-lok/src/view/sheet.rs b/crates/ely-lok/src/view/sheet.rs new file mode 100644 index 0000000..44100ce --- /dev/null +++ b/crates/ely-lok/src/view/sheet.rs @@ -0,0 +1,56 @@ +//! Calc reports print twips; tiles and clicks take tile twips. + +use gpui::Context; + +use super::{LokView, TILE_TWIPS}; +use crate::{Rect, sheet::Geometry}; + +impl LokView { + /// Lays the sheet out the kernel's way. + pub fn set_geometry(&mut self, geometry: Geometry, cx: &mut Context) { + if self.sheet.as_ref() != Some(&geometry) { + self.sheet = Some(geometry); + self.clamp_scroll(); + cx.notify(); + } + } + + /// Device pixels per twip, as the kernel paints our tiles. + fn ppt(&self) -> f64 { + f64::from(self.geom.tile_pixels()) / f64::from(TILE_TWIPS) + } + + /// A print-twips point in tile twips. + pub(super) fn tile_of(&self, (x, y): (i32, i32)) -> (i32, i32) { + match &self.sheet { + Some(sheet) => (sheet.columns.tile(x, self.ppt()), sheet.rows.tile(y, self.ppt())), + None => (x, y), + } + } + + /// A tile-twips point in print twips. + pub(super) fn print_of(&self, (x, y): (i32, i32)) -> (i32, i32) { + match &self.sheet { + Some(sheet) => (sheet.columns.print(x, self.ppt()), sheet.rows.print(y, self.ppt())), + None => (x, y), + } + } + + /// A print-twips rect in tile twips. + pub(super) fn tile_rect(&self, (x, y, w, h): Rect) -> Rect { + let (left, top) = self.tile_of((x, y)); + let (right, bottom) = self.tile_of((x + w, y + h)); + (left, top, right - left, bottom - top) + } + + /// The frozen extent in tile twips. + pub(super) fn frozen_tiles(&self) -> (i32, i32) { + self.tile_of(self.frozen) + } + + /// Where the scrolling pane starts, in print twips. + pub fn scrolled_start(&self) -> (i32, i32) { + let (fx, fy) = self.frozen_tiles(); + self.print_of((fx + self.scroll.0, fy + self.scroll.1)) + } +} diff --git a/crates/ely-lok/src/view/tests/large.rs b/crates/ely-lok/src/view/tests/large.rs index c1db65d..ac1843b 100644 --- a/crates/ely-lok/src/view/tests/large.rs +++ b/crates/ely-lok/src/view/tests/large.rs @@ -60,3 +60,58 @@ fn a_tall_sheet_paints_and_keeps_only_tiles_near_the_screen() { }); }); } + +/// 6000 rows of 0.5 cm; tiles round each to pixels. +const SHORT_ROWS: &str = r#" + + + + + +"#; + +#[test] +fn a_click_far_down_a_sheet_selects_the_cell_under_it() { + 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, "short.fods", SHORT_ROWS)); + let json = cx.update(|app| { + let read = view.read(app); + worker.lock().expect("the worker lock").values(&read.doc, crate::sheet::GEOMETRY).expect("geometry") + }); + let geometry = crate::sheet::geometry(&json).expect("parsed"); + let (row, column) = (4997, 1); + let cell = ( + geometry.columns.start(column), + geometry.rows.start(row), + geometry.columns.size(column), + geometry.rows.size(row), + ); + 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(cell, cx); + let at = view.window_point((cell.0 + cell.2 / 2, cell.1 + cell.3 / 2)); + for kind in [0, 1] { + view.mouse(kind, at, 1, 1, &gpui::Modifiers::default()); + } + }) + }); + let mut address = String::new(); + for _ in 0..200 { + cx.executor().tick(); + address = cx.update(|app| view.read(app).cell().address.clone()); + if address == "B4998" { + break; + } + } + assert_eq!(address, "B4998"); +} diff --git a/crates/ely-lok/src/worker.rs b/crates/ely-lok/src/worker.rs index b739f53..0a86a0e 100644 --- a/crates/ely-lok/src/worker.rs +++ b/crates/ely-lok/src/worker.rs @@ -246,9 +246,15 @@ fn answer(lok: &Lok, tag: u8, id: u64, payload: &[u8]) { .u64() .and_then(|id| { let mut number = || i32::try_from(unpack.i64()?).map_err(io::Error::other); - Ok((id, (number()?, number()?, number()?, number()?))) + Ok((id, (number()?, number()?, number()?, number()?), (number()?, number()?))) + }) + .and_then(|(id, (x, y, w, h), (pixels, twips))| { + // Calc reads clicks at this scale, not the last tile's. + with_doc(id, |doc| { + doc.set_client_zoom(pixels, twips); + doc.set_visible_area(x, y, w, h) + }) }) - .and_then(|(id, (x, y, w, h))| with_doc(id, |doc| doc.set_visible_area(x, y, w, h))) .map(|()| Vec::new()), tag::SELECTION_TEXT => unpack .u64()