987 Commits
Author SHA1 Message Date
fanlusky f486da52b9 fix(ui-html): 悬停在选中候选上不再改变高亮色
fluent 与石墨的 .hover-active .cand:hover 特异度高于 .first,悬停选中项会换成
悬停色;改为 :not(.first),与微信绿、杨柳青、秋桂一致。自定义皮肤的 selected
同时写入 .hover-active .cand.first:hover,避免被 base 皮肤的选中悬停色(如微信绿
#07c160)覆盖。设置页预览样式同步。
2026-09-28 14:20:17 +08:00
fanlusky b8c6a18ca2 fix(server): D2D 候选窗横排首尾高亮与外框同 R,自定义皮肤按 base 取几何
fluent、微信绿横排设置 outerItemRadius,对应 CSS 的首尾项圆角规则。
自定义皮肤包先加载,皮肤分支按 package->base 判断,与 WebView2 注入
base 皮肤 CSS 的行为一致,颜色仍由包覆盖。
2026-09-28 13:11:40 +08:00
fanlusky 841b846f64 feat(server): 整句候选来源标签可在设置页关闭
新增 association.sentence_source_badge(默认开启,保持现有行为),
控制候选窗整句候选后的〔Trigram〕〔Unigram〕〔神经D〕〔神经K〕标签是否显示。
关闭只隐藏标签,不影响候选内容与排序;云/AI 角标不受影响。

同步更新出厂模板、开发配置、两个设置窗口宿主的读写映射和设置页开关。
2026-09-28 12:04:43 +08:00
fanlusky 4e17d27ea7 feat(server,ui-html,installer): 悬浮工具栏支持无交互自动隐藏
新增 general.floating_toolbar_auto_hide(默认关闭)和
general.floating_toolbar_auto_hide_delay(1–60 秒,默认 5)。开启后,工具栏显示满延时且
无交互时约 0.4 秒淡出并隐藏;鼠标悬停在工具栏上时倒计时重置,淡出中移入会立即恢复。
中英、全半角、标点、Caps Lock、英文模式、简繁或日文模式真正变化时重新显示并重新计时;
普通的焦点切换和 IME 激活不会把已自动隐藏的工具栏带回来。

悬停用 GetCursorPos 与 GetWindowRect 轮询判断,两者同为物理像素,不涉及 DPI 换算,
也不需要页面侧新增消息,D2D 与 WebView2 两种后端共用。预热、首帧宽限和 cloak 期间不计时。
淡出结束时以 alpha 0 隐藏,下次显示之前再恢复 255,避免在隐藏瞬间闪出一帧不透明画面。

两个新键已写入出厂模板和开发配置,两个设置宿主的字段列表与分发都已补上,并放在
general.floating_toolbar_ 按钮项前缀兜底之前。设置页在工具栏卡片内新增开关和缩进的
延时行,延时用新增的通用 −/+ 步进器,开关关闭时延时行置灰。
2026-09-28 01:09:10 +08:00
fanlusky 396bd21715 feat(server,ui-html,installer): 整句联想默认开启词格、Google 和神经速度档
sentence_wordlattice、sentence_google、sentence_neural_keyboard 出厂默认改为 true;
sentence_neural_desktop 与 sentence_show_next_on_duplicate 保持关闭。出厂模板、开发配置、
代码缺键回退和设置页开关初始状态一并改。这几个开关尚未发版,不涉及老用户配置迁移。

Prepare-PackageFiles.ps1 增加断言:这三个开关在出厂模板里未开启时打包失败。
2026-09-27 23:43:13 +08:00
fanlusky 0a1b302b1c feat(server,ui-html,installer): 设置窗口后台驻留默认改为关闭后立即退出
settings_window_linger 的出厂默认值从 "10m" 改为 "off"。出厂模板、开发配置、
缺键/非法值回退、设置进程初始值和设置页下拉框初始文字一并改;该功能尚未发版,
不涉及老用户配置迁移。

Prepare-PackageFiles.ps1 增加断言:出厂模板的驻留值不是 off 时打包失败。
2026-09-27 23:02:34 +08:00
Lu Fan(SonnyCalcr) bb8c5e9be8 Merge pull request #495 from fanlusky/refactor/split-config-and-controls
refactor(server,ui): 按职责拆分 ime_config.cpp 与 Controls.cpp
2026-09-27 18:06:05 +08:00
fanlusky b5def6a2e3 refactor(server): 按职责拆分 ime_config.cpp
ime_config.cpp 有 4000 行,配置状态与加载、TOML 文本编辑、升级时的模板合并、
写回与通知 Server,以及上百对 GetConfigured* / SetConfigured* 全挤在一个文件里。
按职责拆成以下几个翻译单元:

- ime_config.cpp:全部配置全局变量的定义(顺序与初始值不变)、语音配置的读写锁
  快照与发布、各项校验白名单、LoadImeConfig(读取与缺省值)、旧版语音配置迁移、
  InitImeConfig / ReloadImeConfigIfChanged / InvalidateImeConfigWriteTime
- ime_config_toml.cpp:保留格式的键值替换与插入、跨行值定位、赋值遍历与解析、
  读文件、原子写文件与 TOML 解析校验
- ime_config_template.cpp:凭证兜底重放、MergeTomlIntoTemplate /
  MergeConfigIntoTemplate、损坏配置的留证与抢救、SyncConfigWithInstalledTemplate、
  旧 ACP 乱码目录的配置找回
- ime_config_write.cpp:WriteConfiguredValues / WriteConfiguredValue 与
  NotifyImeServer*(Aux 管道与窗口消息两条通知路径)
- ime_config_appearance.cpp:每页候选数、字体与字号、文字颜色、候选窗布局与跟随
  光标、小窗 UI 后端、皮肤、预编辑样式、固定徽标、设置窗口保留时长
- ime_config_fonts.cpp:ResolveSystemFontFamilyForCss 与 GetSystemFontFamilies
- ime_config_theme.cpp:主题模式、各界面主题覆盖与 ResolveConfiguredTheme
- ime_config_input.cpp:输入方案与中日文模式、简繁、中英切换键、双拼与五笔方案、
  上屏样式、辅助码、全拼纠错、模糊音、调频
- ime_config_keys.cpp:翻页键、方向键选词、以词定字、智能标点、配对标点、标点锁定
  及发给 TSF 的载荷
- ime_config_toolbar.cpp:悬浮工具栏与光标状态角标
- ime_config_general.cpp:诊断日志、输入统计、中英与表情混输、候选翻译、云候选、
  整句联想、工具模式与剪贴板历史
- ime_config_services.cpp:语音输入、AI 助手、腾讯 TMT、自定义翻译与小牛翻译
- ime_config_internal.h:上述文件之间共享的声明,只给 ime_config*.cpp 包含

纯搬移,不改行为:函数体、注释和格式都没动。原来在匿名命名空间里、现在
要跨文件用的符号放进 ime_config_detail 命名空间并在内部头文件里声明,
避免与 Server 其他翻译单元里的同名内部符号冲突;只被一个文件用到的仍留在
该文件的匿名命名空间,包括 g_session_backend、两个语音 token 槽位补写标记、
g_config_last_write_time、PublishVoiceInput、AiAssistantProviders、LoadImeConfig
和各文件自己的小工具。全局变量的定义都还在 ime_config.cpp,按原顺序排列,
初始化顺序不变;函数内的 static(字体缓存、DirectWrite 工厂、凭证键表、剪贴板
首次加载标记等)仍各自只有一份定义。取值范围常量、kFuzzyPinyinRuleKeys、
ConfigFileLock、ParseScheme 前置声明、模板文件名、TomlAssignmentVisitor 和
ConfigValueUpdate 原样挪进内部头文件。MetasequoiaImeServer、
MetasequoiaImeSettings 和测试工程都编译 ime_config.cpp,三处源文件列表一并
补上新文件。

非搬移改动只有两处:NotifyImeServer 的默认实参从定义挪到了头文件里的声明上;
测试里 kFuzzyPinyinRuleKeys 副本的注释改指 ime_config_internal.h。

用脚本比对了原文件与拆分后全部文件的非空、非 #include 行多重集:除上述
函数签名外,差异只有新增的声明、extern、命名空间包装、using 指令、
#pragma once 和文件头注释。可用 git diff --color-moved 复核。
2026-09-27 17:22:17 +08:00
fanlusky 4b30659fca ci: 单独构建并运行引擎自己的测试
Server 任务以 BUILD_TESTING=OFF 把引擎当库引入(避免 EXCLUDE_FROM_ALL 下的测试
找不到可执行文件),于是引擎的 23 组测试从来没在 CI 里跑过,只在开发机上跑。
server/tests 覆盖不到的行为——比如用户日志的连接管理、关闭它时要放开数据目录里
所有缓存句柄——只有这些测试能发现;#491 的一个回归就是这样在本地才测出来的。

新增 Engine 任务:按 engine/vcpkg.json 单独配置引擎、构建、再用
scripts/ci/test-engine.ps1 在与 Server 相同的锁定词库上跑 ctest。数据目录与
LOCALAPPDATA 都隔离到临时目录,布局与 test-server.ps1 一致。同步更新
server/CMakeLists.txt 里「引擎测试经 server/tests 覆盖」的旧说法。

本地按同样命令(不设 BOOST_ROOT,只靠 vcpkg 清单)配置、构建并运行:23/23 通过。
2026-09-27 16:54:20 +08:00
fanlusky 8f2c81dd0f perf(server,engine): 选词学习的写库挪出按键线程
权重调整(AdjustCandidateRanking)和输入过的英文词(LearnEnteredEnglishWord)
原本和按键排在同一个 worker 队列里:每次选非首位候选都要在下一个键之前付一次
用户日志的提交和 fsync,触发阈值时还有 msime.db 的一次提交。磁盘忙或杀软扫描
时,这就是肉眼可见的卡顿。

- server/src/utils/serial_task_runner:单线程、按提交顺序执行、低优先级,Stop()
  先执行完队列再退出,选词不会在退出时丢失。附单元测试。
- 两类写入改为投递给这个执行器。它们只调用 user_dictionary,不碰输入会话;唯一
  的会话副作用——权重变化后清引擎候选缓存——经 EnqueueResetInputSessionCacheTask
  回到 worker。删除对应的 TaskType 和 Task 字段。main 在 worker join 之后调用
  ShutdownDictionaryWriter()。
- user_dictionary:默认日志连接改为每线程一条。FULLMUTEX 只保证单次调用安全,
  保证不了事务:连接只有一个事务状态,写线程的 BEGIN IMMEDIATE..COMMIT 会把
  worker 的读卷进来,worker 侧 UserDatabase 析构时还会把它回滚。
  close_default_user_database() 递增代号,各线程下次访问时关闭旧连接。
- WorkerThread 取任务改为 move,不再每次复制整个 Task。

行为变化:选词后如果写入还没完成就立刻再打同一串,会短暂看到旧顺序;写完后
worker 清缓存,下一次查询即是新顺序。此前的“下一个键之前一定写完”换成了不阻塞
按键。
2026-09-27 16:09:51 +08:00
fanlusky a8b5246852 refactor(server): 按职责拆分 ime_windows.cpp
ime_windows.cpp 有 4600 行,窗口创建与总窗口过程、候选窗定位与裁剪、WebView2
候选窗显示流程、候选窗消息处理、悬浮工具栏、托盘菜单、设置窗口和光标状态角标
全挤在一个文件里。按职责拆成以下几个翻译单元:

- ime_windows.cpp:共享状态与全局变量、系统明暗切换判定、宿主 cloak 与状态
  描述(DescribeCandidateHostState、DescribeTrayMenuHostState 等)、
  SyncHostWebViewBounds、ApplyConfiguredInputScheme / ApplyConfiguredShuangpinSchema、
  窗口类与消息注册、CreateCandidateWindow、WndProc 分发,以及光标状态角标的
  WndProcCaretStateWindow
- ime_windows_candidate_layout.cpp:光标锚点、四分之一屏宿主尺寸、装饰偏移、
  翻转与裁剪记忆、ReanchorCandidateHostToPaintedCard、
  ClipCandidateWindowToContent / SetWindowRgn、KeepCandidateCardInsideHostAndMonitor
- ime_windows_candidate_show.cpp:RefreshCandidateClipAfterPaint、
  PlaceCandidateHostNearCaret、槽位测量扩展裁剪、FinishCandidateShowAfterSlots
  和两遍测量的 FineTuneWindow
- ime_windows_candidate.cpp:WndProcCandWindow(显示 / 隐藏 / 移动消息、去重与
  隐藏宽限、DPI 变化、状态切换转发、配置同步定时器、候选鼠标动作)
- ime_windows_toolbar.cpp:悬浮工具栏的配置尺寸、拖动后收回可见屏幕、
  LayoutFloatingToolbar、显隐决策与绘制宽限、ApplyConfiguredFloatingToolbarSize、
  WndProcFtbWindow
- ime_windows_menu.cpp:托盘菜单的 DIP 到物理像素换算与 WndProcMenuWindow
- ime_windows_settings.cpp:设置窗口的前台激活与重试、非客户区与最大化按钮、
  鼠标输入转发、WndProcSettingsWindow
- ime_windows_internal.h:上述文件之间共享的声明,只给 ime_windows*.cpp 包含

纯搬移,不改行为:函数体、注释和格式都没动。原来在匿名命名空间里、现在
要跨文件用的符号放进 ime_windows_detail 命名空间并在内部头文件里声明,
避免与 Server 其他翻译单元里的同名内部符号冲突;只被一个文件用到的仍留在
该文件的匿名命名空间,包括只有候选窗消息处理用到的强制重排、隐藏宽限与
去重签名状态,以及只有设置窗口用到的激活重试计数。其余共享全局变量的定义
仍在 ime_windows.cpp。WEBVIEW_DIAG_LOGF 宏、定时器 ID 与时长常量、
FineTuneWindow 等前置声明和 SuppressCandidateDpiChange 原样挪进内部头文件。
非搬移改动只有两处:LogCandidatePositionAudit 和 PlaceCandidateHostNearCaret
的默认实参从定义挪到了头文件里的声明上;DescribeTrayMenuHostState 上方注释
里的文件名改成 windows_webview2_menu.cpp,菜单的生命周期事件在上一次拆分后
已经搬到那里。

用脚本比对了原文件与拆分后全部文件的非空、非 #include 行多重集:除上述
两个函数签名和那一行注释外,差异只有新增的声明、命名空间包装、using 指令、
#pragma once 和文件头注释。可用 git diff --color-moved 复核。
2026-09-27 15:49:35 +08:00
fanlusky f4aba3bcfc refactor(server): 按职责拆分 windows_webview2.cpp
windows_webview2.cpp 有 5500 行,小窗 WebView2 环境与 controller 的排队创建、
Topmost 分阶段提升、皮肤注入、候选窗脚本、托盘菜单、设置窗口和悬浮工具栏全挤
在一个文件里。按职责拆成以下几个翻译单元:

- windows_webview2.cpp:共享状态与全局变量、小窗 WebView2 environment 创建与
  controller 排队重试、导航就绪门控、lazy-topmost 分阶段提升、内容截断回退、
  DPI / RasterizationScale 辅助、可见性与状态查询,以及 GetAppdataPath、
  InitSmallWindowWebviews、PrepareTrayMenuWebviewForShow、ShutdownWebviews
- windows_webview2_skin.cpp:读取页面 HTML、外部候选皮肤 CSS 注入与资源内嵌、
  PrepareHtmlForWnds,以及候选窗、悬浮工具栏的布局 / 主题 / 外观应用
- windows_webview2_candidate.cpp:候选内容与测量区脚本、悬停锁与粘性卡片脚本、
  候选槽位增量更新、测量 bounds、OnControllerCreatedCandWnd 与页面消息处理
- windows_webview2_menu.cpp:托盘菜单 controller 创建与页面消息处理、
  SyncMenuFloatingToolbarToggle
- windows_webview2_settings.cpp:设置窗口的 DirectComposition 视觉树、
  composition controller、设置页消息与 configUpdate 分发、
  PostSettingsWindowState、PostSettingsConfig、InitWebviewSettingsWnd
- windows_webview2_toolbar.cpp:悬浮工具栏状态缓存与渲染脚本、
  OnControllerCreatedFtbWnd 与页面消息处理、各个 UpdateFtb*
- windows_webview2_internal.h:上述文件之间共享的声明,只给
  windows_webview2*.cpp 包含

纯搬移,不改行为:函数体、注释和格式都没动。原来在匿名命名空间里、现在
要跨文件用的符号(导航就绪标志、工具栏导航重试与绘制宽限标志、内容截断冷却
时间戳、JsonNumberAsDouble、HandleContentTruncatedMessage、
OnSmallWindowControllerSettled、NotifySmallWindowNavigationReady)放进
windows_webview2_detail 命名空间并在内部头文件里声明,避免与 Server 其他
翻译单元里的同名内部符号冲突;只被一个文件用到的仍留在该文件的匿名命名空间。
共享全局变量的定义仍在 windows_webview2.cpp。诊断日志的屏蔽宏、json 别名、
前置声明和 kTruncationSizeFactor 原样挪进内部头文件,每个拆出的文件都先包含
它,日志宏在各文件里仍是同样的空展开。

用脚本比对了原文件与拆分后全部文件的非空、非 #include 行多重集:原文件的
每一行都恰好出现一次,差异只有新增的声明、命名空间包装、using 指令、
#pragma once 和文件头注释。可用 git diff --color-moved 复核。
2026-09-27 15:38:01 +08:00
fanlusky b6ca8bd8ba refactor(server): 按职责拆分 event_listener.cpp
event_listener.cpp 有 5700 行,按键、候选组页、异步候选合并、选词、任务
队列和命名管道服务端全挤在一个文件里。按职责拆成以下几个翻译单元:

- event_listener.cpp:共享状态(特殊模式开关、UILess、译文缓存、异步请求
  来源、状态快照、候选 UI 归属)、模式判定与候选窗显隐辅助,以及
  CancelCloudCandidateRequest、RegisterStatusSnapshotWindow、
  SendCurrentDataToClient、SendUiLessCompositionToClient、ClearState
- event_listener_worker.cpp:Task 定义、任务队列、顶字 Hide 抑制记账、
  WorkerThread 分发循环、ReplayDeferredClientActivation 和各个 Enqueue*
- event_listener_pipes.cpp:管道握手与日志辅助、各管道监听线程、主管道
  客户端线程、RegisteredPipeMonitorThread、关停时唤醒监听
- event_listener_candidates.cpp:候选组页与发布(BuildCurrentCandidatePage、
  RefreshCandidatePageUi 等)、调频排序键、翻页扩展、PrepareCandidateList
- event_listener_async_candidates.cpp:云联想、AI、神经重排、英文、译文、
  emoji、颜文字的 Apply* 合并
- event_listener_keys.cpp:按键分类、ApplyCompositionEditKey、译文副候选页、
  五笔顶字负载、PublishRestoredCompositionCandidates、HandleImeKey
- event_listener_selection.cpp:ResolveCandidateItem、
  WaitForCandidateRenderSync、ProcessSelectionKey
- event_listener_internal.h:上述文件之间共享的声明,只给
  event_listener*.cpp 包含

纯搬移,不改行为:函数体、注释和格式都没动。原来在匿名命名空间里、现在
要跨文件用的符号放进 event_listener_detail 命名空间并在内部头文件里声明,
避免与 Server 其他翻译单元里的同名内部符号冲突;只被一个文件用到的仍留在
该文件的匿名命名空间。共享全局变量的定义仍在 event_listener.cpp。唯一的
非搬移改动是 UpdateCloudInput/UpdateEnglishInput/UpdateEmojiInput/
UpdateKaomojiInput/UpdateAiInput 的默认实参从定义挪到了头文件里的声明上。

用脚本比对了原文件与拆分后全部文件的非空、非 #include 行多重集:除上述
五个函数签名外,差异只有新增的声明、命名空间包装、using 指令、
#pragma once 和文件头注释。可用 git diff --color-moved 复核。
2026-09-27 15:26:19 +08:00
fanlusky 356884bf0a perf(server): 译文没有变化时不重绘候选页
ApplyCandidateTranslations 每次收到译文结果都 RefreshCandidatePageUi(true),
即使译文与已有的完全相同。候选页构建时本就从 g_candidate_translation_glosses
读译文,新按键的候选大多已在上一页查过,于是每键多出一次整页发布和绘制,
还会让随后的空格/数字选词多等一次渲染同步。

只有译文表确实新增、改写或删除条目时才刷新。
2026-09-27 15:03:17 +08:00
fanlusky dff42f92df perf(server): D2D 候选窗每次显示只绘制一遍
PlaceAndShow 同步 Present 之后,前面各个 setter 的 InvalidateRect 留下的
WM_PAINT 又把整张卡片(含阴影模糊)画一遍;宿主改尺寸时 WM_SIZE 也会在
显式 Present 之前同步画一遍。同步 Present 后 ValidateRect 丢弃过时的
WM_PAINT;PlaceAndShow 自己触发的 WM_SIZE 只调整交换链尺寸,由随后的
Present 出帧。
2026-09-27 15:03:17 +08:00
fanlusky e829a1b1ab fix(server): 候选卡片只在同一输入内保留尺寸,换键或翻页后不再留空白
3a7beb18 让卡片在整次组合内只增不减,自然尺寸低于保留值 70% 才收回。
这本是为了吸收同一输入下陆续到达的译文、云/AI 候选和混输英文,但换键
后候选本就换了一页,也沿用了旧尺寸:gw(4 行)→ gw'r(3 行)时自然高度
约为 80%,卡片保持旧高度,底部留出一行空白;翻到末页同理。

保留尺寸改为按输入计算,键为 preedit(含光标标记)+ 页码。键不变时仍只
增不减;换键、移动光标或翻页后从自然尺寸开始。WebView2 由 C++ 把键传给
MsimeStickyCandidateCard,D2D 用 stickyCardKey_,两端逻辑一致。
2026-09-27 14:36:48 +08:00
fanlusky 661c622c49 fix(server): Y 模式临时英文查询返回前不再绘制占位页
中文模式下用 Y 触发临时英文时,PrepareCandidateList 会先发布一个只有原样输入
这一行的占位页并请求显示,英文词典的结果回来后再换成完整一页。每次按键卡片都
先缩成一行再撑开,高度明显闪动;缩小幅度远超 30%,会话内只增不减的规则也拦不
住。英文模式此前只处理了「页面为空」这一种占位形态。

改成由 PrepareCandidateList 在每次发出专用英文查询(英文模式、Y 模式)时标记
「回应未到」,ApplyEnglishCandidates 应用回应时清除,期间的显示请求交给回应去
触发。英文 worker 对非空输入一定会回应。造词和译文页状态下回应会被拒绝,不做
推迟;ClearState 也会清除标记。
2026-09-27 12:25:55 +08:00
Lu Fan(SonnyCalcr) cdd47db07e Merge pull request #486 from fanlusky/fix/tray-menu-corner-radius
fix: 托盘菜单项圆角与外框同心对齐,放宽 D2D 菜单间距
2026-09-27 11:40:11 +08:00
fanlusky 1348ca8eb7 fix(server): D2D 托盘菜单项圆角与外框同心对齐,放宽行高与内边距 2026-09-27 11:19:07 +08:00
fanlusky 42cff6de4a fix(server): WebView2 选词不再白等渲染同步超时
WebView2 下几乎每次空格/数字选词都等满渲染同步上限(日志 waited_ms 82-94,
rendered 恒比 current 小 1),体感为上屏顿一下。

每个按键 worker 会以相同内容发布两次页面:第一次走 content-only,回显在
异步 DOM 回调里;第二次命中 dedup,立即回显新代数。随后迟到的 DOM 回调把
回显改回旧代数,选词就一直等到超时。D2D 同步回显,不受影响。

- 渲染回显代数只进不退(AdvanceRenderedCandidateGeneration),迟到的旧回显
  不再覆盖新回显。
- folded-inflight 分支补上回显:进行中的 FineTune 只回显它捕获的那一页,
  之后折叠进来的页面此前永远没有回显。
2026-09-27 10:24:50 +08:00
fanlusky 7e4ffc2dcf fix(server): 状态提示窗层级置于候选框之下,避免打字时重叠闪烁
状态提示窗每次显示和跟随光标移动都用 HWND_TOPMOST 把自己提到置顶带最上层,打字时光标持续移动,会反复盖过候选框,两窗重叠处出现闪烁。改为插在候选框宿主窗口正下方;仅在两者处于同一 z-band 时这样做,否则保持原来的 HWND_TOPMOST / HWND_TOP。
2026-09-27 09:52:30 +08:00
fanlusky 6a172d448d fix(server): D2D 候选宿主窗口不再越过当前显示器工作区
卡片本身已经被夹在工作区内,但宿主窗口还带着阴影内边距和 64px 取整余量,贴近
右缘或下缘时会伸到相邻显示器上。DWM 分别应用窗口移动和交换链呈现,而且先后不
固定,某一帧可能把新卡片配上旧的宿主原点(或反过来),落在卡片外侧的内容就闪
到了旁边的屏幕上。诊断日志显示每次放置时卡片都在屏幕内(右缘 3837 < 3840),
但宿主一直延伸到 3927 左右。

宿主的宽高现在截到工作区的右缘和下缘,不管 DWM 怎么配对,都不会在光标所在显示器
之外画出任何东西。卡片完整保留,只裁掉伸出边缘的那部分阴影。移窗与呈现的先后
排序保留,它只决定哪种中间帧更常见。
2026-09-27 09:13:15 +08:00
fanlusky dcf1b6e2d3 fix(server): D2D 候选窗隐藏时清空交换链,避免重新显示时闪出上一张卡片
Hide() 只把宿主窗口 cloak 起来,交换链里还留着最后一帧。下次显示时先呈现、再
取消 cloak,但 DWM 可能在拿到新帧之前就先合成了已取消 cloak 的窗口,于是有一帧
在新位置画出了上一张卡片。上一张卡片较宽时,新的窄卡片贴屏幕右缘,位置更靠右,
旧画面就会越过边缘落到右侧扩展屏上。删光输入后重新打字、或上屏后开始下一次输入
时最容易出现。

隐藏前先呈现一帧透明画面,交换链里不再留下旧卡片。同时在 candidate-d2d place
诊断日志里补上最终宿主位置、先移动到的位置、卡片像素矩形和显示器工作区。
2026-09-27 09:06:49 +08:00
fanlusky 3a7beb18ab fix(server): 同一次输入内候选卡片只增不减
译文、云/AI 候选、混输英文等内容是陆续到达的,每一批都让卡片在两帧之间改变
宽高。每一帧本身画得完整,但尺寸来回变化看起来就是闪烁。

同一次组合内卡片只增大;自然尺寸低于保留尺寸的 70% 时(页面确实变小了)才跟着
收回,候选窗隐藏后重新开始。D2D 给 card 设置 MinWidth/MinHeight 后重新排版;
WebView2 在更新内容的同一个 JS 任务里给 #realContainer 和 #measureContainer
设置最小宽高,绘制前就是最终尺寸,原生区域测量的也是同一个盒子。两端的收回比例
常量互相注明了对应关系。
2026-09-27 09:00:47 +08:00
fanlusky 444b015a02 fix(server): 候选译文改为跨页缓存,换页时不再先丢后补
译文原先只是当前页的查询结果:每次按键换页、页面为空(英文模式两次按键之间)
或一次输入结束都会清空。新页第一帧因此不带任何译文,行宽先缩回去,查询返回后
再撑开,候选框在两帧之间来回改变大小。

现在 g_candidate_translation_glosses 按 TranslationIdentity 缓存,跨页、跨组合
保留,新页第一帧直接带上已知译文。上限 2048 条;关闭译文或翻译服务商开关变化
时清空;权威查询确认没有译文的词会从缓存中剔除。
2026-09-27 09:00:47 +08:00
fanlusky 5fc9485abf fix(server): D2D 候选窗按移动方向决定移窗与呈现的先后
DWM 分别合成窗口移动和交换链呈现,窗口可见时总有一帧只完成了其中一步。原先
总是先移窗后呈现:卡片在屏幕右缘变窄时要向右挪,上一帧更宽的画面会先出现在新
位置,越过屏幕边缘落到右侧扩展屏上,下一帧再弹回来。

现在按轴分别处理:向左或向上移动的轴先移窗再呈现,向右或向下移动的轴先呈现再
移窗,这样中间帧不会超出新旧两张卡片覆盖的范围。DPI override 提前到调整窗口
尺寸之前,因为 WM_SIZE 里已经会呈现一次。
2026-09-27 09:00:35 +08:00
fanlusky 7802777d06 fix(server): 英文模式查询返回前不再绘制空候选页
专用英文模式在词典查询返回前会发布一个空页。原先照常请求显示,卡片先缩成只剩
预编辑行,查询结果回来后再撑开,每次按键都会闪一下,D2D 与 WebView2 都受影响。

英文 worker 对非空输入一定会回应(无匹配时回退为原样输入),回应本身会请求显示,
因此查询进行中、页面为空时跳过这次显示请求。选词仍按已绘制的页面代次等待,
不会上屏未显示的内容。
2026-09-27 09:00:19 +08:00
fanlusky 57dd0fc2f3 fix(engine,server): 整句来源标签只挂在整句联想候选上
候选窗按 CandidateSource 挂〔Trigram〕/〔Unigram〕,但 Generated/Fallback 同时被
原样上屏兜底、英文模式原文、日期、Unicode、假名、译文义项等合成候选借用,导致
输入 E 等特殊模式前缀时原样候选被误标为〔Unigram〕。

WordItem 新增 sentence_association 标记,只由 Google 解码器整句(全拼、双拼)与
kenlm 词格整句置位;Server 挂标签时同时要求来源与标记。source 本身不变,落库与
学习策略不受影响。
2026-09-27 08:25:51 +08:00
fanlusky d852a05687 fix(windows,server): 选词回复超时不再上屏未显示的候选
电池模式下数字选词常上屏与所见不同的候选。链路是:TSF 只等 50ms
回复;Server 在回复前同步写调频,渲染同步 30ms 到点后按未绘制的页面
选词;超时后 TSF 断管重放按键,Server 又调一次频。

- server:选词调频(拼音/五笔/英文)改为 worker 队列任务,在回复之后、
  下一键之前执行,不再占用回复时间。
- server:渲染同步改为条件变量,绘制回显即唤醒;上限 30ms 提到 80ms。
- windows:上屏类回复等待 300ms;请求已送达仍超时时视为投递不确定,
  只重建前缀并丢弃上屏键,不再无条件重放。
- server:同一选择 3s 内在新连接上再次到达时跳过调频,避免重放重复计数。
2026-09-27 00:47:36 +08:00
fanlusky 7d66b2556c feat(server,ui,ui-html): 候选翻译文本支持在皮肤中单独配色
外部皮肤 skin.toml 的 [candidate.dark] / [candidate.light] 新增 translation 键。
不写时沿用候选文字色叠 62% 不透明度;写了就按原值绘制(含选中行)。
D2D(CandidateList::Appearance::translationColor)、WebView2 注入 CSS
和设置页外部皮肤预览三处一致;预览顺带补上此前漏掉的 number 配色。
2026-09-26 23:38:37 +08:00
fanlusky c539d0cf2a feat(server,ui-html): 设置窗口驻留时长可配置,驻留期间降低内存占用
- 关闭设置窗口后隐藏驻留时,把 WebView2 内存目标设为 LOW 并 TrySuspend 冻结页面;
  重新打开时恢复 NORMAL、Resume 后再显示。本机驻留 60 秒后整棵进程树的工作集
  由约 408 MB 降到约 62 MB,重开仍是瞬间恢复。
- 新增配置项 appearance.settings_window_linger,可选 off / 1m / 5m / 10m / 30m /
  60m / forever,默认 10m。off 表示关闭即退出,forever 表示一直驻留。
- 同步出厂模板、开发配置、ime_config 读写校验和两个设置宿主的快照字段与写入分发;
  设置页“外观 → 界面渲染”新增“设置窗口后台驻留”下拉框。
- 新增测试覆盖模板默认值、缺键、非法值、setter 拒绝和逐值往返。
2026-09-26 22:57:18 +08:00
fanlusky 14815e2f43 perf(server,ui-html): 设置窗口冷启动从约 2.6 秒缩短到约 0.7 秒
- 设置页虚拟主机名改为 imesettings.example / candidate-skins.example:
  不带点的单标签主机名会先走一轮网络名称解析,导航后要空等约 2 秒才开始加载。
- 写 WebView2 用户数据目录和配置目录的 ACL 前先检查:已有 Users 完全控制(可继承)
  和 Medium 标签时跳过 SetNamedSecurityInfoW,避免每次启动都把 ACE 传播到整棵缓存树
  (本机约 120 ms 降到 1.5 ms)。
- 先显示窗口和 splash,再准备 WebView2。
- 显示前最多等 150 ms 取得配置里的主题,浅色主题不再先闪一下深色。
- 新增 METASEQUOIA_IME_SETTINGS_TRACE=1 冷启动打点,写入
  %TEMP%\metasequoia-settings-startup.log。
2026-09-26 22:57:18 +08:00
fanlusky 030f2d4db8 feat(server): D2D 悬浮工具栏跟随候选窗皮肤配色
此前 D2D 工具栏只按明暗切换默认配色,不看皮肤。新增 floating_toolbar_skin,
按 ui-html/webview2/ftb/<skin>(_light).html 给出背景、边框、图标、悬停、分隔线、
拖动条颜色与圆角;外部皮肤沿用其 base 皮肤,因为 toolbar.css 只作用于 WebView2。
2026-09-26 21:49:04 +08:00
fanlusky 8278c065c9 feat(server,ui-html): 新增内置皮肤秋桂,杨柳青 D2D 高亮对齐 WebView2
秋桂 Autumn osmanthus(autumn_osmanthus):暗色底 #7D929F、高亮 #F97D0A,
亮色底 #D6ECF0、高亮 #FFE399。高亮内缩于外框,首项顶部两角与尾项底部两角
和外框同为 10px;预编辑行隐藏时首项才贴顶。WebView2 候选窗与工具栏、D2D
候选窗、设置页卡片与预览一并提供。

ApplyCandidateFrame 只隐藏多余的候选行,:last-child 找不到最后一个可见行,
因此为其加上 .last-visible,杨柳青的 CSS 同步改用它,候选不满一页时尾项的
补边与贴边高亮才会生效。

杨柳青在 D2D 下改为直角、左右贴边的整行高亮,只在外框四角处用 9px 贴合,
行距、行内边距与预编辑行边距按 CSS 对齐。
2026-09-26 21:49:04 +08:00
Lu Fan(SonnyCalcr) 4375d80414 feat: 支持导入自定义辅助码,内置“加加”更名为“拼音加加” (#477)
* feat(engine): 支持 helpcodes/custom 下的自定义辅助码

custom 目录里的每个 .txt 都是一套方案,方案名为 custom/<文件名>。
文件开头的 "# name:" / "# name_en:" 注释给出中英文显示名,缺省用文件名;
兼容 BOM、CRLF 和全角冒号。方案名不能跳出 custom 目录。

* feat(engine): WebView 契约新增 openHelpcodeDirectory

设置页请求打开自定义辅助码目录,仅允许 settings 页发送。同步 ui-html/webview2/shared。

* feat(server): 设置页列出自定义辅助码并可打开其目录

配置快照带上 helpcode.custom_schemas 与 helpcode.custom_directory,每次都会重新扫描;
方案文件被删除或改名后回退到 lantian。两个设置宿主都会处理 openHelpcodeDirectory。

* feat(ui-html): 辅助码设置显示拼音加加与自定义方案

内置方案“加加”改名为“拼音加加”。自定义方案追加在内置方案之后;新增说明区,
提供“打开目录”和“刷新方案”两个按钮,刷新完成后弹出提示。

* feat(installer): 升级与迁移数据目录时保留自定义辅助码

升级清理时只清掉 helpcodes 里的内置表,保留 custom 子目录;更换数据目录时一并迁移;
安装时建出 helpcodes\custom。
2026-09-26 20:21:55 +08:00
fanlusky b3b3b3303c feat(windows): 焦点切换到另一个输入框时提示当前中英文状态
新增开关 general.caret_state_indicator_on_focus(默认关闭,需同时开启光标
状态提示)。

- DLL 把以下情况视为“换了输入框”:焦点窗口变化(Win32 编辑框、对话框、
  其他应用各有自己的窗口);焦点经过一段没有可编辑文档的间隙后落到可
  编辑文档上(Chromium、WPF 等单窗口宿主在字段之间就是先失焦再聚焦);
  焦点会话真正结束后重新开始。Chromium 打字时在两个可编辑文档之间直接
  替换文档管理器,不会触发。从 Win+. 面板返回不计入;线程焦点在真正的
  应用切换后返回计入。一次切换的多次回调由定时器合并为一次提示;组字
  过程中、以及没有光标的控件都保持静默。每次判断在 TSF 诊断日志里记录
  判定标志,不含任何文本
- 协议复用 IMESwitch:wch 改为 FanyImeCaretStateTrigger(UserToggle /
  CapsLockEdge / FocusEntered),CapsLockEdge 保持首发时的 VK_CAPITAL
  取值,Server 对未知值按 UserToggle 处理
- Server 在开关打开时显示当前的有效模式(Caps Lock 开启时为“英”);
  配置项同步写入两份模板和两个设置窗口宿主的读写映射
- 设置页在光标状态提示卡片中增加“切换输入框时提示”开关
2026-09-26 17:09:34 +08:00
fanlusky d99aaa0d7d feat(server): 光标状态提示补全左下方、正下方、右下方
提示位置原先只有左上方、正上方、右上方和“下方”四种,且“下方”在设置页
预览里居中显示,Server 却把它放在光标左侧。

- 新增 bottom-left(左下方)与 bottom-right(右下方)
- bottom 保留原配置值、改名为“正下方”,并改为与 top 对称地居中,
  使运行时位置与预览一致;已选“下方”的用户升级后不会被重置
- 合法位置与“是否在光标下方”的判断集中到 caret_state_indicator_policy.h,
  配置读取、写入与定位共用一份定义
- 同步两份配置模板的注释、设置页下拉项、aria 文案与预览样式,并补充测试
2026-09-26 16:00:59 +08:00
fanlusky 597d087580 fix(server): 光标状态提示仅由用户快捷键触发并收紧协议边界
- DLL 只在 Shift/Ctrl 切换、Shift+Space、Ctrl+.、Caps Lock 与
  Ctrl+Shift+F 处显式请求提示;Server 下发的模式同步、宿主转换模式
  同步和激活初始化不再弹出提示,移除 compartment 回调里的推断逻辑
- 恢复文档切换时候选窗的 OnKillThreadFocus,改用 COM 身份比较
- 能力位改名为 CaretStateIndicator,DLL 仅在 Server 确认后发送切换
  事件;Server 端这些事件只驱动提示,不再改写工具栏或状态快照
- 标点提示的中英模式由 DLL 随 PuncSwitch 携带
- UILess 宿主不显示提示,会话转为 UILess 时隐藏已显示的提示
- Ctrl+Shift+F 的锚点就地读取,不再覆盖 Global::Point,且只采信
  协商了该能力的客户端
- 撤回对所有包默认 point 的修改;提示窗口创建失败不再阻断 Server
  启动;提示内容改为结构化 CaretStateBadge
- Caps Lock 在同一按键重复进入 TestKeyDown 时只处理一次
2026-09-26 13:58:22 +08:00
fanlusky dc7cfa0354 Merge branch 'develop' into feat/issue-229-ime-status-indicator 2026-09-26 13:58:03 +08:00
Jia Sui 6506a7a07f fix(server): 退格删空 raw 不再吞掉已选词
输入 nihaoya、选「你好」、再敲一个字符后连按退格,删掉最后一个 raw 字符
的那一次会把造词态连同已选词一起清掉(尾部「raw 空即清」规则),用户看到
「你好」被直接删掉。这条规则的前提是「客户端会随空 raw 取消组合」,但协商
过 CompositionRestored 的客户端会应用帧、把组合留在「只有已选词」的状态
(与 R3 段删除一致),此时清态反而丢字。

- 新增 ShouldKeepCreatingWordAfterRawEmptied:仅协商过组合恢复、非 UILess
  且有快照时保留造词态与快照;普通退格与 Ctrl+Backspace 段删除共用该谓词。
- 普通退格删空 raw 时置 keep:帧里 word 非空,DLL 走
  _ApplyCreatingWordPayload 的「只有已选词」分支继续组合,不再 _HandleCancel;
  下一次退格由「raw 为空时退格只能是撤销」把最近一次选词撤回,而不是丢整句。
- 未协商与 UILess 客户端维持原行为(本地取消 + 清态),避免两侧状态分叉。
- 测试补 deleting_the_last_raw_character_keeps_the_created_word。
2026-09-25 15:59:05 +08:00
Jia Sui 9aba1a44ea refactor(server): 组合恢复帧统一走候选页发布助手
组合恢复帧的候选发布此前只在注释里按「退格撤销」解释,但条件挂在
composition_restored 上,实际覆盖的是每一帧:退格撤销、Ctrl+Backspace
段删除、Ctrl+方向段跳转。段删除与段跳转同样改了 raw / 光标前缀,旧代码
只发帧不重建页面,页面上留着按键前的候选与选择。

- 把发布逻辑抽成 PublishRestoredCompositionCandidates,注释写明它服务
  的是「每一帧组合恢复」而不是只有撤销,以及两条收起规则(raw 为空、
  前缀为空)各自的理由;抽取不改行为。
- 一次性高亮的消费随之落到助手内部,仍然每帧消费一次,不跨帧泄漏。
2026-09-25 09:46:05 +08:00
Jia Sui a596f8b1bc fix(server): 撤销重高亮只在候选页前缀未变时应用
选词快照记的候选绝对位置对应的是选词那一刻用户眼前的候选页,而撤销
重建的页面由恢复后的 raw 与光标前缀决定:选词之后用 Del 或光标编辑改
动过后缀时,重建页装着另一批候选,旧位置会把高亮落到不相干的项上。

- 新增 FanyImeIpc::NormalizeCandidatePagePrefix,按引擎的页身份把光标
  前缀折成小写(refresh_prefix_candidates 的同一套归一化)并夹到 raw 长度。
- 快照多带一个 selected_page_prefix,取在 advance_composition_after_selection
  之前(那之后引擎已经换成结算后的 raw,不再是用户看到的那一页)。
- 一次性交接从裸 int 改成 RestoredSelectionHighlight(位置 + 页前缀),
  HandleImeKey 重建页面后先比较前缀再套用位置,不一致就只发布新页不高亮。
- 测试更新位置交接用例并覆盖归一化谓词的空串、越界、大小写与改短后缀。
2026-09-25 09:43:38 +08:00
Jia Sui eb6c6f84ce fix(server): 造词态被退格清空时也回帧,避免 TSF 挂起烧满超时
答帧判定用的是按键之后的 creating_word.active,而退格删掉最后一个
raw 字符时尾部会清掉造词态与快照,形态随之消失:这样一次退格不发
帧,而 DLL 的 hold 只看按键前的镜像 word_for_creating_word,仍然
非空,于是白等满 50ms 才回退本地删除,正是本次改动想消除的那类等待。

- HandleImeKey 在 ApplyCompositionEditKey 之前抓一份客户端可见的
  形态,答帧条件改为「撤销过 或 按键前形态 或 按键后形态」,用新的
  ShouldAnswerRetreatBackspace 把这条不变量写成可测谓词。
- 该结局的 payload 是空 raw + 空 word,DLL 侧按 _ApplyCreatingWordPayload
  的空 word 分支立即 _HandleCancel,语义与超时后一致,只是不再等。
- 候选窗收起的判据从「raw 空且造词态还在」放宽为「raw 空」:造词态
  被这次退格终结时同样不能落入重建分支,否则会给空输入发布兜底假候选。
- 测试补 retreat_backspace_reply_survives_the_key_that_ends_the_word。
2026-09-25 09:37:57 +08:00
Jia Sui c06dc17ae1 feat(server,windows): 造词中的退格优先撤销最近一次选词
把"退格撤销上次选词"从旧的两条窄触发(剩余输入只剩一个字符、或光标
正好停在这个词后面)放宽为造词态下的默认行为,对齐 Rime/微信:只要
造词还活着且有可选回的快照,首个退格先撤销最近一次选词,剩余输入
长度与光标位置不再参与判定。

- 选后编辑锁(selected_before_editing):选词后又输入了新字符,退格
  继续删除这些新输入,而不是把选词从脚下抽走;只标记最新快照,与
  Rime 一致。空输入例外——没有字符可删时退格只能是撤销,否则会退回
  #35 的整句丢弃。
- 恢复改为前插:被选词消耗的拼音插回未删除的后缀之前,光标随恢复
  长度平移("输入变长、光标不动"),不再整串覆盖 raw 而丢掉后缀。
- 撤销后重新高亮:选词快照记下所选候选的绝对位置,撤销重建候选页
  后把高亮放回同一项(一次性消费),下次按键可直接改选,不用再找。
- 客户端契约拆成两层:DLL 只凭造词镜像(word_for_creating_word)
  挂起等待回复,看不到 Server 侧的快照与编辑锁,因此新增
  HasRetreatBackspaceShape 描述客户端可见形态,Server 对形态内每个
  退格都必须答帧(撤销、锁定删除或空历史空操作),否则挂起白烧满
  50ms 超时;ShouldRetreatCreatingWordSelection 只在真能改写状态时
  为真。
- 撤销改为纯状态操作,引擎序列、候选重建与光标前缀由尾部统一执行
  一次,替换原先每次撤销都额外跑一遍完整候选查询的 helper。
- DLL 侧:已消耗请求槽位的回复(或光标 0 处的 Normal 应答)之后
  丢弃 request id,让 worker 回退本地读取,而不是为已消失的槽位
  再烧一个 50ms 超时。

Signed-off-by: Jia Sui <jsfaint@gmail.com>
2026-09-24 20:52:12 +08:00
Lu Fan(SonnyCalcr) 2755c77192 Merge pull request #466 from fanlusky/feat/neural-sentence-association
feat: 神经网络整句联想与多来源整句开关
2026-09-24 14:53:55 +08:00
Jia Sui eac9fa72b5 feat(server): 固定排位候选词显示可配置的徽标
固定排位徽标此前硬编码为 📌,嫌吵的用户既关不掉也换不了。
新增两个配置项:candidate_fixed_badge 总开关与 candidate_fixed_badge_style 样式枚举(paperclip/pushpin/dot),设置页外观分组提供开关与下拉,改动后立即刷新候选页。
配置存枚举名而非字面 emoji,TOML 与日志里不出现图形字符;展示层白名单与配置层同宽,手改 TOML 写错只会退成无徽标。默认样式改为 📎,比实心红钉安静。
2026-09-24 14:32:10 +08:00
fanlusky 16877cb2d0 feat(server): 整句联想来源开关接入配置与设置页
新增 [association] 段的五个开关(词格、Google、神经 desktop / keyboard、去重补位),
同步写入出厂模板、工作配置和两个设置窗口宿主的读写映射,设置页输入分区加对应选项。
2026-09-24 14:03:43 +08:00
fanlusky 3929ec482b fix(server): 前缀选词后同步剩余 raw,避免光标被挪到串尾
HandleImeKey 在选词之前写入 raw_input_with_cases,前缀选词把 caret 归 0 后
它仍是选词前的整串,下一次编辑键里「raw 变了且 caret==0 → 光标移到串尾」的
判定误触发:DLL 光标停在后缀首,Server 跳到串尾,之后的插入与移动两侧分叉。
组合继续时统一同步剩余 raw;串尾造词流不受影响(caret 仍被夹到新串尾)。
2026-09-24 12:54:31 +08:00
Jia Sui 3821f3ede8 fix(server): 补回合并时丢失的测试用例花括号 2026-09-24 09:04:27 +08:00
Jia Sui 5b0be4e71d Merge branch 'develop' into feat/caret-driven-resegmentation 2026-09-24 08:45:54 +08:00