From 1e6147ae7754b540bcc05ff8f76e270c74111e5a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=9B=B7=E7=94=B5=E8=8A=BD=E8=A1=A3?= Date: Sun, 18 Oct 2026 22:41:28 -0400 Subject: [PATCH] R8.3: sheet tab strip scrolls the chosen sheet in (Gitea #8) Co-Authored-By: Claude --- Cargo.lock | 24 ++++---- Cargo.toml | 2 +- agents.md | 3 +- apps/ely-table/src/sheets.rs | 23 +++++--- apps/ely-table/src/tests/strip.rs | 96 +++++++++++++++++++++++++++++++ tasks/TASKS.md | 2 +- tasks/reviews/R8.3-r1.md | 5 ++ tasks/reviews/R8.3-r2.md | 3 + tasks/reviews/R8.3-r3.md | 1 + 9 files changed, 136 insertions(+), 23 deletions(-) create mode 100644 apps/ely-table/src/tests/strip.rs create mode 100644 tasks/reviews/R8.3-r1.md create mode 100644 tasks/reviews/R8.3-r2.md create mode 100644 tasks/reviews/R8.3-r3.md diff --git a/Cargo.lock b/Cargo.lock index 98daf21..8c3a3b0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -290,7 +290,7 @@ version = "1.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -301,7 +301,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d" dependencies = [ "anstyle", "once_cell_polyfill", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1805,7 +1805,7 @@ dependencies = [ "libc", "option-ext", "redox_users 0.5.3", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -1986,7 +1986,7 @@ dependencies = [ [[package]] name = "ely-gpui-component" version = "0.1.1" -source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=2e65b05a6592d8c3166a252261fc01bf6215c3c5#2e65b05a6592d8c3166a252261fc01bf6215c3c5" +source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=7d704d97f40a9d47c08891c35d7dbe26a2220e55#7d704d97f40a9d47c08891c35d7dbe26a2220e55" dependencies = [ "alacritty_terminal", "anyhow", @@ -2405,7 +2405,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -4611,7 +4611,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "536bfad37a309d62069485248eeaba1e8d9853aaf951caaeaed0585a95346f08" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -4776,7 +4776,7 @@ version = "0.50.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -6539,7 +6539,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7127,7 +7127,7 @@ dependencies = [ "cfg-if", "libc", "psm", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -7478,7 +7478,7 @@ dependencies = [ "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] @@ -7828,7 +7828,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e" dependencies = [ "memoffset", "tempfile", - "windows-sys 0.61.2", + "windows-sys 0.60.2", ] [[package]] @@ -8633,7 +8633,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.52.0", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 5814770..0ff1eca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ jiff = "0.2" anyhow = "1" directories = "6" dunce = "1" -ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "2e65b05a6592d8c3166a252261fc01bf6215c3c5" } +ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "7d704d97f40a9d47c08891c35d7dbe26a2220e55" } env_logger = "0.11" gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit", "wayland", "x11"] } diff --git a/agents.md b/agents.md index d6eeaf8..086c58b 100644 --- a/agents.md +++ b/agents.md @@ -40,7 +40,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 ## 组件映射 -依赖 `ely-gpui-component`(pin 2e65b05,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 +依赖 `ely-gpui-component`(pin 7d704d9,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 | 需要 | 用 | | --- | --- | @@ -319,3 +319,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-16(用户定):Windows 资源管理器的右键“新建”可建 DOCX、XLSX、PPTX 与 Markdown 文件(R7.9)。空白文件在 `packaging/windows/new`(`templates.sh` 以本构建的内核把空文件转存而得,无作者与日期;Markdown 为空文件),随所属的应用装在安装目录(列在 `.ely-installed` 里),`HKCU\…\.<扩展名>\ShellNew` 的 `FileName` 指向它。资源管理器只在扩展名有默认 ProgID 时列出新建项(实测:只有 `ShellNew`、或只在 `OpenWithProgids` 下时都不列);所以只在该类型没有任何程序处理时(HKCR 下无默认值,或默认值所指的类不存在:本机 Windows 11 的 `.md` 指向不存在的 `Markdown Document`)把默认值设为本应用的 ProgID(写在 HKCU,用户自己的 `UserChoice` 不动),已有的从不改动(装着 Office、WPS 时新建菜单用它们自己的项);没有处理程序的这四种类型从此由 Ely 打开。卸载只删仍是本安装写下的 `ShellNew` 与默认值。ProgID 的名称为“<应用> Document”(新建菜单与类型列显示它)。`tests/scripts/types.sh` 核对四种都有新建项,默认值只在判空之后写。 - 2026-10-16(用户定):ElyMark 打开的块保持原样(R7.5)。组件库 bff7ebc 起,可视模式里按下的块以原文显示在它合上时的框里(同样的边距与透明边框),字号、行高与颜色同合上时:段落、列表、引用等为正文,标题按其级数的字号、1.3 倍行高、加粗(六级为次要色),照原文写出的块(链接定义等)为等宽次要色;不再加深色底板与另一套边距,按下前后不挪位置。代码块、块公式与 front matter 仍有自己的底板(代码与 front matter 为等宽小号,front matter 为次要色,同合上时)。输入框的着色(源码模式同用)给标题与粗体同一字重(原为半粗,压过所设的加粗),六级标题里的字照为次要色(行内代码照为正文色,2e65b05),front matter 照 front matter 读(原读成分隔线加标题,键名变亮变粗)(1ff993d、962964e)。表格单元格照旧。打字机模式的居中另按两端余量判定(原只按块高与视图高,打开前后同高时首块不再居中)。同批带入组件库 cdc5ce8(标题栏拖动区里的控件按下归控件,Windows)。Windows 真窗口核对(`.work/r75`):段落、三级标题、代码块打开前后位置相同。 - 2026-10-18:Word 公式(R8.2,Gitea #9)。附件 math.docx 的六类公式逐类核对(本机无 Word,以文件所写为准):键盘输入、OMML(内核转为公式对象)、MathType(OLE,显示随带的 WMF 预览)、上下标、图片原本就对;差别在两处根因。其一,运行的 `w:position`(升降)原先只给文字,图片与 OLE 公式所在的运行失去它(25 张图片与一个 ½ 浮在行上):补丁 `0049-writer-inline-objects-move-with-their-run.patch` 把它作为固定升降写到对象的锚字符上(按锚字符自己的字号换算百分比,不扩展到其后的文字),版面让“作为字符”的对象随锚字符的固定升降移动(vertAlign 的自动上下标不动对象,同原样);各导出本就写出锚字符的升降,DOCX 往返仍为 `w:position`。其二,EQ 域原先只认 `\o`、`\s\do` 与注音,其余(如分数 `\f`)整个消失:补丁 `0050-writer-eq-field-parser.patch`(读法)与 `0051-writer-eq-fields-as-formulas.patch`(导入)把 EQ 域转为公式对象:`\f` 分数、`\r` 根式、`\s` 上下标、`\b` 括号、`\l` 列表、`\a` 阵列、`\i` 积分与求和求积;`\x` 与 `\o` 只留内容(无框、无叠印),`\d` 只前移,`\ai`、`\di` 只是行距(不升降),开关不分大小写;每个命令字符保留其运行的上下标(Word 照此画),同一段上下标里字样变了仍是一个上下标;斜体字母写成标识符(StarMath 与 OMML 都画成斜体,DOCX 往返不丢),斜体的数字与符号写成 `ital` 文字,存为 DOCX 后成正体(OMML 无此写法,已知限制);没有底的上下标以空文字为底(空的底在 OMML 里是占位符,重开成 ``);公式取域的字号;读不通的命令以其域代码显示为文字,不消失。只有整个域是 `\s\do N(文字)` 时照原样成为下移的文字,其余都经读法(原先小写的 `\s\do` 读到第一个括号即止,其后丢失)。`client/tests/word.rs` 以 `tests/fixtures/office/word.docx`(由 `.work/r8/gen/eq.py` 生成,不入库)核对公式文本(含开头的空格、大写开关、读不通的域、字样变化的下标、`\ai`、`\di`、小写 `\s\do` 的尾部与嵌套)、DOCX 往返后的斜体、下标与无占位符、锚字符的升降与图块里图片低于文字(去掉版面的升降即失败)。审查三轮:第 3 轮的四处已修并以上述测试核对,未再审查(每条至多三轮)。仍与 Word 不同:编辑区里公式与图片四周有内核的辅助框线(`ShowBoundaries`,视图辅助,不印出);DOCX 另存后重开行距变宽,因内核导出改写文档默认字体与段距,与公式无关。 +- 2026-10-18:工作表过多时标签条(R8.3,Gitea #8)。组件库 `navigation::Tabs` 横排时可横向滚动(滚轮同样),标签可窄于自然宽度;所选标签在所选或标签条宽度变了时滚入视口,在绘制时以刚排好的宽度判定(原先只在首帧滚一次,其后右栏打开、标签条变窄,所选标签被挤出视口);手动滚动保留到二者之一再变;横排的标签至多与标签条同宽,名字过长以省略号结尾(组件库 e56a343、3a68d1b、7d704d9,同批带入上游列外壳拖动的四个提交)。ElyTable 的标签条随之收缩,添加按钮留在其旁;标签条的状态按工作簿各记一份(元素 id 带标签号),切到另一工作簿时其所选标签照样滚入。`navigation/tests/tabs.rs` 以 30 个标签核对:首帧滚到所选、滚轮回到第一个、新选的滚入、变窄后所选仍在末端(去掉按宽度判定即失败)、长名的所选标签整个在标签条内(去掉宽度上限即失败);ElyTable 的 `tests/strip.rs` 以 `sheets::strip` 画两个各有 30 张表、都选第 30 张的工作簿,滚回开头后来回切换,所选标签每次都在标签条内(两个工作簿共用一个 id 即失败)。Windows 真窗口(`.work/r8/strip3.png`):30 张表、第 30 张为当前,1100 宽时标签条末端为它。 diff --git a/apps/ely-table/src/sheets.rs b/apps/ely-table/src/sheets.rs index 1130d50..3ca6d17 100644 --- a/apps/ely-table/src/sheets.rs +++ b/apps/ely-table/src/sheets.rs @@ -109,18 +109,12 @@ impl Book { let Some(tab) = self.active_tab() else { return div().into_any_element(); }; - let Some(choices) = visible(&tab.extra.sheets) else { + let Some(tabs) = strip(tab.id, &tab.extra.sheets) else { return div().into_any_element(); }; let this = cx.entity(); let picked = this.clone(); - let tabs = SheetTabs::new( - "table-sheets", - choices, - tab.extra.sheets.current.to_string(), - ) - .placement(TabPlacement::Bottom) - .on_change(move |value, _, cx| { + let tabs = tabs.on_change(move |value, _, cx| { let index: usize = value.parse().expect("sheet tabs are indexes"); picked.update(cx, |book, cx| book.show_sheet(index, cx)); }) @@ -310,6 +304,15 @@ impl Book { } } +/// Workbook `id`'s sheet tabs; each workbook keeps its own scroll. +fn strip(id: u64, sheets: &ely_lok::Sheets) -> Option { + let choices = visible(sheets)?; + Some( + SheetTabs::new(("table-sheets", id), choices, sheets.current.to_string()) + .placement(TabPlacement::Bottom), + ) +} + /// Visible sheets as tabs, once the shown one is known. fn visible(sheets: &ely_lok::Sheets) -> Option> { let shown = sheets.list.get(sheets.current)?; @@ -327,6 +330,10 @@ fn visible(sheets: &ely_lok::Sheets) -> Option> { ) } +#[cfg(test)] +#[path = "tests/strip.rs"] +mod strip_tests; + #[cfg(test)] mod tests { use super::*; diff --git a/apps/ely-table/src/tests/strip.rs b/apps/ely-table/src/tests/strip.rs new file mode 100644 index 0000000..652608e --- /dev/null +++ b/apps/ely-table/src/tests/strip.rs @@ -0,0 +1,96 @@ +//! Each workbook's strip shows its own chosen sheet. + +use ely_lok::{Sheet, Sheets}; +use gpui::{ + Bounds, Context, IntoElement, Modifiers, ParentElement as _, Pixels, Render, RenderOnce as _, + ScrollDelta, ScrollHandle, ScrollWheelEvent, Styled as _, TestAppContext, TouchPhase, + VisualTestContext, Window, div, point, px, +}; + +use super::strip; + +struct Books { + book: u64, + sheets: Sheets, + scroll: Option, +} + +impl Render for Books { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + let tabs = strip(self.book, &self.sheets) + .expect("tabs") + .render(window, cx) + .into_any_element(); + let id = gpui::ElementId::from(("table-sheets", self.book)); + let handle = window.use_keyed_state((id, "scroll"), cx, |_, _| ScrollHandle::new()); + self.scroll = Some(handle.read(cx).clone()); + div() + .w(px(640.)) + .child(div().flex().items_center().px_2().child(tabs)) + } +} + +fn settle(cx: &mut VisualTestContext) { + for _ in 0..5 { + cx.run_until_parked(); + cx.update(|window, _| window.refresh()); + } + cx.run_until_parked(); +} + +/// The strip's viewport and the chosen tab in it. +fn place( + view: &gpui::Entity, + cx: &mut VisualTestContext, +) -> (Bounds, Bounds) { + view.read_with(cx, |view, _| { + let scroll = view.scroll.as_ref().expect("drawn"); + let mut chosen = scroll + .bounds_for_item(view.sheets.current) + .expect("chosen tab"); + chosen.origin += scroll.offset(); + (scroll.bounds(), chosen) + }) +} + +fn shown(view: &gpui::Entity, cx: &mut VisualTestContext) -> bool { + let (viewport, chosen) = place(view, cx); + chosen.left() >= viewport.left() && chosen.right() <= viewport.right() +} + +#[gpui::test] +fn a_switched_workbook_shows_its_chosen_sheet(cx: &mut TestAppContext) { + cx.update(ely_gpui_component::init_for_tests); + let sheets = Sheets { + current: 29, + list: (0..30) + .map(|n| Sheet { + name: format!("Sheet {n}"), + visible: true, + }) + .collect(), + }; + let (view, cx) = cx.add_window_view(|_, _| Books { + book: 1, + sheets, + scroll: None, + }); + settle(cx); + assert!(shown(&view, cx), "the first draw shows the chosen sheet"); + for book in [2, 1, 2] { + cx.simulate_event(ScrollWheelEvent { + position: point(px(60.), px(16.)), + delta: ScrollDelta::Pixels(point(px(0.), px(50_000.))), + modifiers: Modifiers::none(), + touch_phase: TouchPhase::Moved, + }); + settle(cx); + assert!(!shown(&view, cx), "the wheel moves the chosen sheet away"); + view.update(cx, |view, cx| { + view.book = book; + cx.notify(); + }); + settle(cx); + assert!(shown(&view, cx), "workbook {book} shows its chosen sheet"); + } +} diff --git a/tasks/TASKS.md b/tasks/TASKS.md index 078419a..5458f52 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -290,6 +290,6 @@ - [x] R8.1 深色外观下内容照文件原色(§5,用户定,取代 2026-10-04 的“内容区随系统深浅色”):ElyPDF 页面不反相;ElyDoc 页面、ElyTable 单元格照文件的颜色(白底即白底),ElyShow 已是如此(`ely.xcd` 只留浅色方案,每个视图首帧发一次;MuPDF 不再反相;Windows 真窗口深色外观下四个应用都是白纸;测试以深色档案起内核、视图须把页面画白,去掉发送即失败 — 第 1 轮:`theme.sh` 仍要深色网格、测试的内核本就浅色;第 2 轮:两处注释超长;第 3 轮 PASS) - [x] R8.2 Gitea #9 公式不兼容(§7):附件 math.docx 的键盘公式、OMML、MathType(OLE)、EQ 域、上下标、图片公式逐类与 Word 对照,查明各类差异的根因并修;修不了的写明原因与证据(补丁 0049:运行的 `w:position` 随锚字符移动图片与 OLE;0050、0051:EQ 域转为公式对象,读不通的显示域代码;`word.rs` 三项测试;第 3 轮四处修后未再审查) -- [ ] R8.3 Gitea #8 工作表过多时标签条不滚动(§9):活动工作表变化时标签条滚动,使该标签完整可见;被裁到窗口外的工作表可经标签条到达 +- [x] R8.3 Gitea #8 工作表过多时标签条不滚动(§9):活动工作表变化时标签条滚动,使该标签完整可见;被裁到窗口外的工作表可经标签条到达(组件库 7d704d9:按所选与宽度滚入、长名有上限;ElyTable 标签条按工作簿各记状态;`tabs.rs` 与 `strip.rs` 测试 — 第 1 轮:共用 id、长名越宽;第 2 轮:每工作簿状态无测试;第 3 轮 PASS) - [ ] R8.4 ElyMark 公式(§14):行内公式排版(原只显示着色的 TeX 原文);`\(…\)` 读作行内公式,`\[…\]` 读作块公式 - [ ] R8.5 答复 #8、#9,修好的关闭;`agents.md` 随之 diff --git a/tasks/reviews/R8.3-r1.md b/tasks/reviews/R8.3-r1.md new file mode 100644 index 0000000..82fb1df --- /dev/null +++ b/tasks/reviews/R8.3-r1.md @@ -0,0 +1,5 @@ +FAIL + +1. `apps/ely-table/src/sheets.rs:118`(组件库 `src/navigation/tabs.rs:294`):所有工作簿共用 `table-sheets`,所选值只有工作表索引,而自动滚动只比较所选值和视口宽度。打开两个各有 30 张表、当前均为第 30 张的工作簿,在第一个工作簿把标签条滚回开头,再切到第二个;状态键不变,新的活动标签仍在视口外,不符合 R8.3 的活动工作表变化时完整可见。探针测得视口为 `[8,604]`,第二个工作簿的活动标签为 `[1446,1494]`。修正方向:把工作簿身份纳入标签条的状态或跟随判定,并覆盖同索引跨工作簿切换的回归测试。证据:`.work/R8.3-r1/final-probes.log`。 + +2. `C:/Users/zhang/Documents/Github/Ely-GPUI-Components/src/navigation/tabs.rs:250`(标签项在 `:183` 构建):只允许标签条收缩,没有限制单个标签的宽度,长且不能断行的表名仍可宽于整个视口。640 像素窗口显示 280 像素属性栏时,正文宽 360,扣除添加按钮和边距后标签视口宽 316;使用内嵌 Inter 和 Windows DirectWrite,表名为两位序号加 29 个 `W`,切换到第 30 张表后标签宽 390,自动滚动只能将其左端对齐,右侧仍被裁掉,无法满足完整可见。修正方向:约束标签项和其文字的布局,使单项能容纳于当前视口,并用实际字体覆盖长表名与侧栏缩窄的回归测试。证据:`.work/R8.3-r1/final-probes.log` 的 `native_text_keeps_a_valid_31_character_sheet_name_fully_visible`。 diff --git a/tasks/reviews/R8.3-r2.md b/tasks/reviews/R8.3-r2.md new file mode 100644 index 0000000..e773555 --- /dev/null +++ b/tasks/reviews/R8.3-r2.md @@ -0,0 +1,3 @@ +FAIL + +1. `apps/ely-table/src/sheets.rs:118`(同文件 `:331` 的测试):跨工作簿状态隔离的修复缺少回归测试。现有 `sheets::tests` 只测 `visible()`;`a_switch_stays_with_its_workbook` 在不渲染标签条的 `Blank` 窗口里只核对内核状态,组件的标签测试也只用一个固定身份。把此处退回共享的 `table-sheets`,这些测试仍无法发现:两个各有 30 张表、当前索引均为 29 的工作簿,第一个手动滚回开头后切到第二个,活动标签落在视口外。独立探针已核对新键通过、旧键复现(视口 `[8,604]`,旧键的活动标签 `[2490,2574]`),但探针不属于项目测试。修正方向:加入实际渲染 `sheet_strip` 的跨工作簿回归测试,覆盖相同当前索引、手动滚动后切换及切回,并断言活动标签完整位于视口内;恢复旧键时须失败。证据:`.work/R8.3-r2/probe-tests.log`、`probe-source.log`、`validation.log`。 diff --git a/tasks/reviews/R8.3-r3.md b/tasks/reviews/R8.3-r3.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/R8.3-r3.md @@ -0,0 +1 @@ +PASS