feat: add page surfaces and collapsible sidebar
This commit is contained in:
@@ -54,6 +54,29 @@ It does not publish a page or change revision. Console does not use this endpoin
|
||||
All execution endpoints share the eight-entry queue. These are in-process bounded
|
||||
host executions, not a complete untrusted-code sandbox or a process-isolation design.
|
||||
|
||||
## Page modes and camera
|
||||
|
||||
Page.mode is "window" (default), "vertical", "horizontal" or "canvas".
|
||||
Window fills the available page viewport. Vertical constrains width and grows
|
||||
height with children; horizontal constrains height and grows width. Scroll
|
||||
finite pages with the wheel; the offset clamps to their actual extent.
|
||||
Canvas arranges direct children at signed child.x / child.y world positions.
|
||||
page.render(root) replaces content; page.append(child) adds another child.
|
||||
Changing these attributes updates live occurrences, without copying objects.
|
||||
|
||||
Canvas uses Page.panX / panY as the world origin and Page.zoom (0.1..4).
|
||||
Wheel/trackpad pans, Space+left drag or middle drag pans, pinch or Cmd/Ctrl+wheel
|
||||
zooms around the pointer. Positions are finite and bounded to +/-1000000 by the
|
||||
existing renderer budget; canvas navigation is not clipped to content extent.
|
||||
The camera belongs to the Page and survives page switching.
|
||||
Nested ScrollBlock input is handled before page panning.
|
||||
|
||||
Native transport kinds "page-pan" and "page-zoom" use mount=0 and current
|
||||
window/session/layout_version. Pan delta is a two-component viewport-pixel
|
||||
displacement. Zoom uses delta=[positive_scale_factor,0] and point=[x,y] in
|
||||
viewport coordinates. Ordinary click/scroll coordinates remain world coordinates;
|
||||
the host inverts the camera and the worker rechecks visibility and frame identity.
|
||||
|
||||
## Managed visual types
|
||||
|
||||
`Object`, `Type`, `Function`, `Task`, `Block`, `ContainerBlock`, `TextBlock`, `RectBlock`,
|
||||
@@ -67,7 +90,7 @@ 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. Explicit traits and source parameter/return annotation checks are enabled; declared-field construction/write checks are enabled alongside source function checks; complete initialization and static/effect typing 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), moveTo(x,y), setTitle(text) and focus(). Requests project to their actual 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. Open Pages appear in native tab chrome, with page.title, click activation/close, Ctrl+Tab cycling and Cmd/Ctrl+W close when page keyboard focus is active. Overflow arrows reach additional pages. These operations call actual Page methods in the same VM; tab IDs are weak runtime projections and add no GC roots. The 36-logical-pixel bar reduces Page constraint height; window/ui viewport reports the full inner size and native pointer positions are translated into content coordinates. Window() now creates additional native windows in the same VM, up to 16 open resources. Page.open(window)/moveToWindow(window) preserve Page and business identity while replacing Mounts; detachToWindow() creates and returns a new Window. Dragging a tab away from its strip invokes the same method. Window.close()/secondary OS close remove Page roots, unmount, and retire the resource context; retained Pages can reopen in another live window, and closed Window resources reject viewport/resize/title/focus operations. Closing the primary OS shell exits the application. Secondary viewport inputs and frame publication use the same worker. Secondary close after callback/rendering failure uses host-only teardown without invoking source callbacks or resuming execution. After reset/discard, closing an old picture never touches a fresh VM resource with the same number; viewport inputs are also generation-scoped. A teardown failure discards the affected VM and disables remaining pictures. Native initialization/surface errors are reported rather than panicking; failed secondary creation leaves a managed resource closable from Console. Native tab reordering, existing-window drops and release-point placement use the managed methods described below. Application-exit cleanup is described below.
|
||||
Window is a host-provided managed resource: ui.window(), page.window(), page.open(window), window.pages()/id()/viewport(), resize(width,height), moveTo(x,y), setTitle(text) and focus(). Each window has a collapsible left sidebar for pages and Console/Sources/Network/Settings. Cmd/Ctrl+B toggles it. Collapsed, only the sidebar toggle and native macOS window controls remain over page content. Viewport queries report the actual available page area, with no top strip or content inset. Page selection/closure calls actual managed Page methods; Ctrl+Tab cycles pages, Cmd/Ctrl+T creates one and Cmd/Ctrl+W closes the active page. Window() creates another native resource in the same VM, up to 16. Page.open(window), moveToWindow(window) and detachToWindow() preserve business identity and replace occurrence Mounts. Window.close() releases Page roots and retires its presentation; primary native close exits the application. Resource changes publish only after successful input/candidate completion. Frame/session/window generations reject stale events and commands. Sidebar pointer drag/reorder is not implemented.
|
||||
|
||||
Application exit now uses a private VM-worker control channel, independent of HTTP queue capacity. Main native close and a managed primary Window.close request asynchronous cleanup; duplicate requests coalesce and mutation requests are rejected with 503 while exiting. An unpublished candidate/input refresh is cancelled without replay. The current VM cancels all Task entries, closes managed windows, detaches occurrences child-before-parent per window, and invokes normal onUnmount hooks once. It does not run layout/paint during shutdown. All windows are closed before hooks run; new windows and tasks are forbidden. A stopped/faulted VM detaches without source hooks; reset/discarded pictures cannot revive an old VM. A hook error aborts remaining source hooks, releases prepared hook roots, drops the VM and reports failure before native exit. Completed shutdown is acknowledged to the GUI, which then ends the event loop. An OS exit bypassing CloseRequested uses a five-second bounded final fallback; non-cooperative native calls remain atomic and force-kill/crash cannot guarantee notifications. Native stderr records the window/mount/task-entry counts and whether source hooks were enabled, or the cleanup error. See examples/exit-lifecycle.endor. Cross-platform exit/drag validation remains broader follow-up work.
|
||||
|
||||
@@ -78,10 +101,10 @@ Source layout/paint now validate name-level read observations from Aster: each g
|
||||
|
||||
|
||||
|
||||
While a native tab drag is held over an overflow arrow area, the strip waits 350ms then reveals one hidden tab every 120ms, including in a compatible destination window. The preview scroll does not activate Pages or change business state. Drawing, hit testing and insertion gaps share the same transient visible range; release uses its full-order insertion index. Leaving the strip stops the timer; release, Escape, a new press or runtime generation change restores the normal active-tab-centered strip. A resize or changing tab count clamps the range. Native drag/mixed-DPI behavior still requires platform validation; application-exit cleanup is described below.
|
||||
Page navigation is in the left sidebar; the former horizontal strip and its drag/overflow animation are removed. Managed moveTab/moveToWindow/detachToWindow and the version-checked tab-drop API remain available. Sidebar pointer drag/reorder and mixed-DPI cross-window gestures are not implemented in this revision.
|
||||
|
||||
|
||||
ContainerBlock.insert(position,block) accepts a zero-based position through the current count, returns a new stable slot and preserves existing slots/Mounts. Invalid indices and child cycles fail before changing the child list. Page.moveTab(position) uses a final zero-based index in its owning window. Native drops invoke the actual Page moveToWindow/moveTab/activate methods; a declined or redirected window move skips destination reordering. A drop allows 300000 instructions per method. Source overrides participate; earlier writes do not roll back after failure. Window.moveTo(x,y) queues the physical desktop outer origin, with signed integers within +/-1000000. Detached placement preserves the saved grab offset; the window manager may adjust it and unsupported global placement reports a diagnostic. Dragging uses a 12-logical-pixel threshold, visible insertion marker and Escape cancellation. Overflow auto-scroll, external-app occlusion and mixed-DPI verification remain pending. See examples/tab-placement.endor.
|
||||
ContainerBlock.insert(position,block) accepts a zero-based position through the current count, returns a new stable slot and preserves existing slots/Mounts. Invalid indices and child cycles fail before changing the child list. Page.moveTab(position) uses a final zero-based index in its owning window. Native drops invoke the actual Page moveToWindow/moveTab/activate methods; a declined or redirected window move skips destination reordering. A drop allows 300000 instructions per method. Source overrides participate; earlier writes do not roll back after failure. Window.moveTo(x,y) queues the physical desktop outer origin, with signed integers within +/-1000000. Detached placement preserves the saved grab offset; the window manager may adjust it and unsupported global placement reports a diagnostic. These movement methods remain callable from Endor and transport clients; the sidebar currently provides selection and closure rather than pointer dragging. See examples/tab-placement.endor.
|
||||
|
||||
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. Native TextBlock wrapping now uses the occurrence width in measurement/cache/painting; source measurement/arrangement now repeats until constraints and geometry settle; additional allocation policies remain pending; ScrollBlock scrolling is described below.
|
||||
|
||||
|
||||
@@ -479,3 +479,105 @@ page.render(TextBlock("input priority"))
|
||||
let value = runtime.execute("count.value", "app.main").unwrap().value;
|
||||
assert_eq!(value, "1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn canvas_camera_crosses_real_http_route_and_preserves_negative_world_clicks() {
|
||||
let server = Arc::new(Server::http("127.0.0.1:0").unwrap());
|
||||
let namespace = Arc::new(RwLock::new(NamespaceState {
|
||||
viewport: Some(crate::layout::Size {
|
||||
width: 600.0,
|
||||
height: 400.0,
|
||||
}),
|
||||
..NamespaceState::default()
|
||||
}));
|
||||
let api = start_server(server.clone(), namespace.clone(), Arc::new(|| {})).unwrap();
|
||||
let client = TestServer {
|
||||
address: server.server_addr().to_ip().unwrap(),
|
||||
server,
|
||||
api: Some(api),
|
||||
};
|
||||
let source = r#"
|
||||
count=ui.state(0)
|
||||
button=Button("world")
|
||||
button.onClick(count.increment)
|
||||
button.x=-160
|
||||
button.y=-80
|
||||
page=Page()
|
||||
page.mode="canvas"
|
||||
page.render(button)
|
||||
"#;
|
||||
let response = client.request("PUT", "/v1/namespaces/app.main/code", source);
|
||||
assert_eq!(response.0, 200, "{}", response.2);
|
||||
let camera = |kind: &str, delta: [f32; 2], point: Option<[f32; 2]>| {
|
||||
let frame = &namespace.read().unwrap().document.frame;
|
||||
serde_json::to_string(&crate::events::Input {
|
||||
window: 1,
|
||||
session: frame.session,
|
||||
layout_version: frame.layout_version,
|
||||
mount: 0,
|
||||
kind: kind.into(),
|
||||
delta: Some(delta),
|
||||
point,
|
||||
press_point: None,
|
||||
screen_position: None,
|
||||
tab_drop: None,
|
||||
})
|
||||
.unwrap()
|
||||
};
|
||||
let event_path = "/v1/namespaces/app.main/events";
|
||||
let pan = camera("page-pan", [-240.0, -160.0], None);
|
||||
let response = client.request("POST", event_path, &pan);
|
||||
assert_eq!(response.0, 200, "{}", response.2);
|
||||
assert_eq!(
|
||||
namespace.read().unwrap().document.frame.surface.pan,
|
||||
crate::geometry::Point::new(-240.0, -160.0)
|
||||
);
|
||||
assert_eq!(client.request("POST", event_path, &pan).0, 409);
|
||||
let response = client.request(
|
||||
"POST",
|
||||
event_path,
|
||||
&camera("page-zoom", [2.0, 0.0], Some([120.0, 90.0])),
|
||||
);
|
||||
assert_eq!(response.0, 200, "{}", response.2);
|
||||
{
|
||||
let current = namespace.read().unwrap();
|
||||
assert_eq!(current.document.frame.surface.zoom, 2.0);
|
||||
assert_eq!(
|
||||
current.document.frame.surface.pan,
|
||||
crate::geometry::Point::new(-180.0, -115.0)
|
||||
);
|
||||
}
|
||||
let input = {
|
||||
let current = namespace.read().unwrap();
|
||||
let frame = ¤t.document.frame;
|
||||
let mount = frame.mounts.iter().find(|m| m.interactive).unwrap();
|
||||
assert!(mount.x < 0.0 && mount.y < 0.0);
|
||||
crate::events::Input {
|
||||
window: 1,
|
||||
session: frame.session,
|
||||
layout_version: frame.layout_version,
|
||||
mount: mount.id,
|
||||
kind: "click".into(),
|
||||
point: Some([-150.0, -70.0]),
|
||||
press_point: Some([-150.0, -70.0]),
|
||||
delta: None,
|
||||
screen_position: None,
|
||||
tab_drop: None,
|
||||
}
|
||||
};
|
||||
let response = client.request("POST", event_path, &serde_json::to_string(&input).unwrap());
|
||||
assert_eq!(response.0, 200, "{}", response.2);
|
||||
let response = client.request("POST", "/v1/namespaces/app.main/repl", "count.value");
|
||||
assert_eq!(response.0, 200, "{}", response.2);
|
||||
assert_eq!(
|
||||
serde_json::from_str::<serde_json::Value>(&response.2).unwrap()["value"],
|
||||
"1"
|
||||
);
|
||||
let response = client.request(
|
||||
"POST",
|
||||
event_path,
|
||||
&camera("page-zoom", [0.0, 0.0], Some([120.0, 90.0])),
|
||||
);
|
||||
assert_eq!(response.0, 400);
|
||||
assert!(namespace.read().unwrap().document.frame.fault.is_none());
|
||||
}
|
||||
|
||||
@@ -336,6 +336,8 @@ impl Worker {
|
||||
"click"
|
||||
| "focus"
|
||||
| "scroll"
|
||||
| "page-pan"
|
||||
| "page-zoom"
|
||||
| "tab-activate"
|
||||
| "tab-close"
|
||||
| "tab-detach"
|
||||
@@ -422,7 +424,10 @@ impl Worker {
|
||||
|| frame.session != input.session
|
||||
|| (input.kind != "window-close" && frame.layout_version != input.layout_version)
|
||||
|| frame.fault.is_some()
|
||||
|| if input.kind == "window-close" || input.kind.starts_with("developer-open:") {
|
||||
|| if input.kind == "window-close"
|
||||
|| input.kind.starts_with("developer-open:")
|
||||
|| matches!(input.kind.as_str(), "page-pan" | "page-zoom")
|
||||
{
|
||||
input.mount != 0
|
||||
} else if matches!(
|
||||
input.kind.as_str(),
|
||||
@@ -447,6 +452,14 @@ impl Worker {
|
||||
"event targets an obsolete or inactive frame",
|
||||
));
|
||||
}
|
||||
if input.kind == "page-zoom" && frame.surface.mode != crate::page::Mode::Canvas
|
||||
|| input.kind == "page-pan" && frame.surface.mode == crate::page::Mode::Window
|
||||
{
|
||||
return RequestProgress::Complete(error_response(
|
||||
StatusCode(400),
|
||||
"camera input is unavailable in this Page mode",
|
||||
));
|
||||
}
|
||||
Some(input)
|
||||
} else {
|
||||
None
|
||||
|
||||
Reference in New Issue
Block a user