Files
endorphin-tessera-gui/src/model/presentation.rs
T

307 lines
16 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! 一个原生窗口对应一份 Presentation:独立视口、Mount、测量缓存、焦点与依赖记录。
//! 多个窗口仍使用同一个 VM 和共享业务对象。读取版本失效时只标记相关展示需要刷新,不能因此复制一份业务状态。
//!
//! 调用路径:bootstrap/新窗口入口创建 Presentation;UiBridge 收集 VM changes 后对每窗口调用 observe_changes。
//! Block.invalidate 的桥接路径调用 invalidate,源布局/绘制完成后用 split_reads 保存本窗口实际读取。
//! 本结构是 VM 线程内宿主状态,不能把其借用 ObjectRef 或弱表直接交给窗口线程操作。
//!
//! Per-window rendering state. Business objects and code remain in one VM.
//! Registries and caches borrow checked identities; the managed desktop owns
//! presentation roots through its pending/mount dictionaries.
use crate::ui_bridge::UiNode;
use endor_core::{ChangeBatch, NativeContext, ObjectRef, Vm, VmError, WeakObjectRef};
use std::sync::Arc;
#[derive(Debug)]
/// 一个窗口独立的展示状态:活动页面、待提交请求、Mount、布局与源读取缓存;共享 VM 的业务状态不放在这里复制。
pub(crate) struct Presentation {
// 所属托管桌面身份;此结构借用身份,真正的展示根由托管桌面字典维持。
pub desktop: ObjectRef,
// 窗口正在退役的标记,阻止常规新展示请求在关闭阶段复活旧上下文。
pub closing: bool,
// 当前窗口标签字典的借用身份,真正拥有边由托管 desktop 保持。
pub tabs: ObjectRef,
// 当前已生效 Page 的借用身份;页面生命期仍由托管展示根维持。
pub active: Option<ObjectRef>,
// 双层 Option 区分三态:None 无请求,Some(None) 请求清空,Some(Some(page)) 请求切页。
pub pending: Option<Option<ObjectRef>>,
// 最近原生根快照,比较 Arc/内容以决定是否需要新的布局,不强持有 VM 业务对象。
pub last_snapshot: Option<Arc<UiNode>>,
// 弱身份到原生稳定标签 ID 的映射;不能因为标签历史而保活已经关闭的页面。
pub tab_ids: Vec<(WeakObjectRef, u64)>,
// 分配新原生标签 ID 的单调计数,与可被回收的 Page 弱身份分开。
pub next_tab: u64,
// 最近发布的标签值列表,用于检测标题/活动项/顺序变化。
pub last_tabs: Vec<crate::tabs::Tab>,
// 当前页面兼容表面相机和模式,随本窗口展示更新。
pub surface: crate::page::Surface,
// 托管业务图到原生视觉树的缓存及反向依赖,不能替代源测量缓存。
pub render_cache: crate::snapshot::RenderCache,
// 本窗口几何求解、出现身份与布局缓存,多个窗口共享业务树但不共享此状态。
pub layout: crate::layout::LayoutRuntime,
// 本窗口活动 Mount 的弱索引与发布顺序,托管字典负责强拥有。
pub mounts: crate::mount::MountTable,
// 最后完整发布的布局版本,未提交的分片结果不能直接覆盖此边界。
pub published_version: u64,
// 即使根快照身份未变也要求重新发布一帧,例如源绘制读取的数据发生变化时。
pub force_frame: bool,
// 精确名称/字典种类依赖,与下面按整个对象版本记录的 reads 分开检查。
pub paint_names: Vec<endor_core::ReadDependency>,
// 源布局精确名字/字典类别读取,与 layout_reads 的整对象版本分别检查。
pub layout_names: Vec<endor_core::ReadDependency>,
// 源 paint 的对象内容版本观察,仅借用身份,不 retain 对象。
pub paint_reads: std::collections::BTreeMap<ObjectRef, u64>,
// 观察到读取失效的累计次数,饱和累加用于诊断,不作为实际刷新次数。
pub dependency_invalidations: usize,
// 最近失效的阶段/对象或名字诊断;同次两类检查都命中时后者覆盖前者。
pub last_invalid_dependency: String,
// 源 measure/arrange 实际读取的整对象版本,更新后按该版本观察决定刷新。
pub layout_reads: std::collections::BTreeMap<ObjectRef, u64>,
// 源测量的参数/依赖缓存,paint-only 显式失效不清除此缓存。
pub measure_cache: crate::source_layout::Cache,
// 最近快照提取实际重建节点统计,供开发工具解释刷新成本。
pub snapshot_visited: usize,
// 最近快照提取直接复用条目统计,与 visited 分开展示。
pub snapshot_reused: usize,
}
impl Presentation {
/// 比较布局/绘制读取版本和名称依赖,标记需要刷新并通知快照缓存。
pub fn observe_changes(&mut self, vm: &Vm, changes: &ChangeBatch) {
// 先检查粗粒度对象版本,layout 优先于 paint;每类只记录第一个失效项用于诊断。
// 对象已经释放时版本查询失败,也视作失效,而非继续使用旧缓存。
if let Some((phase, object, version)) = self
.layout_reads
.iter()
.map(|(object, version)| ("layout", object, version))
.chain(
self.paint_reads
.iter()
.map(|(object, version)| ("paint", object, version)),
)
.find(|(_, object, version)| vm.change_version(**object).ok() != Some(**version))
{
self.force_frame = true;
self.dependency_invalidations = self.dependency_invalidations.saturating_add(1);
// 失效诊断尽量附 payload 种类;对象已释放时用 released,而不是解引用过期句柄。
let kind = vm
.object(*object)
.map(|value| value.payload().kind_name())
.unwrap_or("released");
self.last_invalid_dependency = format!(
"{phase} {kind} {object}: {version} -> {:?}",
vm.change_version(*object).ok()
);
}
// 再核对名称级依赖,避免仅凭对象版本漏掉名称解析路径上的变化。
// 两个检查独立执行,因此同次观察可能分别增加计数,后者诊断可覆盖前者。
if let Some((phase, dependency)) = self
.layout_names
.iter()
.map(|dependency| ("layout", dependency))
.chain(
self.paint_names
.iter()
.map(|dependency| ("paint", dependency)),
)
.find(|(_, dependency)| !vm.object_store().read_dependency_valid(dependency))
{
self.force_frame = true;
self.dependency_invalidations = self.dependency_invalidations.saturating_add(1);
// Name 依赖显示具体名字,其他精确记录显示字典类别变化,不假装都有属性名。
let name = match dependency {
// 名字解析路径中的具体名称用于调试哪些全局/属性访问使布局或绘制失效。
endor_core::ReadDependency::Name { name, .. } => name.as_str(),
// 非 Name 分支表示字典种类观察,仍需保留其失效事实。
_ => "<dictionary kind>",
};
self.last_invalid_dependency = format!("{phase} name {}:{name}", dependency.object());
}
// 把同一批 VM 写入交给视觉快照缓存,源布局/绘制依赖与原生快照依赖分别维护。
self.render_cache.invalidate(ChangeBatch {
changes: changes.changes.clone(),
rescan_required: changes.rescan_required,
});
}
/// 按尺寸、内容或绘制失效原因清理对应缓存,避免把所有变化都当成相同重建。
pub fn invalidate(
&mut self,
cx: &mut NativeContext<'_>,
affected: &std::collections::HashSet<ObjectRef>,
reason: &str,
) -> Result<(), VmError> {
// 仅绘制失效不影响测量;其余原因清除测量结果并按 Block ID 使布局缓存失效。
if reason != "paint" {
self.measure_cache.invalidate(affected);
// 源测量缓存按业务身份失效,布局缓存则使用其身份文本;同一窗口所有受影响对象都处理。
for block in affected {
self.layout.invalidate(&block.to_string());
}
}
// 内容变化还可能改变组成结构,因此显式失效视觉快照及其父链;其他原因不额外做此传播。
if reason == "content" {
self.render_cache
.invalidate_objects(affected.iter().copied());
}
// 仅当受影响 Block 在该窗口有活动出现时强制刷新,避免无关窗口重复发帧。
// 逐个弱升级本窗口活动出现,只在此窗口确有受影响 Block 时 force_frame。
for weak in self.mounts.active.values() {
// 已消失的 Mount 不触发刷新;升级成功后检查它的业务 Block 是否属于 affected。
if let Some(mount) = cx.upgrade(*weak)?
&& affected.contains(
&cx.store()
.payload::<crate::mount::Mount>(mount, "Mount")?
.block,
)
{
self.force_frame = true;
}
}
Ok(())
}
/// 用桌面与标签注册表身份初始化窗口状态,布局和视觉缓存从空状态开始。
// 这里只初始化窗口级缓存,不创建 VM、不复制业务对象;active/pending 留空等待窗口展示请求。
pub fn new(desktop: ObjectRef, tabs: ObjectRef) -> Self {
// 这里只初始化本窗口宿主状态;不分配新的 VM 或业务对象,尚无页面、根快照和读观察。
Self {
desktop,
closing: false,
tabs,
active: None,
pending: None,
last_snapshot: None,
tab_ids: Vec::new(),
next_tab: 0,
last_tabs: Vec::new(),
surface: crate::page::Surface::default(),
render_cache: crate::snapshot::RenderCache::default(),
layout: crate::layout::LayoutRuntime::default(),
mounts: crate::mount::MountTable::default(),
published_version: 0,
force_frame: false,
paint_names: Vec::new(),
layout_names: Vec::new(),
paint_reads: std::collections::BTreeMap::new(),
dependency_invalidations: 0,
last_invalid_dependency: String::new(),
layout_reads: std::collections::BTreeMap::new(),
measure_cache: crate::source_layout::Cache::default(),
snapshot_visited: 0,
snapshot_reused: 0,
}
}
}
/// 把读取记录拆成对象版本和精确名称依赖,便于按实际观察内容判断失效。
pub(crate) fn split_reads(
reads: Vec<endor_core::ReadDependency>,
) -> (
std::collections::BTreeMap<ObjectRef, u64>,
Vec<endor_core::ReadDependency>,
) {
// 整体对象观察按身份归并到有序表,后续 observe_changes 比较该对象的当前版本。
let mut objects = std::collections::BTreeMap::new();
// 其他精确依赖保持原枚举与顺序,不能降级为对象版本而丢掉名字解析语义。
let mut names = Vec::new();
// 整对象依赖按身份收集到有序表,重复项以最后一次版本为准;其余精确依赖保持原记录。
for dependency in reads {
// 按 ReadDependency 种类拆分;不会 retain 借用身份或执行源函数。
match dependency {
// 同对象重复观察以最后出现的版本覆盖先前版本,与本轮实际观察结束状态一致。
endor_core::ReadDependency::Object { object, version } => {
objects.insert(object, version);
}
// Name/字典类别等精确记录完整保留,交给 ObjectStore 的有效性检查器解释。
dependency => names.push(dependency),
}
}
(objects, names)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{layout::Size, ui_bridge::UiKind};
#[test]
/// 同VM中两Presentation分别布局共享树,尺寸/出现身份独立;读观察可追踪共享对象但不能增添GC根。
fn independent_view_geometry_and_shared_vm_reads_do_not_add_roots() {
// 测试在同一 VM 中建立两个 Presentation,验证各自布局尺寸/出现 ID 独立,读取依赖共享业务对象。
let mut vm = Vm::new().unwrap();
// 第一窗口以独立 desktop/tabs 与独立 LayoutRuntime 初始化,但仍属于同一个 VM。
let mut left = Presentation::new(vm.new_object().unwrap(), vm.new_dict().unwrap());
// 第二窗口使用另一套展示状态,随后给它不同有限视口,不能靠复制业务树隔离几何。
let mut right = Presentation::new(vm.new_object().unwrap(), vm.new_dict().unwrap());
// 左窗口设 100×80,右窗口设 200×160,同一自然 500×500 根会被各自约束。
left.layout.viewport = Some(Size {
width: 100.0,
height: 80.0,
});
right.layout.viewport = Some(Size {
width: 200.0,
height: 160.0,
});
// 手工不可变原生矩形快照以同一 block_id 供两窗口布局,专门隔离展示几何状态。
let root = UiNode {
surface: None,
flex: 0.0,
flex_policy: crate::layout::FlexPolicy::default(),
main_align: crate::layout::MainAlign::Start,
cross_align: crate::layout::CrossAlign::Start,
align_self: None,
margin: crate::layout::Insets::default(),
sizing: crate::box_model::Sizing::default(),
padding_edges: [None; 4],
border: crate::layout::Insets::default(),
border_color: "#D8DCE2".into(),
clip_children: false,
block_id: "shared".into(),
protocol_key: "rect".into(),
slots: Vec::new(),
interactive: false,
pointer_events: false,
kind: UiKind::Rect {
width: 500.0,
height: 500.0,
radius: 0.0,
color: "#237A45".into(),
},
};
// 分别执行真实布局,验证共享快照在左右窗口产生不同分配宽度。
let left_frame = left.layout.frame(&root).unwrap();
// 右窗口也读取同一 root,但不应沿用左侧出现身份或约束结果。
let right_frame = right.layout.frame(&root).unwrap();
assert_eq!(left_frame.mounts[0].width, 100.0);
assert_eq!(right_frame.mounts[0].width, 200.0);
// 只清左窗口出现,下一帧应分配新左 ID,而右窗口不受影响。
left.layout.clear_occurrences();
// 新左帧应更换出现 ID,验证 clear_occurrences 的实际效果。
let new_left = left.layout.frame(&root).unwrap();
// 未清理的右窗口保留原 ID,说明身份序列/缓存没有跨 Presentation 串用。
let same_right = right.layout.frame(&root).unwrap();
assert_ne!(new_left.mounts[0].id, left_frame.mounts[0].id);
assert_eq!(same_right.mounts[0].id, right_frame.mounts[0].id);
// 在 VM 中创建共享业务读取源,让两窗口记录不同阶段对同一对象的依赖。
let shared = vm.new_object().unwrap();
// 保存写入前的版本;left.paint_reads 与 right.layout_reads 都只借用该身份。
let version = vm.change_version(shared).unwrap();
left.paint_reads.insert(shared, version);
right.layout_reads.insert(shared, version);
// 实际写入使对象版本改变;两类读取观察都应设置各自 force_frame。
vm.set_attr(shared, "value", endor_core::Value::Bool(true))
.unwrap();
// 同一 VM 变化批次送给两窗口,记录克隆不能制造业务对象副本或新的 GC 根。
let changes = vm.take_changes();
left.observe_changes(&vm, &changes);
right.observe_changes(&vm, &changes);
assert!(left.force_frame && right.force_frame);
// 最后释放业务对象并回收:两份读取记录都不应成为托管根,弱引用应升级失败。
// 只保留弱句柄再释放业务根,最后 collect_cycles 后应无法升级,证明 reads 不保活源对象。
let weak = vm.downgrade(shared).unwrap();
vm.release(shared).unwrap();
vm.collect_cycles();
assert!(vm.upgrade(weak).unwrap().is_none());
}
}