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 <noreply@anthropic.com>
This commit is contained in:
42 files changed
+204
-209
No files matched your search
@@ -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:提交行窄时作者、提交号与时Line truncated
|
||||
- 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 宽时文件名止于按钮之前(原布局时失败)。
|
||||
|
||||
@@ -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<Showing> {
|
||||
pub fn showing(&self, sf: f32) -> Vec<Showing> {
|
||||
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 };
|
||||
|
||||
@@ -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<Self>) -> 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 {
|
||||
|
||||
+13
-13
@@ -100,11 +100,11 @@ impl PdfView {
|
||||
}
|
||||
|
||||
/// Asks for the first missing picture in `rows`.
|
||||
fn ask_thumb(&mut self, rows: Range<usize>, (sf, dark): (f32, bool), cx: &mut Context<Self>) {
|
||||
fn ask_thumb(&mut self, rows: Range<usize>, sf: f32, cx: &mut Context<Self>) {
|
||||
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<Arc<RenderImage>> {
|
||||
self.thumbs.get(&(page, dark)).cloned()
|
||||
pub fn picture(&self, page: usize) -> Option<Arc<RenderImage>> {
|
||||
self.thumbs.get(&page).cloned()
|
||||
}
|
||||
}
|
||||
|
||||
/// One row: the page number and its picture.
|
||||
fn row(view: &Entity<PdfView>, page: usize, dark: bool, cx: &App) -> gpui::AnyElement {
|
||||
fn row(view: &Entity<PdfView>, 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<PdfView>, 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()
|
||||
|
||||
@@ -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<PdfView>) -> (gpui::Subscription, std::rc::Rc<std::cell::Cell<bool>>) {
|
||||
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::<Vec<_>>()
|
||||
read.showing(sf).iter().map(|page| read.pieces(page).len()).collect::<Vec<_>>()
|
||||
});
|
||||
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");
|
||||
|
||||
@@ -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<u8> = 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");
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
@@ -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<RenderImage>>,
|
||||
/// Page pictures for the side panel, by page.
|
||||
pub thumbs: HashMap<usize, Arc<RenderImage>>,
|
||||
pub thumbs_failed: HashSet<usize>,
|
||||
/// Pictures from before an edit; shown until repainted.
|
||||
pub thumbs_stale: HashSet<(usize, bool)>,
|
||||
pub thumbs_stale: HashSet<usize>,
|
||||
/// A picture is on its way from the worker.
|
||||
pub thumbing: bool,
|
||||
/// Pages picked in the side panel, and the anchor.
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<bool>,
|
||||
/// The pictures were painted with Ely's scheme.
|
||||
pub lit: bool,
|
||||
/// The view was fitted to the slide once.
|
||||
pub fitted: bool,
|
||||
}
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
|
||||
@@ -59,16 +59,14 @@ pub fn discard(worker: Arc<Mutex<Worker>>, 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<ShowSlide>)> {
|
||||
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<Vec<ShowSlide>> {
|
||||
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| {
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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<Rect>,
|
||||
cell: crate::Cell,
|
||||
/// Whether the kernel was last told to paint dark.
|
||||
dark: Option<bool>,
|
||||
/// 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,
|
||||
|
||||
@@ -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<bool> {
|
||||
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).
|
||||
|
||||
@@ -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());
|
||||
|
||||
@@ -24,37 +24,63 @@ fn area() -> Bounds<Pixels> {
|
||||
}
|
||||
}
|
||||
|
||||
/// A user profile whose dark scheme paints the page black.
|
||||
const DARK_PROFILE: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
|
||||
<oor:items xmlns:oor="http://openoffice.org/2001/registry" xmlns:xs="http://www.w3.org/2001/XMLSchema" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<item oor:path="/org.openoffice.Office.UI/ColorScheme/ColorSchemes"><node oor:name="TEST_DARK" oor:op="replace"><node oor:name="DocColor"><prop oor:name="Color" oor:op="fuse"><value>0</value></prop></node></node></item>
|
||||
</oor:items>
|
||||
"#;
|
||||
|
||||
/// Shows a view in a window.
|
||||
struct Host(Entity<LokView>);
|
||||
|
||||
impl gpui::Render for Host {
|
||||
fn render(&mut self, _: &mut gpui::Window, _: &mut gpui::Context<Self>) -> 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]
|
||||
|
||||
@@ -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++)
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
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(),
|
||||
)
|
||||
};
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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<Vec<u8>> {
|
||||
pub(crate) fn drawn(&mut self, id: u64, page: usize, view: (f32, i32), tile: [i32; 4]) -> io::Result<Vec<u8>> {
|
||||
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)?;
|
||||
|
||||
@@ -242,7 +242,6 @@ impl Worker {
|
||||
page: usize,
|
||||
(scale, rotate): (f32, i32),
|
||||
tile: [i32; 4],
|
||||
dark: bool,
|
||||
) -> io::Result<Tile> {
|
||||
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 {
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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<u8> = 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");
|
||||
}
|
||||
@@ -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);
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
|
||||
@@ -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:?}");
|
||||
}
|
||||
|
||||
@@ -36,25 +36,6 @@
|
||||
<oor:component-data oor:package="org.openoffice.Office" oor:name="UI">
|
||||
<node oor:name="ColorScheme">
|
||||
<node oor:name="ColorSchemes">
|
||||
<node oor:name="COLOR_SCHEME_ELY_DARK" oor:op="replace">
|
||||
<node oor:name="DocColor"><prop oor:name="Color"><value>2894892</value></prop></node>
|
||||
<node oor:name="AppBackground"><prop oor:name="Color"><value>2434341</value></prop></node>
|
||||
<node oor:name="FontColor"><prop oor:name="Color"><value>15987184</value></prop></node>
|
||||
<node oor:name="DocBoundaries"><prop oor:name="Color"><value>5592405</value></prop></node>
|
||||
<node oor:name="TableBoundaries"><prop oor:name="Color"><value>5592405</value></prop></node>
|
||||
<node oor:name="CalcGrid"><prop oor:name="Color"><value>5592405</value></prop></node>
|
||||
<node oor:name="Links"><prop oor:name="Color"><value>8303091</value></prop></node>
|
||||
<node oor:name="LinksVisited"><prop oor:name="Color"><value>12297192</value></prop></node>
|
||||
<node oor:name="Shadow"><prop oor:name="Color"><value>0</value></prop></node>
|
||||
<node oor:name="WriterFieldShadings"><prop oor:name="Color"><value>4144959</value></prop></node>
|
||||
<node oor:name="WriterIdxShadings"><prop oor:name="Color"><value>4144959</value></prop></node>
|
||||
<node oor:name="CalcNotesBackground"><prop oor:name="Color"><value>16777152</value></prop></node>
|
||||
<node oor:name="CalcPageBreak"><prop oor:name="Color"><value>6052440</value></prop></node>
|
||||
<node oor:name="CalcHiddenColRow"><prop oor:name="Color"><value>6052440</value></prop></node>
|
||||
<node oor:name="CalcCellFocus"><prop oor:name="Color"><value>6922728</value></prop></node>
|
||||
<node oor:name="WriterPageBreaks"><prop oor:name="Color"><value>6052440</value></prop></node>
|
||||
<node oor:name="DrawGrid"><prop oor:name="Color"><value>5592405</value></prop></node>
|
||||
</node>
|
||||
<node oor:name="COLOR_SCHEME_ELY_LIGHT" oor:op="replace">
|
||||
<node oor:name="DocColor"><prop oor:name="Color"><value>16777215</value></prop></node>
|
||||
<node oor:name="AppBackground"><prop oor:name="Color"><value>15987184</value></prop></node>
|
||||
|
||||
+2
-2
@@ -1,5 +1,5 @@
|
||||
#!/usr/bin/env bash
|
||||
# Usage: review.sh <item-id> <round> [path...] — codex max, fast review.
|
||||
# Usage: review.sh <item-id> <round> [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
|
||||
|
||||
+9
-1
@@ -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` 随之
|
||||
@@ -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.
|
||||
@@ -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.
|
||||
@@ -0,0 +1 @@
|
||||
PASS
|
||||
@@ -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)"
|
||||
Reference in new issue
Block a user