← 返回索引

范围:树结构、脏标记冒泡、输入进树,以及 width() 触发全树重排、渲染前调用 layout()、单次点击的处理路径、浮层与根树的接收顺序。

前置:核心概念与快速开始。


树与层次

界面由两棵树组成:根元素树与浮层树。根元素树是一棵以 LayoutElement 为根的多层结构:StackPanel、Canvas、EntryRow 这类容器通过 addChild 添加子元素,子元素再添加孙元素;所有权属于 FluentScreen 子类,Screen 子类持有 root 字段。浮层树是并列的多根森林,存放 Tooltip、右键菜单这类不进入根树的浮动物,所有权同样在 Screen(OverlayManager 由 Screen 构造),InteractionManager 只借用引用做路由,完整所有权论证见根树与层级系统。FluentScreen 基类本身不持有 root/overlays/interactionManager 三个字段,这三个字段由 QuickStart 模板中的子类声明。

每个 Element 有位置、尺寸、可见性和一条指向父节点的回指;容器额外持有子列表。生命周期为单向序列:

创建 → 挂到父节点 → 标记 dirty → 布局测量 → 渲染 → 事件处理
  1. 创建:new Button("text", 120, 24) 初始化几何与默认属性。
  2. 添加到父节点:parent.addChild(child) 设置回指并触发 markDirty()。
  3. 标记 dirty:链式 setter(width()、size()、margin() 等)赋值后自动调用 markDirty(),并沿父链逐级冒泡到根。
  4. 布局测量:layout() 只在脏时执行 measure() → arrange() → clearDirty()(方法名为 layout(),重排入口不使用其他名称)。
  5. 渲染:extractRenderState() 自上而下遍历树,容器先调用 layout() 再按子节点顺序绘制子元素。
  6. 事件处理:输入从 Screen 转发进 InteractionManager,再按命中结果分发到具体元素的回调。

自界定泛型的链式 API 简化第 3 步的调用写法:Element<T extends Element<T>> 里每个 setter 返回 (T) this,于是 new Button(...).size(100, 24).action(...) 始终保持 Button 类型;一旦把变量声明成 Element<?>,子类方法(如 action)就不可见,类型系统由此保证链式调用不会中断。

尺寸与位置的计算不在每帧执行,而由脏标记触发。


脏与布局

dirty 是一个沿父链冒泡的重算请求。 任何链式 setter 修改几何或外观后都调用 markDirty(),该方法将自身置脏并递归将父节点置脏,直到根节点;唯一的执行入口是 Element.layout():脏时依次执行 measure()(测量自身与子代)、arrange()(排列子代)、clearDirty()(清除标记),干净时整段跳过。LayoutElement.layout() 在其三步之后还会对每个子元素递归调用 layout(),所以根一脏,整条链在下一次渲染前全部重算。

渲染侧的接入点在 LayoutElement.extractRenderState 开头:每帧入口先调用 layout(),保证绘制使用的几何为最新值。该机制即“惰性布局“:每帧只做一次脏检查,未置脏的子树仅执行一次比较即返回。

// 典型写法:改尺寸,等渲染时统一重算
panel.size(400, 300);        // → markDirty() → 置脏并冒泡到根
// 下一次 extractRenderState 进来时,layout() 沿链重算 measure/arrange

调用方无需手动调用 layout(),将其写入渲染路径由框架容器负责。需要强制重排时调用 markDirty(),其语义即“请求下次重算“。绕过 setter 直接写 width = ... 是反面写法:字段已变更但未置脏,界面停留在旧布局上,这是脏机制唯一无法拦截的写法。

脏冒泡与 measure/arrange 的算法细节(含“为什么单点入口“的完整论证)见布局系统;修改布局引擎时以该文档为准。


输入进树的路径

FluentScreen.mouseMoved 直接驱动 InteractionManager.updateHover(鼠标移动即刷新悬停归属),其余八条输入方法转发给 InteractionManager(转发清单见快速开始)。InteractionManager 收到明确输入后的完整流水线:

  1. 右键拦截守卫(BLOCK_RIGHT)与捕获路径(mouseCapture 优先);
  2. suspendLevel > 0 时整体短路:suspend()/resume() 是冻结路由的开关(语义见交互与事件);
  3. 外部关闭清扫 dismissOverlaysOutside(调用点位于四个明确输入分发器内,分发前各执行一次),细节见覆盖层与浮动元素。本次清扫的新增豁免条件为:点击命中子浮层时,其父层与所有祖先层均不参与本轮外关判定;判定顺序见交互与事件的外关流程。
  4. L3 全 UI 命中只算一次:topmostAnywhere 先查浮层树、再查根树,得到的 hit 向下复用,tooltip 消除与焦点转移均读取该结果。这是一条不变量:同一次点击的多个决策读取同一份命中,判定不会分裂。
  5. 浮层树先经 topmostAnywhere 取最上层命中,再沿父链向上逐个尝试消费;
  6. 根树分发:从命中元素沿父链向上逐个尝试消费,第一个返回 true 的元素消费该事件,按下簿记记录消费者。

命中判定本身是分层的。完整标签表与分发优先级的权威在交互引擎。

标签谁在回答这一层问什么
H0Element.rectHit(el, mx, my)点是否落在元素自身的几何矩形内;不可覆写
H1el.shapeHit(mx, my)点是否落在元素的有效形状内,该层是唯一可覆写点,默认实现为 H0
L1Element.isHit(el, mx, my)命中要求形状命中成立,并且落在祖先裁剪范围内,该谓词由分发门控、外部关闭、阻断判定共用
H3Hit.hitOrder / hitPath / topmost单棵树内的搜索:本层全命中、根到最深路径、路径末端
L3topmostAnywhere(mx, my)全 UI 最上层:先浮层树后根树,一次算出向下复用

覆写自定义形状时只覆写 H1 的 shapeHit,且只处理形状(开关门限、动画中间态几何),不处理裁剪与树。L1 会自动将形状判定结果与祖先裁剪合成为统一谓词;在 shapeHit 中查询树或裁剪会重复实现并破坏这层分工(症状与确认方法见交互与事件)。

浮层与根树的架构级关系为:渲染按加入顺序父先子后,输入先取最上层命中再沿父链向上分发,两者方向互逆。外部关闭只响应明确输入等细节见覆盖层与浮动元素。

树提供结构,脏标记驱动重算时机,输入流水线决定事件落点。


不变量

  1. L3 全 UI 命中一次算出、向下复用(topmostAnywhere 算一次,下游复用):tooltip 消除与焦点转移读取同一份命中结果,不各自重算。若分发顺序变更或某环节绕开 hit 重新查询,“看到的悬停“与“点中的目标“判定会分裂。
  2. 重排只有一个入口:Element.layout() 在 dirty 时执行 measure → arrange → clearDirty;绕过 setter 直写字段不置脏,界面停在旧布局。全库的重排入口只有这一个方法名(布局系统同此结论)。
  3. 树所有权:Screen 持有根树与 OverlayManager,InteractionManager 借用引用做路由,不拥有它们,权威在根树与层级系统。FluentScreen 基类不持有 root/overlays/interactionManager 字段。
  4. 命中分层不可越级:组件只覆写 H1 shapeHit 且只处理形状;L1 的裁剪合取、H3 以下的树搜索、L3 的跨树优先都由框架完成。完整标签表见交互引擎。
  5. 渲染与输入的浮层遍历方向互逆:渲染按父先子后自下而上叠加,输入先取最上层命中再沿父链向上分发。任何一侧的遍历序变更而另一侧未同步,浮层会出现“看到的在上、点到的在下“的错位。

改动波及

  • 改 markDirty 的冒泡或 layout() 的执行条件:波及全 UI 所有组件的重算时机,不脏也重算则每帧执行无效计算,漏置脏则界面冻结;布局系统的不变量同步失效。
  • 改 topmostAnywhere 的复用点(L3):波及 tooltip 消除与焦点转移两个读取方,二者可能对同一次点击得出不同“最上层“,症状是关不掉 tooltip 或点 A 焦点落 B。
  • 改四个输入方法里 dismissOverlaysOutside 的调用位置或时机:波及所有可外关浮层,清扫必须在分发前执行,分发过程中新建的浮层才不会被该次清扫关闭;调用位置移到分发后会误关刚弹出的浮层。调用点清单与四象限见覆盖层与浮动元素。
  • 改浮层渲染或分发的遍历序:波及所有多层叠加的浮层(菜单上的子菜单、菜单里的 tooltip),与不变量 5 互为镜像,必须两侧同改。
  • 改 Hit 的候选过滤或 isHit 的裁剪链:波及滚动容器内外的一切命中,症状是裁剪外误点、滚动区外的元素收事件;这是交互引擎与布局系统的交界,改动前须核对两个文档的不变量。

相关文档