diff --git a/Cargo.lock b/Cargo.lock index 755160e..ffdca06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1923,7 +1923,7 @@ dependencies = [ [[package]] name = "ely-gpui-component" version = "0.1.1" -source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=1b5fb07cda3df9e96e17c2aceff187a70c67d28d#1b5fb07cda3df9e96e17c2aceff187a70c67d28d" +source = "git+https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components?rev=6df9ab0aa6cba99e6a79da80f79fc166f09890a8#6df9ab0aa6cba99e6a79da80f79fc166f09890a8" dependencies = [ "alacritty_terminal", "anyhow", diff --git a/Cargo.toml b/Cargo.toml index 452fb81..f544f8a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -19,7 +19,7 @@ jiff = "0.2" anyhow = "1" directories = "6" dunce = "1" -ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "1b5fb07cda3df9e96e17c2aceff187a70c67d28d" } +ely-gpui-component = { git = "https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components", rev = "6df9ab0aa6cba99e6a79da80f79fc166f09890a8" } env_logger = "0.11" gpui = { package = "gpui-pre", version = "=0.3.8", default-features = false, features = ["stacker"] } gpui_platform = { package = "gpui-pre-platform", version = "=0.3.8", features = ["font-kit", "wayland", "x11"] } diff --git a/agents.md b/agents.md index 49001b4..d57053c 100644 --- a/agents.md +++ b/agents.md @@ -40,7 +40,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 ## 组件映射 -依赖 `ely-gpui-component`(pin 1b5fb07,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 +依赖 `ely-gpui-component`(pin 6df9ab0,gpui 为 crates.io 的 `gpui-pre` =0.3.8)。名字取自 `reference/Ely-GPUI-Components/src`。gpui 用法规则见该仓库 `AGENTS.md`。`tests/scripts/agents_md.py` 核对下表每个名字都是组件库公开项。 | 需要 | 用 | | --- | --- | @@ -84,7 +84,7 @@ site/ 官网(Vite 8、pnpm、TypeScript,无框架;等轴 SVG 积木 | `A11yChecker` 的 `audit` 与对照表是私有的;Omarchy 色板要照同一套规则 | `ely-ui` 的 `omarchy/palette.rs` 抄了对照表。上游待办:公开 `audit` | | 组件无 role/aria 语义:菜单、列表等控件在 AX 树仍不可见 | 上游已修核心控件(7380afb:Button、TextInput、Tabs、面板行、对话框等);其余上游待办。项目自绘控件已自带 | | gpui 无 aria-disabled API(`AriaProperties` 无 disabled 字段) | 组件 7380afb 起以本地化 aria_description 表达;上游待办:gpui 补 API | -| 组件内部文案未接 i18n(面板族之外:欢迎页标题、菜单项、`SortBuilder` 与 `FilterBuilder` 等) | 面板族文案已接 i18n(7380afb,内建 en/zh-CN),`CommentSidebar` 与 `RelativeTime` 同(408f2b0);其余上游待办 | +| 组件内部文案未接 i18n(Ely 用到的组件已接:提示、默认占位、补全空行、字号示例,6df9ab0,`i18n/tests/shipped.rs` 防回退;其余组件之外:欢迎页标题、菜单项、`SortBuilder` 与 `FilterBuilder` 等) | 面板族文案已接 i18n(7380afb,内建 en/zh-CN),`CommentSidebar` 与 `RelativeTime` 同(408f2b0);其余上游待办 | | LOK 的 `.uno:StyleApply` 状态给界面名(`Default Paragraph Style`),样式列表给程序名(`Standard`),两者对不上 | ElyDoc 属性栏:列表外的当前样式显示为占位文字;更新样式用状态里的界面名。上游待办:状态带程序名 | ## gpui 规则 @@ -294,3 +294,4 @@ cargo run -p ely-doc 启动一个应用(ElyDoc 先 cargo build -p ely - 2026-10-14(Gitea #1,Linux Wayland 报):ElyShow 左侧缩略图条滚轮不滚。根因:列表包在 `ContextMenu` 宿主里,宿主无尺寸,列表随内容长高、从不溢出,滚轮无处可滚(右栏正常);宿主改为 `size_full`(同 ElyPDF 的三处),`tests/strip.rs` 以 24 张幻灯片核对滚轮滚到最后一张。各应用其余 `ContextMenu` 宿主已核:ElyPDF 三处本有尺寸,ElyTable 的工作表标签条与下拉选项不是滚动列表。 - 2026-10-14:ElyMark 的菜单与快捷键(R4.9,MARK-15)。项与键取自 Typora 1.14.10 主进程字节码的字符串表(`.work/typora-main-menus.txt`)与其 `zh-Hans.lproj/Menu.json`;实际大小在字节码里有 Ctrl+Shift+9 与 Ctrl+Shift+0 两串,照用户所定用 Ctrl+Shift+0,R4.11 在真窗口核对。表格是“段落”的子菜单(照 Typora),顶层不再有“表格”。原 Ctrl+1/2/3 切模式、Ctrl+=/-/0 字号、Ctrl+Shift+F 专注让给 Typora 的键;可视、源码、预览留在“视图”菜单,无键。Ctrl+/ 另绑在 `ElyEditor` 上(gpui 同深度后绑者胜),盖过代码编辑器的 `//` 注释切换(Markdown 没有这种注释);`MarkdownDocument` 的键上下文只在可视模式有。带 Shift 的标点:Windows 写键名(`ctrl-shift-[`),macOS 与 Linux 写字符(`cmd-{`),见 `blocks::shifted`。Ctrl+Enter 在打开的块里原是输入框的 Submit(文档不处理),改绑为下方插入行。拒绝一律成信息提示:当场的由 `Desk::edit` 说,后台读出的经 `DocumentEvent::EditRefused`、`view::Refused` 到宿主。菜单打开时,打开的块把焦点借给菜单,块与选区都留着,菜单命令照样作用于所选(组件 6d90351)。复制为 Markdown 与粘贴为纯文本只作用于文档:焦点在文档外的输入框(查找框、对话框)时交给该框自己的复制、粘贴。表格子菜单末项为删除表格(组件 `TableEdit::Delete`,一步撤销,插入点不在表格里即拒绝;在打开的块里删除后焦点回到文档,撤销键照用,组件 6c2d28c)。Typora 有而组件库暂缺的不进菜单:行内公式、上下标、注释、提示块子菜单、任务状态、列表缩进 Ctrl+[/]、代码工具。 - 2026-10-14:ElyMark 的右键菜单(R4.10)。项、次序与显隐规则取自 Typora 1.14.10 的 `resources/window.html`(`#context-menu` 与其子菜单)与 `frame.js` 的显隐函数,记在 `.work/R4.10/typora-context-menu.txt`。菜单在 `view/menu.rs`,`MarkView` 自持(`ely_office::menu::Offer` 与 `ContextMenu::manual`):可视模式由组件的 `DocumentEvent::Pointed` 开在指针处,源码与预览由视图自己的右键开。剪切、复制、粘贴、删除所选范围为图标行,按下即把其键交回菜单打开前的焦点(`dispatch_keystroke`),与键盘同一路径;无选区时剪切、复制、删除灰显。其余项经各自的动作,显示所绑的键。组件 013f142、1b5fb07:文本框与代码编辑器右键落在选区里时保留选区,否则把插入点移到所点处;可视模式打开的块右键不写回、不关闭,目标在后台按全文读出(块外的引用定义照样解析);`MarkdownDocument::delete_image`(删除图片,一步撤销,无图片即 `EditRefusal::NoImage`);可视无块时 Ctrl+V 粘贴文字(原只收图片,文字被丢弃)。复制为 HTML 代码的图片照原地址(`Pictures::Written`,同 Typora),复制为富文本照旧内嵌。“编辑”菜单的复制项照 Typora 次序:纯文本、Markdown、HTML 代码、富文本。Typora 有而不做的:缩进与减少缩进行(可视只在打开的块里有)、复制表格、代码自动缩进、图片上传、缩放、移动与语法切换、拼写、表情复制、检查元素。gpui 的测试只发按下,菜单在 Enter 松开时选中,测试另发松开(`blocks/context.rs` 的 `choose`)。 +- 2026-10-14(用户报:整个软件中文显示问题很多):中文显示(R4.13)。Windows 上 gpui 以用户区域(本机为 en-US)排版全部文字,DirectWrite 按日文为汉字选回退字体,日文字体缺的简体字再落到另一字体,同一个词里字形与粗细不一(ElyTree 的“打开仓库”:“打开”粗、“仓库”细)。`vendor/gpui-pre-windows` 的 `text_locale`:用户区域不是中日韩时,改用 Windows 语言列表(WinRT `GlobalizationPreferences::Languages`,Win32 的界面语言只给 en-US)里第一个中日韩语言排版(日志 `text: laid out as …`);在 Windows 上打中文须把中文加进该列表,所以它就是用户读的汉字。只换排版的区域,字体族名与字体列表仍按用户区域。读不出语言列表即记警告、照用户区域。组件库 6df9ab0:Ely 用到的组件里写死的英文(下拉的“Choose…”、清除、关闭、移除、取消固定的提示,公式补全的“No suggestions”,设置里字号的重置提示与示例句)改走内建中英目录。仍为英文的是内核的文字(LibreOffice 界面语言固定英文:幻灯片占位“Click to add Title”、母版“Default”、配色名、数字格式代码“General”、函数名),以及启动失败对话框(出现在读入语言之前,中英并列)。 diff --git a/scripts/fetch-references.sh b/scripts/fetch-references.sh index 4eabce0..56e991f 100755 --- a/scripts/fetch-references.sh +++ b/scripts/fetch-references.sh @@ -36,7 +36,7 @@ fetch() { # name url rev [sparse paths...] echo "fetch $name $rev" } -fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 1b5fb07cda3df9e96e17c2aceff187a70c67d28d +fetch Ely-GPUI-Components https://git.zacharyzhang.com/ZacharyZhang-NY/Ely-GPUI-Components.git 6df9ab0aa6cba99e6a79da80f79fc166f09890a8 fetch zed https://github.com/zed-industries/zed.git 1a28cff4b409169bac058bca40dfbfeb7621d19b fetch libreoffice-core https://github.com/LibreOffice/core.git 8221e31b3ac356a1623c672912a3d2b492f7e3d1 \ include/LibreOfficeKit libreofficekit desktop/source/lib desktop/inc \ diff --git a/tasks/TASKS.md b/tasks/TASKS.md index 2267360..be3ff48 100644 --- a/tasks/TASKS.md +++ b/tasks/TASKS.md @@ -241,6 +241,7 @@ - [~] R4.7 组件库:块命令(标题 1–6、段落、标题升降一级;引用、有序、无序、任务列表的切换;插入分隔线、代码块、公式块、目录、front matter、脚注、链接定义、前后空段落;删除块),一步撤销,拒绝说明原因。组件 58e212d 至 0558f0c(已推送):`MarkdownDocument::block` 照 Typora 的 `changeBlock`、`changeHeadingLevel`、`insertBlock`、`toggleIndent`、`insertParagraph`、`deleteBlock`,与格式共用一个后台执行器与 `EditRefusal`;同级标题保持不变(照 Typora 菜单),代码上的标题命令、任务首行的标题与引用拒绝(GFM 读作文字),前后空段落写成空行,越出打开的块先写回(多一步撤销)。审查三轮未过(`tasks/reviews/R4.7-r1..r3.md`):r1 十三项(标题标记里插入崩溃、任务框丢勾、列表里提示块拆开、拆分时行内标记断开、缩进代码改类型、列表项与脚注的前置段落、删除使邻段相接、尾部 `#` 与块起始符、任务首行、跨列表的选区、同级标题当开关);r2 十二项(拆分后重开标记成块语法、闭合 `#`、标记下一行的任务框、提示块的懒行与空行、标记与正文同行、Tab 缩进代码、任务列表包代码、引用里删除、拆分剩下的块起始符、长注释),r2 起标题与拆分写出前重新解析核对;r3 九项:核对忽略代码里的空白、按 mdast 而非可视读法核对(下划线、表情漏判)、提示块正文块失去缩进、按内容认围栏、非 ASCII 空白算缩进、去掉任务框露出块起始符、续行任务框解除时留下、空标题转段落误拒、转义的提示标记误拒。r3 已修:核对经 `MarkdownReader` 照绘制读(代码与公式里的空白计入),提示块的标记按提示读法判定、正文块保留缩进,围栏按源码结构认,缩进只数 ASCII 空格,去掉任务框时转义露出的起始符(续行同),空标题可转空段落;七项回归在 r3 的源码上失败(`.work/R4.7-fix3/red.md`);轮数用尽,未再审。遗留:列表与引用的切换不重新解析核对,只逐项修正 - [~] R4.8 组件库:表格行列上下左右移动、整理表格源码;代码块取代码内容;与已有的增删行列同一套拒绝。组件 bcee87c 至 6b033f3(已推送):`TableEdit` 加上下移行(表头与首行互换,超出表头列数的行不作表头,到边拒绝为 `TableRefusal::Edge`)、左右移列、整理;表格按显示宽度补齐(`unicode-width`,Tab 记 4、控制字符记 0,照 Typora 的 `wcswidth`),写回表格自身的换行;表格编辑并入后台执行器;右键代码块时 `DocumentTarget` 带出代码文字。审查三轮未过(`tasks/reviews/R4.8-r1..r3.md`):r1 两项(纯 CR 文本里整理表格把前文当作容器前缀、行内 Tab 宽度不合 Typora);r2 一项(End 后插入表格拆开 CRLF);r3 一项(r2 的修法让行辅助函数返回光标之前的位置,End 后的块命令崩溃)。r3 已修:编辑器只按 LF 分行,光标可落在 CR 与 LF 之间;执行器与插入表格在入口把选区移到 CR 前,行辅助函数改为断言不收这种位置;两项回归在 r3 的源码上失败(`.work/R4.8-fix3/red.md`);轮数用尽,未再审。遗留:编辑器自身的 CRLF 模型不变,End 仍落在 CR 之后;表情宽度按 `unicode-width` 记 2 列(Typora 记 1) - [x] R4.9 ElyMark:“段落”“格式”菜单与 Typora 同项同键(Ctrl+1–6、Ctrl+0、Ctrl+=/-、Ctrl+B/I/U、Ctrl+Shift+`、Alt+Shift+5、Ctrl+K、Ctrl+Shift+I、Ctrl+\、Ctrl+T、Ctrl+Shift+K/M/Q/[/]);源码模式 Ctrl+/;字号 Ctrl+Shift+=/-/0;专注 F8、打字机 F9;表格菜单加移动行列;复制为 Markdown Ctrl+Shift+C、粘贴为纯文本 Ctrl+Shift+V。ElyMark 5392062、f84f990、09fcb0b,组件 6d90351、24ffc45、6c2d28c(已推送),pin 6c2d28c:键与菜单项取自 Typora 字节码与其中文菜单;表格为“段落”子菜单,加删除表格;拒绝成提示;菜单借打开的块的焦点,块与选区留着;复制为 Markdown、粘贴为纯文本只作用于文档。R1 FAIL 六项、R2 FAIL 四项已修,R3 PASS +- [ ] R4.13 全部应用:中文显示(用户报,2026-10-14):Windows 上同一词里字形粗细不一(gpui 以 en-US 排版,汉字按日文回退);Ely 用到的组件里写死的英文改走中英目录;先于 R4.10 的修复做 - [ ] R4.10 ElyMark:右键菜单照 Typora 按目标变化:剪贴板行(剪切、复制、粘贴、删除)、复制/粘贴为 ▸(Markdown、HTML 代码、纯文本、粘贴为纯文本)、格式行、段落 ▸ 或表格 ▸ 或代码项、插入 ▸、链接(打开、复制地址)、图片(打开所在位置、删除图片)、删除块;预览只读时只留复制类 - [x] R4.12 全部应用:已开着时再打开文件,交给运行中的那份开成新标签并带到前台,不再弹“已在运行”(用户报,2026-10-14);先于 R4.11 做。第 1 轮 4 项:令牌被拒时连接重置使重试失效、慢速来者占住门、主窗口关后仍回执、ElyMail 与 ElyAgent 不到前台;已修,各有回归;第 2 轮通过 - [ ] R4.11 验收:真窗口逐项操作,每项功能可用(不求与 Typora 一一相同);帮助、PRD、`agents.md` 随之;逐条核对表 diff --git a/vendor/gpui-pre-windows/Cargo.toml b/vendor/gpui-pre-windows/Cargo.toml index 52a60ac..ab4445b 100644 --- a/vendor/gpui-pre-windows/Cargo.toml +++ b/vendor/gpui-pre-windows/Cargo.toml @@ -149,11 +149,13 @@ features = [ version = "0.62" features = [ "Data_Xml_Dom", + "Foundation_Collections", "Foundation_Numerics", "Globalization_DateTimeFormatting", "Storage_Search", "Storage_Streams", "System_Threading", + "System_UserProfile", "UI_Notifications", "UI_ViewManagement", "Wdk_System_SystemServices", diff --git a/vendor/gpui-pre-windows/src/direct_write.rs b/vendor/gpui-pre-windows/src/direct_write.rs index 304dc38..fa8216c 100644 --- a/vendor/gpui-pre-windows/src/direct_write.rs +++ b/vendor/gpui-pre-windows/src/direct_write.rs @@ -42,6 +42,8 @@ pub(crate) struct DirectWriteTextSystem { struct DirectWriteComponents { locale: HSTRING, + /// Text's language; picks Han glyphs and fonts. + text_locale: HSTRING, factory: IDWriteFactory5, in_memory_loader: IDWriteInMemoryFontFileLoader, builder: IDWriteFontSetBuilder1, @@ -176,7 +178,9 @@ impl DirectWriteTextSystem { let builder = unsafe { factory.CreateFontSetBuilder()? }; let mut locale = [0u16; LOCALE_NAME_MAX_LENGTH as usize]; unsafe { GetUserDefaultLocaleName(&mut locale) }; - let locale = HSTRING::from_wide(&locale); + let end = locale.iter().position(|unit| *unit == 0).unwrap_or(locale.len()); + let locale = HSTRING::from_wide(&locale[..end]); + let text_locale = text_locale(&locale); let text_renderer = TextRendererWrapper::new(locale.clone()); let gpu_state = directx_devices.map(GPUState::new).transpose()?; @@ -185,6 +189,7 @@ impl DirectWriteTextSystem { let system_ui_font_name = get_system_ui_font_name(); let components = DirectWriteComponents { locale, + text_locale, factory, in_memory_loader, builder, @@ -554,7 +559,7 @@ impl DirectWriteState { font_info.font_face.GetStyle(), DWRITE_FONT_STRETCH_NORMAL, font_size.as_f32(), - &components.locale, + &components.text_locale, )? .cast()?; if let Some(ref fallbacks) = font_info.fallbacks { @@ -1855,6 +1860,48 @@ const fn make_direct_write_tag(tag_name: &str) -> DWRITE_FONT_FEATURE_TAG { } #[inline] +/// The user's locale, or their first Han language. +fn text_locale(user: &HSTRING) -> HSTRING { + if han(&user.to_string()) { + return user.clone(); + } + let languages = maybe!({ + let languages = windows::System::UserProfile::GlobalizationPreferences::Languages()?; + anyhow::Ok(languages.into_iter().map(|tag| tag.to_string()).collect::>()) + }); + match languages { + Ok(languages) => match languages.into_iter().find(|tag| han(tag)) { + Some(tag) => { + log::info!("text: laid out as {tag}, the user's Han language; the locale is {user}"); + HSTRING::from(tag) + } + None => user.clone(), + }, + Err(error) => { + log::warn!("text: the user's languages are unreadable, so text is laid out as {user}: {error:#}"); + user.clone() + } + } +} + +/// Whether `tag` is a language written with Han characters. +fn han(tag: &str) -> bool { + matches!(tag.split('-').next(), Some("zh" | "ja" | "ko")) +} + +#[cfg(test)] +mod han_tests { + #[test] + fn han_languages_are_told_by_their_first_subtag() { + for tag in ["zh-Hans-CN", "zh-TW", "ja-JP", "ko"] { + assert!(super::han(tag), "{tag}"); + } + for tag in ["en-US", "zhx-CN", "", "fr"] { + assert!(!super::han(tag), "{tag}"); + } + } +} + fn get_name(string: IDWriteLocalizedStrings, locale: &HSTRING) -> Result { let mut locale_name_index = 0u32; let mut exists = BOOL(0);