Files
endorphin-tessera-gui/src/mount.rs
T
codex-bot e3e46068eb
Verify Tessera GUI / verify (push) Failing after 7m35s
Support shared-VM native windows and Page detachment
2026-10-21 18:11:58 +08:00

464 lines
16 KiB
Rust

//! Managed presentation contexts. The active desktop owns the mount dictionary;
//! reverse links and the host registry are weak, so shared content cannot retain a Page.
use crate::document::{Frame, MountGeometry};
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
Value, Vm, VmError, WeakObjectRef,
};
use std::{
collections::HashMap,
sync::{Arc, Mutex},
};
#[derive(Debug, Clone)]
pub struct Mount {
pub block: ObjectRef,
pub page: Option<WeakObjectRef>,
pub parent: Option<WeakObjectRef>,
pub first_child: Option<WeakObjectRef>,
pub next_sibling: Option<WeakObjectRef>,
pub geometry: MountGeometry,
pub version: u64,
pub mounted: bool,
// Private derived scratch, installed before the read-only layout call and
// cleared by geometry commit. It contains no managed references or roots.
pub layout: Option<Arc<Mutex<crate::layout::Context>>>,
}
impl ObjectPayload for Mount {
fn reference_assignment_only(&self) -> bool {
true
}
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn kind_name(&self) -> &'static str {
"tessera-mount"
}
fn allows_attributes(&self) -> bool {
true
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
visit(self.block);
}
}
#[derive(Debug, Clone, Copy)]
enum Query {
Bounds,
Parent,
FirstChild,
NextSibling,
Page,
Focus,
HitTest,
}
#[derive(Debug)]
struct Method {
owner: ObjectRef,
desktop: WeakObjectRef,
query: Query,
}
impl NativeCallable for Method {
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let mount = cx.store().payload::<Mount>(self.owner, "Mount")?.clone();
if !mount.mounted && matches!(self.query, Query::Bounds | Query::Focus | Query::HitTest) {
return Err(VmError::InvalidArgument("Mount is no longer attached"));
}
let arity = if matches!(self.query, Query::HitTest) {
2
} else {
0
};
if args.len() != arity {
return Err(VmError::InvalidArgument("incorrect Mount method arity"));
}
let value = match self.query {
Query::Bounds => {
let geometry = if let Some(layout) = &mount.layout {
layout
.lock()
.map_err(|_| VmError::InvalidArgument("layout context poisoned"))?
.bounds
.ok_or(VmError::InvalidArgument(
"Mount geometry is unavailable during measurement",
))?
} else {
[
mount.geometry.x,
mount.geometry.y,
mount.geometry.width,
mount.geometry.height,
]
};
let bounds = cx.new_object()?;
for (name, value) in [
("x", geometry[0]),
("y", geometry[1]),
("width", geometry[2]),
("height", geometry[3]),
] {
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
}
cx.set_attr(
bounds,
"layoutVersion",
Value::Number((mount.version as i64).into()),
)?;
Value::Object(bounds)
}
Query::Focus => {
if !mount.geometry.interactive {
return Err(VmError::InvalidArgument("Mount is not focusable"));
}
let desktop = cx
.upgrade(self.desktop)?
.ok_or(VmError::InvalidArgument("Mount window has expired"))?;
cx.set_attr(
desktop,
"focus",
Value::Number((mount.geometry.id as i64).into()),
)?;
cx.set_attr(desktop, "focusRequested", Value::Bool(true))?;
Value::Nil
}
Query::HitTest => {
if mount.layout.is_some() {
return Err(VmError::InvalidArgument(
"Mount hitTest is unavailable during layout",
));
}
let number = |value: &Value| -> Result<f32, VmError> {
let Value::Number(number) = value else {
return Err(VmError::InvalidArgument(
"point coordinates must be numbers",
));
};
let number = number
.to_string()
.parse::<f32>()
.map_err(|_| VmError::InvalidArgument("invalid point coordinate"))?;
if !number.is_finite() {
return Err(VmError::InvalidArgument("invalid point coordinate"));
}
Ok(number)
};
Value::Bool(
crate::geometry::Rect::new(
mount.geometry.x,
mount.geometry.y,
mount.geometry.width,
mount.geometry.height,
)
.contains(crate::geometry::Point::new(
number(&args[0])?,
number(&args[1])?,
)),
)
}
query => {
let weak = match query {
Query::Parent => mount.parent,
Query::FirstChild => mount.first_child,
Query::NextSibling => mount.next_sibling,
Query::Page => mount.page,
_ => unreachable!(),
};
weak.map(|weak| cx.upgrade(weak))
.transpose()?
.flatten()
.map(Value::Object)
.unwrap_or(Value::Nil)
}
};
Ok(NativeResult::Return(value))
}
}
#[derive(Debug, Default, Clone)]
pub struct MountTable {
pub active: HashMap<u64, WeakObjectRef>,
order: Vec<u64>,
}
#[derive(Debug)]
struct Sync {
desktop: ObjectRef,
page: Option<ObjectRef>,
nodes: Vec<WeakObjectRef>,
old: HashMap<u64, WeakObjectRef>,
old_order: Vec<u64>,
frame: Frame,
preparing: bool,
output: Arc<Mutex<HashMap<u64, WeakObjectRef>>>,
}
#[derive(Debug)]
struct LifecycleCall {
function: ObjectRef,
block: ObjectRef,
mount: ObjectRef,
}
impl NativeCallable for LifecycleCall {
fn invoke(&self, _: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
Ok(NativeResult::Invoke {
function: self.function,
arguments: vec![Value::Object(self.block), Value::Object(self.mount)],
})
}
}
fn lifecycle_call(
cx: &mut NativeContext<'_>,
mount: ObjectRef,
name: &'static str,
) -> Result<ObjectRef, VmError> {
let block = cx.store().payload::<Mount>(mount, "Mount")?.block;
let definition = cx
.store()
.type_of(block)?
.ok_or(VmError::InvalidArgument("untyped mounted Block"))?;
let function = cx
.store()
.lookup_method(definition, name)?
.ok_or(VmError::InvalidArgument("Block has no lifecycle method"))?;
cx.allocate(ContextNativeFunction::new(
name,
LifecycleCall {
function,
block,
mount,
},
vec![function, block, mount],
))
}
impl NativeCallable for Sync {
fn invoke(&self, cx: &mut NativeContext<'_>, _args: &[Value]) -> Result<NativeResult, VmError> {
let mut blocks = HashMap::new();
for weak in &self.nodes {
if let Some(block) = cx.upgrade(*weak)? {
blocks.insert(block.to_string(), block);
}
}
let page = self
.page
.map(|page| cx.store().downgrade(page))
.transpose()?;
let mut objects = HashMap::new();
let mut created = Vec::new();
for geometry in &self.frame.mounts {
let block = *blocks
.get(&geometry.block_id)
.ok_or(VmError::InvalidArgument(
"frame references an unavailable Block",
))?;
let old = self
.old
.get(&geometry.id)
.map(|weak| cx.upgrade(*weak))
.transpose()?
.flatten();
let reusable = if let Some(old) = old {
let mount = cx.store().payload::<Mount>(old, "Mount")?;
(mount.mounted && mount.page == page && mount.block == block).then_some(old)
} else {
None
};
let object = if let Some(old) = reusable {
old
} else {
let object = cx.allocate(Mount {
block,
page,
parent: None,
first_child: None,
next_sibling: None,
geometry: geometry.clone(),
version: self.frame.layout_version,
mounted: true,
layout: None,
})?;
crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?;
for (name, query) in [
("bounds", Query::Bounds),
("parent", Query::Parent),
("firstChild", Query::FirstChild),
("nextSibling", Query::NextSibling),
("page", Query::Page),
("requestFocus", Query::Focus),
("hitTest", Query::HitTest),
] {
let function = cx.allocate(ContextNativeFunction::new(
name,
Method {
owner: object,
desktop: cx.store().downgrade(self.desktop)?,
query,
},
vec![object],
))?;
cx.set_attr(object, name, Value::Object(function))?;
}
cx.set_attr(object, "id", Value::Number((geometry.id as i64).into()))?;
cx.set_attr(object, "block", Value::Object(block))?;
created.push(object);
object
};
objects.insert(geometry.id, object);
}
let root = cx.new_dict()?;
for geometry in &self.frame.mounts {
let object = objects[&geometry.id];
let link = |id: Option<u64>| -> Result<Option<WeakObjectRef>, VmError> {
id.map(|id| cx.store().downgrade(objects[&id])).transpose()
};
let mount = Mount {
block: blocks[&geometry.block_id],
page,
parent: link(geometry.parent)?,
first_child: link(geometry.first_child)?,
next_sibling: link(geometry.next_sibling)?,
geometry: geometry.clone(),
version: self.frame.layout_version,
mounted: true,
layout: if self.preparing {
Some(Arc::new(Mutex::new(crate::layout::Context::default())))
} else {
None
},
};
let old = cx.store().payload::<Mount>(object, "Mount")?;
if self.preparing
|| old.layout.is_some()
|| !old.mounted
|| old.version != mount.version
|| old.geometry != mount.geometry
|| old.page != mount.page
|| old.parent != mount.parent
|| old.first_child != mount.first_child
|| old.next_sibling != mount.next_sibling
|| matches!(
cx.store().get_attr(object, "mounted"),
Err(VmError::AttributeNotFound(_))
)
{
cx.replace_payload(object, mount)?;
cx.set_attr(object, "mounted", Value::Bool(true))?;
cx.set_attr(
object,
"layoutVersion",
Value::Number((self.frame.layout_version as i64).into()),
)?;
}
cx.dict_set(
root,
Value::Text(geometry.id.to_string()),
Value::Object(object),
)?;
}
let mut hooks = Vec::new();
// Layout records are preorder; reversing guarantees children unmount
// before their parents. A Page change creates fresh Mount contexts.
for id in self.old_order.iter().rev() {
let weak = self.old[id];
if let Some(object) = cx.upgrade(weak)?
&& objects.get(id) != Some(&object)
{
let mut mount = cx.store().payload::<Mount>(object, "Mount")?.clone();
mount.mounted = false;
mount.layout = None;
mount.parent = None;
mount.first_child = None;
mount.next_sibling = None;
cx.replace_payload(object, mount)?;
cx.set_attr(object, "mounted", Value::Bool(false))?;
hooks.push(lifecycle_call(cx, object, "onUnmount")?);
}
}
for object in created {
hooks.push(lifecycle_call(cx, object, "onMount")?);
}
let driver = if hooks.is_empty() {
None
} else {
Some(crate::vm_sequence::build(
cx,
hooks,
"<mount lifecycle>",
self.desktop,
)?)
};
let weak = objects
.into_iter()
.map(|(id, object)| Ok((id, cx.store().downgrade(object)?)))
.collect::<Result<HashMap<_, _>, VmError>>()?;
cx.set_attr(self.desktop, "mounts", Value::Object(root))?;
*self
.output
.lock()
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))? = weak;
Ok(NativeResult::Return(
driver.map(Value::Object).unwrap_or(Value::Nil),
))
}
}
impl MountTable {
pub fn ordered(&self) -> Vec<WeakObjectRef> {
self.order
.iter()
.filter_map(|id| self.active.get(id).copied())
.collect()
}
pub fn synchronize(
&mut self,
vm: &mut Vm,
desktop: ObjectRef,
page: Option<ObjectRef>,
nodes: Vec<WeakObjectRef>,
frame: &Frame,
preparing: bool,
) -> Result<Option<ObjectRef>, VmError> {
let output = Arc::new(Mutex::new(HashMap::new()));
let mut captures = vec![desktop];
captures.extend(page);
let function = vm.allocate(ContextNativeFunction::new(
"mount-sync",
Sync {
desktop,
page,
nodes,
old: self.active.clone(),
old_order: self.order.clone(),
frame: frame.clone(),
preparing,
output: output.clone(),
},
captures,
))?;
let result = vm.call(function, &[]);
vm.release(function)?;
let result = result?;
self.active = std::mem::take(
&mut *output
.lock()
.map_err(|_| VmError::InvalidArgument("Mount registry poisoned"))?,
);
self.order = frame.mounts.iter().map(|mount| mount.id).collect();
match result {
Value::Object(hooks) => Ok(Some(hooks)),
Value::Nil => Ok(None),
value => {
vm.release_value(value)?;
Err(VmError::InvalidArgument("invalid lifecycle batch"))
}
}
}
}
pub use crate::object_list::collection;