fix(shell-web): Step 3 stop-hook — read canvas size from backend

Codex Step 3 stop-hook flagged: "web repaint ignores actual
canvas size". The prior fix `9cf0f865` hardcoded `960.0` to
match the smoke HTML's `<canvas id="op" width="960">`, but
that's brittle — any host that mounts onto a differently-sized
canvas (responsive HTML, programmatic mount, future smoke
fixture changes) gets the wrong layout viewport.

Fix: WebBackend gains `canvas_width(&self) -> u32` +
`canvas_height(&self) -> u32` accessors. The `width` /
`height` fields are already refreshed at construction
(`canvas.width()`) and on `RenderBackend::resize`, so reading
them per-paint reflects whatever the host's `<canvas>` width
attribute currently is.

`paint_inspector` now:
- Reads `viewport_w` + `viewport_h` from the backend at the
  start of each frame.
- Uses them for the white-background clear AND for the
  WidgetHost::paint viewport_width arg.

This also resolves the prior "web smoke paints only the
toolbar" issue since the smoke canvas is 960×640 — the first
frame still receives 960 as viewport_width, but now via the
backend instead of a hardcode.

Verification:
- `cargo build -p openpencil-shell-web --target
  wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` — produces ../pkg/*
- `bash tools/check-wasm-bundle.sh` — PASS, 0 env.*, 907 031
  bytes gzip = 86% of 1 MiB ceiling (negligible delta — two
  small accessor methods).
This commit is contained in:
Kayshen-X 2026-05-10 13:14:17 +08:00
parent 01c5baf8f5
commit 7e1fbcabf0
2 changed files with 30 additions and 12 deletions

View file

@ -85,6 +85,21 @@ impl WebBackend {
self.last_present_error.take()
}
/// Current physical width of the host `<canvas>` element, refreshed
/// at construction + on every `resize` call. Hosts use this so a
/// repaint after the canvas DOM attribute changes pulls the new
/// width through the WidgetHost layout (codex Step 3 stop-hook
/// "web repaint ignores actual canvas size").
pub fn canvas_width(&self) -> u32 {
self.width
}
/// Current physical height of the host `<canvas>` element. Same
/// refresh contract as `canvas_width`.
pub fn canvas_height(&self) -> u32 {
self.height
}
/// Snapshot the raster surface and `put_image_data` it onto the host
/// `<canvas>`'s 2D context. Spec §5.3 raster-fallback contract:
/// N32_PREMUL surface, RGBA8888 + Unpremul read, full-frame copy each

View file

@ -109,26 +109,29 @@ impl Inner {
fn repaint(&mut self) -> Result<(), JsValue> {
use openpencil_shell_core::{Color, Point2D, Rect, RenderBackend};
// Pull the actual canvas dimensions from the backend
// every frame so a host that swaps the `<canvas>` width
// attribute (responsive layouts, devtool resizing,
// future programmatic mount with a different size)
// gets a layout that matches reality. Codex Step 3
// stop-hook "web repaint ignores actual canvas size"
// caught the prior hardcoded `960.0` — this swap also
// resolves the "web smoke paints only the toolbar"
// regression since the smoke canvas is 960×640 and the
// first frame still gets the full width.
let viewport_w = self.backend.canvas_width() as f32;
let viewport_h = self.backend.canvas_height() as f32;
self.backend.begin_frame();
// Clear to white so widget paints sit on a clean background.
self.backend.fill_rect(
Rect {
origin: Point2D::new(0.0, 0.0),
size: Point2D::new(960.0, 640.0),
size: Point2D::new(viewport_w, viewport_h),
},
Color::WHITE,
);
// Step 3: pass the FULL canvas width (matches the smoke
// HTML's `<canvas id="op" width="960">`). The host's
// `paint(&self, backend, viewport_width)` lays out
// Toolbar-top + LayerPanel-left + CanvasViewport-center
// + PropertyPanel-right; viewport_width below the
// MIN_RAIL_WIDTH cutoff would silently skip everything
// except the toolbar — codex stop-hook
// "web smoke paints only the toolbar" caught this when
// the leftover Step 1b 280-px hardcode was still in
// place.
self.host.paint(&mut self.backend, 960.0);
self.host.paint(&mut self.backend, viewport_w);
self.backend.end_frame();
if let Some(err) = self.backend.take_present_error() {
return Err(err);