127 lines
7.4 KiB
Markdown
127 lines
7.4 KiB
Markdown
# 新增功能模块 / 模块重构
|
||
|
||
> 本记录描述通用输入系统阶段 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)
|