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

7.4 KiB
Raw Blame History

新增功能模块 / 模块重构

本记录描述通用输入系统阶段 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 宣称完整键盘可访问性。

落地信息