范围:Screen 层的接线(创建两棵树、转发输入、编写渲染顺序)与所有权语义的改动。涉及 root/overlays/interactionManager 的归属、渲染与输入流水线的步骤与方向差异、所有权约定的波及面。
前置:项目概述。
所有权模型
FluentScreen 基类并不持有 root、overlays、interactionManager 这三个字段。FluentScreen.java(43 行)只有三个成员,即 isTextInputFocused()(转查 InteractionManager.active.hasFocusedTextInput())、mouseMoved(...) 覆写(转调 InteractionManager.updateHover,作为悬停的一级驱动)、extractBackground(...)(全景 + 模糊背景逻辑)。它没有输入转发方法,也没有任何树引用。
「Screen 持有三个字段」出自具体 Screen 子类的约定写法,两处源码注释也写明:InteractionManager 的字段注释写着「浮层管理器(Screen 持有,本类只借用引用做路由遍历,不拥有、不管理生命周期)」,OverlayManager 的类注释同样声明所有权在 Screen、InteractionManager 只收引用做路由。由此得到一条结论:持有是子类的约定,借用是 InteractionManager 的假设,两者必须成对理解。当前主仓没有具体的 Screen 子类示例,新页面按 Developer: 快速开始 的最小屏幕示例接线即可,转发方法的写法见该文。
约定下的所有权关系如下。interactionManager 一侧的两个箭头表示引用借用,并非所有权:
Screen(具体子类——单一所有者)
├── root: LayoutElement # 元素树。渲染入口由页面选择。
├── overlays: OverlayManager # 覆盖层系统: 多根森林 + 推送/移除 + 渲染
└── interactionManager: InteractionManager # 借用 root + overlays 引用。不拥有。
创建与接线的顺序体现了这份约定:先创建两棵树,再把两个引用传入 interactionManager。其构造签名是 InteractionManager(LayoutElement<?> root, OverlayManager overlays),此后推送覆盖层经 overlays() 访问器取得的 OverlayManager 完成,所有权不进入该路由器:
OverlayManager overlays = new OverlayManager(); // Screen 拥有覆盖层系统
InteractionManager interactionManager = new InteractionManager(root, overlays); // 借用两个引用
overlays.push(dialog, false); // 通过 overlays 推送,而非让 interactionManager 拥有
overlays.push(...) 有两个重载:push(child)(默认 dismissOnOutsideClick=true)与 push(child, dismissOnOutsideClick);dismissOnOutsideClick 的语义见 Developer: 覆盖层。修改「谁创建、谁销毁、谁借用」时,改动的正是这一段约定:InteractionManager 的构造与 overlays() 访问器全都建立在「引用借自 Screen」的假设上,overlays() 的 javadoc 明言「返回 Screen 持有的浮层管理器(借用引用)。浮层的推入/移除经此访问,本类不代持。」改动前先通读这些注释,确认借用方没有隐藏的生命周期依赖。
渲染流水线
Screen 的 extractRenderState 是两棵树的唯一汇聚点:根树与覆盖层树在此分别绘制,调用顺序决定层级。三步按实际调用顺序排列:
@Override public void extractRenderState(GuiGraphicsExtractor e, int mx, int my, float d) {
Tooltip.cursor(mx, my, width, height); // ① 更新工具提示光标/视口
root.extractRenderState(e, mx, my, d); // ② 渲染根树
overlays.renderOverlay(e, mx, my, d); // ③ 渲染覆盖层树(按添加顺序,父优先)
}
两棵树内部的顺序刻意不同:根树子元素按加入顺序绘制,覆盖层树按根的添加顺序、每棵内部父先子后地遍历(OverlayManager.renderOverlay),所以后推入的浮层覆盖在先推入的浮层之上。
框架的层级口径是列表位置即层级。Hit 命中查找按加入顺序的逆序遍历子元素,所以「末位 = 本容器最高层」,命中与绘制一致。由此得到一条约定:要使某个元素覆盖容器内的其他内容,将其置于 children 末位。
滚动条同样遵循这条约定,没有例外机制:它位于 children 末位,命中按末位优先扫描到它,绘制也按末位置于最上层。容器在测量与排布时按字段引用跳过它,定位覆写 getAbsoluteX/getAbsoluteY 按视口坐标计算(父链中「减去父级滚动量」的处理会使其随内容一同偏移)。
覆盖层的完整渲染语义另见 覆盖层系统。
输入流水线
输入方向与渲染相反:Screen 把输入方法原样转发给 interactionManager,由 InteractionManager 决定先给覆盖层还是根树。转发清单是 mousePressed、mouseReleased、mouseDragged、mouseScrolled、keyPressed、charTyped、preeditUpdated;每个方法的完整签名、键鼠分支写法与 Screen 集成检查清单见 Developer: 快速开始。每帧推进不在 tick() 中执行,而是挂载在渲染帧上:UiTickDriver 把真实帧耗时累加为固定步长,每步向两棵树的所有节点发送 tick,不按可见性过滤。步长与遍历口径见 生命周期系统:
private final UiTickDriver uiTickDriver = new UiTickDriver();
// 每帧推进(接线位置见 Developer: 快速开始)
uiTickDriver.onRenderFrame(interactionManager); // 内部: TickWalker.forEachElement(root, overlays.tree(), Element::tick)
// 输入方法的统一形态——这里以 mousePressed 为例
@Override public boolean mousePressed(double mx, double my, int btn) {
return interactionManager != null && interactionManager.mousePressed(new MousePressedEvent(mx, my, btn, Modifiers.NONE));
}
// mouseReleased / mouseDragged / mouseScrolled / keyPressed / charTyped / preeditUpdated 同构转发
进入 InteractionManager 之后,点击、松开、拖拽、滚轮都会先执行一次「浮层外关闭」清扫(dismissOverlaysOutside,在分发前判定浮层是否关闭,悬停有意不经过该路径),再按覆盖层 → 根的顺序分发;焦点与键盘路由、命中链 H0/H1/L1/H3/L3 的层级,全部以 交互引擎 为准。
渲染与输入互为镜像
两条流水线的遍历方向相反,属于刻意的配对:
- 渲染:根 → 覆盖层(自底向上视觉堆叠,覆盖层始终绘制在最上层)。
- 输入:覆盖层 → 根(自顶向下命中优先级,最上层最先获得输入)。
视觉上最突出的元素(即正在绘制的最上层)总是最先获得输入机会,点击结果与视觉层级一致。这条镜像关系是全仓级的设计决策,论证与备选方案记录在 设计决策(见其中「渲染和输入互为镜像」一节)。
不变量
- 所有权是约定,不是基类字段:
FluentScreen只有isTextInputFocused、mouseMoved→updateHover、extractBackground三个成员;root/overlays/interactionManager 出现在具体子类里,「Screen 持有、InteractionManager 借用」的依据是该类字段注释与OverlayManager类注释的 javadoc 约定,改所有权语义前先读该类对「只借用引用做路由遍历」的假设。 - 渲染必须根在前、覆盖层在后;输入必须覆盖层先于根:顺序反转会让浮层被根树覆盖,或输入先于覆盖层命中,直接破坏镜像不变量。
- 每帧推进必须覆盖两棵树且忽略可见性:
UiTickDriver经TickWalker.forEachElement(root, overlays.tree(), Element::tick)同时向两棵树发送 tick;漏掉浮层树会让覆盖层内的动画、滚动与自动关闭停滞,完整口径见 生命周期系统。 - InteractionManager 不拥有浮层生命周期:推送/移除经
overlays()访问的 OverlayManager,InteractionManager 不代持,overlays()的 javadoc 明言「浮层的推入/移除经此访问,本类不代持」;把所有权迁移进 InteractionManager 等于推翻整套借用约定。
改动波及
- 改
FluentScreen的职责范围(例如把树字段上移进基类),会波及所有按约定自行持树的子类写法、InteractionManager 的借用注释,以及 Developer: 快速开始 的最小屏幕示例。 - 调整渲染三步的顺序(如先根后浮层被误改为先浮层后根),会波及覆盖层可见性与镜像关系,设计决策 的对应论证随之过期。
- 改每帧推进的覆盖范围(去掉
TickWalker或按可见性过滤),会波及依赖每帧推进的滚动插值、提示框收起与浮层自动关闭(机制与全部波及面见 生命周期系统)。 - 改转发方法中的任何一个签名,会波及 Developer: 快速开始 的示例代码与所有既有 Screen 子类的覆写。
相关文档
- 交互引擎:输入进入 InteractionManager 之后的命中链、分发、焦点与捕获细节。
- 生命周期系统:每帧推进的步长、遍历口径与不变量(与输入链互不交叉)。
- 覆盖层系统:覆盖层树的推送、渲染顺序与 dismissOnOutsideClick 语义。
- 项目概述:两棵树各自建立在哪些基类之上。
- 设计决策:镜像关系、惰性布局、多根森林等取舍的完整论证。
- Developer: 快速开始:最小屏幕示例:8 条转发方法与渲染/接线清单。
- Developer: 覆盖层与浮动元素:覆盖层推送参数的选择(使用侧口径)