# 新增功能模块 / 模块重构 > 本记录描述通用输入系统阶段 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.h` - `src/StellarX/FocusManager.cpp` - `include/StellarX/CoreTypes.h` - `include/StellarX/Control.h` / `src/StellarX/Control.cpp` - `include/StellarX/Window.h` / `src/StellarX/Window.cpp` - `include/StellarX/Canvas.h` / `src/StellarX/Canvas.cpp` - `Button`、`TextBox`、`Table`、`TabControl`、`Dialog` 对应实现文件 - `tests/StellarXTests.cpp` - `tests/UiFixture.cpp` - `scripts/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:全部编译/链接成功。 - 最终源码的独立 `focus` UI 连续 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`](./Module-20261001-0001-通用输入焦点与键盘路由系统设计.md)