845 lines
30 KiB
Rust
845 lines
30 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 scroll_offset: [f32; 2],
|
|
pub version: u64,
|
|
pub mounted: bool,
|
|
pub tasks: Option<ObjectRef>,
|
|
// 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);
|
|
if let Some(tasks) = self.tasks {
|
|
visit(tasks);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
enum Query {
|
|
Bounds,
|
|
ScrollTo,
|
|
ScrollBy,
|
|
ScrollPosition,
|
|
ScrollIntoView,
|
|
FitView,
|
|
Pick3D,
|
|
ToView,
|
|
ToLocal,
|
|
Parent,
|
|
FirstChild,
|
|
NextSibling,
|
|
Page,
|
|
Focus,
|
|
HitTest,
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct Method {
|
|
signature: endor_core::CallSignature,
|
|
owner: ObjectRef,
|
|
desktop: WeakObjectRef,
|
|
query: Query,
|
|
}
|
|
|
|
impl NativeCallable for Method {
|
|
fn signature(&self) -> Option<endor_core::CallSignature> {
|
|
Some(self.signature.clone())
|
|
}
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(
|
|
if matches!(
|
|
self.query,
|
|
Query::HitTest
|
|
| Query::Pick3D
|
|
| Query::ToView
|
|
| Query::ToLocal
|
|
| Query::ScrollTo
|
|
| Query::ScrollBy
|
|
) {
|
|
2
|
|
} else {
|
|
0
|
|
},
|
|
))
|
|
}
|
|
|
|
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
|
|
| Query::ScrollTo
|
|
| Query::ScrollBy
|
|
| Query::ScrollIntoView
|
|
| Query::FitView
|
|
| Query::Pick3D
|
|
| Query::ToView
|
|
| Query::ToLocal
|
|
| Query::ScrollPosition
|
|
)
|
|
{
|
|
return Err(VmError::InvalidArgument("Mount is no longer attached"));
|
|
}
|
|
let arity = if matches!(
|
|
self.query,
|
|
Query::HitTest
|
|
| Query::Pick3D
|
|
| Query::ToView
|
|
| Query::ToLocal
|
|
| Query::ScrollTo
|
|
| Query::ScrollBy
|
|
) {
|
|
2
|
|
} else {
|
|
0
|
|
};
|
|
if args.len() != arity {
|
|
return Err(VmError::InvalidArgument("incorrect Mount method arity"));
|
|
}
|
|
let value = match self.query {
|
|
Query::ToView | Query::ToLocal => {
|
|
if mount.layout.is_some() {
|
|
return Err(VmError::InvalidArgument(
|
|
"coordinate conversion requires committed geometry",
|
|
));
|
|
}
|
|
let point = crate::geometry::Point::new(
|
|
crate::scroll::coordinate(&args[0])?,
|
|
crate::scroll::coordinate(&args[1])?,
|
|
);
|
|
let point =
|
|
if matches!(self.query, Query::ToView) {
|
|
let point = mount.geometry.view.unwrap_or_default().point(
|
|
crate::geometry::Point::new(
|
|
point.x + mount.geometry.x,
|
|
point.y + mount.geometry.y,
|
|
),
|
|
);
|
|
mount
|
|
.geometry
|
|
.panel
|
|
.as_ref()
|
|
.map_or(Some(point), |panel| panel.project_point(point))
|
|
} else {
|
|
mount.geometry.logical_point(point).map(|point| {
|
|
crate::geometry::Point::new(
|
|
point.x - mount.geometry.x,
|
|
point.y - mount.geometry.y,
|
|
)
|
|
})
|
|
};
|
|
if let Some(point) = point {
|
|
let result = cx.new_object()?;
|
|
let desktop = cx.upgrade(self.desktop)?.ok_or(VmError::InvalidArgument(
|
|
"coordinate conversion desktop expired",
|
|
))?;
|
|
crate::ui_bridge::attach_desktop_type(cx, result, "Point", desktop)?;
|
|
cx.set_attr(result, "x", crate::native_visual::value(point.x))?;
|
|
cx.set_attr(result, "y", crate::native_visual::value(point.y))?;
|
|
Value::Object(result)
|
|
} else {
|
|
Value::Nil
|
|
}
|
|
}
|
|
Query::ScrollTo | Query::ScrollBy => {
|
|
crate::scroll::set(
|
|
cx,
|
|
self.owner,
|
|
self.desktop,
|
|
[
|
|
crate::scroll::coordinate(&args[0])?,
|
|
crate::scroll::coordinate(&args[1])?,
|
|
],
|
|
matches!(self.query, Query::ScrollBy),
|
|
)?;
|
|
Value::Nil
|
|
}
|
|
Query::ScrollIntoView => {
|
|
crate::scroll::into_view(cx, self.owner, self.desktop)?;
|
|
Value::Nil
|
|
}
|
|
Query::FitView => {
|
|
crate::viewport::fit(cx, &mount)?;
|
|
Value::Nil
|
|
}
|
|
Query::Pick3D => {
|
|
let desktop = cx
|
|
.upgrade(self.desktop)?
|
|
.ok_or(VmError::InvalidArgument("viewport desktop expired"))?;
|
|
crate::scene3d_model::pick(
|
|
cx,
|
|
desktop,
|
|
&mount,
|
|
crate::geometry::Point::new(
|
|
crate::scroll::coordinate(&args[0])?,
|
|
crate::scroll::coordinate(&args[1])?,
|
|
),
|
|
)?
|
|
}
|
|
Query::ScrollPosition => {
|
|
let p = mount
|
|
.geometry
|
|
.scroll
|
|
.ok_or(VmError::InvalidArgument("Mount is not scrollable"))?;
|
|
let result = cx.new_object()?;
|
|
for (name, value) in [("x", p[0]), ("y", p[1]), ("maxX", p[2]), ("maxY", p[3])] {
|
|
cx.set_attr(result, name, Value::Number((value as f64).into()))?;
|
|
}
|
|
Value::Object(result)
|
|
}
|
|
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()?;
|
|
let endor_core::CallType::Single(endor_core::AnnotationType::Nominal(definition)) =
|
|
&self.signature.returns
|
|
else {
|
|
return Err(VmError::InvalidArgument("invalid Mount bounds signature"));
|
|
};
|
|
cx.attach_type(bounds, *definition)?;
|
|
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 => {
|
|
crate::scroll::into_view(cx, self.owner, self.desktop)?;
|
|
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(mount.geometry.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))
|
|
}
|
|
}
|
|
|
|
fn mount_signature(
|
|
store: &endor_core::ObjectStore,
|
|
desktop: ObjectRef,
|
|
query: Query,
|
|
) -> Result<endor_core::CallSignature, VmError> {
|
|
use endor_core::{AnnotationType as T, CallEffect, CallSignature, CallType};
|
|
let nominal = |name| crate::ui_bridge::desktop_type(store, name, desktop).map(T::Nominal);
|
|
let returns = match query {
|
|
Query::Bounds => nominal("Bounds")?.into(),
|
|
Query::Parent | Query::FirstChild | Query::NextSibling => {
|
|
CallType::nullable(nominal("Mount")?)
|
|
}
|
|
Query::Page => CallType::nullable(nominal("Page")?),
|
|
Query::HitTest => T::Bool.into(),
|
|
Query::Pick3D => CallType::nullable(nominal("RayHit3D")?),
|
|
Query::ToView | Query::ToLocal => CallType::nullable(nominal("Point")?),
|
|
Query::ScrollPosition => T::Any.into(),
|
|
_ => T::Unit.into(),
|
|
};
|
|
let effect = if matches!(
|
|
query,
|
|
Query::ScrollTo | Query::ScrollBy | Query::ScrollIntoView | Query::FitView | Query::Focus
|
|
) {
|
|
CallEffect::Mutating
|
|
} else {
|
|
CallEffect::ReadOnly
|
|
};
|
|
let parameters = if matches!(
|
|
query,
|
|
Query::HitTest
|
|
| Query::Pick3D
|
|
| Query::ToView
|
|
| Query::ToLocal
|
|
| Query::ScrollTo
|
|
| Query::ScrollBy
|
|
) {
|
|
vec![T::Number, T::Number]
|
|
} else {
|
|
vec![]
|
|
};
|
|
let mut signature = CallSignature::exact(parameters, T::Any, effect);
|
|
signature.returns = returns;
|
|
Ok(signature)
|
|
}
|
|
|
|
#[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,
|
|
unmount: bool,
|
|
}
|
|
impl NativeCallable for LifecycleCall {
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(0))
|
|
}
|
|
|
|
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
|
if self.unmount {
|
|
crate::tasks::cancel_owner(cx, self.mount)?;
|
|
}
|
|
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,
|
|
unmount: name == "onUnmount",
|
|
},
|
|
vec![function, block, mount],
|
|
))
|
|
}
|
|
|
|
impl NativeCallable for Sync {
|
|
fn arity(&self) -> Option<endor_core::CallArity> {
|
|
Some(endor_core::CallArity::exact(0))
|
|
}
|
|
|
|
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(),
|
|
scroll_offset: [0.0, 0.0],
|
|
version: self.frame.layout_version,
|
|
mounted: true,
|
|
tasks: None,
|
|
layout: None,
|
|
})?;
|
|
crate::ui_bridge::attach_desktop_type(cx, object, "Mount", self.desktop)?;
|
|
for (name, query) in [
|
|
("bounds", Query::Bounds),
|
|
("scrollTo", Query::ScrollTo),
|
|
("scrollBy", Query::ScrollBy),
|
|
("scrollPosition", Query::ScrollPosition),
|
|
("scrollIntoView", Query::ScrollIntoView),
|
|
("fitView", Query::FitView),
|
|
("pick3D", Query::Pick3D),
|
|
("toView", Query::ToView),
|
|
("toLocal", Query::ToLocal),
|
|
("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 {
|
|
signature: mount_signature(cx.store(), self.desktop, query)?,
|
|
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 {
|
|
tasks: cx.store().payload::<Mount>(object, "Mount")?.tasks,
|
|
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(),
|
|
scroll_offset: if self.preparing {
|
|
cx.store().payload::<Mount>(object, "Mount")?.scroll_offset
|
|
} else {
|
|
geometry.scroll.map(|s| [s[0], s[1]]).unwrap_or([0.0, 0.0])
|
|
},
|
|
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),
|
|
))
|
|
}
|
|
}
|
|
|
|
/// Explicitly owned roots protect the old presentation while preparation
|
|
/// temporarily replaces its desktop dictionary and Mount payloads.
|
|
pub struct Checkpoint {
|
|
table: MountTable,
|
|
dictionary: Option<ObjectRef>,
|
|
states: Vec<(ObjectRef, Mount, Option<Value>, Option<Value>)>,
|
|
roots: Vec<ObjectRef>,
|
|
}
|
|
|
|
impl Checkpoint {
|
|
pub fn release(self, vm: &mut Vm) -> Result<(), VmError> {
|
|
let mut result = Ok(());
|
|
for root in self.roots.into_iter().rev() {
|
|
if let Err(error) = vm.release(root) {
|
|
result = Err(error);
|
|
}
|
|
}
|
|
result
|
|
}
|
|
|
|
pub fn restore(
|
|
self,
|
|
vm: &mut Vm,
|
|
desktop: ObjectRef,
|
|
table: &mut MountTable,
|
|
) -> Result<(), VmError> {
|
|
let result = (|| {
|
|
let old: std::collections::HashSet<_> =
|
|
self.states.iter().map(|state| state.0).collect();
|
|
for weak in table.ordered() {
|
|
if let Some(object) = vm.upgrade(weak)? {
|
|
let result = (|| {
|
|
if !old.contains(&object) {
|
|
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
|
mount.mounted = false;
|
|
mount.layout = None;
|
|
mount.parent = None;
|
|
mount.first_child = None;
|
|
mount.next_sibling = None;
|
|
vm.replace_payload(object, mount)?;
|
|
vm.set_attr(object, "mounted", Value::Bool(false))?;
|
|
}
|
|
Ok::<_, VmError>(())
|
|
})();
|
|
vm.release(object)?;
|
|
result?;
|
|
}
|
|
}
|
|
for (object, mount, mounted, version) in &self.states {
|
|
vm.replace_payload(*object, mount.clone())?;
|
|
for (name, value) in [("mounted", mounted), ("layoutVersion", version)] {
|
|
if let Some(value) = value {
|
|
vm.set_attr(*object, name, value.clone())?;
|
|
} else {
|
|
match vm.del_attr(*object, name) {
|
|
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if let Some(dictionary) = self.dictionary {
|
|
vm.set_attr(desktop, "mounts", Value::Object(dictionary))?;
|
|
} else {
|
|
match vm.del_attr(desktop, "mounts") {
|
|
Ok(()) | Err(VmError::AttributeNotFound(_)) => {}
|
|
Err(error) => return Err(error),
|
|
}
|
|
}
|
|
*table = self.table.clone();
|
|
Ok(())
|
|
})();
|
|
let released = self.release(vm);
|
|
result?;
|
|
released
|
|
}
|
|
}
|
|
|
|
impl MountTable {
|
|
pub fn checkpoint(&self, vm: &mut Vm, desktop: ObjectRef) -> Result<Checkpoint, VmError> {
|
|
let dictionary = match vm.get_attr(desktop, "mounts") {
|
|
Ok(Value::Object(dictionary)) => Some(dictionary),
|
|
Err(VmError::AttributeNotFound(_)) => None,
|
|
Ok(_) => return Err(VmError::InvalidArgument("invalid mount dictionary")),
|
|
Err(error) => return Err(error),
|
|
};
|
|
let mut roots = Vec::new();
|
|
if let Some(dictionary) = dictionary {
|
|
vm.retain(dictionary)?;
|
|
roots.push(dictionary);
|
|
}
|
|
let mut checkpoint = Checkpoint {
|
|
table: self.clone(),
|
|
dictionary,
|
|
states: Vec::new(),
|
|
roots,
|
|
};
|
|
let result = (|| {
|
|
for weak in self.ordered() {
|
|
if let Some(object) = vm.upgrade(weak)? {
|
|
checkpoint.roots.push(object);
|
|
let mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
|
let mut attributes = Vec::new();
|
|
for name in ["mounted", "layoutVersion"] {
|
|
let value = match vm.get_attr(object, name) {
|
|
Ok(value) => Some(value),
|
|
Err(VmError::AttributeNotFound(_)) => None,
|
|
Err(error) => return Err(error),
|
|
};
|
|
if let Some(Value::Object(object)) = &value {
|
|
vm.retain(*object)?;
|
|
checkpoint.roots.push(*object);
|
|
}
|
|
attributes.push(value);
|
|
}
|
|
checkpoint.states.push((
|
|
object,
|
|
mount,
|
|
attributes.remove(0),
|
|
attributes.remove(0),
|
|
));
|
|
}
|
|
}
|
|
Ok::<_, VmError>(())
|
|
})();
|
|
if let Err(error) = result {
|
|
checkpoint.release(vm)?;
|
|
return Err(error);
|
|
}
|
|
Ok(checkpoint)
|
|
}
|
|
|
|
/// A faulted domain cannot run user onUnmount callbacks. Still detach every
|
|
/// context and release its owning dictionary through the normal heap barrier.
|
|
pub fn retire(&mut self, vm: &mut Vm, desktop: ObjectRef) -> Result<(), VmError> {
|
|
for weak in self.ordered().into_iter().rev() {
|
|
if let Some(object) = vm.upgrade(weak)? {
|
|
let result = (|| {
|
|
let mut mount = vm.payload::<Mount>(object, "Mount")?.clone();
|
|
mount.mounted = false;
|
|
mount.layout = None;
|
|
mount.parent = None;
|
|
mount.first_child = None;
|
|
mount.next_sibling = None;
|
|
vm.replace_payload(object, mount)?;
|
|
vm.set_attr(object, "mounted", Value::Bool(false))
|
|
})();
|
|
vm.release(object)?;
|
|
result?;
|
|
}
|
|
}
|
|
let dictionary = vm.new_dict()?;
|
|
let result = vm.set_attr(desktop, "mounts", Value::Object(dictionary));
|
|
vm.release(dictionary)?;
|
|
result?;
|
|
self.active.clear();
|
|
self.order.clear();
|
|
Ok(())
|
|
}
|
|
|
|
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;
|