Codex stop-hook flagged: "web Step 3 cannot render the claimed
canvas text". Root cause: WebBackend::draw_text was a Phase A
no-op stub. CanvasViewport calls draw_text for "Hello
OpenPencil" / "Click me" / Layer panel labels / etc — none of
those text strings actually rendered in the browser.
# Fix
`crates/openpencil-shell-web/src/backend/mod.rs::WebBackend`:
- Embeds `assets/Roboto-Regular.ttf` (Apache 2.0, 35 KB, copied
from rust-skia test resources) via `include_bytes!`. The
C-hard wasm32-unknown-unknown skia build uses
`skia_enable_fontmgr_custom_empty=yes` (see
`vendor/skia-safe-op/skia-bindings/build_support/platform/
wasm_unknown.rs`), so there are no system fonts and we have
to bake the bytes in.
- New `typeface: Option<Typeface>` field, lazy-init on first
draw_text via `FontMgr::custom_empty().and_then(|m|
m.new_from_data(ROBOTO_TTF, None))`. Build failure → None
silently no-ops subsequent draws (no panic — text just
doesn't render).
- `draw_text` now iterates `layout.runs()` and calls
`Canvas::draw_str` per run with a `Font::new(typeface,
font_size)` + `Paint` from the run color.
`crates/openpencil-shell-web/Cargo.toml`:
- Drops `textlayout` skia-safe feature. We use raw `draw_str`
not paragraph builder; textlayout pulled ICU + Harfbuzz +
~400 KB gzip + a swarm of font-lookup imports we don't
exercise.
# 24 new env.* imports — wasm-libc-shim expansion
Even without textlayout, Skia's font path imports 24 libc
symbols our prior C-hard.2 shim didn't cover. All resolved:
`crates/wasm-libc-shim/src/imp.rs` — Rust extern "C" shims:
- string ops (real impls): strncmp, strncpy, strstr, strrchr,
strcat, strtol, tolower, qsort (insertion sort, fits Skia's
small-array call sites; debug_assert on element size > 256)
- file I/O (sentinel error returns, no filesystem on wasm):
fopen → null, fread → 0, fclose → 0, fputc → c, fileno → -1,
fstat → -1, pread → -1, ftell → -1, fseek → -1
- env: getenv → null
- mmap: returns MAP_FAILED ((void*)-1); munmap → -1
- setjmp/longjmp: setjmp returns 0 (treat as initial call);
longjmp panics — happy text path through in-memory TTF parse
should never trigger it
- C++ nothrow new: `_ZnwmRKSt9nothrow_t` forwards to malloc,
returns nullptr on OOM (the nothrow contract)
`crates/wasm-libc-shim/src/stdio_stub.c`:
- fprintf C-side variadic stub (Skia diagnostic path) that
routes into the same panic helper as snprintf / vsnprintf /
vfprintf — same fail-fast policy.
# Verification
- `cargo build -p openpencil-shell-web --target
wasm32-unknown-unknown --features skia --release` — green
- `wasm-bindgen --target web` produces ../pkg/openpencil_shell
_web.{js,_bg.wasm}
- `bash tools/check-wasm-bundle.sh` — PASS:
- 0 env.* imports preserved (24 new ones absorbed by shim)
- 906 935 bytes gzip = 86% of 1 MiB ceiling (+286 KB vs
pre-text — Skia font/freetype subsystem is substantial.
Headroom: 14% of ceiling)
- `cargo test -p openpencil-shell-core --lib` — 39 tests
- `cargo build -p openpencil-shell-native --example
inspector_window` — green (desktop demo unchanged — uses
jian-skia textlayout via NativeBackend, not the web font
path)
- `cargo check -p openpencil-shell-native --target
aarch64-apple-ios` — green
- `cargo check -p openpencil-shell-native --target
aarch64-linux-android` — green
# Re-run the demo
```
EMSDK="$HOME/.emsdk" cargo build -p openpencil-shell-web \
--target wasm32-unknown-unknown --features skia --release
wasm-bindgen --target web --out-dir crates/openpencil-shell-web/pkg \
target/wasm32-unknown-unknown/release/openpencil_shell_web.wasm
cd crates/openpencil-shell-web/smoke
python3 -m http.server 8000
# Browser: http://localhost:8000/step-1b.html
```
Now the canvas viewport renders "Hello OpenPencil" + "Click me"
text in addition to the rect/stroke geometry.
90 lines
3.5 KiB
TOML
90 lines
3.5 KiB
TOML
[package]
|
|
name = "openpencil-shell-web"
|
|
version.workspace = true
|
|
edition.workspace = true
|
|
rust-version.workspace = true
|
|
license.workspace = true
|
|
description = "OpenPencil shell — wasm32-unknown-unknown web bundle entry (Step 1b §1.2, C-hard pipeline)"
|
|
|
|
[lib]
|
|
name = "openpencil_shell_web"
|
|
path = "src/lib.rs"
|
|
crate-type = ["cdylib", "rlib"]
|
|
|
|
[dependencies]
|
|
openpencil-shell-core = { path = "../openpencil-shell-core", version = "0.1.0" }
|
|
# Step 1b §3.6: handwritten DOM mirror consumes accesskit::TreeUpdate (0.24).
|
|
accesskit = "0.24"
|
|
console_error_panic_hook = "0.1"
|
|
js-sys = "0.3"
|
|
# Phase C1 DOM event mappers build `jian_core::gesture::WheelEvent` etc
|
|
# whose position/delta fields use `jian_core::geometry::Point`. The
|
|
# `geometry` module is not re-exported by shell-core (which only
|
|
# re-exports `gesture::*`), so pull jian-core directly. Same path-with-
|
|
# version pattern as shell-core's own jian-core dep.
|
|
jian-core = { path = "../../vendor/jian/crates/jian-core", version = "0.0.1" }
|
|
# Pin to 0.2.117 — last wasm-bindgen-cli release compatible with the
|
|
# workspace's Rust 1.85 toolchain. Newer 0.2.120+ requires rustc 1.86.
|
|
# When the workspace bumps Rust, raise this bound.
|
|
wasm-bindgen = "=0.2.117"
|
|
|
|
# Step 1b §2.2 (post C-hard.2 lock-in 2026-05-09): wasm32-unknown-unknown
|
|
# via the vendor/skia-safe-op fork + crates/wasm-libc-shim. Optional dep
|
|
# behind the `skia` feature so `cargo check --target wasm32-unknown-
|
|
# unknown --no-default-features --features web` (the kickoff §1.2
|
|
# wasm32-clean CI baseline) still compile-checks without pulling skia
|
|
# — that path uses the stub mount entry. Phase A and onward opt in via
|
|
# `--features skia` to get the real WebBackend + raster paint loop.
|
|
[dependencies.skia-safe]
|
|
version = "0.97.0"
|
|
default-features = false
|
|
# Step 3 codex stop-hook fix: text rendering uses
|
|
# `Canvas::draw_str` + raw Font + Typeface (NOT skia paragraph
|
|
# builder), so the `textlayout` feature is unnecessary on web.
|
|
# Dropping it removes ICU + Harfbuzz from the bundle (~400 KB
|
|
# gzip) and avoids the env.* import explosion that came with
|
|
# textlayout's filesystem font lookups.
|
|
features = ["binary-cache"]
|
|
optional = true
|
|
|
|
# C-hard.2 libc/libcxx/libm shim — only linked when targeting
|
|
# wasm32-unknown-unknown with the `skia` feature on. The crate's lib.rs
|
|
# guards every symbol with `cfg(all(target_arch = "wasm32", target_os
|
|
# = "unknown"))` so native builds (where this would conflict with the
|
|
# host C runtime) link an empty crate.
|
|
[target.'cfg(all(target_arch = "wasm32", target_os = "unknown"))'.dependencies]
|
|
wasm-libc-shim = { path = "../wasm-libc-shim", optional = true }
|
|
|
|
[dependencies.web-sys]
|
|
version = "0.3"
|
|
features = [
|
|
"CanvasRenderingContext2d",
|
|
"CompositionEvent",
|
|
"console",
|
|
"Document",
|
|
"Element",
|
|
"Event",
|
|
"EventTarget",
|
|
"FocusEvent",
|
|
"HtmlCanvasElement",
|
|
"HtmlElement",
|
|
"ImageData",
|
|
"KeyboardEvent",
|
|
"PointerEvent",
|
|
"WheelEvent",
|
|
"Window",
|
|
]
|
|
|
|
[features]
|
|
# `web` keeps the kickoff §1.2 wasm32-unknown-unknown CI baseline green —
|
|
# the crate compile-checks as a wasm32-clean stub without pulling skia.
|
|
default = ["web"]
|
|
web = []
|
|
# `skia` opts into the Phase A WebBackend (skia-safe + raster surface).
|
|
# When the cargo target is wasm32-unknown-unknown the build also links
|
|
# the libc/libcxx/libm shim crate so the bundle is runtime-loadable.
|
|
# Native targets resolve the target-conditional shim dep to nothing.
|
|
skia = ["dep:skia-safe", "wasm-libc-shim"]
|
|
# Internal feature toggled on by `skia` to flip the cfg-target dep.
|
|
wasm-libc-shim = ["dep:wasm-libc-shim"]
|