7.4 KiB
7.4 KiB
新增功能模块 / 模块重构
本记录描述通用输入系统阶段 2 的实际实现边界。阶段 2 建立逻辑焦点地基,但不切换键盘业务轨道。
基本信息
- 模块 ID:Module-20261008-0006
- 模块名称:阶段 2 通用焦点管理器与复合控件焦点树
- 状态:实现及回归归因均已关闭,可以进入阶段 3
- 类型:新增模块 / 架构演进
- 所属系统 / 子系统:StellarX Core / Window / Control / Focus
- 版本 / 分支:
master,未提交工作区 - 环境:Windows + Visual Studio 2022 + MSVC v143 + EasyX + MBCS 工程配置
- 负责人:Codex 协作实现
背景与目标
- 阶段 1 已建立跨线程输入采集和观察队列,但没有“哪个控件持有逻辑焦点”的统一模型。
- 焦点不能成为 TextBox 私有状态;Button、TextBox、Table 分页 Button、TabControl 页签和活动页面必须进入同一递归焦点树。
- 回调允许删除控件、切页、修改可见性和打开更高作用域,因此焦点切换必须与 mutation transaction、稳定身份和重入保护一起实现。
模块边界
已实现
FocusTargetId { ControlHandle owner, uint32_t subNode };阶段 2 现有节点全部subNode == 0。- 每 Window 一个
FocusManager,公开程序化申请、清焦点、查询 active、Tab stop 快照和 FocusScope 机制。 Control瞬时焦点快照协议:逻辑父级、policy、effective visible、动态 enabled、tabStop 和 tabIndex。- Window、Canvas、Table、TabControl 的递归解析、收集和焦点状态提交。
- Button/TextBox 默认支持 Programmatic、Pointer、Tab;PointerDown 申请焦点并绘制焦点框。
- Table 复用真实 prev/next Button 句柄,按当前页和总页数动态过滤首尾分页资格。
- TabControl 复用真实页签 Button,非活动页面通过 effective visibility 退出焦点树。
- pending removal 在入队时立即使焦点子树失效;批量 clear 先批量失效再寻找后备。
- FocusScope push/pop、外层焦点恢复和回调中 push 的稳定恢复身份。
明确未实现
- Tab/Shift+Tab 键盘遍历、KeyboardRouter 和祖先冒泡。
EX_KEY业务换轨;阶段 1 的旧Control::handleEvent(EX_KEY)兼容旁路保持不变。- Button 的 Enter/Space 键盘语义、Table 键盘翻页、TabControl roving-tab-stop 与方向键切换。
- 真实 Dialog 打开/关闭与 FocusScope 的绑定;阶段 2 只实现并测试作用域机制。
- TextBox UTF-16 编辑内核、caret、选区、剪贴板和 IME anchor。
- 窗口激活态对键盘路由的门控;当前只保留逻辑焦点,待阶段 3 使用阶段 1 的 WindowActivationEvent。
关键设计
身份与快照
- 当前
ControlHandle为进程内单调递增且不复用的uint64_t,不是 slot/generation 句柄。 - FocusManager 长期只保存
FocusTargetId、FocusScope 条目和待失效根句柄,不保存裸指针。 Window::findControl()只查直接子级,焦点系统使用独立递归协议。- 快照只在当前调用中有效;每次用户回调之后重新解析并重拉 visible/enabled/focusable/tabStop。
重入事务
- 普通
requestFocus()立即在 mutation transaction 内执行。 - 焦点回调中的请求进入单槽 pending,后一次覆盖前一次;回合结束只验证最终请求。
- pending 最终请求失效时丢弃,不恢复被覆盖请求。
- active 焦点失效独立执行“下一个 Tab stop → 上一个 → initialFocus → 无焦点”回退。
- 单次外层切换最多 64 回合;超限返回
FocusError::ReentrancyLimit,清空 pending,保留最后一个仍有效的已提交焦点。 - FocusLost 回调中 push scope 时,以正在丢失的旧目标作为恢复点;FocusGained 回调中以新目标作为恢复点。
mutation 与复合控件
- remove/clear 一经入队,FocusManager 立即把对应子树视为无效,即使对象尚未物理释放。
clearControls()先收集并批量标记所有现有根,避免回退到同批待删兄弟。- Canvas 可见性改变在整棵子树状态更新后通知 FocusManager。
- Table 分页 Button 在首次 draw 后创建,真实 UI fixture 验证第一页只有 next 进入 Tab stop。
- TabControl 页签 Button 始终使用真实句柄;只有活动页面的后代进入焦点树。
实现文件
include/StellarX/FocusManager.hsrc/StellarX/FocusManager.cppinclude/StellarX/CoreTypes.hinclude/StellarX/Control.h/src/StellarX/Control.cppinclude/StellarX/Window.h/src/StellarX/Window.cppinclude/StellarX/Canvas.h/src/StellarX/Canvas.cppButton、TextBox、Table、TabControl、Dialog对应实现文件tests/StellarXTests.cpptests/UiFixture.cppscripts/run-ui-tests.ps1
测试与验证
逻辑测试
- 嵌套 Canvas 递归解析与稳定目标身份。
- Button/TextBox PointerDown 焦点切换。
- FocusLost 删除/禁用新目标后的完整快照复验。
- FocusGained 删除自身后的 active 回退。
- 单槽 latest-wins 不复活旧请求。
- FocusGained 与 FocusLost 回调中 push scope,pop 后恢复真实外层目标。
- 64 轮 A/B 重入循环返回
ReentrancyLimit。 - active removal 下一个 Tab stop 回退和 clear 批量失效。
- TabControl 真实页签句柄、
subNode == 0和非活动页面过滤。
自动结果
- Debug x64:40/40。
- x64 ASan:40/40,无 sanitizer 报错。
- Release x64:40/40。
- Debug Win32:40/40。
- KEY1–KEY6:全部编译/链接成功。
- 最终源码的独立
focusUI 连续 3 轮通过,包含 Button/TextBox 焦点框像素、Table 分页资格、TabControl 活动页过滤和 subNode 检查。 - 初次完整 UI 两次复跑分别在既有 Tooltip 像素等待和 geometry 点击结果等待处超时,因此追加阶段 1 基线
81341fa与阶段 2 工作树的受控 A/B,而不以场景执行顺序直接排除焦点影响。 - 受控 A/B 共 62/62 个结构化样本通过:geometry、tooltip 两端各累计 8/8,共有七场景两端各 3/3;当前阶段 2 正式完整 UI 八场景再执行三轮,共 24/24 通过,未发现阶段 2 相关失败或耗时劣化。
- 裁决为排除阶段 2 稳定引入的隐性回归;此前超时定性为间歇性桌面 fixture / 输入时序波动,关闭阶段门槛不等于证明未来永不发生残余偶发波动。
- A/B 运行器首次把成功进程的
ExitCode=0误读为空值,已修正并重新构建、重跑;原误报保留,并在TestResults/Stage2RegressionAB/20261008-120909-362/manifest.json中登记。
兼容性与风险
- 新增
Control虚接口会改变 vtable;二进制派生控件必须重新编译。StellarX 以源码方式使用时,正常全量重编译即可。 - 没有删除既有公开接口,没有停止
EX_KEY旧入口,阶段 2 不构成键盘迁移点。 - Table 内部按钮仍只在首次 draw 后存在;在 draw 前收集焦点树不会返回它们,这是当前生命周期事实。
- Button/TextBox 焦点框是阶段 2 的最小视觉反馈,最终主题化样式可在后续阶段增强。
- 真实 Dialog scope、窗口激活门控和键盘导航仍未接入,不能仅凭阶段 2 宣称完整键盘可访问性。
落地信息
- 关联功能变更 ID:无
- 关联 BUG / Fix:无
- Commit:待提交
- PR:无
- 发布版本:未发布
- 相关文档:
Module-20261001-0001-通用输入焦点与键盘路由系统设计.md