Add managed Window resource and native shell operations
Verify Tessera GUI / verify (push) Failing after 7m25s

This commit is contained in:
2026-10-21 17:12:55 +08:00
parent 3ea4bac48c
commit 76e293d3b8
11 changed files with 681 additions and 41 deletions
+1
View File
@@ -13,6 +13,7 @@
| mount.rs | Managed per-occurrence contexts, weak reverse links, ordered mount/unmount hooks, geometry and focus queries |
| 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 |
| window.rs | Managed native Window resource, ordered Page roots, read-only guards and coalesced scalar GUI projections |
| vm_sequence.rs | Shared validated call-sequence builder for event and lifecycle batches in the existing VM frame loop |
| source_layout.rs | Read-only source layout, per-Mount measurement caches with subcall dependencies, viewport constraints, typed/slot validation and occurrence geometry |
| source_hit.rs | Read-only source/native pointer target selectors, reverse paint order, strict Bool results and matching press/release targets |
+62 -20
View File
@@ -441,17 +441,31 @@ impl Worker {
return;
}
let id = runtime.id;
let result = catch_unwind(AssertUnwindSafe(|| runtime.resize(viewport)));
let result = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
let document = runtime.resize(viewport)?;
Ok((document, runtime.take_window_commands()?))
}));
let mut current = match namespace.write() {
Ok(current) => current,
Err(_) => return,
};
if current.submission != epoch || current.document.frame.session != id {
if current.submission != epoch || current.runtime_session != id {
return;
}
let document = match result {
Ok(Ok(Some(document))) => document,
Ok(Ok(None)) => return,
let (document, commands) = match result {
Ok(Ok((document, commands))) => {
if let Some(document) = document {
(document, Some(commands))
} else {
let projected = !commands.is_empty();
current.publish_window_commands(id, commands);
drop(current);
if projected {
notify();
}
return;
}
}
failure => {
let mut document = current.document.clone();
document.frame.fault = Some(match failure {
@@ -461,13 +475,18 @@ impl Worker {
.collect(),
_ => {
self.repl = None;
current.runtime_session = 0;
current.window_commands.clear();
"Runtime discarded after an internal viewport failure".into()
}
});
document
(document, None)
}
};
let changed = matches!(current.commit(epoch, document), Ok(Some(_)));
if changed && let Some(commands) = commands {
current.publish_window_commands(id, commands);
}
drop(current);
if changed {
notify();
@@ -499,6 +518,8 @@ impl Worker {
if let Ok(mut current) = namespace.write() {
current.document.frame.session = 0;
current.document.frame.focus = None;
current.runtime_session = 0;
current.window_commands.clear();
}
notify();
return json(
@@ -547,21 +568,32 @@ impl Worker {
.as_ref()
.is_none_or(|(current, _)| *current != epoch)
{
self.next_session += 1;
self.next_session = match self.next_session.checked_add(1) {
Some(id) => id,
None => return error_response(StatusCode(500), "session identities exhausted"),
};
let viewport = namespace.read().ok().and_then(|current| current.viewport);
match endor_host::ReplSession::new_with_viewport(self.next_session, viewport) {
Ok(session) => self.repl = Some((epoch, session)),
Ok(session) => {
if let Ok(mut current) = namespace.write()
&& current.submission == epoch
{
current.publish_window_commands(session.id, Vec::new());
}
self.repl = Some((epoch, session));
}
Err(error) => return error_response(StatusCode(500), error.to_string()),
}
}
let session = &mut self.repl.as_mut().expect("initialized REPL").1;
let id = session.id;
let result = catch_unwind(AssertUnwindSafe(|| {
if let Some(event) = &event {
session.dispatch(event)
let result = catch_unwind(AssertUnwindSafe(|| -> Result<_> {
let result = if let Some(event) = &event {
session.dispatch(event)?
} else {
session.execute(&job.source, &job.namespace)
}
session.execute(&job.source, &job.namespace)?
};
Ok((result, session.take_window_commands()?))
}));
let faulted = session.is_faulted();
let Ok(mut current) = namespace.write() else {
@@ -573,7 +605,8 @@ impl Worker {
return superseded(job.submission);
}
match result {
Ok(Ok(result)) => {
Ok(Ok((result, commands))) => {
let projected = !commands.is_empty();
let rendered = result.document.is_some();
if let Some(document) = result.document {
match current.commit(epoch, document) {
@@ -582,9 +615,10 @@ impl Worker {
Err(error) => return error_response(StatusCode(500), error.to_string()),
}
}
current.publish_window_commands(id, commands);
let revision = current.revision;
drop(current);
if rendered {
if rendered || projected {
notify();
}
json(
@@ -606,6 +640,8 @@ impl Worker {
let mut document = current.document.clone();
document.frame.fault =
Some("Runtime discarded after an internal execution failure".into());
current.runtime_session = 0;
current.window_commands.clear();
let _ = current.commit(epoch, document);
drop(current);
self.repl = None;
@@ -640,17 +676,19 @@ impl Worker {
.read()
.map_err(|_| anyhow::anyhow!("namespace lock poisoned"))?
.viewport;
if viewport.is_some() {
let (runtime, document) = if viewport.is_some() {
endor_host::ReplSession::load_with_viewport(
&job.source,
&job.namespace,
job.submission,
id,
viewport,
)
)?
} else {
endor_host::ReplSession::load(&job.source, &job.namespace, job.submission, id)
}
endor_host::ReplSession::load(&job.source, &job.namespace, job.submission, id)?
};
let commands = runtime.take_window_commands()?;
Ok::<_, anyhow::Error>((runtime, document, commands))
}));
let Ok(mut current) = namespace.write() else {
return error_response(StatusCode(500), "namespace lock poisoned");
@@ -659,8 +697,10 @@ impl Worker {
return superseded(job.submission);
}
match result {
Ok(Ok((runtime, document))) => match current.commit(job.submission, document) {
Ok(Ok((runtime, document, commands))) => match current.commit(job.submission, document)
{
Ok(Some(revision)) => {
current.publish_window_commands(id, commands);
self.repl = Some((job.submission, runtime));
drop(current);
notify();
@@ -848,6 +888,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.
Window is a host-provided managed resource: ui.window(), page.window(), page.open(window), window.pages()/id()/viewport(), resize(width,height), setTitle(text) and focus(). Requests project to the actual main native window only after successful input/candidate completion and carry the current runtime session. Resize is asynchronous and the reported viewport is authoritative; this host accepts 1..4096 logical dimensions with a native 720x480 minimum. Failed input/candidates discard pending requests and read-only rendering rejects window effects. Additional windows, Window() construction, tab UI and detach/move remain pending.
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. Page constraints now use the current logical window viewport. Column/Button propagate cross-axis width and Row propagates cross-axis height; desired sizes are clamped. ui.viewport() provides a managed snapshot whose private input participates in rendering dependencies. Native resize updates are coalesced on the same worker without adding source inputs. Source measurement caches use Mount/method/children/constraints and actual subcall object reads, including returned-result decoding; Mount-reading methods conservatively bypass caching. ui.stats() exposes sourceMeasureHits/sourceMeasureMisses outside read-only rendering. General constraint negotiation, wrapping and scrolling 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.
+34 -1
View File
@@ -75,6 +75,33 @@ impl TesseraApp {
})
}
fn apply_window_commands(&self) {
let Some(window) = &self.window else {
return;
};
let Ok(mut namespace) = self.namespace.write() else {
return;
};
let commands = std::mem::take(&mut namespace.window_commands);
for envelope in commands {
if envelope.session == 0
|| envelope.session != namespace.runtime_session
|| envelope.command.window != crate::window::MAIN_WINDOW
{
continue;
}
match envelope.command.action {
crate::window::Action::Resize { width, height } => {
let _ = window.request_inner_size(LogicalSize::new(width, height));
}
crate::window::Action::SetTitle(title) => window.set_title(&title),
crate::window::Action::Focus => window.focus_window(),
}
}
drop(namespace);
self.publish_viewport();
}
fn publish_viewport(&self) {
let Some(window) = &self.window else {
return;
@@ -110,6 +137,7 @@ impl TesseraApp {
}
fn document_updated(&mut self) {
self.apply_window_commands();
if let Ok(namespace) = self.namespace.read() {
self.developer_tools.document_updated(&namespace.document);
let frame = &namespace.document.frame;
@@ -373,7 +401,11 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
let attributes = WindowAttributes::default()
.with_title("Tessera")
.with_inner_size(LogicalSize::new(1_280.0, 800.0))
.with_min_inner_size(LogicalSize::new(720.0, 480.0));
.with_min_inner_size(LogicalSize::new(720.0, 480.0))
.with_max_inner_size(LogicalSize::new(
crate::window::MAX_WINDOW_DIMENSION,
crate::window::MAX_WINDOW_DIMENSION,
));
let window = Arc::new(
event_loop
.create_window(attributes)
@@ -387,6 +419,7 @@ impl ApplicationHandler<AppEvent> for TesseraApp {
self.window = Some(window.clone());
self.context = Some(context);
self.surface = Some(surface);
self.apply_window_commands();
self.publish_viewport();
window.request_redraw();
}
+56
View File
@@ -19,6 +19,8 @@ pub struct NamespaceState {
pub submission: u64,
pub document: RenderDocument,
pub viewport: Option<crate::layout::Size>,
pub runtime_session: u64,
pub window_commands: Vec<crate::window::Envelope>,
}
impl Default for NamespaceState {
@@ -29,11 +31,32 @@ impl Default for NamespaceState {
submission: 0,
document: RenderDocument::default(),
viewport: None,
runtime_session: 0,
window_commands: Vec::new(),
}
}
}
impl NamespaceState {
pub fn publish_window_commands(&mut self, session: u64, commands: Vec<crate::window::Command>) {
if self.runtime_session != session {
self.window_commands.clear();
}
self.runtime_session = session;
if session == 0 {
self.window_commands.clear();
return;
}
for command in commands {
self.window_commands
.retain(|old| !old.command.same_kind(&command));
self.window_commands
.push(crate::window::Envelope { session, command });
}
self.window_commands
.sort_by_key(|command| command.command.sequence);
}
pub fn next_submission(&self) -> Result<u64> {
self.submission
.checked_add(1)
@@ -61,6 +84,8 @@ impl NamespaceState {
.context("revision counter exhausted")?;
self.submission = submission;
self.document = RenderDocument::default();
self.runtime_session = 0;
self.window_commands.clear();
self.revision = revision;
Ok(revision)
}
@@ -477,3 +502,34 @@ mod tests {
);
}
}
#[cfg(test)]
mod window_projection_tests {
use super::*;
#[test]
fn replacement_and_reset_discard_old_window_projections() {
let mut state = NamespaceState::default();
let command = crate::window::Command {
window: 1,
sequence: 1,
action: crate::window::Action::Focus,
};
state.publish_window_commands(1, vec![command.clone()]);
let resize = crate::window::Command {
window: 1,
sequence: 2,
action: crate::window::Action::Resize {
width: 900.0,
height: 600.0,
},
};
state.publish_window_commands(1, vec![resize.clone()]);
assert_eq!(state.window_commands.len(), 2);
state.publish_window_commands(2, vec![resize]);
assert_eq!(state.window_commands.len(), 1);
assert_eq!(state.window_commands[0].session, 2);
state.clear().unwrap();
assert_eq!(state.runtime_session, 0);
assert!(state.window_commands.is_empty());
}
}
+144
View File
@@ -454,6 +454,7 @@ impl ReplSession {
})
})();
if result.is_err() {
self.take_window_commands()?;
self.bridge.discard_pending(&mut self.candidate.vm)?;
if let Some(node) = self.nodes.last_mut() {
node.name
@@ -510,6 +511,10 @@ impl ReplSession {
result
}
pub fn take_window_commands(&self) -> Result<Vec<crate::window::Command>> {
self.bridge.take_window_commands(&self.candidate.vm)
}
pub fn resize(
&mut self,
viewport: Option<crate::layout::Size>,
@@ -522,6 +527,7 @@ impl ReplSession {
}
let result = self.refresh_document();
if result.is_err() {
self.take_window_commands()?;
self.bridge.discard_pending(&mut self.candidate.vm)?;
}
self.bridge
@@ -556,6 +562,7 @@ impl ReplSession {
})
})();
if result.is_err() {
self.take_window_commands()?;
self.faulted = true;
self.bridge.discard_pending(&mut self.candidate.vm)?;
}
@@ -838,6 +845,143 @@ mod tests {
const EXAMPLE: &str = include_str!("../examples/hello.endor");
const PAGE: &str = include_str!("../examples/page.endor");
#[test]
fn managed_window_requests_are_coalesced_and_failed_input_discards_pending_requests() {
let mut session = ReplSession::new(111).unwrap();
session
.execute(
r#"window = ui.window()
window.resize(900,600)
window.resize(1000,700)
window.setTitle("Endor")
window.focus()"#,
"app.main",
)
.unwrap();
let requests = session.take_window_commands().unwrap();
assert_eq!(requests.len(), 3);
assert!(matches!(
requests[0].action,
crate::window::Action::Resize {
width: 1000.0,
height: 700.0
}
));
assert!(
matches!(&requests[1].action,crate::window::Action::SetTitle(title) if title=="Endor")
);
assert!(session.take_window_commands().unwrap().is_empty());
assert!(
session
.execute("window.resize(1400,900)\nwindow.resize(0,600)", "app.main")
.is_err()
);
assert!(session.take_window_commands().unwrap().is_empty());
assert!(session.execute("window.resize(0,600)", "app.main").is_err());
assert!(session.take_window_commands().unwrap().is_empty());
}
#[test]
fn window_page_roots_follow_open_close_and_creation_order() {
let source = r#"first = Page()
second = Page()
first.render(TextBlock("first"))
second.render(TextBlock("second"))
window = ui.window()"#;
let (mut session, _) = ReplSession::load(source, "app.main", 112, 1).unwrap();
assert_eq!(
session
.execute("window.pages().count", "app.main")
.unwrap()
.value,
"2"
);
let first = session.execute("first.window()", "app.main").unwrap().value;
assert_eq!(
first,
session
.execute("second.window()", "app.main")
.unwrap()
.value
);
session.execute("first.close()", "app.main").unwrap();
assert_eq!(
session.execute("first.window()", "app.main").unwrap().value,
"nil"
);
assert_eq!(
session
.execute("window.pages().count", "app.main")
.unwrap()
.value,
"1"
);
session.execute("first.open(window)", "app.main").unwrap();
assert_eq!(
session
.execute("window.pages().count", "app.main")
.unwrap()
.value,
"2"
);
assert_eq!(
session
.execute("window.pages().at(0)", "app.main")
.unwrap()
.value,
session.execute("second", "app.main").unwrap().value
);
assert!(session.execute("first.open(Object())", "app.main").is_err());
}
#[test]
fn closed_page_is_not_kept_alive_by_window_page_order() {
let (mut session, _) = ReplSession::load(
"page = Page()\npage.render(TextBlock(\"page\"))",
"app.main",
113,
1,
)
.unwrap();
let globals = session.candidate.globals.unwrap();
let Value::Object(page) = session
.candidate
.vm
.dict_get(globals, &Value::Text("page".into()))
.unwrap()
.unwrap()
else {
panic!("Page")
};
let weak = session.candidate.vm.downgrade(page).unwrap();
session.execute("page.close()", "app.main").unwrap();
session
.candidate
.vm
.dict_delete(globals, &Value::Text("page".into()))
.unwrap();
session.candidate.vm.collect_cycles();
assert!(session.candidate.vm.upgrade(weak).unwrap().is_none());
}
#[test]
fn rendering_cannot_enqueue_native_window_side_effects() {
let source = r##"class BadRect: RectBlock {
function measure(self: BadRect, mount: Mount, constraints: Constraints): MeasureResult {
ui.window().resize(900,600)
return MeasureResult(80,20)
}
}
page = Page()
page.render(BadRect(80,20,0,"#DDEEFF"))"##;
let error = ReplSession::load(source, "app.main", 114, 1).err().unwrap();
assert!(
error
.to_string()
.contains("window operations are forbidden")
);
}
fn source_measure_stat(session: &mut ReplSession, name: &str) -> i64 {
let Value::Object(function) = session
.candidate
+1
View File
@@ -26,6 +26,7 @@ mod system_ui;
mod text;
mod ui_bridge;
mod vm_sequence;
mod window;
use std::sync::{Arc, RwLock};
+65 -19
View File
@@ -29,6 +29,7 @@ pub const TYPE_NAMES: &[&str] = &[
"Column",
"Button",
"Page",
"Window",
"Mount",
"Event",
"Subscription",
@@ -195,6 +196,8 @@ enum Operation {
Mounts,
Stats,
Viewport,
Window,
PageWindow,
State,
StateSet,
Increment,
@@ -440,11 +443,8 @@ fn new_typed_block(
fn show(cx: &mut NativeContext<'_>, page: ObjectRef, session: &Shared) -> Result<(), VmError> {
let mut state = session.lock().map_err(|_| invalid("UI session poisoned"))?;
cx.dict_set(
state.tabs,
Value::Text(page.to_string()),
Value::Object(page),
)?;
let window = object(Some(&cx.store().get_attr(state.desktop, "window")?))?;
crate::window::register_page(cx, window, page)?;
cx.set_attr(state.desktop, "pending", Value::Object(page))?;
state.pending = Some(Some(page));
Ok(())
@@ -496,7 +496,7 @@ impl NativeCallable for Function {
use Operation::*;
let (minimum, maximum) = match self.operation {
IsInstance | IsSubtype => (2, 2),
Stats | Viewport | Mounts => (0, 0),
Stats | Viewport | Window | PageWindow | Mounts => (0, 0),
State | StateSet => (1, 1),
Increment => (0, 0),
TextOf => (2, 4),
@@ -507,7 +507,8 @@ impl NativeCallable for Function {
Row | Column => (2, MAX_UI_NODES + 2),
Button => (1, 1),
Replace | Move => (2, 2),
Page | Unmount | Open | Close | Activate | Back | Forward | Children => (0, 0),
Page | Unmount | Close | Activate | Back | Forward | Children => (0, 0),
Open => (0, 1),
Click | Invalidate => (0, 1),
On => (2, 3),
Dispatch => (1, 2),
@@ -608,6 +609,16 @@ impl NativeCallable for Function {
)?;
return Ok(NativeResult::Return(Value::Object(set)));
}
if matches!(self.operation, Window) {
let desktop = self
.session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
return Ok(NativeResult::Return(
cx.store().get_attr(desktop, "window")?,
));
}
if matches!(self.operation, Viewport) {
let desktop = self
.session
@@ -939,7 +950,34 @@ impl NativeCallable for Function {
}];
show(cx, owner, &self.session)?;
}
PageWindow => {
let desktop = self
.session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
let window = object(Some(&cx.store().get_attr(desktop, "window")?))?;
let value = if crate::window::pages(cx.store(), window)?.contains(&owner) {
Value::Object(window)
} else {
Value::Nil
};
return Ok(NativeResult::Return(value));
}
Open | Activate => {
if let Some(requested) = args.first()
&& !matches!(requested, Value::Nil)
{
let desktop = self
.session
.lock()
.map_err(|_| invalid("UI session poisoned"))?
.desktop;
let window = cx.store().get_attr(desktop, "window")?;
if requested != &window {
return Err(invalid("Page.open requires this runtime's native Window"));
}
}
show(cx, owner, &self.session)?;
return Ok(NativeResult::Return(Value::Object(owner)));
}
@@ -951,19 +989,10 @@ impl NativeCallable for Function {
.session
.lock()
.map_err(|_| invalid("UI session poisoned"))?;
cx.dict_delete(session.tabs, &Value::Text(owner.to_string()))?;
let window = object(Some(&cx.store().get_attr(session.desktop, "window")?))?;
crate::window::unregister_page(cx, window, owner)?;
if session.active == Some(owner) || session.pending == Some(Some(owner)) {
let next =
cx.store()
.dict_items(session.tabs)?
.first()
.and_then(|(_, value)| {
if let Value::Object(page) = value {
Some(*page)
} else {
None
}
});
let next = crate::window::pages(cx.store(), window)?.first().copied();
cx.set_attr(
session.desktop,
"pending",
@@ -1050,6 +1079,18 @@ pub struct UiBridge {
}
impl UiBridge {
pub fn take_window_commands(&self, vm: &Vm) -> Result<Vec<crate::window::Command>> {
let desktop = self
.session
.lock()
.map_err(|_| anyhow::anyhow!("UI session poisoned"))?
.desktop;
let Value::Object(window) = vm.get_attr(desktop, "window")? else {
bail!("native Window is unavailable");
};
Ok(crate::window::take_commands(vm, window)?)
}
pub fn set_viewport(&self, vm: &mut Vm, viewport: Option<crate::layout::Size>) -> Result<bool> {
if let Some(size) = viewport
&& (!size.width.is_finite()
@@ -1493,6 +1534,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
("textOf", Operation::TextOf),
("stats", Operation::Stats),
("viewport", Operation::Viewport),
("window", Operation::Window),
] {
let function = vm.allocate(ContextNativeFunction::new(
name,
@@ -1526,6 +1568,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
("Column", "ContainerBlock", Some("column")),
("Button", "ContainerBlock", Some("button")),
("Page", "ContainerBlock", Some("page")),
("Window", "Object", None),
("Mount", "Object", None),
("Event", "Object", Some("event")),
("Subscription", "Object", None),
@@ -1645,6 +1688,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
"textOf",
"stats",
"viewport",
"window",
"isInstance",
"isSubtype",
] {
@@ -1672,6 +1716,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
("Page", "render", Operation::Render),
("Page", "unmount", Operation::Unmount),
("Page", "open", Operation::Open),
("Page", "window", Operation::PageWindow),
("Page", "close", Operation::Close),
("Page", "activate", Operation::Activate),
("Page", "navigate", Operation::Navigate),
@@ -1692,6 +1737,7 @@ pub fn install(vm: &mut Vm, roots: &mut Vec<ObjectRef>) -> Result<UiBridge> {
vm.attach_type(method, function_type)?;
vm.define_method(expected, name, method)?;
}
crate::window::install(vm, roots, desktop, tabs)?;
let block_type = object(Some(&vm.get_attr(ui, "Block")?))?;
crate::native_visual::install(vm, roots, desktop, block_type, function_type)?;
let event_type = object(Some(&vm.get_attr(ui, "Event")?))?;
+284
View File
@@ -0,0 +1,284 @@
//! Managed window resources and versioned, coalesced native projections.
//! The VM owns Page roots; only scalar commands cross to the GUI thread.
use crate::{native_visual, object_list, ui_bridge::desktop_type};
use endor_core::{
ContextNativeFunction, NativeCallable, NativeContext, NativeResult, ObjectPayload, ObjectRef,
ObjectStore, Value, Vm, VmError,
};
use std::sync::{Arc, Mutex};
pub const MAIN_WINDOW: u64 = 1;
pub const MAX_WINDOW_DIMENSION: f64 = 4096.0;
#[derive(Debug, Clone, PartialEq)]
pub enum Action {
Resize { width: f64, height: f64 },
SetTitle(String),
Focus,
}
#[derive(Debug, Clone, PartialEq)]
pub struct Command {
pub window: u64,
pub sequence: u64,
pub action: Action,
}
impl Command {
pub fn same_kind(&self, other: &Self) -> bool {
self.window == other.window
&& std::mem::discriminant(&self.action) == std::mem::discriminant(&other.action)
}
}
#[derive(Debug, Clone)]
pub struct Envelope {
pub session: u64,
pub command: Command,
}
#[derive(Debug, Clone)]
pub struct ManagedWindow {
pub id: u64,
tabs: ObjectRef,
pages: Vec<ObjectRef>,
sequence: u64,
// Transport state only. Draining it must not change a language object's
// version or create a render-dependency feedback loop.
outbox: Arc<Mutex<Vec<Command>>>,
}
impl ObjectPayload for ManagedWindow {
fn as_any(&self) -> &dyn std::any::Any {
self
}
fn kind_name(&self) -> &'static str {
"tessera-window"
}
fn allows_attributes(&self) -> bool {
true
}
fn reference_assignment_only(&self) -> bool {
true
}
fn trace(&self, visit: &mut dyn FnMut(ObjectRef)) {
visit(self.tabs);
for page in &self.pages {
visit(*page);
}
}
}
fn invalid(message: &'static str) -> VmError {
VmError::InvalidArgument(message)
}
fn scalar(value: &Value) -> Result<f64, VmError> {
let Value::Number(value) = value else {
return Err(invalid("window size requires numbers"));
};
let number = value
.to_string()
.parse::<f64>()
.map_err(|_| invalid("invalid window size"))?;
if !number.is_finite() || !(1.0..=MAX_WINDOW_DIMENSION).contains(&number) {
return Err(invalid("window size exceeds limits"));
}
Ok(number)
}
#[derive(Debug, Clone, Copy)]
enum Operation {
Id,
Viewport,
Pages,
Resize,
SetTitle,
Focus,
}
#[derive(Debug)]
struct Method {
operation: Operation,
desktop: ObjectRef,
}
impl NativeCallable for Method {
fn invoke(&self, cx: &mut NativeContext<'_>, args: &[Value]) -> Result<NativeResult, VmError> {
let Some(Value::Object(owner)) = args.first() else {
return Err(invalid("Window method requires a Window"));
};
let data = cx
.store()
.payload::<ManagedWindow>(*owner, "Window")?
.clone();
if data.id != MAIN_WINDOW {
return Err(invalid("native window is unavailable"));
}
let expected = match self.operation {
Operation::Resize => 3,
Operation::SetTitle => 2,
_ => 1,
};
if args.len() != expected {
return Err(invalid("incorrect Window method argument count"));
}
let result = match self.operation {
Operation::Id => Value::Number((data.id as i64).into()),
Operation::Pages => {
Value::Object(object_list::collection(cx, data.pages, self.desktop)?)
}
Operation::Viewport => {
let Value::Object(input) = cx.store().get_attr(self.desktop, "viewport")? else {
return Err(invalid("window viewport is unavailable"));
};
let width = cx.store().get_attr(input, "width")?;
let height = cx.store().get_attr(input, "height")?;
Value::Object(native_visual::record(
cx,
self.desktop,
"MeasureResult",
vec![("width", width), ("height", height)],
)?)
}
operation => {
if cx.store().is_read_only() {
return Err(invalid(
"window operations are forbidden during read-only rendering",
));
}
let action = match operation {
Operation::Resize => Action::Resize {
width: scalar(&args[1])?,
height: scalar(&args[2])?,
},
Operation::SetTitle => {
let Value::Text(title) = &args[1] else {
return Err(invalid("window title requires text"));
};
if title.len() > 4096 || title.chars().any(char::is_control) {
return Err(invalid("invalid window title"));
}
Action::SetTitle(title.clone())
}
Operation::Focus => Action::Focus,
_ => unreachable!(),
};
enqueue(cx, *owner, action)?;
Value::Nil
}
};
Ok(NativeResult::Return(result))
}
}
fn enqueue(cx: &mut NativeContext<'_>, owner: ObjectRef, action: Action) -> Result<(), VmError> {
let mut data = cx
.store()
.payload::<ManagedWindow>(owner, "Window")?
.clone();
data.sequence = data
.sequence
.checked_add(1)
.ok_or(invalid("window command sequence exhausted"))?;
let command = Command {
window: data.id,
sequence: data.sequence,
action,
};
let queue = data.outbox.clone();
let mut queue = queue
.lock()
.map_err(|_| invalid("window outbox poisoned"))?;
// The heap barrier succeeds before any external transport state changes.
cx.replace_payload(owner, data)?;
queue.retain(|old| !old.same_kind(&command));
queue.push(command);
Ok(())
}
pub fn register_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
page: ObjectRef,
) -> Result<(), VmError> {
if cx.store().is_read_only() {
return Err(invalid(
"opening a Page is forbidden during read-only rendering",
));
}
let mut data = cx
.store()
.payload::<ManagedWindow>(window, "Window")?
.clone();
if !data.pages.contains(&page) {
data.pages.push(page);
cx.dict_set(
data.tabs,
Value::Text(page.to_string()),
Value::Object(page),
)?;
cx.replace_payload(window, data)?;
}
Ok(())
}
pub fn unregister_page(
cx: &mut NativeContext<'_>,
window: ObjectRef,
page: ObjectRef,
) -> Result<(), VmError> {
if cx.store().is_read_only() {
return Err(invalid(
"closing a Page is forbidden during read-only rendering",
));
}
let mut data = cx
.store()
.payload::<ManagedWindow>(window, "Window")?
.clone();
if !data.pages.contains(&page) {
return Ok(());
}
data.pages.retain(|value| *value != page);
cx.dict_delete(data.tabs, &Value::Text(page.to_string()))?;
cx.replace_payload(window, data)?;
Ok(())
}
pub fn pages(store: &ObjectStore, window: ObjectRef) -> Result<&[ObjectRef], VmError> {
Ok(&store.payload::<ManagedWindow>(window, "Window")?.pages)
}
pub fn take_commands(vm: &Vm, window: ObjectRef) -> Result<Vec<Command>, VmError> {
let data = vm.payload::<ManagedWindow>(window, "Window")?;
let mut queue = data
.outbox
.lock()
.map_err(|_| invalid("window outbox poisoned"))?;
let mut commands = std::mem::take(&mut *queue);
commands.sort_by_key(|command| command.sequence);
Ok(commands)
}
pub fn install(
vm: &mut Vm,
roots: &mut Vec<ObjectRef>,
desktop: ObjectRef,
tabs: ObjectRef,
) -> Result<ObjectRef, VmError> {
let definition = desktop_type(vm.object_store(), "Window", desktop)?;
let function_type = desktop_type(vm.object_store(), "Function", desktop)?;
for (name, operation) in [
("id", Operation::Id),
("viewport", Operation::Viewport),
("pages", Operation::Pages),
("resize", Operation::Resize),
("setTitle", Operation::SetTitle),
("focus", Operation::Focus),
] {
let function = vm.allocate(ContextNativeFunction::new(
name,
Method { operation, desktop },
vec![desktop],
))?;
roots.push(function);
vm.attach_type(function, function_type)?;
vm.define_method(definition, name, function)?;
}
let window = vm.allocate(ManagedWindow {
id: MAIN_WINDOW,
tabs,
pages: Vec::new(),
sequence: 0,
outbox: Arc::new(Mutex::new(Vec::new())),
})?;
roots.push(window);
vm.attach_type(window, definition)?;
vm.set_attr(desktop, "window", Value::Object(window))?;
Ok(window)
}