← 返回索引

范围: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),所以后推入的浮层覆盖在先推入的浮层之上。

Note

框架的层级口径是列表位置即层级。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 子类的覆写。

相关文档