14 KiB
14 KiB
新增功能模块 / 模块重构
本记录描述通用输入系统阶段 3 的实际实现、生产换轨边界与验证证据。阶段 3 完成 KeyEvent 路由和 Button 接入,但不代表 TextBox 文本编辑、真实 Dialog FocusScope 或复合控件高级键盘行为已经完成。
基本信息
- 模块 ID:Module-20261009-0007
- 模块名称:阶段 3 键盘定向路由、Button 接入与 EX_KEY 生产换轨
- 状态:已完成;实现、自动回归、微软拼音/搜狗及 KEY1/KEY5 人工门槛全部关闭
- 类型:新增模块 / 架构演进 / 兼容性迁移
- 所属系统 / 子系统:StellarX Core / Window / Native Input / Focus / Keyboard Routing / Button
- 版本 / 分支:
master,实现提交29df994 - 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + MBCS 工程配置
- 负责人:Codex 协作实现
背景与目标
- 阶段 1 已建立 WndProc 采集、规范化和跨线程 NativeInputQueue;阶段 2 已建立稳定身份焦点树和 FocusScope 机制。
- 阶段 3 要把
KeyEvent从当前逻辑焦点叶子沿逻辑父链定向冒泡,并接入 Tab、Button Enter/Space、窗口激活与 IME 所有权门控。 - 最终切片必须一次性停止旧
EX_KEY -> Control::handleEvent()生产交付,同时继续排空 EasyX 队列,避免新旧业务源并存或内部队列累积。
实现边界
已实现
- 公共
RoutedKeyEvent、KeyRouteResult和Control::handleKeyEvent()默认空实现。 - 内部
KeyboardRouter:固定容量原始身份路径、逐节点重新解析、叶子到祖先冒泡、独立的停止冒泡与阻止框架默认动作结果位。 - 单事件 mutation transaction 与可嵌套
managedDispatchActive边界;用户回调后不保留裸Control*。 - 采集侧原子
{ keyboardFocusActive, generation }快照;失活、跨激活代次、IME 所有和 Alt/System 输入在业务路由前被抑制。 - 失活和模态入口沿当前焦点路径取消 keyboard armed 等瞬时状态,但保留逻辑焦点。
- Tab / Shift+Tab 冒泡后默认动作:每次按键重新采集活动 FocusScope 的当前焦点树,支持循环、无焦点首/末项与
preventFrameworkDefault。 - Button 独立
keyboardArmed:Space 首次 KeyDown 按下、KeyUp 确认;Enter 首次 KeyDown 确认;自动重复不重复激活。 - Escape、焦点丢失、模式变化、隐藏、窗口失活和模态隔离取消 armed;按压色与阶段 2 焦点框叠加。
- 生产默认改为 Routed;Window 主循环和 Dialog 模态循环同时继续排空
EX_KEY,但不再交给handleEvent()。 easyXKeyboardMessagesDrained与legacyEasyXKeyboardBusinessDeliveries诊断计数,用于证明排空持续且旧业务为零。WM_GETDLGCODE窄同步分支:先取 EasyX 旧窗口过程结果,只对明确普通 Tab 查询合并DLGC_WANTTAB。- UI 运行器可指定 fixture、结果根目录和 Legacy/Routed 预期,并输出每场景结构化耗时
run-summary.json。 - 生产端到端
keyboard-routing场景覆盖 Space 激活首按钮、Tab 换焦和 Enter 激活第二按钮。 - Window 背景与非模态 Canvas 空白左键清焦点并消费,避免焦点框残留和重叠兄弟穿透。
- Table 内部分页 Button 焦点变化提升为 Table 整体重绘;同父 Canvas 的下层持久 coverage 命中上层时,上层完整恢复优先于局部脏子树提交。
明确未实现
TextInputEvent/CompositionEvent向 TextBox 的正式业务交付;当前仍只排空、观察和计数。- TextBox UTF-16 编辑内核、自绘、caret、选区、剪贴板和 IME anchor。
- Table 方向键/翻页命令、TabControl roving-tab-stop 和方向键切页。
- 真实 Dialog FocusScope、模态 Dialog 子树键盘路由、默认按钮和 Escape 关闭;当前模态循环抑制底层 Window 业务路由。
DLGC_WANTALLKEYS、DLGC_WANTCHARS、DLGC_WANTARROWS;没有相应功能前不提前声明。- WndProc 生产消费白名单;阶段 0B 没有给出跨 IME 安全白名单,因此原生按键仍按默认策略采集并转发。
关键契约
- NativeInputQueue 是生产键盘业务源;EasyX
EX_KEY仅排空和观察。 - Routed 与 LegacyCompatibility 互斥。Legacy 仅供测试在事件循环锁定前选择,不能运行中切换,也不是公开兼容开关。
- 每次只从队列弹出一条事件,释放队列锁后才进入焦点、控件和用户回调;不得预取整批事件。
- 路由开始时构造原始身份路径,之后逐节点重新解析。早期回调删除的后续节点跳过;回调改变焦点只影响下一事件。
stopBubble与preventFrameworkDefault独立;二者都不能回溯改变已经完成的 WndProc 返回值。- 原始
VK_PROCESSKEY是单键 IME 所有权威;imeComposingAtCapture是独立生命周期轴。两者任一命中时,阶段 3 不执行控件、Tab 或默认动作。 - 激活门控同时比较事件采集快照和当前原子 generation;窗口重新激活不能让旧周期积压键重新生效。
- Button Space 在 KeyUp 确认,Enter 在首次 KeyDown 确认;命令阻止框架默认动作但继续允许祖先观察。
- WndProc
WM_GETDLGCODE只在 Routed 普通 Tab 查询时增加DLGC_WANTTAB;System/Alt+Tab 不认领。 EX_MOUSE永久保留在Control::handleEvent();本次迁移只切断EX_KEY业务交付。
六个实现切片
切片 1:公共契约与互斥门控
- 新增 routed key 公共类型、Control 默认入口和内部 Legacy/Routed 模式。
- Window 与 Dialog 共用旧业务交付判定,测试 Routed 模式从第一天起就先关闭自己的旧 EX_KEY 业务路径,避免双投递。
切片 2:身份路由与 mutation
- 固定容量 256 的
FocusTargetId路径;循环、超深或构造期失效时在调用控件前 fail closed。 - Control、Canvas、Table、TabControl 提供递归身份分发;借用指针不逃逸当前调用。
切片 3:激活、IME 与模态门控
- NativeInputEvent 携带采集时激活快照。
VK_PROCESSKEY、标准组合生命周期内按键、Alt/System、失活、跨代和模态期底层输入不进入业务回调。- 失活与模态入口调用焦点路径暂停协议,保留逻辑焦点。
切片 4:Tab 默认动作
- 无 Ctrl 的 Tab KeyDown 在冒泡后执行;Shift 反向、边界循环、无焦点进入首/末项。
- 每键重新获取焦点树;Table 分页 Button 在首次 draw 前的空结果不缓存。
切片 5:Button 键盘状态机
keyboardArmed与鼠标/Toggle 的click分离,只在绘制层合并按压视觉。- Space/Enter、自动重复、Escape 和全部取消边界均由单测覆盖。
切片 6:生产原子换轨
Window::routedKeyboardPipeline生产默认改为 true;主循环和模态循环同时切断旧 EX_KEY 业务交付。- 新增窄
WM_GETDLGCODE分支、排空/旧业务计数、生产端到端 UI 场景、迁移说明和换轨前后 A/B。 TextInputEvent与CompositionEvent不提前交给控件,避免阶段 3 越界实现 TextBox。
实现文件
include/StellarX/KeyboardRouting.hinclude/StellarX/InputEvents.hinclude/StellarX/Control.h/src/StellarX/Control.cppinclude/StellarX/FocusManager.h/src/StellarX/FocusManager.cppinclude/StellarX/Button.h/src/StellarX/Button.cppinclude/StellarX/Window.h/src/StellarX/Window.cppsrc/StellarX/KeyboardRouter.cppsrc/StellarX/Dialog.cppsrc/StellarX/SxNativeInput.cppsrc/StellarX/internal/SxKeyboardRouterInternal.hsrc/StellarX/internal/SxEventLoopInternal.hsrc/StellarX/internal/SxNativeInputInternal.hCanvas、Table、TabControl对应递归分发实现tests/StellarXTests.cpptests/UiFixture.cppscripts/run-ui-tests.ps1
自动验证
逻辑与构建矩阵
- Debug x64:67/67。
- x64 ASan:67/67,无 sanitizer 报错。
- Release x64:67/67。
- Debug Win32:67/67。
- KEY1–KEY6:全部编译/链接成功。
- 覆盖路由顺序、结果位独立性、删除/改焦点重入、激活代次、IME/Alt/System/模态抑制、Tab 当前树、Button 状态机、Routed 默认/Legacy 锁定和窄
DLGC_WANTTAB。
换轨前后 UI A/B
- 换轨前 fixture:
TestResults/Stage3CutoverAB/baseline/StellarX.UiFixture.exe。 - 基线 SHA-256:
3F6267B6D5EE85A7A97244C3DAE21DB44CEFBD185F56A38516E5E2F6F6234974。 - 共同八场景执行 5 对 5:Legacy 40/40、Routed 40/40。
- 结构化证据:
TestResults/Stage3CutoverAB/legacy/20261009-152906-951-18728TestResults/Stage3CutoverAB/legacy-expanded/20261009-153941-779-34800TestResults/Stage3CutoverAB/routed-formal/20261009-153745-045-33364TestResults/Stage3CutoverAB/routed-expanded/20261009-154044-849-12588
- Routed 首轮 ownership 为
1330 ms,其余四次531–601 ms,与 Legacy517–709 ms重叠;其它场景平均差为-19 ms至+43 ms。未观察到稳定耗时劣化,但仍承认交互桌面存在残余偶发波动。
Routed 正式 UI
- 连续两轮九个业务场景共 18/18;普通/模态原生关闭检查每轮通过。
keyboard-routing2/2:首 Button Space 激活一次,Tab 聚焦第二 Button,Enter 激活一次。input-shadow两轮一致:- Routed 4000/4000,Down/Up 各 2000;
- EasyX
EX_KEY排空 4000; - 旧 EX_KEY 业务交付 0;
- pushed/popped 4000/4000、depth 0、maximumDepth 40;
- sequence 严格、零 fault、零规范化异常、内存增长 0。
- 最初受限桌面运行因
MoveMouseToScreen返回 Access denied,未进入产品断言;授权交互桌面后全部重跑,失败样本不计作产品回归。 - PowerShell 7 宿主同时注入
PATH/Path时,旧test.ps1会让 MSBuild 在创建CL.exe前以MSB6001失败。运行器已改为只在检测到重复键时通过cmd子进程删除大写PATH、保留同值Path;不修改调用者环境。修复后 PowerShell 7 与 Windows PowerShell 5.1 的 Debug x64 均为 66/66。
人工反馈修复自动复验
- 新增
CanvasBackgroundPointerDownClearsFocusAndConsumes后,四配置单测均为 67/67,KEY1–KEY6 全部编译/链接成功。 - 扩展
focusUI 连续 3/3 通过,证据位于TestResults/ui/20261009-220625-941-15096;覆盖 Table 分页焦点框即时出现/消失、Window/Canvas 空白清焦点和 KEY5 同批上下层重绘。 - 同一 fixture 的完整 UI 1/1、九个业务场景 9/9 通过,证据位于
TestResults/ui/20261009-220940-941-31488;Tooltip、Dialog、input-shadow 与 keyboard-routing 均通过。 - 移除 UiFixture 中用于定位问题的临时 Button JSON 诊断字段后,用户从项目终端重新执行最终源码完整 UI。证据位于
TestResults/ui/20261009-223506-755-18932:fixture SHA-256 为F439C0636CD6B979A47C4594BC5AB3728F6342F49602FE43078435011A877793,与run-summary.json一致;九个业务场景 9/9 通过,普通/模态原生关闭均通过。 - 自动化过程中曾分别出现一次沙箱
SendInput Access denied、一次 Button 点击未命中和一次 ownership 等待超时;失败证据保留。随后定向 3/3 与完整 UI 通过只说明未形成稳定产品回归,不把这些桌面输入波动写成从未发生。
兼容性与迁移
Control::handleEvent()仍保留,但生产循环不再向其交付EX_KEY;第三方源码控件必须把键盘逻辑迁到handleKeyEvent()。EX_MOUSE和既有鼠标命中/分发保持不变。- 新增虚接口再次改变
Controlvtable;已编译的第三方派生控件必须使用当前头文件和 Core 库重新编译。 - 默认
handleKeyEvent()为空,因此未迁移的第三方控件不会崩溃,但其旧键盘能力不会继续生效。 - 详细迁移示例见
../MIGRATION.md。
人工门槛与后续
- 微软拼音与搜狗已分别完成真实候选输入复验:候选出现/取消和 Space 交互正常,未报告卡顿、重复提交或输入法按键误触发 Button。
- Button Space/Enter 行为已由人工确认符合锁定语义:Space 按下显示按压态、松开执行;Enter 首次按下立即执行且不显示持续按压态。
- KEY1/KEY5 人工反馈暴露三项问题:Table 分页焦点框延迟、Window/Canvas 空白点击不清焦点、同父 Canvas 下层重绘覆盖 D-2 上层区域;对应实现和自动回归已补齐。
- 模态 Dialog Tab 不可用和非模态作用域顺序属于阶段 4 真实 Dialog FocusScope 范围,本轮不做临时补丁。
- 用户已定向复测 KEY1 与 KEY5,暂未发现问题:Table 分页焦点框即时出现/消失,Window/Canvas 空白点击清焦点,KEY5 D-1 → D-2 未再发生覆盖。
- 阶段 3 验收至此关闭;后续可单独讨论阶段 4 的 Table、TabControl 和真实 Dialog FocusScope 接入,但本记录不提前宣称这些能力已经实现。
落地信息
- 关联功能变更 ID:无
- 关联 BUG / Fix:
BUG-20261009-0001/Fix-BUG-20261009-0001;BUG-20261009-0002/Fix-BUG-20261009-0002 - Commit:
29df994(阶段 3 键盘路由、生产换轨与焦点重绘收口) - PR:无
- 发布版本:未发布
- 相关文档: