diff --git a/crates/openpencil-shell-web/src/lib.rs b/crates/openpencil-shell-web/src/lib.rs index a9293c261..a5b3b1d1e 100644 --- a/crates/openpencil-shell-web/src/lib.rs +++ b/crates/openpencil-shell-web/src/lib.rs @@ -16,6 +16,8 @@ #[cfg(feature = "skia")] mod backend; +#[cfg(feature = "skia")] +mod widget_host; // Force the wasm32-unknown-unknown libc/libcxx/libm shim to be linked // even though no Rust code calls it — its `#[no_mangle]` symbols are @@ -38,21 +40,22 @@ use wasm_bindgen::prelude::*; pub struct WebShell { #[cfg(feature = "skia")] backend: backend::WebBackend, + #[cfg(feature = "skia")] + host: widget_host::WidgetHost, } #[cfg(feature = "skia")] impl WebShell { - /// Phase A red-rect demo: clear to white, draw a centered red rect, - /// snapshot to the host canvas. Returns the present error if the - /// final ImageData round-trip failed — callers MUST propagate this - /// to JS instead of treating mount as successful when the canvas - /// stayed blank. - /// - /// Phase B+ replaces this with the widget host paint loop. - fn paint_phase_a(&mut self) -> Result<(), JsValue> { + /// Phase B paint pass: clear the canvas to white, then dispatch to + /// the four shell-core inspector widgets via `WidgetHost`. Returns + /// the present error if the final ImageData round-trip failed — + /// callers MUST propagate this to JS instead of treating mount as + /// successful when the canvas stayed blank. + fn paint_inspector(&mut self) -> Result<(), JsValue> { use openpencil_shell_core::{Color, Point2D, Rect, RenderBackend}; + self.backend.begin_frame(); - // Clear background. + // Clear to white so widget paints sit on a clean background. self.backend.fill_rect( Rect { origin: Point2D::new(0.0, 0.0), @@ -60,14 +63,8 @@ impl WebShell { }, Color::WHITE, ); - // Centered red rect: 320×120 inside the 960×640 canvas. - self.backend.fill_rect( - Rect { - origin: Point2D::new(320.0, 260.0), - size: Point2D::new(320.0, 120.0), - }, - Color::RED, - ); + // Inspector slice: 280 px wide column on the left half. + self.host.paint(&mut self.backend, 280.0); self.backend.end_frame(); if let Some(err) = self.backend.take_present_error() { return Err(err); @@ -107,11 +104,12 @@ pub fn mount(canvas_id: &str) -> Result { .map_err(|_| JsValue::from_str("mount: target element is not "))?; let backend = backend::WebBackend::new(canvas)?; - let mut shell = WebShell { backend }; - // Phase A demo paints synchronously inside mount(); any present error - // (read_pixels / put_image_data) MUST surface as a JS exception so - // callers do not see Ok with an unpainted canvas. - shell.paint_phase_a()?; + let host = widget_host::WidgetHost::new(); + let mut shell = WebShell { backend, host }; + // Phase B inspector paints synchronously inside mount(); any present + // error (read_pixels / put_image_data) MUST surface as a JS exception + // so callers do not see Ok with an unpainted canvas. + shell.paint_inspector()?; Ok(shell) } diff --git a/crates/openpencil-shell-web/src/widget_host.rs b/crates/openpencil-shell-web/src/widget_host.rs new file mode 100644 index 000000000..a23c5c4df --- /dev/null +++ b/crates/openpencil-shell-web/src/widget_host.rs @@ -0,0 +1,77 @@ +//! Step 1b §1.4 widget glue — the only file in shell-web allowed to call +//! into `openpencil_shell_core::widgets`. All widget logic (state, paint, +//! layout, accesskit) lives in shell-core; this host is a thin paint-loop +//! adapter that takes a `&mut WebBackend` and dispatches to the four +//! inspector widgets. +//! +//! Any function that pulls in `openpencil_shell_core::widgets::*` MUST +//! live in this file (per spec §1.4). The B4 boundary check script +//! greps for the `// glue:` marker on the paint signature so the +//! invariant survives drift. + +use crate::backend::WebBackend; +use openpencil_shell_core::widgets::{ + Dropdown, LayoutCx, PaintCx, PropertyRow, TextInput, TreeWidget, Widget, +}; +use openpencil_shell_core::{Point2D, Rect, RenderBackend}; + +/// The Step 1b inspector composition. Owns one of each widget kind so +/// the first-frame Phase B mount paints something meaningful; Phase C +/// event handling will mutate the dropdown / text-input state in place. +pub struct WidgetHost { + tree: TreeWidget, + width: PropertyRow, + dropdown: Dropdown, + text_input: TextInput, +} + +impl WidgetHost { + pub fn new() -> Self { + Self { + tree: TreeWidget::sample(), + width: PropertyRow::new(200, "Width", "960"), + dropdown: Dropdown::sample(), + text_input: TextInput::sample(), + } + } + + /// Dispatches paint to the shell-core widgets stacked vertically with + /// 12 px gaps. The `// glue:` marker on the same line as the + /// signature is what `tools/check-widget-boundary.sh` (Phase B4) + /// greps for to confirm widget calls happen here and nowhere else + /// in shell-web. + pub fn paint(&self, backend: &mut WebBackend, available_width: f32) { // glue: + let layout = LayoutCx { + available_width, + dpi: backend.dpi_scale(), + }; + let mut y = 16.0; + let widgets: [&dyn Widget; 4] = [ + &self.tree, + &self.width, + &self.dropdown, + &self.text_input, + ]; + for widget in widgets { + let box_ = widget.layout(&layout); + let rect = Rect { + origin: Point2D::new(16.0, y), + size: Point2D::new(available_width, box_.rect.size.y), + }; + // Reborrow on every iteration so the `&mut WebBackend` is not + // moved into the first PaintCx — without `&mut *backend` the + // second iteration would fail borrow check. + let mut cx = PaintCx { + backend: &mut *backend, + }; + widget.paint(&mut cx, rect); + y += rect.size.y + 12.0; + } + } +} + +impl Default for WidgetHost { + fn default() -> Self { + Self::new() + } +}