Files
StellarX-kaifa/开发记录/模块/Module-20261009-0007-阶段3键盘定向路由与Button接入实现.md

14 KiB
Raw Permalink Blame History

新增功能模块 / 模块重构

本记录描述通用输入系统阶段 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 安全白名单,因此原生按键仍按默认策略采集并转发。

关键契约

  1. NativeInputQueue 是生产键盘业务源;EasyX EX_KEY 仅排空和观察。
  2. Routed 与 LegacyCompatibility 互斥。Legacy 仅供测试在事件循环锁定前选择,不能运行中切换,也不是公开兼容开关。
  3. 每次只从队列弹出一条事件,释放队列锁后才进入焦点、控件和用户回调;不得预取整批事件。
  4. 路由开始时构造原始身份路径,之后逐节点重新解析。早期回调删除的后续节点跳过;回调改变焦点只影响下一事件。
  5. stopBubble 与 preventFrameworkDefault 独立;二者都不能回溯改变已经完成的 WndProc 返回值。
  6. 原始 VK_PROCESSKEY 是单键 IME 所有权威;imeComposingAtCapture 是独立生命周期轴。两者任一命中时,阶段 3 不执行控件、Tab 或默认动作。
  7. 激活门控同时比较事件采集快照和当前原子 generation;窗口重新激活不能让旧周期积压键重新生效。
  8. Button Space 在 KeyUp 确认,Enter 在首次 KeyDown 确认;命令阻止框架默认动作但继续允许祖先观察。
  9. WndProc WM_GETDLGCODE 只在 Routed 普通 Tab 查询时增加 DLGC_WANTTAB;System/Alt+Tab 不认领。
  10. 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.h
  • include/StellarX/InputEvents.h
  • include/StellarX/Control.h / src/StellarX/Control.cpp
  • include/StellarX/FocusManager.h / src/StellarX/FocusManager.cpp
  • include/StellarX/Button.h / src/StellarX/Button.cpp
  • include/StellarX/Window.h / src/StellarX/Window.cpp
  • src/StellarX/KeyboardRouter.cpp
  • src/StellarX/Dialog.cpp
  • src/StellarX/SxNativeInput.cpp
  • src/StellarX/internal/SxKeyboardRouterInternal.h
  • src/StellarX/internal/SxEventLoopInternal.h
  • src/StellarX/internal/SxNativeInputInternal.h
  • Canvas、Table、TabControl 对应递归分发实现
  • tests/StellarXTests.cpp
  • tests/UiFixture.cpp
  • scripts/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-18728
    • TestResults/Stage3CutoverAB/legacy-expanded/20261009-153941-779-34800
    • TestResults/Stage3CutoverAB/routed-formal/20261009-153745-045-33364
    • TestResults/Stage3CutoverAB/routed-expanded/20261009-154044-849-12588
  • Routed 首轮 ownership 为 1330 ms,其余四次 531–601 ms,与 Legacy 517–709 ms 重叠;其它场景平均差为 -19 ms 至 +43 ms。未观察到稳定耗时劣化,但仍承认交互桌面存在残余偶发波动。

Routed 正式 UI

  • 连续两轮九个业务场景共 18/18;普通/模态原生关闭检查每轮通过。
  • keyboard-routing 2/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 全部编译/链接成功。
  • 扩展 focus UI 连续 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 和既有鼠标命中/分发保持不变。
  • 新增虚接口再次改变 Control vtable;已编译的第三方派生控件必须使用当前头文件和 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 接入,但本记录不提前宣称这些能力已经实现。

落地信息