Run source measurement and arrangement in the frame pipeline
Verify Tessera GUI / verify (push) Failing after 7m30s
Verify Tessera GUI / verify (push) Failing after 7m30s
This commit is contained in:
@@ -166,7 +166,7 @@ candidate and returns 500, retaining the previous document.
|
||||
|
||||
This stage supports live mounted UI for both complete programs and Console inputs, with immutable window frames on the existing worker.
|
||||
Native mouse/keyboard activation and code-driven callbacks share onClick/click. Managed Mount objects expose each occurrence, and property dependencies and measurement caches avoid recomputing clean visual subtrees. Arrangement still traverses occurrences.
|
||||
Tab chrome/multiple windows, trait definitions and custom visual measurement/arrangement, window-size language inputs and process isolation are still being implemented. Worker threads,
|
||||
Tab chrome/multiple windows, trait definitions and complete visual type/effect contracts, window-size language inputs and process isolation are still being implemented. Worker threads,
|
||||
source budgets and VM instruction limits are not a complete untrusted-code sandbox.
|
||||
|
||||
## Developer tools
|
||||
@@ -194,9 +194,13 @@ Saved detached Mounts expose `mounted = false`, cleared parent/child/sibling lin
|
||||
|
||||
Native visual methods are now available on Block: `measure(mount, Constraints(minWidth, minHeight, maxWidth, maxHeight))`, `arrange(mount, Bounds(x, y, width, height))`, `paint(mount, Bounds(...))` and `hitTest(mount, Point(x, y))`. The Mount must be live and belong to that exact Block occurrence. These query methods return managed MeasureResult, Arrangement and DisplayList objects, using the renderer's native geometry and painting functions. They do not publish geometry or pixels. Results are readable in Endor: `result.width`, `arrangement.children.at(0).x`, `drawing.commands.at(0).color`; indexed ObjectList results provide count, first and at(index), with traced references. Arrangement child bounds also carry their stable slot. Paint returns only that object's own commands; descendants are separate occurrences.
|
||||
|
||||
Native measurement finds intrinsic subtree size and clamps the returned size to the given constraints. Native Row/Column arrangement uses intrinsic child sizes, padding and gaps; it does not stretch children to fill the supplied bounds. Hit testing uses local coordinates against the current Mount dimensions. Coordinates/dimensions must be finite and remain within the native layout limit. Width/height and constraint ranges cannot be negative or inverted. The value constructors preserve fields as ordinary objects; methods validate their contents when used, and global annotation type checking remains pending.
|
||||
Outside a renderer layout call, native measurement queries find intrinsic native subtree size and clamp it to the given constraints. During layout, native base methods use the staged measured children of that occurrence. Native Row/Column arrangement uses child sizes, padding and gaps; it does not stretch children to fill the supplied bounds. Hit testing uses local coordinates against the current Mount dimensions. Coordinates/dimensions must be finite and remain within the native layout limit. Width/height and constraint ranges cannot be negative or inverted. The value constructors preserve fields as ordinary objects; methods validate their contents when used, and global annotation type checking remains pending.
|
||||
|
||||
`invalidate("paint")` requests a frame while retaining measurement caches; `invalidate("layout")` also clears affected intrinsic measurements; `invalidate("content")` also invalidates derived content and ancestor dependencies. The default reason is layout. Requests coalesce at the refresh boundary, cover all aliases/occurrences of the same current content and preserve Mount identities. Hidden objects do not force visible frames. See [visual-protocols.endor](examples/visual-protocols.endor). Source paint overrides now run during actual frame publication; measure/arrange overrides during layout and contexts before first measurement remain future work, so this is not the complete custom visual protocol.
|
||||
`invalidate("paint")` requests a frame while retaining measurement caches; `invalidate("layout")` also clears affected intrinsic measurements; `invalidate("content")` also invalidates derived content and ancestor dependencies. The default reason is layout. Requests coalesce at the refresh boundary, cover all aliases/occurrences of the same current content and preserve Mount identities. Hidden objects do not force visible frames. See [visual-protocols.endor](examples/visual-protocols.endor). Source measure/arrange overrides now participate in actual layout with structural Mount contexts available before first measurement. Source paint and hitTest also run in the frame/input pipeline. Explicit traits, full type/effect contracts, viewport inputs and further layout caching remain future work.
|
||||
|
||||
A source subclass can override `measure(self, mount, constraints): MeasureResult` and `arrange(self, mount, geometry): Arrangement`. The persistent renderer creates stable Mount identities and parent/child/sibling links before measuring. Measurement proceeds bottom-up with finite unconstrained page bounds (0 to 1000000); native parents and explicit native base calls use the current occurrence's measured child sizes, including source overrides. Arrangement proceeds top-down; each child Bounds must be assigned exactly once using its stable slot, with nominal result types and finite geometry checked. Source arrangements can resize or move children; omission, duplicate/foreign slots and invalid geometry reject the frame. See [source-layout.endor](examples/source-layout.endor).
|
||||
|
||||
Mount.bounds() rejects reads during measurement, including old geometry on an existing Mount. The current occurrence's allocated Bounds become available while its arrange method runs. Structural contexts are prepared before layout, geometry is committed after successful layout, and onMount/onUnmount notifications then run before paint/publication. Both layout phases share one read-only VM call and instruction budget; temporary results can be modified, existing business state cannot. Read versions are captured before hooks, so a hook changing a measured dependency schedules another pass before publication. Dependencies are currently tracked per object. Source layout methods run again for each requested frame; native measurements keep the existing shape cache, and a tree without layout overrides uses the native fast path. This is an initial unconstrained layout policy, without viewport constraint propagation, source measurement caching or a raw window-size input API.
|
||||
|
||||
Native pointer clicks now resolve source `hitTest(self, mount, point): Bool` overrides in reverse paint order, then dispatch through the existing capture/target/bubble route. The Point is local to that occurrence; `point.inside(bounds)` uses a half-open rectangle and `point.inCircle(cx, cy, radius)` includes the circle boundary. Each candidate is first restricted to its Mount rectangle, so hit shapes can refine that area, but cannot expand it. A rejected target falls through to another interactive occurrence, including a parent. Ordinary Blocks become interactive through a live click subscription or a source onEvent override; disconnecting the final click subscription updates that status. See [source-hit.endor](examples/source-hit.endor).
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
| File | Purpose |
|
||||
| --- | --- |
|
||||
| [source-layout.endor](source-layout.endor) | Source measurement reads its initial Mount parent; source arrangement moves a child and a click updates the shared layout model. |
|
||||
| [source-hit.endor](source-hit.endor) | Source circle hit testing, ordinary Block click subscriptions and fallback to the parent through transparent space. |
|
||||
| [source-paint.endor](source-paint.endor) | Source paint overrides render checked commands in the window; a source Button method updates the shared tint model. |
|
||||
| [visual-protocols.endor](visual-protocols.endor) | Real source calls for constrained native measurement, arrangement and managed paint-command results from live Mounts. |
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
class Metrics {
|
||||
width: Number
|
||||
left: Number
|
||||
function expand(self: Metrics, event: Event): Unit {
|
||||
self.width = 260
|
||||
self.left = 68
|
||||
}
|
||||
}
|
||||
class MeasuredRect: RectBlock {
|
||||
function measure(self: MeasuredRect, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
parent = mount.parent()
|
||||
return MeasureResult(parent.block.metrics.width, 60)
|
||||
}
|
||||
}
|
||||
class OffsetColumn: Column {
|
||||
metrics: Metrics
|
||||
function arrange(self: OffsetColumn, mount: Mount, geometry: Bounds): Arrangement {
|
||||
positions = Column.arrange(self, mount, geometry)
|
||||
positions.children.at(0).x = self.metrics.left
|
||||
return positions
|
||||
}
|
||||
}
|
||||
metrics = Metrics(200, 44)
|
||||
rect = MeasuredRect(80, 20, 0, "#526AC7")
|
||||
button = Button("更新布局")
|
||||
root = OffsetColumn(24, 16, rect, TextBlock("尺寸来自源码 measure,位置来自源码 arrange"), button, metrics)
|
||||
connection = button.on("click", metrics.expand)
|
||||
page = Page()
|
||||
page.render(root)
|
||||
@@ -14,6 +14,7 @@
|
||||
| events.rs | Bounded native input transport to the owning VM |
|
||||
| event_dispatch.rs | Managed Event, weak Subscription tokens, Block-owned connections and occurrence-specific capture/target/bubble through a budgeted VM driver |
|
||||
| vm_sequence.rs | Shared validated call-sequence builder for event and lifecycle batches in the existing VM frame loop |
|
||||
| source_layout.rs | Read-only bottom-up source measurement and top-down source arrangement, typed/slot validation and occurrence-specific geometry |
|
||||
| source_hit.rs | Read-only source/native pointer target selectors, reverse paint order, strict Bool results and matching press/release targets |
|
||||
| source_paint.rs | Read-only source paint dispatch, shared VM budget, checked command conversion and versioned object dependencies |
|
||||
| native_visual.rs | Managed native visual protocol queries with occurrence validation, value conversion and bounded results |
|
||||
|
||||
@@ -770,6 +770,8 @@ declarations, explicit first `self` parameters, positional field construction an
|
||||
member assignment. Inherited methods bind the actual receiver; overridden methods
|
||||
are dynamically selected through self. Concrete native visual subclasses reuse real native allocators; abstract bases remain unconstructible. Traits and annotation checking remain pending. Native Block/Page methods use the same type method tables and binding path.
|
||||
|
||||
Source measure and arrange overrides execute bottom-up and top-down in one read-only VM layout call. Structural Mount links exist before first measurement; bounds queries reject premature geometry. Native base layout uses current occurrence child measurements, and Arrangement must assign every stable child slot exactly once. Layout dependencies changed by lifecycle hooks trigger another pass before publication. Constraints are currently finite/unconstrained page inputs; viewport propagation and source measurement caching remain pending.
|
||||
|
||||
Source paint overrides execute in the actual frame pipeline under a read-only heap policy. Source hitTest overrides similarly refine native pointer targets in VM paint order; Point.inside(bounds) and Point.inCircle(cx, cy, radius) provide primitive shape queries. Managed DisplayList/ObjectList/PaintCommand and Constraints/Bounds/Point results are ordinary runtime objects.
|
||||
|
||||
Native input POST /events is a transport envelope, not Endor source. The window
|
||||
|
||||
+130
-8
@@ -452,6 +452,10 @@ impl ReplSession {
|
||||
let Some(root) = self.bridge.prepare_frame(&mut self.candidate.vm)? else {
|
||||
return Ok(last);
|
||||
};
|
||||
mutated_mounts = true;
|
||||
let frame = self
|
||||
.bridge
|
||||
.layout_frame(&mut self.candidate.vm, root.clone())?;
|
||||
let mut document = RenderDocument {
|
||||
source: self.source.clone(),
|
||||
root: Some(self.program_root.clone().unwrap_or_else(|| CodeNode {
|
||||
@@ -462,11 +466,7 @@ impl ReplSession {
|
||||
end_line: self.source.lines().count(),
|
||||
children: self.nodes.clone(),
|
||||
})),
|
||||
frame: root
|
||||
.as_ref()
|
||||
.map(|root| self.bridge.layout_frame(&self.candidate.vm, root))
|
||||
.transpose()?
|
||||
.unwrap_or_default(),
|
||||
frame,
|
||||
ui_root: root.clone(),
|
||||
};
|
||||
document.frame.session = self.id;
|
||||
@@ -475,9 +475,6 @@ impl ReplSession {
|
||||
if work > 200_000 {
|
||||
bail!("layout dependencies did not stabilize within the rendering work budget");
|
||||
}
|
||||
mutated_mounts = true;
|
||||
self.bridge
|
||||
.commit_frame(&mut self.candidate.vm, root.clone(), &document.frame)?;
|
||||
self.bridge.paint_frame(
|
||||
&mut self.candidate.vm,
|
||||
root.as_deref(),
|
||||
@@ -801,6 +798,131 @@ mod tests {
|
||||
const EXAMPLE: &str = include_str!("../examples/hello.endor");
|
||||
const PAGE: &str = include_str!("../examples/page.endor");
|
||||
|
||||
const SOURCE_LAYOUT: &str = include_str!("../examples/source-layout.endor");
|
||||
|
||||
#[test]
|
||||
fn source_layout_uses_initial_mount_links_child_measurements_and_model_dependencies() {
|
||||
let (mut session, initial) = ReplSession::load(SOURCE_LAYOUT, "app.main", 91, 1).unwrap();
|
||||
let rect = initial
|
||||
.frame
|
||||
.mounts
|
||||
.iter()
|
||||
.find(|mount| mount.width == 200.0 && mount.height == 60.0)
|
||||
.unwrap();
|
||||
assert_eq!((rect.x, rect.y), (44.0, 24.0));
|
||||
let id = rect.id;
|
||||
assert!(initial.frame.commands.iter().any(|command| matches!(command,RenderCommand::Text { value,y,.. } if value.starts_with("尺寸来自") && *y == 100.0)));
|
||||
let globals = session.candidate.globals.unwrap();
|
||||
let Value::Object(metrics) = session
|
||||
.candidate
|
||||
.vm
|
||||
.dict_get(globals, &Value::Text("metrics".into()))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("Metrics")
|
||||
};
|
||||
session
|
||||
.candidate
|
||||
.vm
|
||||
.set_attr(metrics, "width", Value::Number(260.into()))
|
||||
.unwrap();
|
||||
let next = session.refresh_document().unwrap().unwrap();
|
||||
let rect = next
|
||||
.frame
|
||||
.mounts
|
||||
.iter()
|
||||
.find(|mount| mount.id == id)
|
||||
.unwrap();
|
||||
assert_eq!((rect.x, rect.width), (44.0, 260.0));
|
||||
assert!(session.refresh_document().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn shared_block_source_measurements_belong_to_each_occurrence() {
|
||||
let source = SOURCE_LAYOUT.replace(
|
||||
"page.render(root)",
|
||||
r#"otherMetrics = Metrics(140, 12)
|
||||
other = OffsetColumn(24, 16, rect, otherMetrics)
|
||||
page.render(Row(0, 0, root, other))"#,
|
||||
);
|
||||
let (mut session, initial) = ReplSession::load(&source, "app.main", 92, 1).unwrap();
|
||||
let globals = session.candidate.globals.unwrap();
|
||||
let Value::Object(rect) = session
|
||||
.candidate
|
||||
.vm
|
||||
.dict_get(globals, &Value::Text("rect".into()))
|
||||
.unwrap()
|
||||
.unwrap()
|
||||
else {
|
||||
panic!("Rect")
|
||||
};
|
||||
let widths: Vec<_> = initial
|
||||
.frame
|
||||
.mounts
|
||||
.iter()
|
||||
.filter(|mount| mount.block_id == rect.to_string())
|
||||
.map(|mount| mount.width)
|
||||
.collect();
|
||||
assert_eq!(widths, vec![200.0, 140.0]);
|
||||
assert!(session.refresh_document().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn layout_dependencies_changed_by_mount_hook_are_measured_again_before_publication() {
|
||||
let source = r##"class Metrics { width: Number }
|
||||
class InitialRect: RectBlock {
|
||||
metrics: Metrics
|
||||
function measure(self: InitialRect, mount: Mount, constraints: Constraints): MeasureResult {
|
||||
return MeasureResult(self.metrics.width, 20)
|
||||
}
|
||||
function onMount(self: InitialRect, mount: Mount): Unit { self.metrics.width = 140 }
|
||||
}
|
||||
metrics = Metrics(80)
|
||||
rect = InitialRect(80,20,0,"#DDEEFF",metrics)
|
||||
page = Page()
|
||||
page.render(rect)"##;
|
||||
let (mut session, document) = ReplSession::load(source, "app.main", 93, 1).unwrap();
|
||||
assert!(
|
||||
document
|
||||
.frame
|
||||
.mounts
|
||||
.iter()
|
||||
.any(|mount| mount.width == 140.0 && mount.height == 20.0)
|
||||
);
|
||||
assert!(session.refresh_document().unwrap().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_layout_rejects_premature_geometry_writes_wrong_types_and_foreign_slots() {
|
||||
for (before, after, message) in [
|
||||
(
|
||||
"return MeasureResult(parent.block.metrics.width, 60)",
|
||||
"return mount.bounds()",
|
||||
"geometry is unavailable during measurement",
|
||||
),
|
||||
(
|
||||
"return MeasureResult(parent.block.metrics.width, 60)",
|
||||
"parent.block.metrics.width = 260\nreturn MeasureResult(260,60)",
|
||||
"read-only",
|
||||
),
|
||||
(
|
||||
"return MeasureResult(parent.block.metrics.width, 60)",
|
||||
"return Point(0,0)",
|
||||
"wrong result type",
|
||||
),
|
||||
(
|
||||
"positions.children.at(0).x = self.metrics.left",
|
||||
"positions.children.at(0).slot = 999999",
|
||||
"foreign child slot",
|
||||
),
|
||||
] {
|
||||
let source = SOURCE_LAYOUT.replace(before, after);
|
||||
let error = ReplSession::load(&source, "app.main", 94, 1).err().unwrap();
|
||||
assert!(error.to_string().contains(message), "{error}");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn source_hit_filters_shape_falls_back_and_requires_matching_press_release() {
|
||||
for (press, release, expected_hits, expected_background) in [
|
||||
|
||||
+60
-34
@@ -107,6 +107,31 @@ fn measure_shape(shape: &Shape) -> Result<Size> {
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn shape(node: &UiNode, children: Vec<Size>) -> Result<Shape> {
|
||||
Ok(match &node.kind {
|
||||
UiKind::Text { value, size, .. } => Shape::Text(value.clone(), *size),
|
||||
UiKind::Rect { width, height, .. } => Shape::Rect(*width, *height),
|
||||
UiKind::Spacer(size) => Shape::Rect(*size, *size),
|
||||
_ => {
|
||||
let (row, padding, gap) = container(node).context("unknown native visual protocol")?;
|
||||
Shape::Container(row, padding, gap, children)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn measure_native(node: &UiNode, children: Vec<Size>) -> Result<Size> {
|
||||
measure_shape(&shape(node, children)?)
|
||||
}
|
||||
|
||||
/// Private derived scratch space for one occurrence during layout. Contains no
|
||||
/// managed references; source code cannot mutate it through heap attributes.
|
||||
#[derive(Debug, Default)]
|
||||
pub struct Context {
|
||||
pub node: Option<std::sync::Arc<UiNode>>,
|
||||
pub children: Vec<Size>,
|
||||
pub bounds: Option<[f32; 4]>,
|
||||
}
|
||||
|
||||
pub fn paint_native(node: &UiNode, x: f32, y: f32, size: Size) -> Option<RenderCommand> {
|
||||
match &node.kind {
|
||||
UiKind::Text { value, size, color } => Some(RenderCommand::Text {
|
||||
@@ -170,12 +195,35 @@ impl LayoutRuntime {
|
||||
// Stage all cache/identity changes; a rejected frame leaves the last
|
||||
// valid occurrence registry intact and never publishes partial geometry.
|
||||
let mut next = self.clone();
|
||||
let frame = next.build(root)?;
|
||||
let frame = next.build(root, false)?;
|
||||
*self = next;
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
fn build(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
/// Allocate stable occurrence identities without measuring or publishing geometry.
|
||||
pub fn structure(&mut self, root: &UiNode) -> Result<Frame> {
|
||||
let mut next = self.clone();
|
||||
let frame = next.build(root, true)?;
|
||||
*self = next;
|
||||
Ok(frame)
|
||||
}
|
||||
|
||||
pub fn measure_node(&mut self, node: &UiNode, children: Vec<Size>) -> Result<Size> {
|
||||
let shape = shape(node, children)?;
|
||||
if let Some((_, size)) = self
|
||||
.sizes
|
||||
.get(&node.block_id)
|
||||
.filter(|(old, _)| *old == shape)
|
||||
{
|
||||
return Ok(*size);
|
||||
}
|
||||
self.measured += 1;
|
||||
let size = measure_shape(&shape)?;
|
||||
self.sizes.insert(node.block_id.clone(), (shape, size));
|
||||
Ok(size)
|
||||
}
|
||||
|
||||
fn build(&mut self, root: &UiNode, structural: bool) -> Result<Frame> {
|
||||
self.measured = 0;
|
||||
self.arranged = 0;
|
||||
let mut items = Vec::<Item<'_>>::new();
|
||||
@@ -221,36 +269,11 @@ impl LayoutRuntime {
|
||||
}
|
||||
}
|
||||
let mut sizes = vec![Size::default(); items.len()];
|
||||
for (index, item) in items.iter().enumerate().rev() {
|
||||
let shape = match &item.node.kind {
|
||||
UiKind::Text { value, size, .. } => Shape::Text(value.clone(), *size),
|
||||
UiKind::Rect { width, height, .. } => Shape::Rect(*width, *height),
|
||||
UiKind::Spacer(size) => Shape::Rect(*size, *size),
|
||||
_ => {
|
||||
let (row, padding, gap) =
|
||||
container(item.node).context("unknown native visual protocol")?;
|
||||
Shape::Container(
|
||||
row,
|
||||
padding,
|
||||
gap,
|
||||
item.children.iter().map(|i| sizes[*i]).collect(),
|
||||
)
|
||||
}
|
||||
};
|
||||
let size = if let Some((old_shape, size)) = self
|
||||
.sizes
|
||||
.get(&item.node.block_id)
|
||||
.filter(|(old, _)| *old == shape)
|
||||
{
|
||||
let _ = old_shape;
|
||||
*size
|
||||
} else {
|
||||
self.measured += 1;
|
||||
let size = measure_shape(&shape)?;
|
||||
self.sizes.insert(item.node.block_id.clone(), (shape, size));
|
||||
size
|
||||
};
|
||||
sizes[index] = size;
|
||||
if !structural {
|
||||
for (index, item) in items.iter().enumerate().rev() {
|
||||
sizes[index] = self
|
||||
.measure_node(item.node, item.children.iter().map(|i| sizes[*i]).collect())?;
|
||||
}
|
||||
}
|
||||
let mut points = vec![(0.0_f32, 0.0_f32); items.len()];
|
||||
let mut mounts = Vec::with_capacity(items.len());
|
||||
@@ -262,7 +285,7 @@ impl LayoutRuntime {
|
||||
}
|
||||
}
|
||||
for (index, item) in items.iter().enumerate() {
|
||||
self.arranged += 1;
|
||||
self.arranged += usize::from(!structural);
|
||||
let (x, y) = points[index];
|
||||
let size = sizes[index];
|
||||
bounded(x)?;
|
||||
@@ -281,9 +304,12 @@ impl LayoutRuntime {
|
||||
height: size.height,
|
||||
interactive: item.node.interactive,
|
||||
});
|
||||
if let Some(command) = paint_native(item.node, x, y, size) {
|
||||
if !structural && let Some(command) = paint_native(item.node, x, y, size) {
|
||||
commands.push(command);
|
||||
}
|
||||
if structural {
|
||||
continue;
|
||||
}
|
||||
let child_sizes: Vec<_> = item.children.iter().map(|child| sizes[*child]).collect();
|
||||
for (child, point) in
|
||||
item.children
|
||||
|
||||
@@ -19,6 +19,7 @@ mod scene;
|
||||
mod snapshot;
|
||||
mod software_renderer;
|
||||
mod source_hit;
|
||||
mod source_layout;
|
||||
mod source_paint;
|
||||
mod sources;
|
||||
mod system_ui;
|
||||
|
||||
+41
-5
@@ -20,6 +20,9 @@ pub struct Mount {
|
||||
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 {
|
||||
@@ -74,12 +77,28 @@ impl NativeCallable for Method {
|
||||
}
|
||||
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", mount.geometry.x),
|
||||
("y", mount.geometry.y),
|
||||
("width", mount.geometry.width),
|
||||
("height", mount.geometry.height),
|
||||
("x", geometry[0]),
|
||||
("y", geometry[1]),
|
||||
("width", geometry[2]),
|
||||
("height", geometry[3]),
|
||||
] {
|
||||
cx.set_attr(bounds, name, Value::Number((value as f64).into()))?;
|
||||
}
|
||||
@@ -103,6 +122,11 @@ impl NativeCallable for Method {
|
||||
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(
|
||||
@@ -164,6 +188,7 @@ struct Sync {
|
||||
old: HashMap<u64, WeakObjectRef>,
|
||||
old_order: Vec<u64>,
|
||||
frame: Frame,
|
||||
preparing: bool,
|
||||
output: Arc<Mutex<HashMap<u64, WeakObjectRef>>>,
|
||||
}
|
||||
|
||||
@@ -251,6 +276,7 @@ impl NativeCallable for Sync {
|
||||
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 [
|
||||
@@ -295,9 +321,16 @@ impl NativeCallable for Sync {
|
||||
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 !old.mounted
|
||||
if self.preparing
|
||||
|| old.layout.is_some()
|
||||
|| !old.mounted
|
||||
|| old.version != mount.version
|
||||
|| old.geometry != mount.geometry
|
||||
|| old.page != mount.page
|
||||
@@ -333,6 +366,7 @@ impl NativeCallable for Sync {
|
||||
{
|
||||
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;
|
||||
@@ -384,6 +418,7 @@ impl MountTable {
|
||||
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];
|
||||
@@ -397,6 +432,7 @@ impl MountTable {
|
||||
old: self.active.clone(),
|
||||
old_order: self.order.clone(),
|
||||
frame: frame.clone(),
|
||||
preparing,
|
||||
output: output.clone(),
|
||||
},
|
||||
captures,
|
||||
|
||||
+40
-10
@@ -22,7 +22,7 @@ fn object(value: &Value) -> Result<ObjectRef, VmError> {
|
||||
Err(invalid("visual protocol requires objects"))
|
||||
}
|
||||
}
|
||||
fn number(
|
||||
pub(crate) fn number(
|
||||
cx: &NativeContext<'_>,
|
||||
object: ObjectRef,
|
||||
name: &str,
|
||||
@@ -40,10 +40,10 @@ fn number(
|
||||
}
|
||||
Ok(value)
|
||||
}
|
||||
fn value(value: f32) -> Value {
|
||||
pub(crate) fn value(value: f32) -> Value {
|
||||
Value::Number((value as f64).into())
|
||||
}
|
||||
fn record(
|
||||
pub(crate) fn record(
|
||||
cx: &mut NativeContext<'_>,
|
||||
desktop: ObjectRef,
|
||||
kind: &str,
|
||||
@@ -93,15 +93,37 @@ impl NativeCallable for Method {
|
||||
));
|
||||
}
|
||||
let geometry = mount.geometry.clone();
|
||||
let staged = mount
|
||||
.layout
|
||||
.as_ref()
|
||||
.map(|layout| {
|
||||
let layout = layout
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout context poisoned"))?;
|
||||
Ok::<_, VmError>((layout.node.clone(), layout.children.clone()))
|
||||
})
|
||||
.transpose()?;
|
||||
let input = object(input)?;
|
||||
if matches!(self.query, Query::HitTest) {
|
||||
if staged.is_some() {
|
||||
return Err(invalid("hitTest is unavailable during layout"));
|
||||
}
|
||||
let x = number(cx, input, "x", true)?;
|
||||
let y = number(cx, input, "y", true)?;
|
||||
return Ok(NativeResult::Return(Value::Bool(
|
||||
x >= 0.0 && y >= 0.0 && x < geometry.width && y < geometry.height,
|
||||
)));
|
||||
}
|
||||
let node = block::snapshot(cx.store(), owner)?;
|
||||
if matches!(&staged, Some((None, _))) {
|
||||
return Err(invalid(
|
||||
"layout context is not prepared for this occurrence",
|
||||
));
|
||||
}
|
||||
let node = if let Some((Some(node), _)) = &staged {
|
||||
node.clone()
|
||||
} else {
|
||||
block::snapshot(cx.store(), owner)?
|
||||
};
|
||||
let result = match self.query {
|
||||
Query::Measure => {
|
||||
let min_w = number(cx, input, "minWidth", false)?;
|
||||
@@ -111,7 +133,12 @@ impl NativeCallable for Method {
|
||||
if min_w > max_w || min_h > max_h {
|
||||
return Err(invalid("minimum exceeds maximum constraint"));
|
||||
}
|
||||
let size = size(&node)?;
|
||||
let size = if let Some((_, children)) = &staged {
|
||||
layout::measure_native(&node, children.clone())
|
||||
.map_err(|_| invalid("native measurement exceeds layout limits"))?
|
||||
} else {
|
||||
size(&node)?
|
||||
};
|
||||
Value::Object(record(
|
||||
cx,
|
||||
self.desktop,
|
||||
@@ -130,11 +157,14 @@ impl NativeCallable for Method {
|
||||
if x + width > MAX_DIMENSION as f32 || y + height > MAX_DIMENSION as f32 {
|
||||
return Err(invalid("visual bounds exceed limits"));
|
||||
}
|
||||
let children: Vec<_> = node
|
||||
.children()
|
||||
.iter()
|
||||
.map(|child| size(child))
|
||||
.collect::<Result<_, _>>()?;
|
||||
let children = if let Some((_, children)) = &staged {
|
||||
children.clone()
|
||||
} else {
|
||||
node.children()
|
||||
.iter()
|
||||
.map(|child| size(child))
|
||||
.collect::<Result<Vec<_>, _>>()?
|
||||
};
|
||||
let points = layout::arrange_native(&node, x, y, &children)
|
||||
.map_err(|_| invalid("native arrangement exceeds limits"))?;
|
||||
let mut result = Vec::new();
|
||||
|
||||
@@ -0,0 +1,449 @@
|
||||
//! Bottom-up measurement and top-down arrangement share one read-only VM call.
|
||||
//! The host scratch plan contains derived data only, never a second business heap.
|
||||
use crate::{
|
||||
document::Frame,
|
||||
layout::{self, LayoutRuntime, Size},
|
||||
mount::Mount,
|
||||
native_visual::{number, record, value},
|
||||
object_list,
|
||||
ui_bridge::{MAX_DIMENSION, UiNode, desktop_type},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use endor_core::{
|
||||
CodeLayout, ContextNativeFunction, EndorCode, EndorCodeObject, Instruction, NativeCallable,
|
||||
NativeContext, NativeResult, ObjectRef, Opcode, Value, Vm, VmError, WeakObjectRef,
|
||||
};
|
||||
use std::{
|
||||
collections::{BTreeSet, HashMap},
|
||||
sync::{Arc, Mutex},
|
||||
};
|
||||
|
||||
fn invalid(message: &'static str) -> VmError {
|
||||
VmError::InvalidArgument(message)
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Plan {
|
||||
nodes: Vec<Arc<UiNode>>,
|
||||
children: Vec<Vec<usize>>,
|
||||
sizes: Vec<Size>,
|
||||
bounds: Vec<Option<[f32; 4]>>,
|
||||
runtime: LayoutRuntime,
|
||||
custom: bool,
|
||||
}
|
||||
type Shared = Arc<Mutex<Plan>>;
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
enum Phase {
|
||||
Measure,
|
||||
Arrange,
|
||||
}
|
||||
impl Phase {
|
||||
fn method(self) -> &'static str {
|
||||
match self {
|
||||
Self::Measure => "measure",
|
||||
Self::Arrange => "arrange",
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Step {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
phase: Phase,
|
||||
mount: ObjectRef,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Step {
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mount = cx.store().payload::<Mount>(self.mount, "Mount")?.clone();
|
||||
let definition = cx
|
||||
.store()
|
||||
.type_of(mount.block)?
|
||||
.ok_or(invalid("untyped layout target"))?;
|
||||
let method = cx
|
||||
.store()
|
||||
.lookup_method(definition, self.phase.method())?
|
||||
.ok_or(invalid("missing layout method"))?;
|
||||
let native = cx
|
||||
.store()
|
||||
.lookup_method(
|
||||
desktop_type(cx.store(), "Block", self.desktop)?,
|
||||
self.phase.method(),
|
||||
)?
|
||||
.ok_or(invalid("missing native layout method"))?;
|
||||
let (node, children, bounds, native_size) = {
|
||||
let mut plan = self
|
||||
.plan
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout plan poisoned"))?;
|
||||
let node = plan.nodes[self.index].clone();
|
||||
let children = plan.children[self.index]
|
||||
.iter()
|
||||
.map(|child| plan.sizes[*child])
|
||||
.collect::<Vec<_>>();
|
||||
let bounds = plan.bounds[self.index];
|
||||
let native_size = if matches!(self.phase, Phase::Measure) && method == native {
|
||||
Some(
|
||||
plan.runtime
|
||||
.measure_node(&node, children.clone())
|
||||
.map_err(|_| invalid("native measurement exceeds layout limits"))?,
|
||||
)
|
||||
} else {
|
||||
if method != native {
|
||||
plan.custom = true;
|
||||
}
|
||||
if matches!(self.phase, Phase::Measure) {
|
||||
plan.runtime.measured += 1;
|
||||
}
|
||||
None
|
||||
};
|
||||
if matches!(self.phase, Phase::Arrange) {
|
||||
plan.runtime.arranged += 1;
|
||||
}
|
||||
(node, children, bounds, native_size)
|
||||
};
|
||||
let context = mount.layout.ok_or(invalid("missing pre-layout context"))?;
|
||||
{
|
||||
let mut context = context
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout context poisoned"))?;
|
||||
context.node = Some(node);
|
||||
context.children = children;
|
||||
context.bounds = bounds;
|
||||
}
|
||||
if let Some(size) = native_size {
|
||||
return Ok(NativeResult::Return(Value::Object(record(
|
||||
cx,
|
||||
self.desktop,
|
||||
"MeasureResult",
|
||||
vec![("width", value(size.width)), ("height", value(size.height))],
|
||||
)?)));
|
||||
}
|
||||
let input = match self.phase {
|
||||
Phase::Measure => record(
|
||||
cx,
|
||||
self.desktop,
|
||||
"Constraints",
|
||||
vec![
|
||||
("minWidth", value(0.0)),
|
||||
("minHeight", value(0.0)),
|
||||
("maxWidth", value(MAX_DIMENSION as f32)),
|
||||
("maxHeight", value(MAX_DIMENSION as f32)),
|
||||
],
|
||||
)?,
|
||||
Phase::Arrange => {
|
||||
let [x, y, width, height] =
|
||||
bounds.ok_or(invalid("arrangement has no allocated bounds"))?;
|
||||
record(
|
||||
cx,
|
||||
self.desktop,
|
||||
"Bounds",
|
||||
vec![
|
||||
("x", value(x)),
|
||||
("y", value(y)),
|
||||
("width", value(width)),
|
||||
("height", value(height)),
|
||||
],
|
||||
)?
|
||||
}
|
||||
};
|
||||
Ok(NativeResult::Invoke {
|
||||
function: method,
|
||||
arguments: vec![
|
||||
Value::Object(mount.block),
|
||||
Value::Object(self.mount),
|
||||
Value::Object(input),
|
||||
],
|
||||
})
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Collect {
|
||||
plan: Shared,
|
||||
index: usize,
|
||||
phase: Phase,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Collect {
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let [Value::Object(result)] = args else {
|
||||
return Err(invalid("layout must return a managed result"));
|
||||
};
|
||||
let expected = match self.phase {
|
||||
Phase::Measure => "MeasureResult",
|
||||
Phase::Arrange => "Arrangement",
|
||||
};
|
||||
if !cx
|
||||
.store()
|
||||
.is_instance(*result, desktop_type(cx.store(), expected, self.desktop)?)?
|
||||
{
|
||||
return Err(invalid("layout returned the wrong result type"));
|
||||
}
|
||||
let mut plan = self
|
||||
.plan
|
||||
.lock()
|
||||
.map_err(|_| invalid("layout plan poisoned"))?;
|
||||
match self.phase {
|
||||
Phase::Measure => {
|
||||
let size = Size {
|
||||
width: number(cx, *result, "width", false)?,
|
||||
height: number(cx, *result, "height", false)?,
|
||||
};
|
||||
plan.sizes[self.index] = size;
|
||||
if self.index == 0 {
|
||||
plan.bounds[0] = Some([0.0, 0.0, size.width, size.height]);
|
||||
}
|
||||
}
|
||||
Phase::Arrange => {
|
||||
let Value::Object(list) = cx.store().get_attr(*result, "children")? else {
|
||||
return Err(invalid("Arrangement.children must be an ObjectList"));
|
||||
};
|
||||
let items = object_list::items(cx.store(), list)?;
|
||||
let children = plan.children[self.index].clone();
|
||||
if items.len() != children.len() {
|
||||
return Err(invalid(
|
||||
"Arrangement must assign every child slot exactly once",
|
||||
));
|
||||
}
|
||||
let slots: HashMap<_, _> = plan.nodes[self.index]
|
||||
.slots
|
||||
.iter()
|
||||
.copied()
|
||||
.enumerate()
|
||||
.map(|(index, slot)| (slot, index))
|
||||
.collect();
|
||||
let mut assigned = BTreeSet::new();
|
||||
let expected = desktop_type(cx.store(), "Bounds", self.desktop)?;
|
||||
for item in items {
|
||||
if !cx.store().is_instance(*item, expected)? {
|
||||
return Err(invalid("Arrangement child must be Bounds"));
|
||||
}
|
||||
let Value::Number(slot) = cx.store().get_attr(*item, "slot")? else {
|
||||
return Err(invalid("Arrangement child requires a slot"));
|
||||
};
|
||||
let slot = slot
|
||||
.as_i64()
|
||||
.and_then(|value| u64::try_from(value).ok())
|
||||
.ok_or(invalid("invalid arrangement slot"))?;
|
||||
let position = slots
|
||||
.get(&slot)
|
||||
.copied()
|
||||
.ok_or(invalid("Arrangement references a foreign child slot"))?;
|
||||
if !assigned.insert(slot) {
|
||||
return Err(invalid("duplicate arrangement slot"));
|
||||
}
|
||||
let bounds = [
|
||||
number(cx, *item, "x", false)?,
|
||||
number(cx, *item, "y", false)?,
|
||||
number(cx, *item, "width", false)?,
|
||||
number(cx, *item, "height", false)?,
|
||||
];
|
||||
if bounds[0] + bounds[2] > MAX_DIMENSION as f32
|
||||
|| bounds[1] + bounds[3] > MAX_DIMENSION as f32
|
||||
{
|
||||
return Err(invalid("arrangement bounds exceed limits"));
|
||||
}
|
||||
plan.bounds[children[position]] = Some(bounds);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(NativeResult::Return(Value::Nil))
|
||||
}
|
||||
}
|
||||
#[derive(Debug)]
|
||||
struct Prepare {
|
||||
plan: Shared,
|
||||
mounts: Vec<WeakObjectRef>,
|
||||
desktop: ObjectRef,
|
||||
}
|
||||
impl NativeCallable for Prepare {
|
||||
fn invoke(&self, cx: &mut NativeContext<'_>, _: &[Value]) -> Result<NativeResult, VmError> {
|
||||
let mut instructions = Vec::new();
|
||||
let mut constants = Vec::new();
|
||||
for phase in [Phase::Measure, Phase::Arrange] {
|
||||
let indices: Vec<_> = match phase {
|
||||
Phase::Measure => (0..self.mounts.len()).rev().collect(),
|
||||
Phase::Arrange => (0..self.mounts.len()).collect(),
|
||||
};
|
||||
for index in indices {
|
||||
let mount = cx
|
||||
.upgrade(self.mounts[index])?
|
||||
.ok_or(invalid("layout Mount expired"))?;
|
||||
let step = cx.allocate(ContextNativeFunction::new(
|
||||
"layout step",
|
||||
Step {
|
||||
plan: self.plan.clone(),
|
||||
index,
|
||||
phase,
|
||||
mount,
|
||||
desktop: self.desktop,
|
||||
},
|
||||
vec![mount, self.desktop],
|
||||
))?;
|
||||
let collector = cx.allocate(ContextNativeFunction::new(
|
||||
"collect layout",
|
||||
Collect {
|
||||
plan: self.plan.clone(),
|
||||
index,
|
||||
phase,
|
||||
desktop: self.desktop,
|
||||
},
|
||||
vec![self.desktop],
|
||||
))?;
|
||||
let start = constants.len() as u32;
|
||||
constants.extend([Value::Object(collector), Value::Object(step)]);
|
||||
instructions.extend([
|
||||
Instruction::new(Opcode::LoadConst, start),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::LoadConst, start + 1),
|
||||
Instruction::simple(Opcode::PushNull),
|
||||
Instruction::new(Opcode::Call, 0),
|
||||
Instruction::new(Opcode::Call, 1),
|
||||
Instruction::simple(Opcode::PopTop),
|
||||
]);
|
||||
}
|
||||
}
|
||||
let end = constants.len() as u32;
|
||||
constants.push(Value::Nil);
|
||||
instructions.extend([
|
||||
Instruction::new(Opcode::LoadConst, end),
|
||||
Instruction::simple(Opcode::ReturnValue),
|
||||
]);
|
||||
let code = EndorCode::from_instructions(&instructions, constants, CodeLayout::default())?
|
||||
.with_location("<source layout>", "<tessera>", 1);
|
||||
let code = cx.allocate(EndorCodeObject::new(code))?;
|
||||
let globals = cx.new_dict()?;
|
||||
let function = cx.new_function("source layout", code, globals)?;
|
||||
Ok(NativeResult::Invoke {
|
||||
function,
|
||||
arguments: vec![],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(
|
||||
vm: &mut Vm,
|
||||
desktop: ObjectRef,
|
||||
mounts: Vec<WeakObjectRef>,
|
||||
root: Arc<UiNode>,
|
||||
frame: &mut Frame,
|
||||
runtime: &mut LayoutRuntime,
|
||||
) -> Result<BTreeSet<ObjectRef>> {
|
||||
// Preserve the existing native measurement cache and avoid VM/result
|
||||
// allocation for trees without custom layout methods. Contexts already exist.
|
||||
let block_type = desktop_type(vm.object_store(), "Block", desktop)?;
|
||||
let native_measure = vm.object_store().lookup_method(block_type, "measure")?;
|
||||
let native_arrange = vm.object_store().lookup_method(block_type, "arrange")?;
|
||||
let mut custom = false;
|
||||
for weak in &mounts {
|
||||
let mount = vm
|
||||
.upgrade(*weak)?
|
||||
.ok_or_else(|| anyhow::anyhow!("layout Mount expired"))?;
|
||||
let result = (|| {
|
||||
let block = vm.payload::<Mount>(mount, "Mount")?.block;
|
||||
let definition = vm
|
||||
.object_store()
|
||||
.type_of(block)?
|
||||
.ok_or(invalid("untyped layout target"))?;
|
||||
Ok::<_, VmError>(
|
||||
vm.object_store().lookup_method(definition, "measure")? != native_measure
|
||||
|| vm.object_store().lookup_method(definition, "arrange")? != native_arrange,
|
||||
)
|
||||
})();
|
||||
vm.release(mount)?;
|
||||
custom |= result?;
|
||||
}
|
||||
if !custom {
|
||||
let mut native = runtime.frame(&root)?;
|
||||
// Only the structural pass consumes a published version.
|
||||
native.layout_version = frame.layout_version;
|
||||
runtime.version = frame.layout_version;
|
||||
*frame = native;
|
||||
return Ok(BTreeSet::new());
|
||||
}
|
||||
let mut nodes = Vec::new();
|
||||
let mut work = vec![root];
|
||||
while let Some(node) = work.pop() {
|
||||
work.extend(node.children().iter().rev().cloned());
|
||||
nodes.push(node);
|
||||
}
|
||||
anyhow::ensure!(
|
||||
nodes.len() == mounts.len() && nodes.len() == frame.mounts.len(),
|
||||
"layout structure does not match Mount contexts"
|
||||
);
|
||||
let indices: HashMap<_, _> = frame
|
||||
.mounts
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, mount)| (mount.id, index))
|
||||
.collect();
|
||||
let mut children = vec![Vec::new(); nodes.len()];
|
||||
for (index, mount) in frame.mounts.iter().enumerate() {
|
||||
if let Some(parent) = mount.parent {
|
||||
children[indices[&parent]].push(index);
|
||||
}
|
||||
}
|
||||
let plan = Arc::new(Mutex::new(Plan {
|
||||
sizes: vec![Size::default(); nodes.len()],
|
||||
bounds: vec![None; nodes.len()],
|
||||
nodes,
|
||||
children,
|
||||
runtime: runtime.clone(),
|
||||
custom: false,
|
||||
}));
|
||||
let prepare = vm.allocate(ContextNativeFunction::new(
|
||||
"prepare source layout",
|
||||
Prepare {
|
||||
plan: plan.clone(),
|
||||
mounts,
|
||||
desktop,
|
||||
},
|
||||
vec![desktop],
|
||||
))?;
|
||||
let result = vm.call_read_only_with_limits(
|
||||
prepare,
|
||||
&[],
|
||||
endor_core::ExecutionLimits {
|
||||
max_frames: 128,
|
||||
max_instructions: 1_000_000,
|
||||
},
|
||||
);
|
||||
vm.release(prepare)?;
|
||||
let (value, reads) = result.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
|
||||
vm.release_value(value)?;
|
||||
let plan = plan
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("layout plan poisoned"))?;
|
||||
frame.commands.clear();
|
||||
for (index, mount) in frame.mounts.iter_mut().enumerate() {
|
||||
let [x, y, width, height] =
|
||||
plan.bounds[index].ok_or_else(|| anyhow::anyhow!("unarranged occurrence"))?;
|
||||
mount.x = x;
|
||||
mount.y = y;
|
||||
mount.width = width;
|
||||
mount.height = height;
|
||||
if let Some(command) =
|
||||
layout::paint_native(&plan.nodes[index], x, y, Size { width, height })
|
||||
{
|
||||
command.validate()?;
|
||||
frame.commands.push(command);
|
||||
}
|
||||
}
|
||||
*runtime = plan.runtime.clone();
|
||||
if !plan.custom {
|
||||
return Ok(BTreeSet::new());
|
||||
}
|
||||
let registry = match vm.get_attr(desktop, "types")? {
|
||||
Value::Object(object) => object,
|
||||
_ => anyhow::bail!("missing type registry"),
|
||||
};
|
||||
Ok(reads
|
||||
.into_iter()
|
||||
.filter(|object| {
|
||||
*object != desktop
|
||||
&& *object != registry
|
||||
&& vm
|
||||
.object(*object)
|
||||
.is_ok_and(|object| !object.payload().as_any().is::<Mount>())
|
||||
})
|
||||
.collect())
|
||||
}
|
||||
+93
-15
@@ -180,6 +180,7 @@ struct Session {
|
||||
published_version: u64,
|
||||
force_frame: bool,
|
||||
paint_reads: std::collections::BTreeMap<ObjectRef, u64>,
|
||||
layout_reads: std::collections::BTreeMap<ObjectRef, u64>,
|
||||
snapshot_visited: usize,
|
||||
snapshot_reused: usize,
|
||||
}
|
||||
@@ -1151,6 +1152,7 @@ impl UiBridge {
|
||||
if state
|
||||
.paint_reads
|
||||
.iter()
|
||||
.chain(state.layout_reads.iter())
|
||||
.any(|(object, version)| vm.change_version(*object).ok() != Some(*version))
|
||||
{
|
||||
state.force_frame = true;
|
||||
@@ -1188,7 +1190,9 @@ impl UiBridge {
|
||||
let page = state.pending.unwrap_or(state.active);
|
||||
let desktop = state.desktop;
|
||||
let nodes = state.nodes.clone();
|
||||
let hooks = state.mounts.synchronize(vm, desktop, page, nodes, frame)?;
|
||||
let hooks = state
|
||||
.mounts
|
||||
.synchronize(vm, desktop, page, nodes, frame, false)?;
|
||||
let result = (|| {
|
||||
if snapshot.is_none() {
|
||||
state.layout.clear_occurrences();
|
||||
@@ -1263,20 +1267,93 @@ impl UiBridge {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn layout_frame(&self, vm: &Vm, root: &UiNode) -> Result<crate::document::Frame> {
|
||||
let mut state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||||
let mut frame = state.layout.frame(root)?;
|
||||
frame.focus = match vm.get_attr(state.desktop, "focus") {
|
||||
Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()),
|
||||
_ => None,
|
||||
pub fn layout_frame(
|
||||
&self,
|
||||
vm: &mut Vm,
|
||||
root: Option<Arc<UiNode>>,
|
||||
) -> Result<crate::document::Frame> {
|
||||
let Some(root) = root else {
|
||||
self.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
|
||||
.layout_reads
|
||||
.clear();
|
||||
let frame = crate::document::Frame::default();
|
||||
self.commit_frame(vm, None, &frame)?;
|
||||
return Ok(frame);
|
||||
};
|
||||
frame.focus = frame
|
||||
.focus
|
||||
.filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive));
|
||||
Ok(frame)
|
||||
let (mut layout, desktop, page, nodes) = {
|
||||
let state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||||
(
|
||||
state.layout.clone(),
|
||||
state.desktop,
|
||||
state.pending.unwrap_or(state.active),
|
||||
state.nodes.clone(),
|
||||
)
|
||||
};
|
||||
let mut frame = layout.structure(&root)?;
|
||||
let (hooks, mounts) = {
|
||||
let mut state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||||
let hooks = state
|
||||
.mounts
|
||||
.synchronize(vm, desktop, page, nodes, &frame, true)?;
|
||||
(hooks, state.mounts.ordered())
|
||||
};
|
||||
let result = (|| {
|
||||
let reads = crate::source_layout::render(
|
||||
vm,
|
||||
desktop,
|
||||
mounts,
|
||||
root.clone(),
|
||||
&mut frame,
|
||||
&mut layout,
|
||||
)?;
|
||||
// Capture versions before lifecycle hooks can change a dependency.
|
||||
let reads = reads
|
||||
.into_iter()
|
||||
.map(|object| vm.change_version(object).map(|version| (object, version)))
|
||||
.collect::<Result<std::collections::BTreeMap<_, _>, _>>()?;
|
||||
frame.focus = match vm.get_attr(desktop, "focus") {
|
||||
Ok(Value::Number(id)) => id.as_i64().and_then(|n| u64::try_from(n).ok()),
|
||||
_ => None,
|
||||
};
|
||||
frame.focus = frame
|
||||
.focus
|
||||
.filter(|id| frame.mounts.iter().any(|m| m.id == *id && m.interactive));
|
||||
{
|
||||
let mut state = self
|
||||
.session
|
||||
.lock()
|
||||
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?;
|
||||
state.layout = layout;
|
||||
state.layout_reads = reads;
|
||||
}
|
||||
self.commit_frame(vm, Some(root), &frame)?;
|
||||
if let Some(hooks) = hooks {
|
||||
let value = vm
|
||||
.call_with_limits(
|
||||
hooks,
|
||||
&[],
|
||||
endor_core::ExecutionLimits {
|
||||
max_frames: 128,
|
||||
max_instructions: 1_000_000,
|
||||
},
|
||||
)
|
||||
.map_err(|error| crate::endor_host::runtime_error(vm, error))?;
|
||||
vm.release_value(value)?;
|
||||
}
|
||||
Ok(frame)
|
||||
})();
|
||||
if let Some(hooks) = hooks {
|
||||
vm.release(hooks)?;
|
||||
}
|
||||
result
|
||||
}
|
||||
pub fn rendered(&self, vm: &Vm) -> Result<Option<Arc<UiNode>>> {
|
||||
let state = self
|
||||
@@ -1313,6 +1390,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
|
||||
published_version: 0,
|
||||
force_frame: false,
|
||||
paint_reads: std::collections::BTreeMap::new(),
|
||||
layout_reads: std::collections::BTreeMap::new(),
|
||||
snapshot_visited: 0,
|
||||
snapshot_reused: 0,
|
||||
}));
|
||||
@@ -1560,7 +1638,7 @@ pub fn attach_desktop_type(
|
||||
cx.attach_type(instance, definition)
|
||||
}
|
||||
|
||||
fn desktop_type(
|
||||
pub(crate) fn desktop_type(
|
||||
store: &endor_core::ObjectStore,
|
||||
name: &str,
|
||||
desktop: ObjectRef,
|
||||
|
||||
Reference in New Issue
Block a user