feat(shell-web): Phase B3 — wire WidgetHost into mount path
Replaces the Phase A red-rect demo with the Step 1b inspector
composition: WebShell now owns a `WidgetHost` and `mount()` paints
the four shell-core widgets (Tree / PropertyRow / Dropdown /
TextInput) into a 280-px column on a white-cleared canvas.
What's added:
- `crates/openpencil-shell-web/src/widget_host.rs` — the only file
in shell-web that calls into `openpencil_shell_core::widgets::*`,
per spec §1.4 boundary. Module doc anchors the invariant; the
paint signature carries the `// glue:` marker that the Phase B4
boundary check script (`tools/check-widget-boundary.sh`) will
grep for.
- `WidgetHost { tree, width, dropdown, text_input }` owns one of
each kind; `WidgetHost::new()` populates them from the B2 sample
/ new constructors. `Default` forwards to `new()`.
- `WidgetHost::paint(&self, backend, available_width)` builds a
`LayoutCx`, iterates a `[&dyn Widget; 4]` array, places each at
x=16 with 12-px vertical gaps. Reborrows backend each iteration
(`&mut *backend`) so subsequent iterations don't fail
borrow-check on the moved `&mut WebBackend`.
shell-web/src/lib.rs:
- Adds `mod widget_host;` cfg-gated to the `skia` feature.
- WebShell gains `host: WidgetHost`.
- Renames `paint_phase_a` → `paint_inspector`. Body clears the
canvas to white, dispatches via `self.host.paint(...)`, then
surfaces `take_present_error()` as JsValue exception. Same
panic-safe + canvas-type-check + present-error-propagation
pattern as Phase A C-hard.2 (codex Phase A gate review approved).
- The stub mount entry (no skia feature) is unchanged — the
kickoff §1.2 wasm32-clean compile guard CI still uses it.
Plan-vs-implementation deviations (deliberate):
- Plan B3 step 2 simplifies `mount()` in a way that drops the
panic hook + canvas-type-check + present-error propagation.
Preserved all three because the codex Phase A gate review
explicitly approved them as "panic-safe mount". Plan body's
`mount` block is treated as historical sketch.
- Plan body's `let mut cx = PaintCx { backend };` would move the
reference and fail borrow-check on the second iteration.
Changed to explicit reborrow `&mut *backend`. This is the
plan's intent, just with the borrow-checker subtlety made
explicit.
Verification:
- `cargo build -p openpencil-shell-web --target
wasm32-unknown-unknown --features skia --release` — green
- `cargo check -p openpencil-shell-web --target
wasm32-unknown-unknown --no-default-features --features web` —
green (compile guard)
- `bash tools/check-wasm-bundle.sh` — PASS
- 0 env.* imports (bundle still LinkError-free)
- 615 764 bytes gzip = 58% of 1 MiB ceiling
- +2 KiB vs Phase A C-hard.2 (~613 KiB) — widget code is small
- `cargo check -p openpencil-shell-native` — green (no regression)
Codex iterate review: 1 round → GO with 2 NITs (script name
singular vs plural, stale "Phase A red-rect" phrase) — both
fixed in this commit.
This commit is contained in:
parent
f214f4b4e3
commit
d8d6e127cd
|
|
@ -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<WebShell, JsValue> {
|
|||
.map_err(|_| JsValue::from_str("mount: target element is not <canvas>"))?;
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
|
|
|
|||
77
crates/openpencil-shell-web/src/widget_host.rs
Normal file
77
crates/openpencil-shell-web/src/widget_host.rs
Normal file
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
Loading…
Reference in a new issue