From 6f1eebb4b522287696c1cfd366b54e4c81a55ac2 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 19:42:13 -0400 Subject: [PATCH] R8.1: document content keeps the file's colours in a dark app Pages, cells, slides and PDF pages paint as the file says in any appearance; only the chrome around them follows dark mode. The kernel keeps Ely's one light scheme, sent once a view, and MuPDF no longer inverts. Reviews now run gpt-6.1-sol at xhigh. Co-Authored-By: Claude --- agents.md | 6 +- apps/ely-pdf/src/backdrop.rs | 6 +- apps/ely-pdf/src/pages.rs | 14 ++-- apps/ely-pdf/src/side.rs | 26 +++---- apps/ely-pdf/src/tests/input.rs | 17 +++-- apps/ely-pdf/src/tests/structure.rs | 2 +- apps/ely-pdf/src/tests/view.rs | 20 +++++- apps/ely-pdf/src/view.rs | 6 +- apps/ely-show/src/deck.rs | 8 +-- apps/ely-show/src/extra.rs | 4 +- apps/ely-show/src/output.rs | 2 +- apps/ely-show/src/preview.rs | 4 +- apps/ely-show/src/slideshow/presenter.rs | 2 +- apps/ely-show/src/slideshow/stage.rs | 6 +- apps/ely-show/src/slideshow/start.rs | 11 ++- crates/ely-lok/src/client/tests/paint.rs | 24 ++----- crates/ely-lok/src/client/tests/sheet.rs | 12 ++-- crates/ely-lok/src/lib.rs | 2 +- crates/ely-lok/src/view.rs | 8 +-- crates/ely-lok/src/view/reads.rs | 6 +- crates/ely-lok/src/view/render.rs | 38 ++++------ crates/ely-lok/src/view/tests/theme.rs | 72 +++++++++++++------ crates/ely-pdf-engine/shim/ely_mupdf.c | 4 +- crates/ely-pdf-engine/shim/ely_shim.h | 2 +- crates/ely-pdf-engine/shim/ely_write.c | 2 +- .../src/bin/ely-pdf-worker/ffi.rs | 3 - .../src/bin/ely-pdf-worker/main.rs | 3 +- .../src/bin/ely-pdf-worker/output.rs | 4 +- crates/ely-pdf-engine/src/client.rs | 3 +- crates/ely-pdf-engine/tests/edit.rs | 2 +- crates/ely-pdf-engine/tests/marks.rs | 2 +- crates/ely-pdf-engine/tests/ocr.rs | 10 +-- crates/ely-pdf-engine/tests/output.rs | 12 ++-- crates/ely-pdf-engine/tests/redact.rs | 6 +- crates/ely-pdf-engine/tests/worker.rs | 14 +--- packaging/libreoffice/ely.xcd | 19 ----- scripts/review.sh | 4 +- tasks/TASKS.md | 10 ++- tasks/reviews/R8.1-r1.md | 5 ++ tasks/reviews/R8.1-r2.md | 5 ++ tasks/reviews/R8.1-r3.md | 1 + tests/scripts/theme.sh | 6 +- 42 files changed, 204 insertions(+), 209 deletions(-) create mode 100644 tasks/reviews/R8.1-r1.md create mode 100644 tasks/reviews/R8.1-r2.md create mode 100644 tasks/reviews/R8.1-r3.md diff --git a/agents.md b/agents.md index c6de9e0..3277699 100644 --- a/agents.md +++ b/agents.md @@ -123,7 +123,7 @@ scripts/fetch-references.sh 取上游源码到 reference/ scripts/fetch-tessdata.sh 取 OCR 语言数据到 engines/tessdata(钉提交,核对散列) scripts/build-git.sh 构建钉住的 Git 到 engines/git(ElyTree 随包) scripts/fetch-git-lfs.sh 取 git-lfs 放进 engines/git(核对散列) -scripts/review.sh ID 轮次 路径 审查一条任务(herdr 右侧分栏,codex gpt-6-astra,effort max,fast) +scripts/review.sh ID 轮次 路径 审查一条任务(herdr 右侧分栏,codex gpt-6.1-sol,effort xhigh,fast) scripts/apply-lo-patches.sh engines/lo-src 找出源码已到补丁序列第几个,补上其余(构建脚本调用) tests/scripts/lo-patches.sh 补丁序列从中途接着打、再打一次为空操作 tests/scripts/media-targets.sh 媒体播放接口在 Linux、Windows 目标可编译(缺目标时 SKIP) @@ -213,7 +213,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-05(用户定):ElyAgent 的后端与内置模型的登录配置以 Kigi CLI(https://git.zacharyzhang.com/ZacharyZhang-NY/Kigi-CLI,Rust)为基础,以 Rust 依赖直接嵌入,不用 ACP;沿用其提供商登录(订阅账号或 API 密钥)与模型配置。 - 2026-10-05(用户定):八个应用都不许卡顿:键入、选择、滚动、文档渲染都要即时(GPUI 的 120 FPS 手感),树莓派 3 上也要流畅。输入延迟与帧时间是验收项,要实测;界面线程不等内核、不持 worker 锁做同步请求,空闲不做事。 - 2026-10-05:LOK 的输入与事件路径(实测后定)。原样:视图的事件监听长轮询 worker(持 worker 锁最多 50 毫秒,worker 读线程也随之阻塞最多 250 毫秒),按键排在锁后,键入锁等待中位 290 毫秒、最长 9 秒;输入队列在界面执行器上逐条跳回界面线程,每键约一帧,连续键入积压到 1 至 2 秒。现在:worker 有新事件即主动推送(`tag::EVENTS`,推送线程在输出锁下取批,批次即发送时状态),客户端读线程把回复与事件分开,事件存入 `client::Feed`,视图以异步方式等它(`Feed::next`,有新批次时唤醒,不占线程、不碰 worker 锁;原先每个打开的文档阻塞一个后台线程最多一秒,文档多于核数时会饿死输入与图块;监听任务归视图所有,视图释放即停);每个视图一条后台通道(`view/lane.rs`),输入优先、图块其次,连续执行,界面线程只收落地结果;文档尺寸与页框只在内核报尺寸变化时在后台读;批次弄脏的已显示图块即刻重绘,不等下一帧;视图的 `Reported` 事件只在内核回报或输入落地时发出,滚动与图块到达不触发应用的观察钩子;编辑与光标移动后应用要读的内核列表(ElyDoc 导航、字数、审阅,ElyTable 工作表与几何、单元格规则、格式面板)经 `ely_lok::read_off` 在后台读,一次一个,结果按发起者核对。实测(macOS,`tests/scripts/lag.sh`):按打字速度键入时输入落地 0 至 1 毫秒,图块 1 至 3 毫秒,按键到新图块 ElyDoc 中位 19 毫秒、ElyTable 3 毫秒;连续快速键入(脚本一次送出近百字)时,各次运行在 3 至 225 毫秒的中位之间摆动:新文档首次键入时内核有一次 150 至 230 毫秒的冷启动(首块图块与拼写等初始化),同一批字排在它之后;原为中位 1 秒、p95 1.9 秒。树莓派 3 未实测(本机无设备)。界面线程不碰 worker:应用对内核的读取、保存与另存为的导出、导出 PDF、比较、邮件合并都经 `Tabs::read_in`(或 `ely_lok::read_off`)在后台执行,回到窗口时按发起的标签与读取时的状态核对;保存导出期间标签状态为“正在保存”,同一标签的后续保存排队到它结束;另存为在写入前按文件身份再查一次目标是否已被别的标签持有;导出期间又有编辑时,“保存后关闭”不关闭,回到关闭询问;关闭标签时的撤销历史也在后台读;邮件合并预览与比较在发起时就保留自己的标签号与工作目录;读取失败不立即重试,等下一次状态变化;载入时一并在后台读版面与撤销历史(`ely_lok::layout`,`LokView::open` 接收版面)。窗口关闭回调先不放行,在后台逐个读标签的撤销历史并关闭(有改动的照常询问),最后一个标签关闭后移除窗口;内核文档的关闭也在后台(2026-10-06 起,原先同步读历史、在界面线程持 worker 锁关闭)。 -- 2026-10-04(用户定):八个应用的内容区一律随系统深浅色:Doc 页面、Table 网格、Show 幻灯片、PDF 页面、邮件正文、Mark 编辑区、Agent 输出。自动颜色随之翻转,文件里写明的颜色照原样;打印与导出 PDF 仍为浅色。各引擎(LOK、MuPDF)接入应用主题,不留引擎默认色。LOK:`ely.xcd` 定义 `COLOR_SCHEME_ELY_DARK` 与 `COLOR_SCHEME_ELY_LIGHT`(文档底色、自动文字色、网格、链接等取 Ely 默认色板;内核自带的深色方案颜色留空、按应用外观取默认值,传方案名不改外观,所以不用它),`LokView` 在主题深浅变化时经有序队列发 `.uno:ChangeTheme`(不算编辑),页面底色深色取 `surface`、浅色取 `paper`;Omarchy 色板与这两套固定色可能略有差别。导出 PDF 实测仍为白底。图块作废时旧图仍显示,新图到达后替换,编辑与主题切换都不闪。 +- 2026-10-04(用户定),2026-10-18 改(用户定):Doc 页面、Table 单元格、Show 幻灯片、PDF 页面在任何外观下都照文件原色(白底即白底),只有页面四周的界面随深浅色;邮件正文、Mark 编辑区、Agent 输出随系统深浅色。LOK:`ely.xcd` 只有 `COLOR_SCHEME_ELY_LIGHT`(文档底色白、自动文字色黑、网格、链接等);方案按视图记(LOK 下全局改动只作用于当前视图),每个 `LokView` 首帧经有序队列发一次 `.uno:ChangeTheme`(`ely_lok::SCHEME`,不算编辑),ElyShow 放映与输出的副本同样各发一次;旧版写进内核配置的 `COLOR_SCHEME_ELY_DARK` 已删,内核读不到即退回自动方案,首帧的那次改回 Ely 方案。页面底色 `ely_lok::paper()` 为白,同方案的 `DocColor`。MuPDF 照原样画,不反相。图块作废时旧图仍显示,新图到达后替换,编辑不闪。 - 2026-10-01:主题。`Appearance` 为系统、浅色、深色。系统档在有 Omarchy 时跟随 Omarchy(`ThemeSettings::omarchy_sync`)。读 Omarchy 的 `$HOME/.local/state/omarchy/current/theme/colors.toml`(Omarchy 写死此路径,不随 `XDG_STATE_HOME`),目录由 `ELY_OMARCHY_DIR` 可改。读法与回退链照 `reference/omarchy/bin/omarchy-theme-color`:松散引号、`colorN`、`purple`、`selection_background`、`theme_type`、`light.mode`、亮度和 382 比。缺的色相用 Ely 自己的色并警告。色板照组件库 `A11yChecker` 的规则收口:正文、状态色、代码色在各自的表面上 4.5:1,焦点与图表色在页面上 3:1;先拉近前景再拉到纯黑白,最后逐项核对,核对不过的主题整套忽略。切换时 Omarchy 先暂存 `next-theme` 再整体替换 `theme`,用文件监视跟随,不用它的钩子。新主题无 `colors.toml` 时退回 Ely 色板。`ELY_APPEARANCE`、`ELY_OMARCHY_SYNC`、`ELY_REDUCE_MOTION` 是覆盖开关,设置页接入前先用环境变量。 - 2026-10-05:组件库 8658999(组件库重写历史后的 main,加本项目三个提交):属主不经 `Close` 移除的浮层(一起移除的嵌套浮层同样,回到最外层打开前之处)(对话框动作只清自身状态、异步校验通过后才关)照样把焦点交回打开前之处;原先焦点落在根上,键入无处可去,须先点一下网格。同批带入上游的 ColumnShell、输入法组字时光标重新闪烁、`Icon::from_path`、MIT 许可。旧 pin 2f02c82 已不在 main 上,只能按完整哈希取得。`Select::label` 与 `TextInput::label` 给控件无障碍名称而不绘制;对话框里描述列表旁的控件以此命名,开关用自身的 `label`。7186179 加 `NumberInput::label`(经内部文本框的 `TextInput::set_label`),同批带入上游的 rustfmt 修正。80c46b1:`TemplatePicker` 的列表是一个 Tab 停靠点,上下、Home、End 移动,回车使用,行带列表项语义,搜索提示、全部、使用、无匹配的文案进内建中英目录。7df3045:`SelectableList` 在 AX 树里是列表(`label` 命名),`ListItem` 是列表项(标题为名,带说明、选中、在可选列表中的序号与总数);原先行无角色,不在 AX 树里。af54877:`NumberInput::blank` 显示占位文字而非数字(混合选区),空着回车或失焦不提交,键入后照常。408f2b0:`CommentSidebar` 与 `RelativeTime` 的文案进内建中英目录;属主不处理“解决”时(无 `on_resolve`)不分未解决与已解决,一列显示全部。bcf0378:`CommentSidebar` 的批注列是列表,每条是 Tab 停靠点(列表项语义,名为作者与正文,带选中、序号与总数),回车或空格选中。71bfc93:`init_for_tests`(`test-support` 下)装主题与键位、不要资源,应用单测可渲染 Ely 组件。617a8d6:`FindWidget` 在查询框里按 Esc 即关闭(调 `on_close`)。2ce3368:`ContextMenu` 可定宿主大小。3d08744:`DescriptionList` 的值占满所在列,其中的文本框随之全宽;同批带入上游的图表、分页改动。a6b1bb0:`SignaturePad` 的“在此签名”与“清除”进内建中英目录;同批带入上游 P01 至 P15(各组件的越界与失效状态记日志而不 panic)。490b022:`ChangesList` 分列冲突、已暂存、改动、未跟踪、已忽略(各有其动作:冲突与已忽略只可打开),标题与提示进内建中英目录;同批带入上游 P16 至 P20。7bb53f4(ElyTree 用):`ChangesList` 行数可缺、按区报按下、当前行;`DiffViewer` 外算的段、块按钮、可选的变动行、逐行读给辅助技术;`CommitList`、`FileHistory`、`BlameView`、`ThreeWayMerge` 只画可见行,`CommitList` 近尾加载;`DescriptionList::text`;`SegmentedControl` 各段为单选;`BranchList`、`TagList` 可选中、只画属主处理的行内动作;`marked`、`conflicts`、`resolve` 按标记宽度(`MARKER` 为默认 7),a284c71 起只按 LF 断行;c5799cc:`StashList` 文案进内建中英目录,行可选(`selected`、`on_pick`),只画属主处理的行内动作;7fab072 至 3741d5d:分支、标签、储藏的行分两行(上行名称与行内动作,下行说明与行尾文字),窄时名称与说明先让;行内动作一律显示(原先只在悬停时显示,键盘停在其上时看不见),改动列表同;各 git 行带透明的 1 像素边框,焦点框才画得出(原先 `focus_ring` 无边框宽度,键盘焦点不可见);同批带入上游 P21 至 P29。4f8f3bd:`Changed::lines` 可缺(二进制文件、文件夹不计行数,不显示);37c5187 缩短其注释。19b0b29:`DiffViewer::from_stretches`(别处算好的差异,如 Git 自己的)、`hunk_action`(每块标题上一个按钮)、`selected` 与 `on_line`(改动行可按,带 Shift),`diff::mark_words` 给外来的行标出改动的词,“行内”“并排”“N 行未改动”进内建中英目录;30fea9e:有块按钮时各行同高,底色相接。edd2c12:`ChangesList` 每次按下带分区(`ChangeSection`),行是 Tab 停靠点(列表项语义),`selected` 标出当前文件。2e080b8:`SegmentedControl` 每段是单选按钮(名为其文字,带选中状态),原先不在 AX 树里。e226d79:差异查看器的行模型与配色移入 `git/rows.rs`(文件不超过 500 行);7b807a9 缩短其注释。51d2aea:`CommitList`、`FileHistory`、`BlameView` 只画视口内的行(`uniform_list`,填满所在的框;`CommitList` 收 `Rc` 的提交与 `lanes` 算好的图,`on_end` 在接近末尾时要下一页),各行是 Tab 停靠点(列表项语义,选中状态;Blame 每段首行可按,名为作者、时间与摘要),`FileHistory` 的行数可缺;`CommitInput` 的“提交到”“修改最近提交”进内建中英目录。e6220ad 与 3fc5a46:提交行窄时作者、提交号与时间先让,摘要至少 6rem。d9cbe99:`DescriptionList::text`(标签与文字值,行的无障碍名为“标签: 值”),提交行的无障碍名带上引用。fb7ee42 与 fcc45cc:`DiffViewer` 只读时每行也是标签(“行号 + 文字”),块头为标题,折叠行有名;`BlameView` 每行为“行号 文字”,段首按钮的名略去空的部分。3d0a3de 至 949d776:差异与内建目录的注释缩短。829ef68:`ThreeWayMerge` 只画视口内的行(填满所在的框;每行三侧各一行,冲突处一行带三个选择按钮,各行同高;不再附结果与统计,结果由属主显示),文案进内建中英目录,每行与冲突行在 AX 树里有文字;`merging::conflicts` 认 diff3 的 `|||||||` 段,`resolve` 取当前侧时不带基底段;`BranchList` 与 `TagList` 的行可选(`selected`、`on_pick`,Tab 停靠点、列表项语义)。63d4a45 至 922cd53:`BranchList` 只画属主处理的行内动作;`merging::marked` 按标记文字里 Git 的分组得出各段(diff3 段为基底);窄时行尾文字先让,行内动作不被挤出。 2ce3368:`ContextMenu` 实现 `Styled`,样式定其宿主大小,其中的虚拟列表才能撑满(原宿主无尺寸,`uniform_list` 高度为零)。 - 2026-10-05:ElyTable 透视表与图表(C09.4,TABLE-10、11)。补丁 `0019-lok-calc-pivot-tables.patch` 与 `0020-lok-calc-charts.patch`:`.uno:DataPilotExec` 收 `Pivot`(JSON:源区域、行、列、筛选、值字段与函数 sum/count/average/max/min),光标在透视表上即原地修改,否则从源区域在新工作表创建;行、列、筛选里的空名表示内核的数据布局字段(多个值字段横排或竖排的位置),读出与写回都保留,已在表中的行、列、筛选字段保留各自的小计函数,读出的布局原样写回不改表;照内核布局对话框的步骤(`ConvertOrientation`,保留各字段的成员、排序与版式),新表的筛选按钮、钻取、展开收起都关(同对话框初值);放到新工作表时插表与透视表是两步撤销(同桌面版)。刷新 `.uno:RecalcPivotTable` 在内核里本就不进撤销(它改缓存)。`.uno:InsertObjectChart` 收 `Chart`(JSON:类型 column/bar/line/scatter/pie、标题、图例、X/Y 轴标题;`other` 保持原类型),LOK 下不开图表向导、插入为一步撤销、插入后图表选中;给出的区域每段都要解析得了、所指工作表都要存在,否则插入与修改都拒绝、文档不动(内核原会改用示例数据,修改时 `ChartModel::setArguments` 抛出未捕获的异常使 worker 退出);`.uno:ChangeChartData` 也收 `Chart`,区域与外观合成一步撤销(撤销保存整个图表的图示与主标题的副本,堆积等外观以外的设置一并恢复)(列表动作带当前视图号,否则 LOK 拒绝撤销别的视图的动作)。读取:`.uno:Chart`(选中图表的对象名、区域、表头、外观)、`.uno:PivotTable`(光标处透视表的名称、位置、源区域、字段与布局),`.uno:SelectedRange` 加 `data`(选区内数据的绝对区域)。界面在“插入”菜单:数据透视表、刷新数据透视表、图表;对话框在光标处有透视表、或有选中图表时为修改,否则以选中的数据(单格扩到数据区)新建,源与图表区域取 `data`,不含空行。为对话框发起的读取(`Tabs::ask_in`)回来时,发起的工作簿已不是当前标签即丢弃,不为隐藏的工作簿开对话框(ElyTable 全部对话框、合并询问)。没有数据时恢复原光标(`select.data` 会把单格标成区域,回车随之只在格内打转)。公式语法是 Excel A1,区域写 `Sheet1!$A$1:$C$5`。 @@ -277,7 +277,7 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-10:Windows 与 Linux 的构建环境(E-01)。Windows:MSVC(VS 2022,v143);MuPDF 由 `build-mupdf.sh` 以 MSBuild 编上游 `platform/win32` 的 `libmupdf`(静态运行库 /MT,同 Rust 的 `crt-static`;`HAVE_LIBCRYPTO`,`libmupdf.lib` 内含第三方、Tesseract 与 PKCS#7);静态 OpenSSL 取自钉在 2026.07.29 的 vcpkg(`engines/vcpkg`,`openssl:x64-windows-static`);Git 为 Git for Windows 的 MinGit 2.56.0(钉散列,`engines/git/cmd/git.exe`,LFS 在 `ucrt64/libexec/git-core`);`%TEMP%` 下的程序被系统的应用控制拦下,构建与探针都不放那里;cmake 与 ninja 须在 PATH;LibreOffice 的 configure 只认 VS 自带的 CMake(组件 `VC.CMake.Project`)。仓库与 `reference/` 一律 LF(`core.autocrlf false`、`core.eol lf`;`fetch-references.sh` 对每个上游照设)。Linux:WSL 的 Ubuntu 26.04;静态 libcrypto 另需其 pkg-config 列出的库(jitterentropy、zstd、z)。LibreOffice 在 Linux 照 Collabora 的 LOKit 配置构建(无图形后端、cairo RGBA、自带各库)。 - 2026-10-10:Windows 与 Linux 的内核与 worker(W-01、K-01,用户定:0.1.3 先三端发布)。内核只认合并库(`ffi::KERNEL`:`libmergedlo.dylib`、`libmergedlo.so`、`mergedlo.dll`;`sofficeapp` 在合并构建里是文本占位);Windows 载入时一并搜索该 DLL 所在的 `program/`。查找次序照旧,Windows 与 Linux 的包内位置是应用旁的 `libreoffice/program`,开发构建为 `engines/lo-src/instdir/program`。worker 的主循环:macOS 照旧以 GCD;Linux 以 unipoll,`runLoop` 的轮询回调里每次答一个请求,内核的唤醒回调往同一队列放信号(`worker/unipoll.rs`;回调的数据指针须非空,VCL 见空即不调用);Windows 也以 unipoll:LOK 不另起主线程,worker 的主线程跑 `runLoop`(Windows 的消息循环),请求经只收消息的窗口投到这条线程上执行(`worker/messages.rs`),同 macOS 的 GCD;原先 LOK 自起主线程、worker 在另一线程作答,载入表格即死锁。Windows 的循环不调轮询回调,但须传非空的回调,`isUnipoll` 只看它。补丁 `0043-lok-callbacks-flush-inline-everywhere.patch`:回调队列在各平台都即时冲出(原只 macOS),否则请求里等的命令结果与状态要等下一轮主循环才到。`0042-svx-boost-json-needs-no-library.patch`:svxcore 与 sd 定义 `BOOST_ALL_NO_LIB`,MSVC 不再按 Boost 的自动链接索要 `libboost_json`、`libboost_container`。Linux 的内核以 cairo 画图块(worker 设 `SAL_DISABLESKIA`;构建去掉 `--enable-cairo-rgba`,自带的 cairo 要清掉重建),字节为 BGRA,同 macOS 与 Windows;`getTileMode` 只看 cairo 开关,不可信。worker 启动核对在三端都画一段红字,按 BGRA 找不到红色即拒绝。`scripts/apply-lo-patches.sh` 的临时库设 `core.autocrlf false`(Windows 默认转换使任何前缀都对不上)。Windows 的 LibreOffice 在 Cygwin 下构建(`build-libreoffice.sh` 的 CYGWIN 分支:去掉 Unix 专用开关,加 VS 2022,用 `/opt/lo/bin` 的 make,清掉 Git Bash 带进来的 `MSYSTEM`);构建目录与 Cygwin 排除在 Defender 扫描之外(否则并行 configure 删不掉 `conftest.dir`)。英文词典扩展里的 Lightproof(Python 写的语法检查)在构建后删去:macOS 与 Linux 的内核不带 Python,它本不生效;Windows 必带内置 Python(Cygwin 下 configure 不许去掉),第二次用同一配置启动时初始化它,worker 挂起。worker 以 `SAL_NOLOCK_PROFILE` 运行,`ely.xcd` 关掉 LibreOffice 的文档锁(`Common/Misc/UseLocking`):被强杀的 worker 留下的锁会让下次启动或载入弹出交互而挂起;每个应用只有一个 worker 用自己的配置,冲突由 Ely 自己核对。三端发布(CI-01):`.gitea/workflows/release.yml` 在打标签时于三台运行器上执行 `scripts/ci.sh`:macOS 在 Mac mini 的常驻克隆里借用开发克隆 `~/Documents/GitHub/ElyOfficeSuite` 的引擎(`ELY_SHARE`;2026-10-11 起:从零构建 LibreOffice 超过 act_runner 默认的 3 小时,v0.1.3 的 Mac 任务被杀、无 dmg),Windows 在本机运行器上把 `engines` 与 `reference` 接到开发克隆,Linux 由同一台 Windows 运行器经 WSL 构建(服务器上的 Linux 运行器是一次性容器,建不了内核);各自打包后上传到该标签的发布。Windows 为 NSIS 安装程序,Linux 为 AppImage、deb、rpm 与 Arch 包(引擎构建与打包都经 `packaging/linux/container.sh` 进 Ubuntu 26.04 容器,WSL 里须有 docker;`ELY_SHARE` 的目录一并挂进容器),都由 `packaging/stage.sh` 分装引擎;本提交的源码包只由 Linux 上传(`packaging/source.sh`;macOS 照建供 `release-macos.sh` 核对,不上传,两端并行时不重复;Windows 的检出把符号链接写成文件,不出);重跑时所传文件有任一同名的已在发布里即失败、什么也不传,写明须先删去哪些(重建的字节不同,更新清单的散列会对不上已在的 dmg);更新清单只含 macOS(Windows 与 Linux 的自动安装未做)。 - 2026-10-11:gpui 的 Windows 窗口关闭(W-03 冒烟查出)。`gpui-pre-windows` 0.3.8 在窗口关闭时报三条错:系统已销毁窗口(ElyTree 的关窗交给系统)后 Drop 仍对旧句柄 `RevokeDragDrop` 与 `DestroyWindow`(拖放处理器因此不释放,连带窗口状态);销毁时的失活回报排在异步任务里,执行时窗口已移除(“window not found”);退出后后台任务向已销毁的平台窗口投递(`Invalid window handle`)。修复在 `vendor/gpui-pre-windows`:`WM_DESTROY` 时记下已销毁、在句柄仍有效时撤销拖放、去掉失活回调,Drop 见已销毁即不再动句柄;平台窗口已不在时投递失败只记 debug(应用已退出),其余投递失败照报。三个应用关窗与退出的日志无错。 -- 2026-10-11(用户报:Windows 启动带黑色命令行窗口、“打开方式”里无图标、点 X 不退出、表格全黑无网格线):四处根因。命令行窗口:应用是控制台子系统;发行构建改为 `windows_subsystem = "windows"`(开发构建照旧有控制台看日志),子进程(两个 worker、Git、`cmd start`)一律经 `ely_ipc::windowless` 以 `CREATE_NO_WINDOW` 启动。图标:exe 里只有 gpui 的清单资源;各应用共用构建脚本 `packaging/windows/icon.rs`,把 `assets/icons/<应用>.ico` 编成资源 1(gpui 的窗口图标也取资源 1)。点 X 不退出:窗口其实弹了“保存更改”询问。`ely.xcd` 深色方案的 `CalcNotesBackground` 与浅色不同,Calc 在配色变化时改写批注样式并记一步撤销(`ScDetectiveFunc::UpdateAllComments`),未动的表格切到深色即算已修改;两套方案的批注底色改为同一值(LibreOffice 默认的浅黄),切换主题不再改文档,三端同此(`sheet.rs` 的 `a_theme_switch_is_no_edit`)。全黑:Win32 的虚拟设备没有 `SetSizeUsingBuffer`(基类默认返回假,断言在发行构建里不在),给客户缓冲区的设备停在 1×1 像素,Calc 图块只画了一个像素,放映图层同空;补丁 `0044-lok-win32-paints-then-copies.patch` 让 Windows 走 macOS 的路:按全尺寸画进设备,再拷进客户缓冲区(`doc_paintTile` 与 `SlideshowLayerRenderer`)。Writer 自设设备尺寸,原本不受影响。`tests/scripts/lo-patches.sh` 的临时库也设 `core.autocrlf false`(原先在 Windows 上恒失败)。 +- 2026-10-11(用户报:Windows 启动带黑色命令行窗口、“打开方式”里无图标、点 X 不退出、表格全黑无网格线):四处根因。命令行窗口:应用是控制台子系统;发行构建改为 `windows_subsystem = "windows"`(开发构建照旧有控制台看日志),子进程(两个 worker、Git、`cmd start`)一律经 `ely_ipc::windowless` 以 `CREATE_NO_WINDOW` 启动。图标:exe 里只有 gpui 的清单资源;各应用共用构建脚本 `packaging/windows/icon.rs`,把 `assets/icons/<应用>.ico` 编成资源 1(gpui 的窗口图标也取资源 1)。点 X 不退出:窗口其实弹了“保存更改”询问。`ely.xcd` 深色方案的 `CalcNotesBackground` 与浅色不同,Calc 在配色变化时改写批注样式并记一步撤销(`ScDetectiveFunc::UpdateAllComments`),未动的表格切到深色即算已修改;两套方案的批注底色改为同一值(LibreOffice 默认的浅黄),切换主题不再改文档,三端同此(2026-10-18 起只剩浅色方案,`sheet.rs` 的 `the_scheme_is_no_edit`)。全黑:Win32 的虚拟设备没有 `SetSizeUsingBuffer`(基类默认返回假,断言在发行构建里不在),给客户缓冲区的设备停在 1×1 像素,Calc 图块只画了一个像素,放映图层同空;补丁 `0044-lok-win32-paints-then-copies.patch` 让 Windows 走 macOS 的路:按全尺寸画进设备,再拷进客户缓冲区(`doc_paintTile` 与 `SlideshowLayerRenderer`)。Writer 自设设备尺寸,原本不受影响。`tests/scripts/lo-patches.sh` 的临时库也设 `core.autocrlf false`(原先在 Windows 上恒失败)。 - 2026-10-11(用户报:Windows 上标题栏的最小化、最大化、关闭三个按钮都按不动):gpui 回答系统的命中测试(`WM_NCHITTEST`)时按绘制次序取第一个含指针的窗口控制区,标题栏的拖动区先于其中的按钮画出,按钮上一律答“标题栏”,系统把按下当作拖动窗口的开始,点击到不了按钮。修复在 `vendor/gpui-pre`(原样一个提交,其后是修复):按命中测试的次序取最上层的控制区,且只看未被遮挡的部分(同 `is_hovered`,盖在标题栏上的对话框挡住其下的按钮)。实测(发行构建,DPI 感知的探针移动真实指针):自右向左依次答关闭、最大化、最小化、拖动;按下最小化即最小化,最大化即最大化,关闭即退出、worker 随之结束;v0.1.3 的安装版同一探针处处答拖动。该回调只有 Windows 平台调用。 - 2026-10-11:Linux 标题栏按钮(核查 Windows 按钮问题时查出)。macOS 的红绿灯由系统画,不受影响;Linux 为客户端装饰,按钮由组件库画在标题栏的拖动区里,拖动区对任何按下都准备移动窗口,按住按钮时指针动一像素即开始移动窗口,合成器接管指针,松开到不了按钮,点击落空。组件库 3eb4d34:`drag_region` 只在按下未被里面的按钮认领(按钮以 `prevent_default` 认领)时才准备移动;`shell/title_tests.rs` 核对晃动的按下点得动 Linux 的关闭、标题栏空处照样移动窗口(去掉修复时前者失败)。未在真 Linux 桌面上点过。 - 2026-10-11(用户报:ElyTree 改动列表里文件名长时盖住行尾的打开按钮):组件库 bd01470:`ChangesList` 行里的文件名原为不缩的盒子,长名越过行宽压在按钮下;现可缩、以省略号结尾,先于按钮让位;`git/tests/narrow.rs` 核对 260 宽时文件名止于按钮之前(原布局时失败)。 diff --git a/apps/ely-pdf/src/backdrop.rs b/apps/ely-pdf/src/backdrop.rs index e0e122a..0302674 100644 --- a/apps/ely-pdf/src/backdrop.rs +++ b/apps/ely-pdf/src/backdrop.rs @@ -36,7 +36,7 @@ fn fraction(key: &TileKey, size: (f32, f32)) -> [f32; 4] { impl PdfView { /// The pages in view and the tiles each needs. - pub fn showing(&self, sf: f32, dark: bool) -> Vec { + pub fn showing(&self, sf: f32) -> Vec { let laid = self.laid_out(); let (room_w, room_h) = (f32::from(self.room.size.width), f32::from(self.room.size.height)); let (left, top) = (-f32::from(self.scroll.offset().x), -f32::from(self.scroll.offset().y)); @@ -50,7 +50,7 @@ impl PdfView { let keys = ys .flat_map(|ty| xs.clone().map(move |tx| (tx, ty))) .map(|(tx, ty)| { - let key = TileKey { page: place.page, scale, rotate: self.rotate, dark, tx, ty }; + let key = TileKey { page: place.page, scale, rotate: self.rotate, tx, ty }; (key, [tx * TILE, ty * TILE, TILE.min(dw - tx * TILE), TILE.min(dh - ty * TILE)]) }) .collect(); @@ -123,7 +123,7 @@ mod tests { #[test] fn a_tile_keeps_its_place_as_a_fraction_of_its_page() { - let key = TileKey { page: 0, scale: 2000, rotate: 0, dark: false, tx: 1, ty: 0 }; + let key = TileKey { page: 0, scale: 2000, rotate: 0, tx: 1, ty: 0 }; // A 612 by 792 page at 2x: 1224 by 1584. assert_eq!(fraction(&key, (612.0, 792.0)), [512.0 / 1224.0, 0.0, 512.0 / 1224.0, 512.0 / 1584.0]); let turned = TileKey { rotate: 90, ..key }; diff --git a/apps/ely-pdf/src/pages.rs b/apps/ely-pdf/src/pages.rs index b5f5fb2..65201b3 100644 --- a/apps/ely-pdf/src/pages.rs +++ b/apps/ely-pdf/src/pages.rs @@ -16,14 +16,13 @@ use crate::view::{PdfView, Reported}; /// A tile's side in device pixels. pub const TILE: i32 = 512; -/// A page tile at one scale, turn and theme. +/// A page tile at one scale and turn. #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub struct TileKey { pub page: usize, /// Device pixels a point, times a thousand. pub scale: u32, pub rotate: i32, - pub dark: bool, pub tx: i32, pub ty: i32, } @@ -123,17 +122,14 @@ impl PdfView { /// Pages in view and their tiles; missing ones asked. pub fn page_view(&mut self, window: &mut Window, cx: &mut Context) -> AnyElement { - let (sf, dark) = (window.scale_factor(), cx.theme().is_dark()); - let (sunken, paper) = { - let colors = &cx.theme().colors; - (colors.sunken, if dark { colors.surface } else { colors.paper }) - }; + let sf = window.scale_factor(); + let (sunken, paper) = (cx.theme().colors.sunken, gpui::white()); let laid = self.laid_out(); if let Some((x, y)) = self.go_to.take() { self.scroll.set_offset(point(px(-x), px(-y))); } let top = -f32::from(self.scroll.offset().y); - let showing = self.showing(sf, dark); + let showing = self.showing(sf); // The current page: first showing more than a sliver. if let Some(first) = showing.iter().map(|s| s.place).find(|p| p.y + p.h > top + GAP * 3.0) { let paired = showing.iter().any(|s| s.place.page == self.current) && first.page / 2 == self.current / 2; @@ -266,7 +262,7 @@ impl PdfView { self.painting = true; let (doc, scale, version) = (self.doc, key.scale as f32 / 1000.0, self.version); self.off( - move |worker| worker.render(doc, key.page, (scale, key.rotate), rect, key.dark), + move |worker| worker.render(doc, key.page, (scale, key.rotate), rect), move |view, painted, cx| { view.painting = false; if view.version != version { diff --git a/apps/ely-pdf/src/side.rs b/apps/ely-pdf/src/side.rs index 62fbe33..8085d53 100644 --- a/apps/ely-pdf/src/side.rs +++ b/apps/ely-pdf/src/side.rs @@ -100,11 +100,11 @@ impl PdfView { } /// Asks for the first missing picture in `rows`. - fn ask_thumb(&mut self, rows: Range, (sf, dark): (f32, bool), cx: &mut Context) { + fn ask_thumb(&mut self, rows: Range, sf: f32, cx: &mut Context) { if self.thumbing { return; } - let due = |page: &usize| !self.thumbs.contains_key(&(*page, dark)) || self.thumbs_stale.contains(&(*page, dark)); + let due = |page: &usize| !self.thumbs.contains_key(page) || self.thumbs_stale.contains(page); let Some(page) = rows.clone().find(|page| due(page) && !self.thumbs_failed.contains(page)) else { return; }; @@ -114,7 +114,7 @@ impl PdfView { let (doc, version) = (self.doc, self.version); self.thumbing = true; self.off( - move |worker| worker.render(doc, page, (scale, 0), tile, dark), + move |worker| worker.render(doc, page, (scale, 0), tile), move |view, painted, cx| { view.thumbing = false; if view.version != version { @@ -128,8 +128,8 @@ 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.thumbs_stale.remove(&page); + if let Some(old) = view.thumbs.insert(page, image) { view.retired.push(old); } } @@ -149,18 +149,18 @@ impl PdfView { impl PdfView { /// The picture a row draws; an old one until repainted. - pub fn picture(&self, page: usize, dark: bool) -> Option> { - self.thumbs.get(&(page, dark)).cloned() + pub fn picture(&self, page: usize) -> Option> { + self.thumbs.get(&page).cloned() } } /// One row: the page number and its picture. -fn row(view: &Entity, page: usize, dark: bool, cx: &App) -> gpui::AnyElement { +fn row(view: &Entity, page: usize, 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.picture(page, dark); + let picture = read.picture(page); 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()); @@ -210,7 +210,7 @@ fn row(view: &Entity, page: usize, dark: bool, cx: &App) -> gpui::AnyEl .h(px(height)) .border_1() .border_color(if picked { colors.focus } else { colors.border }) - .bg(if dark { colors.surface } else { colors.paper }) + .bg(gpui::white()) .when_some(picture, |frame, picture| frame.child(img(picture).size_full())), ) .child(div().text_xs().text_color(colors.fg_subtle).child((page + 1).to_string())) @@ -258,12 +258,12 @@ impl Render for Side { Pane::Notes => crate::notelist::list(&view, cx), Pane::Files => crate::attach::list(&view, cx), Pane::Pages => { - let shown = (window.scale_factor(), cx.theme().is_dark()); + let sf = window.scale_factor(); let count = view.read(cx).sizes.len(); let rows = view.clone(); let list = uniform_list("pdf-thumbs", count, move |range, _, cx| { - rows.update(cx, |view, cx| view.ask_thumb(range.clone(), shown, cx)); - range.map(|page| row(&rows, page, shown.1, cx)).collect() + rows.update(cx, |view, cx| view.ask_thumb(range.clone(), sf, cx)); + range.map(|page| row(&rows, page, cx)).collect() }) .size_full(); div() diff --git a/apps/ely-pdf/src/tests/input.rs b/apps/ely-pdf/src/tests/input.rs index c87f9d2..1caa794 100644 --- a/apps/ely-pdf/src/tests/input.rs +++ b/apps/ely-pdf/src/tests/input.rs @@ -188,13 +188,13 @@ fn tiles_beside_the_view_are_neither_asked_nor_kept() { /// Flags any notify leaving a shown page with nothing drawn. fn never_bare(cx: &mut VisualTestContext, view: &Entity) -> (gpui::Subscription, std::rc::Rc>) { - let (sf, dark) = cx.update(|window, app| (window.scale_factor(), ely_gpui_component::theme::ActiveTheme::theme(app).is_dark())); + let sf = cx.update(|window, _| window.scale_factor()); let bare = std::rc::Rc::new(std::cell::Cell::new(false)); let seen = bare.clone(); let watch = cx.update(|_, app| { app.observe(view, move |view, app| { let read = view.read(app); - if read.showing(sf, dark).iter().any(|page| read.pieces(page).is_empty()) { + if read.showing(sf).iter().any(|page| read.pieces(page).is_empty()) { seen.set(true); } }) @@ -248,10 +248,10 @@ fn a_new_scale_keeps_the_old_tiles_until_new_ones_land() { cx.simulate_resize(gpui::size(px(700.), px(800.))); // The new size lays out before any new tile lands. cx.run_until_parked(); - let (sf, dark) = cx.update(|window, app| (window.scale_factor(), ely_gpui_component::theme::ActiveTheme::theme(app).is_dark())); + let sf = cx.update(|window, _| window.scale_factor()); let now = cx.update(|_, app| { let read = view.read(app); - read.showing(sf, dark).iter().map(|page| read.pieces(page).len()).collect::>() + read.showing(sf).iter().map(|page| read.pieces(page).len()).collect::>() }); assert!(now.iter().all(|count| *count > 0), "drawn right after the resize: {now:?}"); settle(cx, |app| view.read(app).laid_out().scale != scale && repainted(view.read(app))); @@ -315,7 +315,7 @@ fn old_tiles_are_painted_and_freed_with_their_tab() { let shown = cx.update(|window, app| { let read = view.read(app); let room = read.room; - read.showing(window.scale_factor(), false).iter().map(|s| { + read.showing(window.scale_factor()).iter().map(|s| { let (left, top) = (-f32::from(read.scroll.offset().x), -f32::from(read.scroll.offset().y)); let w = (s.place.x + s.place.w).min(left + f32::from(room.size.width)) - s.place.x.max(left); let h = (s.place.y + s.place.h).min(top + f32::from(room.size.height)) - s.place.y.max(top); @@ -333,9 +333,8 @@ fn an_edit_keeps_page_pictures_until_they_repaint() { 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")); + settle(cx, |app| view.read(app).picture(0).is_some() && repainted(view.read(app))); + let old = cx.update(|_, app| view.read(app).picture(0).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))); @@ -349,7 +348,7 @@ fn an_edit_keeps_page_pictures_until_they_repaint() { 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")); + let new = cx.update(|_, app| view.read(app).picture(0).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"); diff --git a/apps/ely-pdf/src/tests/structure.rs b/apps/ely-pdf/src/tests/structure.rs index 4a75498..1e9991d 100644 --- a/apps/ely-pdf/src/tests/structure.rs +++ b/apps/ely-pdf/src/tests/structure.rs @@ -158,7 +158,7 @@ fn scanned(dir: &std::path::Path) -> std::path::PathBuf { let doc = worker.open(&source).expect("opens").doc; let s: f32 = 200.0 / 72.0; let (w, h) = ((612.0 * s).ceil() as i32, (792.0 * s).ceil() as i32); - let bgra = worker.render(doc, 0, (s, 0), [0, 0, w, h], false).expect("draws").bgra; + let bgra = worker.render(doc, 0, (s, 0), [0, 0, w, h]).expect("draws").bgra; let gray: Vec = bgra.chunks_exact(4).map(|p| ((u16::from(p[0]) + u16::from(p[1]) + u16::from(p[2])) / 3) as u8).collect(); let path = dir.join("scan.pdf"); std::fs::write(&path, scan((612.0, 792.0), (w as u32, h as u32), &gray)).expect("writes"); diff --git a/apps/ely-pdf/src/tests/view.rs b/apps/ely-pdf/src/tests/view.rs index af3bea1..c803e41 100644 --- a/apps/ely-pdf/src/tests/view.rs +++ b/apps/ely-pdf/src/tests/view.rs @@ -1,6 +1,6 @@ //! Password prompts, zoomed pairs, jumps across (C11.1). -use ely_office::{Dialog, Dialogs as _, Tabs as _}; +use ely_office::{Dialog, Dialogs as _, Opening as _, Tabs as _}; use gpui::{Entity, Modifiers, ScrollDelta, ScrollWheelEvent, TestAppContext, VisualTestContext, point, px}; use super::{file, frames, settle, shown, windowed}; @@ -144,3 +144,21 @@ fn ctrl_and_a_wheel_notch_step_the_zoom_once() { frames(cx); assert_eq!(read(cx).0, Zoom::Scale(crate::keys::stepped(1.0, false))); } + +#[test] +fn a_dark_app_shows_the_page_as_the_file_paints_it() { + use ely_gpui_component::theme::{Mode, Theme}; + let dir = tempfile::tempdir().expect("a temp dir"); + let mut cx = TestAppContext::single(); + let (viewer, cx) = windowed(&mut cx, dir.path(), &[], true); + cx.update(|_, app| Theme::set_mode_now(Mode::Dark, app)); + let path = file(dir.path(), "one.pdf", &[(612.0, 792.0, &["alpha"])]); + cx.update(|_, app| viewer.update(app, |viewer, cx| viewer.open_file(path, cx))); + settle(cx, |app| viewer.read(app).active_view().is_some()); + let view = shown(cx, &viewer); + let corner = |app: &gpui::App| view.read(app).tiles.iter().find(|(key, _)| key.tx == 0 && key.ty == 0).map(|(_, image)| image.clone()); + settle(cx, |app| corner(app).is_some()); + let tile = cx.update(|_, app| corner(app)).expect("the first tile"); + let bytes = tile.as_bytes(0).expect("its pixels"); + assert_eq!(&bytes[..3], &[255, 255, 255], "the paper stays white"); +} diff --git a/apps/ely-pdf/src/view.rs b/apps/ely-pdf/src/view.rs index ad0904f..4ffdfcb 100644 --- a/apps/ely-pdf/src/view.rs +++ b/apps/ely-pdf/src/view.rs @@ -87,11 +87,11 @@ pub struct PdfView { pub saved: String, /// Saves so far: a read from before one is stale. pub saves: u64, - /// Page pictures for the side panel, by page and darkness. - pub thumbs: HashMap<(usize, bool), Arc>, + /// Page pictures for the side panel, by page. + pub thumbs: HashMap>, pub thumbs_failed: HashSet, /// Pictures from before an edit; shown until repainted. - pub thumbs_stale: HashSet<(usize, bool)>, + pub thumbs_stale: HashSet, /// A picture is on its way from the worker. pub thumbing: bool, /// Pages picked in the side panel, and the anchor. diff --git a/apps/ely-show/src/deck.rs b/apps/ely-show/src/deck.rs index df7223d..1411027 100644 --- a/apps/ely-show/src/deck.rs +++ b/apps/ely-show/src/deck.rs @@ -100,12 +100,12 @@ impl Host for Deck { view.turn_parts(); }); } - // Pictures follow the kernel's light or dark scheme. - let dark = view.read(cx).dark(); + // Pictures before the scheme landed are repainted. + let lit = view.read(cx).lit(); if let Some(tab) = self.tab_mut(id) - && tab.extra.dark != dark + && tab.extra.lit != lit { - tab.extra.dark = dark; + tab.extra.lit = lit; self.repaint_all(id, cx); } self.offer_menu(id, view, cx); diff --git a/apps/ely-show/src/extra.rs b/apps/ely-show/src/extra.rs index 0921607..86c4bfb 100644 --- a/apps/ely-show/src/extra.rs +++ b/apps/ely-show/src/extra.rs @@ -50,8 +50,8 @@ pub struct Extra { pub painting: bool, /// The parts are masters. pub masters: bool, - /// The scheme the pictures were painted with. - pub dark: Option, + /// The pictures were painted with Ely's scheme. + pub lit: bool, /// The view was fitted to the slide once. pub fitted: bool, } diff --git a/apps/ely-show/src/output.rs b/apps/ely-show/src/output.rs index adfb8fd..abd7b8c 100644 --- a/apps/ely-show/src/output.rs +++ b/apps/ely-show/src/output.rs @@ -267,7 +267,7 @@ impl Deck { let range = output.pages(); let (w, h) = tab.extra.size; let size = (PICTURE, (f64::from(PICTURE) * h as f64 / w.max(1) as f64).round() as u32); - let (copy, light) = (tab.work.join("output.odp"), ely_lok::scheme(false)); + let (copy, light) = (tab.work.join("output.odp"), ely_lok::SCHEME); let (shown, opened) = (folder.display().to_string(), folder.clone()); log::info!("show: writes slide pictures"); self.read_in( diff --git a/apps/ely-show/src/preview.rs b/apps/ely-show/src/preview.rs index b87c8c6..3d8ec0c 100644 --- a/apps/ely-show/src/preview.rs +++ b/apps/ely-show/src/preview.rs @@ -252,7 +252,7 @@ impl Deck { // A show starts from black; effects play on the page. let behind = match &preview.playing { Playing::Transition { .. } => gpui::black(), - Playing::Effects { .. } => ely_lok::paper(cx), + Playing::Effects { .. } => ely_lok::paper(), }; let content = match &preview.playing { Playing::Effects { @@ -266,7 +266,7 @@ impl Deck { } else { (t / preview.length) as f32 }; - let paper = ely_lok::paper(cx); + let paper = ely_lok::paper(); // Exits are not read; the old slide shows everything. let from = (from.as_deref()) .map(|from| compose::slide(from, |_| true, frame, paper).into_any_element()); diff --git a/apps/ely-show/src/slideshow/presenter.rs b/apps/ely-show/src/slideshow/presenter.rs index 5fa4b13..34ea8f1 100644 --- a/apps/ely-show/src/slideshow/presenter.rs +++ b/apps/ely-show/src/slideshow/presenter.rs @@ -159,7 +159,7 @@ impl Render for Presenter { let big = stage::fit(show.slide, (room.width * 0.6, room.height * 0.8)); let small = stage::fit(show.slide, (room.width * 0.3, room.height * 0.35)); let next = match show.upcoming() { - Some(part) => stage::still(show, part, small, cx), + Some(part) => stage::still(show, part, small), None => div().w(small.0).h(small.1).into_any_element(), }; let notes = show.part().map(|part| show.slides[part].notes.clone()); diff --git a/apps/ely-show/src/slideshow/stage.rs b/apps/ely-show/src/slideshow/stage.rs index faa9479..4a627f4 100644 --- a/apps/ely-show/src/slideshow/stage.rs +++ b/apps/ely-show/src/slideshow/stage.rs @@ -27,7 +27,7 @@ pub fn stage(show: &Show, (w, h): (Pixels, Pixels), cx: &App) -> AnyElement { let Some(stack) = show.stack(part) else { return black((w, h)); }; - let paper = ely_lok::paper(cx); + let paper = ely_lok::paper(); let t = show.entered.elapsed().as_secs_f64(); let slide = &show.slides[part]; let shown = match &show.leaving { @@ -76,9 +76,9 @@ pub fn stage(show: &Show, (w, h): (Pixels, Pixels), cx: &App) -> AnyElement { } /// A slide as it first shows, still. -pub fn still(show: &Show, part: usize, size: (Pixels, Pixels), cx: &App) -> AnyElement { +pub fn still(show: &Show, part: usize, size: (Pixels, Pixels)) -> AnyElement { match show.stack(part) { - Some(stack) => compose::slide(stack, |visible| visible, size, ely_lok::paper(cx)) + Some(stack) => compose::slide(stack, |visible| visible, size, ely_lok::paper()) .into_any_element(), None => black(size), } diff --git a/apps/ely-show/src/slideshow/start.rs b/apps/ely-show/src/slideshow/start.rs index d61ce44..5130772 100644 --- a/apps/ely-show/src/slideshow/start.rs +++ b/apps/ely-show/src/slideshow/start.rs @@ -59,16 +59,14 @@ pub fn discard(worker: Arc>, doc: Doc, copy: PathBuf, cx: &App) { fn copy( worker: &mut Worker, doc: &Doc, - (copy, scheme): (&std::path::Path, Option<&'static str>), + copy: &std::path::Path, ) -> io::Result<(usize, Doc, Vec)> { let current = doc.sheets(worker)?.current; let url = ely_lok::file_url(copy); doc.save_as(worker, &url, "odp")?; let shown = worker.load(&url)?; let read = |worker: &mut Worker| -> io::Result> { - if let Some(scheme) = scheme { - worker.run(&shown, "view.theme", &[scheme])?; - } + worker.run(&shown, "view.theme", &[ely_lok::SCHEME])?; ely_lok::show::slide_show(&worker.values(&shown, ely_lok::show::SLIDE_SHOW)?) }; match read(worker) { @@ -161,14 +159,13 @@ impl Deck { self.show_starting = false; return; }; - let scheme = tab.view.read(cx).dark().map(ely_lok::scheme); let path = tab.work.join(format!("show-{}.odp", self.shows)); self.shows += 1; let into = path.clone(); let worker = self.core.worker.clone(); self.read_in( id, - move |worker, doc| copy(worker, doc, (&into, scheme)), + move |worker, doc| copy(worker, doc, &into), move |deck, read, _, cx| { deck.show_starting = false; let (current, doc, slides) = match read { @@ -382,7 +379,7 @@ mod tests { let deck = deck.read(app); let tab = deck.active_tab().expect("a tab"); let mut worker = deck.core.worker.lock().expect("the worker lock"); - let (_, doc, slides) = super::copy(&mut worker, &tab.doc, (&path, None)).expect("a copy"); + let (_, doc, slides) = super::copy(&mut worker, &tab.doc, &path).expect("a copy"); super::Starting { tab: tab.id, slides, first: 0, doc, copy: path.clone(), audience: 99, presenter: false } }); update(&mut cx, &deck, |deck, cx| { diff --git a/crates/ely-lok/src/client/tests/paint.rs b/crates/ely-lok/src/client/tests/paint.rs index 3736788..31c712a 100644 --- a/crates/ely-lok/src/client/tests/paint.rs +++ b/crates/ely-lok/src/client/tests/paint.rs @@ -350,7 +350,7 @@ fn brightness(tile: &Tile) -> u32 { } #[test] -fn the_page_follows_a_dark_theme_and_back() { +fn ely_scheme_paints_the_page_white() { let Some(install) = install() else { eprintln!("SKIP: no pinned LOK build"); return; @@ -363,23 +363,11 @@ fn the_page_follows_a_dark_theme_and_back() { let doc = worker .load(&crate::file_url(&source)) .expect("the document loads"); - let area = (1440, 1440, 3840, 3840); - let seen = poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false); - let light = brightness(&doc.paint_tile(&mut worker, area, (64, 64)).expect("a tile")); - let paint = |worker: &mut Worker, scheme: &str, seen: &Events| { - worker.run(&doc, "view.theme", &[scheme]).expect("a theme"); - let repainted = poll_until(worker, &doc, seen, SETTLE, |seen| seen.all); - assert!(repainted.all, "{name}: {scheme} repaints the view"); - let tile = doc.paint_tile(worker, area, (64, 64)).expect("a tile"); - (brightness(&tile), repainted) - }; - let (dark, seen) = paint(&mut worker, "COLOR_SCHEME_ELY_DARK", &seen); - let (back, _) = paint(&mut worker, "COLOR_SCHEME_ELY_LIGHT", &seen); - eprintln!("THEME {name} light {light} dark {dark} back {back}"); - assert!( - light > 200 && dark < 100 && back > 200, - "{name}: {light} {dark} {back}" - ); + poll_until(&mut worker, &doc, &Events::default(), QUIET, |_| false); + worker.run(&doc, "view.theme", &["COLOR_SCHEME_ELY_LIGHT"]).expect("the scheme"); + let tile = doc.paint_tile(&mut worker, (1440, 1440, 3840, 3840), (64, 64)).expect("a tile"); + let shade = brightness(&tile); + assert!(shade > 200, "{name}: the page is light, {shade}"); worker.close_doc(doc).expect("the document closes"); } } diff --git a/crates/ely-lok/src/client/tests/sheet.rs b/crates/ely-lok/src/client/tests/sheet.rs index 9f892d4..875295e 100644 --- a/crates/ely-lok/src/client/tests/sheet.rs +++ b/crates/ely-lok/src/client/tests/sheet.rs @@ -182,18 +182,16 @@ pub(super) fn open_book(text: &str) -> (tempfile::TempDir, Worker, Doc) { } #[test] -fn a_theme_switch_is_no_edit() { +fn the_scheme_is_no_edit() { if install().is_none() { eprintln!("SKIP: no pinned LOK build"); return; } let (_dir, mut worker, doc) = open_book(FODS); - for scheme in ["COLOR_SCHEME_ELY_DARK", "COLOR_SCHEME_ELY_LIGHT"] { - worker.run(&doc, "view.theme", &[scheme]).expect("the theme runs"); - let undo = worker.values(&doc, ".uno:Undo").expect("the undo stack"); - let undo: serde_json::Value = serde_json::from_str(&undo).expect("undo JSON"); - assert_eq!(undo["actions"], "", "{scheme} left undo steps"); - } + worker.run(&doc, "view.theme", &["COLOR_SCHEME_ELY_LIGHT"]).expect("the scheme runs"); + let undo = worker.values(&doc, ".uno:Undo").expect("the undo stack"); + let undo: serde_json::Value = serde_json::from_str(&undo).expect("undo JSON"); + assert_eq!(undo["actions"], "", "the scheme left undo steps"); } #[test] diff --git a/crates/ely-lok/src/lib.rs b/crates/ely-lok/src/lib.rs index 76dcb86..e2777eb 100644 --- a/crates/ely-lok/src/lib.rs +++ b/crates/ely-lok/src/lib.rs @@ -21,7 +21,7 @@ pub use client::{ #[cfg(feature = "view")] pub use view::{ CONTEXT, Copy, Cut, Geom, Layout, LokError, LokView, Paste, Reported, ZOOM_STEPS, notch, - Zoomed, init, layout, paper, read_off, scheme, + SCHEME, Zoomed, init, layout, paper, read_off, }; #[cfg(feature = "view")] mod view; diff --git a/crates/ely-lok/src/view.rs b/crates/ely-lok/src/view.rs index a1f0a95..2b5e9bf 100644 --- a/crates/ely-lok/src/view.rs +++ b/crates/ely-lok/src/view.rs @@ -32,7 +32,7 @@ mod scroll; mod sheet; pub use input::{CONTEXT, Copy, Cut, Paste, init}; pub use off::read_off; -pub use render::{paper, scheme}; +pub use render::{SCHEME, paper}; mod turn; pub use turn::notch; @@ -123,8 +123,8 @@ pub struct LokView { pointer: gpui::CursorStyle, selection: Vec, cell: crate::Cell, - /// Whether the kernel was last told to paint dark. - dark: Option, + /// Whether the kernel was told Ely's scheme. + lit: bool, /// The zoom follows the viewport so a page fits. fit: bool, /// Set when the wheel and Up, Down turn parts. @@ -218,7 +218,7 @@ impl LokView { pointer: gpui::CursorStyle::Arrow, selection: Vec::new(), cell: crate::Cell::default(), - dark: None, + lit: false, fit: false, turning: None, part: 0, diff --git a/crates/ely-lok/src/view/reads.rs b/crates/ely-lok/src/view/reads.rs index 14f6d30..d4c223c 100644 --- a/crates/ely-lok/src/view/reads.rs +++ b/crates/ely-lok/src/view/reads.rs @@ -6,9 +6,9 @@ use super::LokView; use crate::Rect; impl LokView { - /// Whether the kernel paints dark; None before the first frame. - pub fn dark(&self) -> Option { - self.dark + /// Whether the kernel was told Ely's scheme. + pub fn lit(&self) -> bool { + self.lit } /// The kernel's state for `uno`, as LOK reports it (LOK-05). diff --git a/crates/ely-lok/src/view/render.rs b/crates/ely-lok/src/view/render.rs index 74c15fc..7e83263 100644 --- a/crates/ely-lok/src/view/render.rs +++ b/crates/ely-lok/src/view/render.rs @@ -15,33 +15,21 @@ use gpui::{Pixels, RenderImage, point}; use super::{CONTEXT, LokView, TILE_TWIPS, TWIPS_PER_PIXEL, handles, input}; use crate::Rect; -/// Ely's kernel color schemes, from `ely.xcd`. -const DARK: &str = "COLOR_SCHEME_ELY_DARK"; -const LIGHT: &str = "COLOR_SCHEME_ELY_LIGHT"; +/// Ely's kernel color scheme, from `ely.xcd`. +pub const SCHEME: &str = "COLOR_SCHEME_ELY_LIGHT"; -/// The kernel scheme for a light or dark app. -pub fn scheme(dark: bool) -> &'static str { - if dark { DARK } else { LIGHT } -} - -/// A page's colour; the kernel's Ely schemes share it. -pub fn paper(cx: &gpui::App) -> gpui::Hsla { - let theme = cx.theme(); - if theme.is_dark() { - theme.colors.surface - } else { - theme.colors.paper - } +/// A page's colour, the scheme's `DocColor`. +pub fn paper() -> gpui::Hsla { + gpui::white() } impl LokView { - /// Paints with the app's light or dark scheme. - pub(super) fn follow_theme(&mut self, dark: bool) { - if self.dark != Some(dark) { - self.dark = Some(dark); - let scheme = scheme(dark); - log::info!("lok: paints with {scheme}"); - self.send(input::Input::Theme(scheme)); + /// Content keeps the file's colours in any app theme. + pub(super) fn paint_light(&mut self) { + if !self.lit { + self.lit = true; + log::info!("lok: paints with {SCHEME}"); + self.send(input::Input::Theme(SCHEME)); } } } @@ -53,11 +41,11 @@ impl Render for LokView { self.keep_scroll(|view| view.geom.dpi = dpi); self.retire_tiles(); } - self.follow_theme(cx.theme().is_dark()); + self.paint_light(); let this = cx.entity().clone(); let focus = self.focus.clone(); let colors = &cx.theme().colors; - let paper = super::paper(cx); + let paper = super::paper(); let (selection_color, caret_color) = (colors.selection, colors.focus); let canvas_color = colors.sunken; let caret_width = cx.theme().caret_width().to_pixels(window.rem_size()); diff --git a/crates/ely-lok/src/view/tests/theme.rs b/crates/ely-lok/src/view/tests/theme.rs index af5b99b..2ca2413 100644 --- a/crates/ely-lok/src/view/tests/theme.rs +++ b/crates/ely-lok/src/view/tests/theme.rs @@ -24,37 +24,63 @@ fn area() -> Bounds { } } +/// A user profile whose dark scheme paints the page black. +const DARK_PROFILE: &str = r#" + +0 + +"#; + +/// Shows a view in a window. +struct Host(Entity); + +impl gpui::Render for Host { + fn render(&mut self, _: &mut gpui::Window, _: &mut gpui::Context) -> impl gpui::IntoElement { + self.0.clone() + } +} + #[test] -fn the_view_paints_the_app_theme_and_back() { +fn a_dark_app_paints_the_page_white() { let Some(install) = crate::client::tests::install() else { eprintln!("SKIP: no pinned LOK build"); return; }; - let cx = TestAppContext::single(); - let (view, worker) = cx.update(|app| live_view(app, &install)); - let doc = cx.update(|app| view.read(app).doc.clone()); - let mut shades = Vec::new(); - for dark in [true, false] { - cx.update(|app| view.update(app, |view, _| view.follow_theme(dark))); - assert!( - settle(&cx, |app| view.read(app).settled()), - "the theme goes out" - ); + let mut cx = TestAppContext::single(); + cx.update(|app| { + ely_gpui_component::init_for_tests(app); + ely_gpui_component::theme::Theme::set_mode_now(ely_gpui_component::theme::Mode::Dark, app); + }); + // A dark profile, as an old Ely left. + let dir = tempfile::tempdir().expect("a temp dir"); + let profile = dir.path().join("profile"); + std::fs::create_dir_all(profile.join("user")).expect("a profile"); + std::fs::write(profile.join("user/registrymodifications.xcu"), DARK_PROFILE).expect("the scheme writes"); + let source = dir.path().join("note.fodt"); + std::fs::write(&source, crate::client::tests::FODT).expect("the fixture writes"); + let mut started = crate::Worker::spawn(&install, &profile).expect("the worker"); + let doc = started.load(&crate::file_url(&source)).expect("the document loads"); + let laid = super::super::layout(&mut started, &doc).expect("the layout reads"); + let worker = Arc::new(Mutex::new(started)); + let shade = || { let mut worker = worker.lock().expect("the worker"); - let tile = doc - .paint_tile(&mut worker, (1440, 1440, 3840, 3840), (64, 64)) - .expect("a tile"); - let sum: u64 = tile - .pixels - .chunks_exact(4) - .map(|px| u64::from(px[0]) + u64::from(px[1]) + u64::from(px[2])) - .sum(); - shades.push(sum / (64 * 64 * 3)); - } + let tile = doc.paint_tile(&mut worker, (1440, 1440, 3840, 3840), (64, 64)).expect("a tile"); + let sum: u64 = tile.pixels.chunks_exact(4).map(|px| u64::from(px[0]) + u64::from(px[1]) + u64::from(px[2])).sum(); + sum / (64 * 64 * 3) + }; + worker.lock().expect("the worker").run(&doc, "view.theme", &["TEST_DARK"]).expect("the dark scheme"); + let dark = shade(); + assert!(dark < 100, "the kernel starts dark: {dark}"); + let view = cx.update(|app| LokView::open(Arc::clone(&worker), doc.clone(), laid, app)); + let shown = view.clone(); + cx.add_window(move |_, _| Host(shown)); assert!( - shades[0] < 100 && shades[1] > 200, - "dark then light: {shades:?}" + settle(&cx, |app| view.read(app).lit() && view.read(app).settled()), + "the scheme goes out" ); + assert_eq!(paper(), gpui::white(), "the page mask is the scheme's white"); + let light = shade(); + assert!(light > 240, "the view paints the page white: {light}"); } #[test] diff --git a/crates/ely-pdf-engine/shim/ely_mupdf.c b/crates/ely-pdf-engine/shim/ely_mupdf.c index 41b6ace..4736d7c 100644 --- a/crates/ely-pdf-engine/shim/ely_mupdf.c +++ b/crates/ely-pdf-engine/shim/ely_mupdf.c @@ -224,7 +224,7 @@ static fz_matrix placed(fz_rect box, float scale, int rotate) /* Paints device pixels x, y, w, h as BGRA. */ int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int rotate, - const int tile[4], int dark, unsigned char *bgra) + const int tile[4], unsigned char *bgra) { fz_page *page = NULL; fz_pixmap *pix = NULL; @@ -242,8 +242,6 @@ int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int r dev = fz_new_draw_device(ctx, fz_identity, pix); fz_run_page(ctx, page, dev, ctm, NULL); fz_close_device(ctx, dev); - if (dark) - fz_invert_pixmap_luminance(ctx, pix); int stride = (int)fz_pixmap_stride(ctx, pix); unsigned char *rows = fz_pixmap_samples(ctx, pix); for (int y = 0; y < tile[3]; y++) diff --git a/crates/ely-pdf-engine/shim/ely_shim.h b/crates/ely-pdf-engine/shim/ely_shim.h index 92b355c..5c61393 100644 --- a/crates/ely-pdf-engine/shim/ely_shim.h +++ b/crates/ely-pdf-engine/shim/ely_shim.h @@ -7,7 +7,7 @@ int ely_fail(fz_context *ctx); const char *ely_error(void); -int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int rotate, const int tile[4], int dark, +int ely_render(fz_context *ctx, fz_document *doc, int number, float scale, int rotate, const int tile[4], unsigned char *bgra); void ely_fail_also(fz_context *ctx, const char *what); pdf_document *ely_pdf_of(fz_context *ctx, fz_document *doc); diff --git a/crates/ely-pdf-engine/shim/ely_write.c b/crates/ely-pdf-engine/shim/ely_write.c index eabedc4..1ccd1cb 100644 --- a/crates/ely-pdf-engine/shim/ely_write.c +++ b/crates/ely-pdf-engine/shim/ely_write.c @@ -244,7 +244,7 @@ static void preview(fz_context *ctx, pdf_document *pdf, int number, int block, c tile[0] = (int)floorf(region.x0 * scale), tile[1] = (int)floorf(region.y0 * scale); tile[2] = (int)ceilf(region.x1 * scale) - tile[0], tile[3] = (int)ceilf(region.y1 * scale) - tile[1]; *pixels = fz_malloc(ctx, (size_t)tile[2] * tile[3] * 4); - if (ely_render(ctx, copy, number, scale, 0, tile, 0, *pixels) < 0) + if (ely_render(ctx, copy, number, scale, 0, tile, *pixels) < 0) fz_throw(ctx, FZ_ERROR_GENERIC, "%s", ely_error()); *w = tile[2], *h = tile[3]; } diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/ffi.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/ffi.rs index 3f39381..713f3c7 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/ffi.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/ffi.rs @@ -31,7 +31,6 @@ unsafe extern "C" { scale: f32, rotate: c_int, tile: *const c_int, - dark: c_int, bgra: *mut u8, ) -> c_int; fn ely_text( @@ -143,7 +142,6 @@ impl Mupdf { number: usize, (scale, rotate): (f32, i32), tile: [i32; 4], - dark: bool, ) -> io::Result> { let (w, h) = (usize::try_from(tile[2]), usize::try_from(tile[3])); let (Ok(w), Ok(h)) = (w, h) else { @@ -158,7 +156,6 @@ impl Mupdf { scale, rotate, tile.as_ptr(), - c_int::from(dark), pixels.as_mut_ptr(), ) }; diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs index ec31b84..9dae960 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/main.rs @@ -403,8 +403,7 @@ impl Worker { for value in &mut tile { *value = i32::try_from(ask.i64()?).map_err(io::Error::other)?; } - let dark = ask.u64()? != 0; - self.drawn(id, page, (scale, rotate), tile, dark) + self.drawn(id, page, (scale, rotate), tile) } tag::TEXT => { let (id, page) = (ask.u64()?, ask.u64()? as usize); diff --git a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs index 0c90dc3..fb147b1 100644 --- a/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs +++ b/crates/ely-pdf-engine/src/bin/ely-pdf-worker/output.rs @@ -101,9 +101,9 @@ impl crate::Worker { } /// A tile, and whether drawing it met damage. - pub(crate) fn drawn(&mut self, id: u64, page: usize, view: (f32, i32), tile: [i32; 4], dark: bool) -> io::Result> { + pub(crate) fn drawn(&mut self, id: u64, page: usize, view: (f32, i32), tile: [i32; 4]) -> io::Result> { unsafe { ely_watch_begin(self.mupdf.ctx) }; - let pixels = self.mupdf.render(self.doc(id)?, page, view, tile, dark); + let pixels = self.mupdf.render(self.doc(id)?, page, view, tile); let warned = unsafe { ely_watch_end(self.mupdf.ctx) } != 0; if warned { self.note_damage(id)?; diff --git a/crates/ely-pdf-engine/src/client.rs b/crates/ely-pdf-engine/src/client.rs index 9e5b2ac..1b3fda3 100644 --- a/crates/ely-pdf-engine/src/client.rs +++ b/crates/ely-pdf-engine/src/client.rs @@ -242,7 +242,6 @@ impl Worker { page: usize, (scale, rotate): (f32, i32), tile: [i32; 4], - dark: bool, ) -> io::Result { let mut ask = self.on(&doc)? .u64(page as u64) @@ -251,7 +250,7 @@ impl Worker { for value in tile { ask = ask.i64(i64::from(value)); } - let reply = self.request(tag::RENDER, &ask.u64(u64::from(dark)).bytes())?; + let reply = self.request(tag::RENDER, &ask.bytes())?; let mut reply = Unpack::new(&reply); let (damaged, bgra) = (reply.u64()? != 0, reply.blob()?); match bgra.len() == tile[2] as usize * tile[3] as usize * 4 { diff --git a/crates/ely-pdf-engine/tests/edit.rs b/crates/ely-pdf-engine/tests/edit.rs index ec2c1e3..bc2e980 100644 --- a/crates/ely-pdf-engine/tests/edit.rs +++ b/crates/ely-pdf-engine/tests/edit.rs @@ -257,7 +257,7 @@ fn long_clashing_field_names_still_get_a_number() { /// Red pixels of a page at one pixel a point. fn red(worker: &mut Worker, doc: Doc, page: usize) -> usize { - let pixels = worker.render(doc, page, (1.0, 0), [0, 0, 612, 792], false).expect("renders").bgra; + let pixels = worker.render(doc, page, (1.0, 0), [0, 0, 612, 792]).expect("renders").bgra; pixels.chunks_exact(4).filter(|p| p[2] > 180 && p[1] < 80 && p[0] < 80).count() } diff --git a/crates/ely-pdf-engine/tests/marks.rs b/crates/ely-pdf-engine/tests/marks.rs index 8fcee7a..bfc68ab 100644 --- a/crates/ely-pdf-engine/tests/marks.rs +++ b/crates/ely-pdf-engine/tests/marks.rs @@ -26,7 +26,7 @@ fn note(page: usize, shape: Shape) -> NewAnnot { /// Whether page one's pixels `at` hold colour `bgra`. fn has_color(worker: &mut Worker, doc: Doc, at: [i32; 4], bgra: [u8; 3]) -> bool { - let pixels = worker.render(doc, 0, (1.0, 0), at, false).expect("draws").bgra; + let pixels = worker.render(doc, 0, (1.0, 0), at).expect("draws").bgra; pixels.chunks_exact(4).any(|p| p[..3].iter().zip(bgra).all(|(a, b)| a.abs_diff(b) < 40)) } diff --git a/crates/ely-pdf-engine/tests/ocr.rs b/crates/ely-pdf-engine/tests/ocr.rs index 9560047..d201f5c 100644 --- a/crates/ely-pdf-engine/tests/ocr.rs +++ b/crates/ely-pdf-engine/tests/ocr.rs @@ -18,7 +18,7 @@ const SCALE: f32 = 200.0 / 72.0; /// Page one of `doc`, saved as a gray scan. fn scan_of(worker: &mut Worker, doc: Doc, out: PathBuf) -> PathBuf { let (w, h) = ((612.0 * SCALE).ceil() as i32, (792.0 * SCALE).ceil() as i32); - let bgra = worker.render(doc, 0, (SCALE, 0), [0, 0, w, h], false).expect("draws").bgra; + let bgra = worker.render(doc, 0, (SCALE, 0), [0, 0, w, h]).expect("draws").bgra; let gray: Vec = bgra.chunks_exact(4).map(|p| ((u16::from(p[0]) + u16::from(p[1]) + u16::from(p[2])) / 3) as u8).collect(); std::fs::write(&out, scan((612.0, 792.0), (w as u32, h as u32), &gray)).expect("writes"); out @@ -49,12 +49,12 @@ fn a_scan_reads_and_its_text_layer_finds_without_drawing() { assert!(read.iter().any(|l| l.contains("Hello world")) && read.iter().any(|l| l.contains("Second line")), "{read:?}"); let hello = lines.iter().find(|l| l.text.contains("Hello")).expect("hello"); assert!((hello.rect[0] - 72.0).abs() < 6.0 && (hello.rect[3] - 76.0).abs() < 8.0, "in page points: {:?}", hello.rect); - let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra; + let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra; let mark = worker.text_layer(doc, &[(0, lines)], false).expect("lays"); doc = worker.now(doc).expect("the state"); assert!(words(&mut worker, doc).contains("Hello world"), "searchable now"); assert_eq!(worker.search(doc, 0, "second line").expect("finds").len(), 1); - assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra == before, "nothing drawn"); + assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra == before, "nothing drawn"); worker.undo(doc, false).expect("undoes"); doc = worker.now(doc).expect("the state"); assert_eq!(words(&mut worker, doc), "", "one step"); @@ -144,9 +144,9 @@ fn text_added_after_a_layer_still_shows() { let line = OcrLine { rect: [72.0, 60.0, 200.0, 78.0], text: "Hello world".into() }; worker.text_layer(doc, &[(0, vec![line])], false).expect("lays"); doc = worker.now(doc).expect("the state"); - let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra; + let before = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra; let writing = Writing { block: None, expected: String::new(), text: "VISIBLE".into(), font: "helvetica".into(), size: 24.0, rgb: 0, bx: [72.0, 600.0, 300.0] }; assert!(matches!(worker.put_text(doc, 0, &writing, 0.0).expect("writes"), Wrote::Done(_))); doc = worker.now(doc).expect("the state"); - assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra != before, "the new text is drawn"); + assert!(worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra != before, "the new text is drawn"); } diff --git a/crates/ely-pdf-engine/tests/output.rs b/crates/ely-pdf-engine/tests/output.rs index cba5a6b..8e867e8 100644 --- a/crates/ely-pdf-engine/tests/output.rs +++ b/crates/ely-pdf-engine/tests/output.rs @@ -199,7 +199,7 @@ fn a_print_copy_shows_what_prints_to_any_screen_renderer() { // MuPDF draws by screen rules, as Windows' renderer does. let mut third = self::worker(); let printed = third.open(&print).expect("opens").doc; - let bgra = third.render(printed, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws").bgra; + let bgra = third.render(printed, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws").bgra; let at = |x: usize, y: usize| -> [u8; 3] { let p = &bgra[((792 - y) * 612 + x) * 4..][..3]; [p[2], p[1], p[0]] @@ -219,7 +219,7 @@ fn a_broken_picture_shows_when_drawn_or_checked() { std::fs::write(&path, &bytes).expect("writes"); let (mut worker, doc) = opened(&path); assert_eq!(worker.repair_report(doc).expect("asks"), None, "pictures wait to be drawn"); - let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws"); assert!(tile.damaged, "drawing finds it"); assert!(worker.repair_report(doc).expect("asks").is_some()); let (mut other, again) = opened(&path); @@ -249,7 +249,7 @@ fn an_unembedded_font_is_no_damage() { let (mut worker, doc) = opened(&path); // Read unwatched first: its repeat is pending. worker.text(doc, 0).expect("reads"); - let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws"); assert!(!tile.damaged, "a substituted font draws whole"); assert_eq!(worker.repair_report(doc).expect("asks"), None); } @@ -262,7 +262,7 @@ fn unreadable_content_still_draws_as_damage() { let path = dir.path().join("doc.pdf"); std::fs::write(&path, one_page("", "", content, &[])).expect("writes"); let (mut worker, doc) = opened(&path); - let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws"); assert!(tile.damaged, "{content} draws as damage"); } } @@ -327,7 +327,7 @@ fn a_broken_picture_under_two_filters_fails_exports() { let (mut worker, doc) = opened(&path); assert!(worker.export_image(doc, 0, 72.0, &dir.path().join("page.png")).is_err(), "its page read whole"); let (mut worker, doc) = opened(&path); - let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws"); assert!(tile.damaged, "the picture draws as damage"); } @@ -340,7 +340,7 @@ fn a_broken_picture_read_before_is_still_damage() { for (path, first) in files.iter().flat_map(|p| read.map(|r| (p, r))) { let (mut worker, doc) = opened(path); first(&mut worker, doc); - let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792], false).expect("draws"); + let tile = worker.render(doc, 0, (1.0, 0), [0, 0, 612, 792]).expect("draws"); assert!(tile.damaged, "the picture draws as damage"); assert!(worker.repair_report(doc).expect("asks").is_some()); let (mut worker, doc) = opened(path); diff --git a/crates/ely-pdf-engine/tests/redact.rs b/crates/ely-pdf-engine/tests/redact.rs index 789ada8..3dfab50 100644 --- a/crates/ely-pdf-engine/tests/redact.rs +++ b/crates/ely-pdf-engine/tests/redact.rs @@ -67,9 +67,9 @@ fn a_picture_under_a_region_loses_those_pixels() { let out = dir.path().join("out.pdf"); worker.redact(doc, &[Region { page: 0, rect: [72.0, 242.0, 122.0, 292.0] }], &out).expect("removes"); let (mut other, written) = reopen(&out); - let left = other.render(written, 0, (1.0, 0), [124, 244, 46, 46], false).expect("draws").bgra; + let left = other.render(written, 0, (1.0, 0), [124, 244, 46, 46]).expect("draws").bgra; assert!(left.chunks_exact(4).any(|p| p[0] > 100 || p[1] > 100 || p[2] > 100), "the right half keeps its colour"); - let gone = other.render(written, 0, (1.0, 0), [74, 244, 46, 46], false).expect("draws").bgra; + let gone = other.render(written, 0, (1.0, 0), [74, 244, 46, 46]).expect("draws").bgra; assert!(gone.chunks_exact(4).all(|p| p[0] < 40 && p[1] < 40 && p[2] < 40), "the left half is black"); } @@ -166,7 +166,7 @@ fn a_shape_touching_a_region_goes_whole() { let report = worker.redact(doc, &[Region { page: 0, rect: [90.0, 45.0, 180.0, 100.0] }], &out).expect("removes"); assert_eq!(report.objects, 0); let (mut other, read) = reopen(&out); - let right = other.render(read, 0, (1.0, 0), [200, 55, 90, 30], false).expect("draws").bgra; + let right = other.render(read, 0, (1.0, 0), [200, 55, 90, 30]).expect("draws").bgra; assert!(right.chunks_exact(4).all(|p| p[2] < 200 || p[1] > 200), "no red left past the box"); } diff --git a/crates/ely-pdf-engine/tests/worker.rs b/crates/ely-pdf-engine/tests/worker.rs index 941e8c8..6751508 100644 --- a/crates/ely-pdf-engine/tests/worker.rs +++ b/crates/ely-pdf-engine/tests/worker.rs @@ -36,23 +36,15 @@ fn pages_measure_render_and_read_their_text() { ); // Page one's top left, twice its size: text is dark. let tile = worker - .render(doc, 0, (2.0, 0), [0, 0, 600, 300], false) + .render(doc, 0, (2.0, 0), [0, 0, 600, 300]) .expect("renders").bgra; assert_eq!(tile.len(), 600 * 300 * 4); let dark = tile.chunks_exact(4).filter(|p| p[0] < 128).count(); assert!(dark > 100, "text shows: {dark}"); - let night = worker - .render(doc, 0, (2.0, 0), [0, 0, 600, 300], true) - .expect("renders dark").bgra; - let light = night.chunks_exact(4).filter(|p| p[0] > 128).count(); - assert!( - light > 100 && night[0] < 64, - "dark mode turns paper dark and text light" - ); // A quarter turn makes the page tall side wide. assert!( worker - .render(doc, 1, (1.0, 90), [0, 0, 595, 842], false) + .render(doc, 1, (1.0, 90), [0, 0, 595, 842]) .is_ok() ); let text: String = worker @@ -135,7 +127,7 @@ fn a_thousand_pages_show_the_first_in_time() { assert_eq!(worker.pages(doc).expect("counts"), 1000); // What the viewer asks before its first frame. worker.bounds(doc, 0, 100).expect("the first sizes"); - worker.render(doc, 0, (2.0, 0), [0, 0, 512, 512], false).expect("the first tile"); + worker.render(doc, 0, (2.0, 0), [0, 0, 512, 512]).expect("the first tile"); let took = started.elapsed(); assert!(took < std::time::Duration::from_secs(3), "PERF-05: {took:?}"); } diff --git a/packaging/libreoffice/ely.xcd b/packaging/libreoffice/ely.xcd index 6c65b2e..2c1beca 100644 --- a/packaging/libreoffice/ely.xcd +++ b/packaging/libreoffice/ely.xcd @@ -36,25 +36,6 @@ - - 2894892 - 2434341 - 15987184 - 5592405 - 5592405 - 5592405 - 8303091 - 12297192 - 0 - 4144959 - 4144959 - 16777152 - 6052440 - 6052440 - 6922728 - 6052440 - 5592405 - 16777215 15987184 diff --git a/scripts/review.sh b/scripts/review.sh index d13867d..26fcc84 100755 --- a/scripts/review.sh +++ b/scripts/review.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# Usage: review.sh [path...] — codex max, fast review. +# Usage: review.sh [path...] — codex xhigh, fast review. set -euo pipefail [ "${HERDR_ENV:-}" = 1 ] || { echo "review.sh: not inside herdr" >&2; exit 2; } @@ -37,7 +37,7 @@ pane=$(herdr pane split --current --direction right --cwd "$PWD" --no-focus | jq echo "$pane" >"$last" started= for _ in 1 2 3 4 5; do - if err=$(herdr agent start "$name" --kind codex --pane "$pane" --timeout 120000 -- --yolo --model gpt-6-astra -c 'model_reasoning_effort="max"' -c 'service_tier="fast"' -c check_for_update_on_startup=false 2>&1); then + if err=$(herdr agent start "$name" --kind codex --pane "$pane" --timeout 120000 -- --yolo --model gpt-6.1-sol -c 'model_reasoning_effort="xhigh"' -c 'service_tier="fast"' -c check_for_update_on_startup=false 2>&1); then started=1 break fi diff --git a/tasks/TASKS.md b/tasks/TASKS.md index c6af501..6e02d86 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -2,7 +2,7 @@ 进度真相。自上而下,逐条完成,不跳项。编号前缀是 PRD 章节号,`C00` 是用户指令的前置项。 -每条完成后:`scripts/review.sh ID 轮次` 在 herdr 开新分栏,codex --yolo(gpt-6-astra,effort max,fast)审查,最多三轮。有问题先修,复审通过才勾选。审查只看缺陷,不收“建议增强”。每轮结论写入 `tasks/reviews/<编号>-r<轮>.md`。 +每条完成后:`scripts/review.sh ID 轮次` 在 herdr 开新分栏,codex --yolo(gpt-6.1-sol,effort xhigh,fast)审查,最多三轮。有问题先修,复审通过才勾选。审查只看缺陷,不收“建议增强”。每轮结论写入 `tasks/reviews/<编号>-r<轮>.md`。 状态:`[ ]` 未做 `[x]` 完成且审查通过 `[~]` 三轮用尽,末轮缺陷已修并有回归测试,未经复审 `[!]` 外部阻塞,写明原因与证据,不算完成。 @@ -285,3 +285,11 @@ - [x] R7.7 发布 0.1.6:版本号,三端建包、签名公证、上传,合集包一并上传,本机装上(v0.1.6 共 46 个文件,出自 a1b8c18:六个公证 dmg 与 ElyOffice dmg、六个安装程序与合集 setup.exe、24 个 Linux 包、一对源码包;`crates.py` 改问 cargo 本机链进的 crate,补 BSD-3-Clause;源码包只由 Linux 上传,重跑遇同名即失败、什么也不传(`ci-upload.sh`);本机六个应用静默装上 0.1.6、冒烟通过 — 第 1 轮:macOS 与 Linux 并行会重复上传源码;第 2 轮:重跑跳过同名会使更新清单与 dmg 散列不符、注释超长;第 3 轮:部分已传的重跑未测、注释超长,修后不再审查) - [x] R7.8 答复 #3 至 #6:每条写已修于 0.1.6;#6 的工作区建议另开 [FEATURE REQUEST] 并链过去(#3、#4、#5、#6 各一条答复,#7 为 [FEATURE REQUEST] 工作区与常用文件夹;#3、#5、#6 关闭,#4 开着等报告者的文件 — 第 1 轮:#5 的答复说只有读不出的内容才算损坏,超出 0.1.6 所修,改为只说未嵌入字体的提示不再算损坏;第 2 轮 PASS) - [x] R7.9 Windows 右键“新建”(用户 2026-10-16 定,发 0.1.7,只发 Windows):资源管理器的新建菜单可建 DOCX、XLSX、PPTX、Markdown 文件,各由所属的应用装上与卸下(`types.sh` 测试另以两个变体核对:去掉判空、去掉 md 的模板各自失败;本机在 0.1.6 上静默装 0.1.7 的 ElyDoc、ElyTable、ElyShow、ElyMark:新建菜单列出四项,无 `.ely-old`,四个模板按新建的方式复制后在所属应用里打开,日志无错误与警告;卸载 ElyMark 后 `.md` 的新建项与默认值都删去、`UserChoice` 未动,重装复原;`windows-installer.sh`、`windows-bundle.sh` 通过。未送审:用户要求从简) + +## R8 内容配色、Gitea #8 与 #9、ElyMark 公式(用户 2026-10-18 报;审查改为 gpt-6.1-sol,effort xhigh,fast) + +- [x] R8.1 深色外观下内容照文件原色(§5,用户定,取代 2026-10-04 的“内容区随系统深浅色”):ElyPDF 页面不反相;ElyDoc 页面、ElyTable 单元格照文件的颜色(白底即白底),ElyShow 已是如此(`ely.xcd` 只留浅色方案,每个视图首帧发一次;MuPDF 不再反相;Windows 真窗口深色外观下四个应用都是白纸;测试以深色档案起内核、视图须把页面画白,去掉发送即失败 — 第 1 轮:`theme.sh` 仍要深色网格、测试的内核本就浅色;第 2 轮:两处注释超长;第 3 轮 PASS) +- [ ] R8.2 Gitea #9 公式不兼容(§7):附件 math.docx 的键盘公式、OMML、MathType(OLE)、EQ 域、上下标、图片公式逐类与 Word 对照,查明各类差异的根因并修;修不了的写明原因与证据 +- [ ] R8.3 Gitea #8 工作表过多时标签条不滚动(§9):活动工作表变化时标签条滚动,使该标签完整可见;被裁到窗口外的工作表可经标签条到达 +- [ ] R8.4 ElyMark 公式(§14):行内公式排版(原只显示着色的 TeX 原文);`\(…\)` 读作行内公式,`\[…\]` 读作块公式 +- [ ] R8.5 答复 #8、#9,修好的关闭;`agents.md` 随之 diff --git a/tasks/reviews/R8.1-r1.md b/tasks/reviews/R8.1-r1.md new file mode 100644 index 0000000..3edaa15 --- /dev/null +++ b/tasks/reviews/R8.1-r1.md @@ -0,0 +1,5 @@ +FAIL + +- crates/ely-lok/src/view/render.rs:44 — The unconditional light scheme makes the retained macOS regression test contradict R8.1. `tests/scripts/theme.sh:49` still requires the dark-app grid's mean brightness to be below 80. On a Mac with screen-capture permission, the required white grid therefore fails that test. Update the test to require the file's original colors under both application appearances. + +- crates/ely-lok/src/view/tests/theme.rs:47 — The new test does not establish a dark kernel state: it changes GPUI's theme, then opens a fresh LibreOffice profile that already renders light. In the isolated WSL copy, removing the only `self.send(input::Input::Theme(SCHEME))` call still leaves `a_dark_app_paints_the_page_white` passing; see `.work/r81-review/linux-test-lok-theme-mutant.log`. A missing initialization command could leave an existing dark engine/profile dark without this test detecting the regression. Establish and verify a dark kernel state before showing the view, then assert that the view's initialization changes its rendered pixels to white. diff --git a/tasks/reviews/R8.1-r2.md b/tasks/reviews/R8.1-r2.md new file mode 100644 index 0000000..ab16845 --- /dev/null +++ b/tasks/reviews/R8.1-r2.md @@ -0,0 +1,5 @@ +FAIL + +- crates/ely-lok/src/view/tests/theme.rs:54 — The new profile comment contains 11 words, exceeding agents.md rule 5. Running `python tests/scripts/comments.py` fails on this line (see `.work/r81-r2/comments.log`). Shorten the comment to ten words or fewer. + +- tests/scripts/theme.sh:2 — The revised introductory comment contains 11 words, exceeding agents.md rule 5. Running `python tests/scripts/comments.py` also reports this line and exits unsuccessfully. Shorten the comment to ten words or fewer. diff --git a/tasks/reviews/R8.1-r3.md b/tasks/reviews/R8.1-r3.md new file mode 100644 index 0000000..7ef22e9 --- /dev/null +++ b/tasks/reviews/R8.1-r3.md @@ -0,0 +1 @@ +PASS diff --git a/tests/scripts/theme.sh b/tests/scripts/theme.sh index b236b1c..d7c4597 100755 --- a/tests/scripts/theme.sh +++ b/tests/scripts/theme.sh @@ -1,5 +1,5 @@ #!/usr/bin/env bash -# ElyTable's grid follows the app's dark and light mode. +# ElyTable's cells stay white in dark and light mode. set -euo pipefail root=$(cd "$(dirname "$0")/../.." && pwd) @@ -46,6 +46,6 @@ check dark "$(shade_of dark)" dark=$shade check light "$(shade_of light)" light=$shade -[ "$dark" -lt 80 ] || fail "the dark grid paints at $dark" -[ "$light" -gt 200 ] || fail "the light grid paints at $light" +[ "$dark" -gt 200 ] || fail "the dark app's grid paints at $dark" +[ "$light" -gt 200 ] || fail "the light app's grid paints at $light" echo "ok: theme (dark $dark, light $light)"