Files
StellarX-kaifa/开发记录/模块/Module-20261008-0006-阶段2通用焦点管理器实现.md

127 lines
7.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 新增功能模块 / 模块重构
> 本记录描述通用输入系统阶段 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)