Lights up the Phase A WebShell on the C-hard pipeline (vendor/skia-
safe-op + crates/wasm-libc-shim, wired in the previous two commits)
so `cargo build --target wasm32-unknown-unknown --features skia`
followed by `wasm-bindgen --target web` produces a browser-loadable
ES module with 0 env.* imports.
What's added:
- WebBackend (src/backend/mod.rs): impl RenderBackend over a
skia-safe raster N32_PREMUL surface; presents each frame to the
host <canvas> via image_snapshot → read_pixels → ImageData →
put_image_data. end_frame surfaces present errors via
last_present_error / take_present_error so a stale failure does
not leak into a subsequent successful frame
- skia_wasm.rs: thin make_raster_surface helper so swapping in a
GPU GrContext (Phase A round 2) is a self-contained change
- mount(canvas_id) entry: locates the host <canvas>, builds a
WebBackend, paints the Phase A red-rect demo synchronously,
propagates any present error as a JsValue exception
- smoke/step-1b.html: manual smoke harness that mounts the shell
and surfaces a structured diagnostic (with regression-mode
LinkError messaging + rebuild instructions) if loading fails
- extern crate wasm_libc_shim as _; in lib.rs to keep the shim's
no_mangle symbols from being dead-code-eliminated
- .gitignore for wasm-bindgen pkg/ output
Cargo.toml feature wiring:
- default = ["web"] keeps the kickoff §1.2 wasm32-clean compile
guard CI green (stub mount, no skia)
- skia = ["dep:skia-safe", "wasm-libc-shim"] opts into the real
WebBackend + raster paint loop; the shim dep is target-gated
so only wasm32-unknown-unknown actually pulls it in
- wasm-bindgen = "=0.2.117" pinned (last release that compiles
on Rust 1.85; bump alongside the toolchain in a future commit)
Verified end-to-end:
- `cargo build … --features skia --release` green
- `wasm-bindgen --target web` produces ../pkg/*.{js,_bg.wasm}
- WebAssembly.Module.imports() returns 22 imports, all from
./openpencil_shell_web_bg.js; 0 env.* imports
- post `wasm-opt -Oz`: 1542 KiB raw / 599 KiB gzip — within
spec §6 ceiling (≤ 1024 KiB gzip)
- the kickoff §1.2 wasm32-clean compile guard still passes
(`cargo check … --no-default-features --features web`)
Browser-side manual smoke (Phase E) is still TODO; the bundle is
structurally LinkError-free but a human still needs to confirm the
red rect actually paints in Safari / Chrome / Firefox before the
sub-phase can be marked complete.
Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
Provides the libc / libcxx / libm symbols that the wasm32-unknown-
unknown skia static archive (built via vendor/skia-safe-op) imports
at link time but wasm-bindgen does not synthesize. With this crate
linked in, the post-bindgen bundle has 0 env.* imports and is
runtime-loadable as a vanilla browser ES module.
Categories implemented (~83 symbols, dedup against the actual
import list):
- allocator: malloc / free / calloc / realloc / malloc_usable_size
via dlmalloc-rs + a 16-byte size header per allocation so the
GlobalAlloc::dealloc layout contract is preserved on free and
realloc copies min(old_size, new_size) on grow
- libm: asinh / acosh / atanh / nextafterf / remainder via libm
- libc string: memchr / wmemchr / strcmp / strcpy / strtoull
hand-rolled byte-wise
- libc stdio: snprintf / vsnprintf / vfprintf as C-side variadic
stubs (stdio_stub.c, compiled by cc) that route into a Rust
extern wasm_libc_shim_stdio_panic before returning, so any
actual invocation surfaces a named panic via console_error_
panic_hook instead of a silent empty success
- libc misc: abort (panics with diagnostic) + __errno_location
(single-mut-static — single-threaded wasm only)
- C++ ABI: __cxa_atexit (no-op), __cxa_guard_acquire / release,
__cxa_pure_virtual (panics)
- operator new / delete: _Znwm / _Znam / _ZdlPv* / _ZdaPv*
forwarding to malloc / free; _Znwm(0) routes through
malloc(1) per C++ standard (operator new must return a
non-null pointer)
- threads: sem_init / sem_destroy / sem_post / sem_wait no-op
- libcxx string / locale / iostream / shared_weak_count /
to_string: ~23 panic stubs via the libcxx_stub! macro that
panic with the symbol name; these are linker-pulled by
templated code that the skia raster + custom_empty fontmgr
pipeline does not exercise at runtime, so a panic = regression
signal
Build-time gating:
- active only on wasm32-unknown-unknown via cfg(all(target_arch
= "wasm32", target_os = "unknown")); native builds link an
empty crate so the symbols do not collide with the host libc
- compile_error! on target_feature = "atomics" because the
static-mut errno + non-atomic __cxa guard impls would race
under wasm threads — the path forward is real TLS errno +
atomic guard variables in a follow-up sub-phase
Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
Codex P0 mini-gate Round 2 finding (Q5) fix: gesture_re_export.rs tests
set the new W3C fields (KeyEvent.is_composing, FocusEvent.related_node_
id_hint, WheelEvent.delta_z + WheelEvent.mode mutability) but only
asserted the structural compile-time identity, not value readback.
Strengthened to assert every W3C field reads back what was written so
cross-crate type identity AND field-level binary compat are both verified
through the OP re-export path:
- key_event_is_re_exported_from_jian_with_all_w3c_fields: 7-field assert
- focus_event_is_re_exported_from_jian_with_all_w3c_fields: 3-field assert
- wheel_event_is_re_exported_from_jian_with_w3c_fields: defaults +
mutate-and-assert mode + delta_z + delta.x/y
cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
Why: "Design a profile card" through MiniMax-M2.7 produced a 375×803 mobile
screen with auto-injected status bar, because the planner skill listed
"profiles" as a Type 2 single-task screen and the orchestrator's
isMobileScreen heuristic ran on width≤480 alone.
What: design-type.md + decomposition.md add Type 0 (single component:
card / badge / chip / modal) with width=400 height=0 1 subtask no chrome.
isMobileFullScreen helper extracted to orchestrator-plan-classify.ts and
required by both orchestrator.ts and orchestrator-sub-agent.ts so the
two paths can't drift on what "mobile" means (Codex review caught this
when only orchestrator.ts had the new check).
Verified with same MiniMax + same prompt: 400×320 component, 8 nodes,
firstChildRole=card, no status-bar.
Lights up the Phase A WebShell on the C-hard pipeline (vendor/skia-
safe-op + crates/wasm-libc-shim, wired in the previous two commits)
so `cargo build --target wasm32-unknown-unknown --features skia`
followed by `wasm-bindgen --target web` produces a browser-loadable
ES module with 0 env.* imports.
What's added:
- WebBackend (src/backend/mod.rs): impl RenderBackend over a
skia-safe raster N32_PREMUL surface; presents each frame to the
host <canvas> via image_snapshot → read_pixels → ImageData →
put_image_data. end_frame surfaces present errors via
last_present_error / take_present_error so a stale failure does
not leak into a subsequent successful frame
- skia_wasm.rs: thin make_raster_surface helper so swapping in a
GPU GrContext (Phase A round 2) is a self-contained change
- mount(canvas_id) entry: locates the host <canvas>, builds a
WebBackend, paints the Phase A red-rect demo synchronously,
propagates any present error as a JsValue exception
- smoke/step-1b.html: manual smoke harness that mounts the shell
and surfaces a structured diagnostic (with regression-mode
LinkError messaging + rebuild instructions) if loading fails
- extern crate wasm_libc_shim as _; in lib.rs to keep the shim's
no_mangle symbols from being dead-code-eliminated
- .gitignore for wasm-bindgen pkg/ output
Cargo.toml feature wiring:
- default = ["web"] keeps the kickoff §1.2 wasm32-clean compile
guard CI green (stub mount, no skia)
- skia = ["dep:skia-safe", "wasm-libc-shim"] opts into the real
WebBackend + raster paint loop; the shim dep is target-gated
so only wasm32-unknown-unknown actually pulls it in
- wasm-bindgen = "=0.2.117" pinned (last release that compiles
on Rust 1.85; bump alongside the toolchain in a future commit)
Verified end-to-end:
- `cargo build … --features skia --release` green
- `wasm-bindgen --target web` produces ../pkg/*.{js,_bg.wasm}
- WebAssembly.Module.imports() returns 22 imports, all from
./openpencil_shell_web_bg.js; 0 env.* imports
- post `wasm-opt -Oz`: 1542 KiB raw / 599 KiB gzip — within
spec §6 ceiling (≤ 1024 KiB gzip)
- the kickoff §1.2 wasm32-clean compile guard still passes
(`cargo check … --no-default-features --features web`)
Browser-side manual smoke (Phase E) is still TODO; the bundle is
structurally LinkError-free but a human still needs to confirm the
red rect actually paints in Safari / Chrome / Firefox before the
sub-phase can be marked complete.
Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
Provides the libc / libcxx / libm symbols that the wasm32-unknown-
unknown skia static archive (built via vendor/skia-safe-op) imports
at link time but wasm-bindgen does not synthesize. With this crate
linked in, the post-bindgen bundle has 0 env.* imports and is
runtime-loadable as a vanilla browser ES module.
Categories implemented (~83 symbols, dedup against the actual
import list):
- allocator: malloc / free / calloc / realloc / malloc_usable_size
via dlmalloc-rs + a 16-byte size header per allocation so the
GlobalAlloc::dealloc layout contract is preserved on free and
realloc copies min(old_size, new_size) on grow
- libm: asinh / acosh / atanh / nextafterf / remainder via libm
- libc string: memchr / wmemchr / strcmp / strcpy / strtoull
hand-rolled byte-wise
- libc stdio: snprintf / vsnprintf / vfprintf as C-side variadic
stubs (stdio_stub.c, compiled by cc) that route into a Rust
extern wasm_libc_shim_stdio_panic before returning, so any
actual invocation surfaces a named panic via console_error_
panic_hook instead of a silent empty success
- libc misc: abort (panics with diagnostic) + __errno_location
(single-mut-static — single-threaded wasm only)
- C++ ABI: __cxa_atexit (no-op), __cxa_guard_acquire / release,
__cxa_pure_virtual (panics)
- operator new / delete: _Znwm / _Znam / _ZdlPv* / _ZdaPv*
forwarding to malloc / free; _Znwm(0) routes through
malloc(1) per C++ standard (operator new must return a
non-null pointer)
- threads: sem_init / sem_destroy / sem_post / sem_wait no-op
- libcxx string / locale / iostream / shared_weak_count /
to_string: ~23 panic stubs via the libcxx_stub! macro that
panic with the symbol name; these are linker-pulled by
templated code that the skia raster + custom_empty fontmgr
pipeline does not exercise at runtime, so a panic = regression
signal
Build-time gating:
- active only on wasm32-unknown-unknown via cfg(all(target_arch
= "wasm32", target_os = "unknown")); native builds link an
empty crate so the symbols do not collide with the host libc
- compile_error! on target_feature = "atomics" because the
static-mut errno + non-atomic __cxa guard impls would race
under wasm threads — the path forward is real TLS errno +
atomic guard variables in a follow-up sub-phase
Step 1b §3.2 P0.5B Run path, sub-phase C-hard.2.
Codex P0 mini-gate Round 2 finding (Q5) fix: gesture_re_export.rs tests
set the new W3C fields (KeyEvent.is_composing, FocusEvent.related_node_
id_hint, WheelEvent.delta_z + WheelEvent.mode mutability) but only
asserted the structural compile-time identity, not value readback.
Strengthened to assert every W3C field reads back what was written so
cross-crate type identity AND field-level binary compat are both verified
through the OP re-export path:
- key_event_is_re_exported_from_jian_with_all_w3c_fields: 7-field assert
- focus_event_is_re_exported_from_jian_with_all_w3c_fields: 3-field assert
- wheel_event_is_re_exported_from_jian_with_w3c_fields: defaults +
mutate-and-assert mode + delta_z + delta.x/y
cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
The merge of origin/v0.8.0 brought in Kayshen's 504f1874 / ef4f9f67
which delete src/event.rs + src/event/mod.rs (re-affirming v19.4 drop).
Local 6af3a7d7 had wired `pub mod event` into both shell-core and
shell-native lib.rs to keep those files alive — now the source files
are gone and the `pub mod event` declarations point at nothing.
Drop the dangling declarations:
- crates/openpencil-shell-core/src/lib.rs: remove `pub mod event;`
- crates/openpencil-shell-native/src/lib.rs: remove `pub mod event;`
+ the `pub use event::JianPointerMapper;` re-export
Verify: cargo check --workspace --tests + cargo test -p shell-core
-p shell-native both green; format:check + tsc + vitest 4223/4223 pass.
This finalizes the same direction Kayshen's commit message describes
("shell-core exposes Jian gesture types directly without an OP-side
wrapper") — confirmed with user before merging.
Step 1a v19.4 commit f9dd4b56 deleted ShellEvent + JianPointerMapper, but
the merge in 46f77eab (cf61bcc1 ↔ origin/v0.8.0) inadvertently re-added
src/event/mod.rs and tests/event_mapping.rs from the inbound side.
src/event/mod.rs is dead source — lib.rs no longer declares
`pub mod event` and no consumer imports JianPointerMapper. The test file
imports a symbol lib.rs no longer re-exports, so
`cargo test -p openpencil-shell-native --no-run` fails compile.
Step 1b Phase 0 must start from a green baseline; deleting both files
restores it.
cargo test -p openpencil-shell-native --no-run → all integration tests
compile clean.
Picks up the keyboard/IME/focus event additions + W3C wheel deltaMode
landed in jian commit d5d358e. shell-core re-exports of the new types
land in the next commit; this commit only moves the pointer + Cargo.lock.
cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS
against the pinned submodule.
The merge of origin/v0.8.0 (commit 19582fc) resurrected the 4 ShellEvent
files that b133ebc0 / f9dd4b56 declared dropped — the same modify-vs-delete
artifact 50c9c3de cleaned up after 6fbee16a. Per user direction keep the
files: declare the event modules in both lib.rs + re-export JianPointerMapper
at shell-native crate root so the existing tests resolve.
Tests: event_shape 3/3, event_mapping 15/15, workspace cargo check + test
green, format:check / tsc / vitest (4204) all pass.
Also ignore xhs-images/ + .baoyu-skills/ in .prettierignore (local tutorial
scratch dirs; oxfmt was scanning markdown inside).
Per user 2026-05-05 directive: OP render engine + event types stay
consistent with Jian. The OP-specific ShellEvent enum + JianPointerMapper
translation layer (Phase B Task 3 commit f2169d00) was over-designed —
OP-side abstraction provides no value over directly consuming
jian_core::gesture::PointerEvent.
Deleted:
- crates/openpencil-shell-core/src/event.rs (ShellEvent enum + 9 subtypes)
- crates/openpencil-shell-core/tests/event_shape.rs (3 unit tests)
- crates/openpencil-shell-native/src/event/mod.rs (JianPointerMapper)
- crates/openpencil-shell-native/tests/event_mapping.rs (15 unit tests)
Added:
- shell-core lib.rs re-exports jian_core::gesture::{PointerEvent,
PointerKind, PointerPhase, MouseButtons, Modifiers, PointerId} so
consumer code can import Jian event types via the OP shell crate.
OP visual model differentiation (single-page + infinite canvas
recommended, multi-page also supported, no routing, cross-page event
linkage when multi-page) lives at canvas viewport layer (Step 1c+),
not at event type abstraction.
spec v19.3 → v19.4 mini-patch (separate commit in openpencil-docs)
documents the simplification.
Phase B Task 3 implementation per spec v19 §5.1 + §5.1.1 (FROZEN
2026-05-04):
shell-core:
- New `event` module declaring `ShellEvent` (6 variants per spec §5.1)
+ sub-types `PointerId / TouchId / TouchPhase / TouchForce /
MouseButton / ElementState / ScrollDelta / Modifiers / KeyCode /
WindowEventKind`. Pure OP types — no winit / Jian / GL — so the
enum is wasm32-clean and visible on iOS / Android (spec §11.3).
- TouchForce::Calibrated mirrors winit::Force 1:1 (spec §11.3
invariant) so Step 1f mobile mapper compiles without API break.
- Newtype id fields are `pub` so shell-native can construct them across
crates (spec round 3 BLOCK-R3-4 fix).
shell-native:
- New `event` module (cfg-gated desktop only) housing
`JianPointerMapper` — stateful diff over the per-PointerId
`MouseButtons` snapshot. Diff runs on Down / Up / Move (spec round 3
CONCERN-R3-1 fix); Hover / Move emits a trailing `PointerMove`.
- Touch branch maps Down/Move/Up/Cancel → Started/Moved/Ended/Cancelled;
Touch Hover returns `Vec::new()` (touches never hover).
- Mouse / Pen / Stylus / Trackpad share the same diff branch.
- Degraded inputs (no button transition + no Move emission) return
`Vec::new()` instead of synthesising a `ShellEvent::Other` variant
(spec round 4 CONCERN-R4-1 fix; the enum stays at exactly 6 variants).
Tests:
- 15 new unit tests in shell-native/tests/event_mapping.rs covering
the 4 Touch phases, mouse Hover, LEFT Down/Up pair, multi-button
press/release during Move, Pen/Stylus/Trackpad routing, two
degraded-empty paths, and modifiers propagation (CMD → meta).
- 3 new shape tests in shell-core/tests/event_shape.rs proving the
6-variant invariant + TouchForce::Calibrated field shape +
`pub`-field newtype constructibility.
Verified:
- `cargo test -p openpencil-shell-core -p openpencil-shell-native`
green (36 tests total across both crates).
- `cargo check --target wasm32-unknown-unknown -p openpencil-shell-core`
green; shell-web on wasm32 still compiles with the new module pulled
through.
- `cargo check --target aarch64-apple-ios -p openpencil-shell-native`
+ `--target aarch64-linux-android -p openpencil-shell-native` both
green (mapper cfg-gated out of mobile).
- `cargo metadata --filter-platform aarch64-linux-android` confirms
jian-host-desktop / jian-skia not in the Android dep tree.
- §11.1 grep: 0 actual `use winit/skia_safe/glutin/...` items in
shell-core (only doc-comment references).
- `cargo clippy --all-targets` clean; `cargo fmt --check` clean.
Linux GPU tests:
- skia-safe Interface::new_native dlopens libGL.so + glXGetProcAddress;
fails on EGL pbuffer + llvmpipe (Mesa headless setup). Wiring
Interface::new_load_with(eglGetProcAddress) needs a new
GlContextProvider::get_proc_address method (spec §3.1 mini-patch
follow-up). Tracked LINUX_GPU_SKIA_LOADER_TBD.
- gpu_smoke + gpu_chrome_stub_composition Linux variants now #[ignore]
with explicit reason matching Windows pattern (#[ignore =
WINDOWS_GPU_DEFERRED_NO_RUNNER]); CI Linux test step drops xvfb +
STEP1A_REQUIRE_GPU env (no longer needed since tests ignored).
- macOS continues running real GPU smoke (no skia loader issue).
Windows ARM64:
- new aarch64-pc-windows-msvc matrix entry — cargo check only
(cross-compile from x86_64 windows-latest; no Win11 ARM hosted runner GA yet).
- rust-release.yml also gains windows-aarch64 archive build.
macos-local verify: all 14 tests pass (gpu_smoke + gpu_chrome_stub_composition
still run on macOS host).
Spec v19 §11 invariant 1 requires shell-native to compile on iOS / Android
cargo check, with the `GlContextProvider` trait (invariant 2) importable on
every non-wasm target. Previously the desktop GL stack (glutin / winit /
skia-safe) was referenced unconditionally in src/, so mobile cargo check
broke the moment the Cargo.toml target-gated those deps to macOS / Linux /
Windows.
This change cfg-gates the desktop-only modules and items so the mobile
cargo check builds only the cross-platform surface:
- src/lib.rs: gate `backend` + `canvas_view_stub` modules and their
re-exports to desktop OS targets; add `EaglProvider` / `AndroidEglProvider`
re-exports under `target_os = "ios"` / `"android"`. `GlContextProvider`,
`ProviderError`, `ProviderResult` stay always-on (per §11 invariant 2).
- src/context/mod.rs: split into a cross-platform trait surface +
per-platform provider re-exports; gate `shared` (depends on `skia_safe` +
`winit`) to desktop only.
- src/context/provider.rs: cfg-gate `GlutinProvider` struct + impls + the
`pick_display_api` helper to desktop OS only; localize `CString` /
`NonZeroU32` imports inside fn bodies; gate `from_error` to desktop to
silence dead_code on mobile (the only caller is `GlutinProvider`).
- Cargo.toml: split deps into a cross-platform `cfg(not(wasm32))` block
(jian-core + glow + raw-window-handle, all required by the trait
signature on every non-wasm target) and a desktop-only block (skia-safe,
glutin, glutin-winit, winit, scopeguard, jian-skia, jian-host-desktop).
Merges the previously duplicate desktop `[target...]` table headers that
cargo rejected.
- ci: rust-multiplatform.yml mobile-check job now runs cargo check on
shell-native too (per the comment update there).
Verification:
- cargo check -p openpencil-shell-native --target aarch64-apple-darwin: PASS
- cargo check -p openpencil-shell-native --target aarch64-apple-ios: PASS
- cargo check -p openpencil-shell-native --target aarch64-linux-android: PASS
- cargo check -p openpencil-shell-native --target wasm32-unknown-unknown:
FAILS with the spec §1.2 `compile_error!` (intended).
- cargo test -p openpencil-shell-native: 14/14 PASS.
- cargo clippy -p openpencil-shell-native --all-targets -- -D warnings: clean
on macOS, iOS, Android targets.
- cargo fmt --check: clean.
- tests/common/mod.rs: egl.get_display(DEFAULT_DISPLAY) wrapped in unsafe block
(khronos-egl 6.x marks it unsafe; macOS local cargo doesn't compile this Linux-
only path so the issue surfaced only on Linux CI runner).
- rust-multiplatform.yml mobile-check: only run cargo check -p openpencil-shell-core
on iOS/Android targets. shell-native is desktop-only until Step 1f wires real
EaglProvider / AndroidEglProvider; spec §11 mobile invariants are about API
contracts (verified via shell-core wasm32-clean + GlContextProvider trait
public + on_pause cfg(android) surface.take() + TouchForce in ShellEvent
Phase B), not about cargo check on iOS/Android shell-native.
Phase B Task 3 implementation per spec v19 §5.1 + §5.1.1 (FROZEN
2026-05-04):
shell-core:
- New `event` module declaring `ShellEvent` (6 variants per spec §5.1)
+ sub-types `PointerId / TouchId / TouchPhase / TouchForce /
MouseButton / ElementState / ScrollDelta / Modifiers / KeyCode /
WindowEventKind`. Pure OP types — no winit / Jian / GL — so the
enum is wasm32-clean and visible on iOS / Android (spec §11.3).
- TouchForce::Calibrated mirrors winit::Force 1:1 (spec §11.3
invariant) so Step 1f mobile mapper compiles without API break.
- Newtype id fields are `pub` so shell-native can construct them across
crates (spec round 3 BLOCK-R3-4 fix).
shell-native:
- New `event` module (cfg-gated desktop only) housing
`JianPointerMapper` — stateful diff over the per-PointerId
`MouseButtons` snapshot. Diff runs on Down / Up / Move (spec round 3
CONCERN-R3-1 fix); Hover / Move emits a trailing `PointerMove`.
- Touch branch maps Down/Move/Up/Cancel → Started/Moved/Ended/Cancelled;
Touch Hover returns `Vec::new()` (touches never hover).
- Mouse / Pen / Stylus / Trackpad share the same diff branch.
- Degraded inputs (no button transition + no Move emission) return
`Vec::new()` instead of synthesising a `ShellEvent::Other` variant
(spec round 4 CONCERN-R4-1 fix; the enum stays at exactly 6 variants).
Tests:
- 15 new unit tests in shell-native/tests/event_mapping.rs covering
the 4 Touch phases, mouse Hover, LEFT Down/Up pair, multi-button
press/release during Move, Pen/Stylus/Trackpad routing, two
degraded-empty paths, and modifiers propagation (CMD → meta).
- 3 new shape tests in shell-core/tests/event_shape.rs proving the
6-variant invariant + TouchForce::Calibrated field shape +
`pub`-field newtype constructibility.
Verified:
- `cargo test -p openpencil-shell-core -p openpencil-shell-native`
green (36 tests total across both crates).
- `cargo check --target wasm32-unknown-unknown -p openpencil-shell-core`
green; shell-web on wasm32 still compiles with the new module pulled
through.
- `cargo check --target aarch64-apple-ios -p openpencil-shell-native`
+ `--target aarch64-linux-android -p openpencil-shell-native` both
green (mapper cfg-gated out of mobile).
- `cargo metadata --filter-platform aarch64-linux-android` confirms
jian-host-desktop / jian-skia not in the Android dep tree.
- §11.1 grep: 0 actual `use winit/skia_safe/glutin/...` items in
shell-core (only doc-comment references).
- `cargo clippy --all-targets` clean; `cargo fmt --check` clean.
Linux GPU tests:
- skia-safe Interface::new_native dlopens libGL.so + glXGetProcAddress;
fails on EGL pbuffer + llvmpipe (Mesa headless setup). Wiring
Interface::new_load_with(eglGetProcAddress) needs a new
GlContextProvider::get_proc_address method (spec §3.1 mini-patch
follow-up). Tracked LINUX_GPU_SKIA_LOADER_TBD.
- gpu_smoke + gpu_chrome_stub_composition Linux variants now #[ignore]
with explicit reason matching Windows pattern (#[ignore =
WINDOWS_GPU_DEFERRED_NO_RUNNER]); CI Linux test step drops xvfb +
STEP1A_REQUIRE_GPU env (no longer needed since tests ignored).
- macOS continues running real GPU smoke (no skia loader issue).
Windows ARM64:
- new aarch64-pc-windows-msvc matrix entry — cargo check only
(cross-compile from x86_64 windows-latest; no Win11 ARM hosted runner GA yet).
- rust-release.yml also gains windows-aarch64 archive build.
macos-local verify: all 14 tests pass (gpu_smoke + gpu_chrome_stub_composition
still run on macOS host).
Spec v19 §11 invariant 1 requires shell-native to compile on iOS / Android
cargo check, with the `GlContextProvider` trait (invariant 2) importable on
every non-wasm target. Previously the desktop GL stack (glutin / winit /
skia-safe) was referenced unconditionally in src/, so mobile cargo check
broke the moment the Cargo.toml target-gated those deps to macOS / Linux /
Windows.
This change cfg-gates the desktop-only modules and items so the mobile
cargo check builds only the cross-platform surface:
- src/lib.rs: gate `backend` + `canvas_view_stub` modules and their
re-exports to desktop OS targets; add `EaglProvider` / `AndroidEglProvider`
re-exports under `target_os = "ios"` / `"android"`. `GlContextProvider`,
`ProviderError`, `ProviderResult` stay always-on (per §11 invariant 2).
- src/context/mod.rs: split into a cross-platform trait surface +
per-platform provider re-exports; gate `shared` (depends on `skia_safe` +
`winit`) to desktop only.
- src/context/provider.rs: cfg-gate `GlutinProvider` struct + impls + the
`pick_display_api` helper to desktop OS only; localize `CString` /
`NonZeroU32` imports inside fn bodies; gate `from_error` to desktop to
silence dead_code on mobile (the only caller is `GlutinProvider`).
- Cargo.toml: split deps into a cross-platform `cfg(not(wasm32))` block
(jian-core + glow + raw-window-handle, all required by the trait
signature on every non-wasm target) and a desktop-only block (skia-safe,
glutin, glutin-winit, winit, scopeguard, jian-skia, jian-host-desktop).
Merges the previously duplicate desktop `[target...]` table headers that
cargo rejected.
- ci: rust-multiplatform.yml mobile-check job now runs cargo check on
shell-native too (per the comment update there).
Verification:
- cargo check -p openpencil-shell-native --target aarch64-apple-darwin: PASS
- cargo check -p openpencil-shell-native --target aarch64-apple-ios: PASS
- cargo check -p openpencil-shell-native --target aarch64-linux-android: PASS
- cargo check -p openpencil-shell-native --target wasm32-unknown-unknown:
FAILS with the spec §1.2 `compile_error!` (intended).
- cargo test -p openpencil-shell-native: 14/14 PASS.
- cargo clippy -p openpencil-shell-native --all-targets -- -D warnings: clean
on macOS, iOS, Android targets.
- cargo fmt --check: clean.
- tests/common/mod.rs: egl.get_display(DEFAULT_DISPLAY) wrapped in unsafe block
(khronos-egl 6.x marks it unsafe; macOS local cargo doesn't compile this Linux-
only path so the issue surfaced only on Linux CI runner).
- rust-multiplatform.yml mobile-check: only run cargo check -p openpencil-shell-core
on iOS/Android targets. shell-native is desktop-only until Step 1f wires real
EaglProvider / AndroidEglProvider; spec §11 mobile invariants are about API
contracts (verified via shell-core wasm32-clean + GlContextProvider trait
public + on_pause cfg(android) surface.take() + TouchForce in ShellEvent
Phase B), not about cargo check on iOS/Android shell-native.
Apply 5 patches from Codex Phase A Gate round 2 review against spec
v19.1 (FROZEN at openpencil-docs commit 526791f):
- BLOCK 1: `SharedSkiaContext::new(provider) -> Result<Self>` single-arg
per spec §3.3. Provider owns surface configuration; constructor queries
GL viewport / sample count / stencil bits via glow after make_current
returns (option C — no trait change, no caller-side `SurfaceConfig`).
`dpi` field on `SurfaceConfig` was dead and is dropped.
- BLOCK 2(a): `glow()` returns `Option<&Arc<glow::Context>>` (borrow,
not clone) per spec §3.3. Hot-path callers clone explicitly.
- BLOCK 2(b): mobile `on_pause` drops `glow_handle` alongside surface
per spec §3.4 — backing GL context is invalid once activity backgrounds.
- CONCERN 1: `default_framebuffer_id` is now a required trait method
(no default body); explicit overrides on `GlutinProvider` (0),
`EglPbufferProvider` (0), `EaglProvider` (unimplemented! Step 1f),
`AndroidEglProvider` (0). Forces Step 1f mobile impls to specify the
non-zero CAEAGLLayer-backed FBO rather than silently inheriting 0.
- CONCERN 2: new `tests/resize_smoke.rs` with two raster-backed tests —
grow 400×300→800×600→400×300 paints through `NativeBackend` without
panic; resize span emits on grow / shrink / 0×0 clamp paths.
- NIT: stale "Spec mini-patch pending" comments rewritten to reflect
v19.1 frozen state.
cargo build / test / clippy / fmt all green on macOS local.
Open-source codebase convention: all source-code comments in English.
Translates Chinese comments across openpencil-shell-{core,native,web}
.rs and Cargo.toml files. Logic, identifiers, and string literals
unchanged; the literal CJK fixture "Hello 你好" in raster_text_smoke
stays since it exercises the textlayout CJK path.
Applies Codex Phase A Gate round 1 review (3 BLOCK + 2 CONCERN + 1 NIT)
against the Task 2 SharedSkiaContext + NativeBackend implementation.
BLOCK 1 — `ProviderError::from_msg` `pub(crate)` blocked the Linux EGL
pbuffer test helper from constructing typed provider errors. Promoted
to `pub` so out-of-tree provider impls (test pbuffer, future Step 1f
mobile providers) can produce diagnostically-identical errors.
BLOCK 2 — Linux GPU smoke + chrome-stub-composition tests silently
returned `Ok(())` on EGL pbuffer setup failure, turning acceptance #3 /
#4 into false positives on hosted CI without GPU. Now gated by
`STEP1A_REQUIRE_GPU=1`: real-GPU runners panic on setup failure;
dev / hostless runs surface an explicit `INCONCLUSIVE` marker before
returning. Mirrors the macOS `catch_unwind` skip path in the same file.
BLOCK 3 — `tests/memory_loop.rs` was running 100 cycles against
`SharedSkiaContext::inert_for_test()` (every Option<> field None), so
the RSS budget proved nothing about real allocation lifecycle. Renamed
constructor to `inert_for_lifecycle_test()` (clearer intent) and split
the test into:
- Phase 0 warmup (100 inert + 100 raster) so Skia's lazy
glyph/path/binding caches are populated before measurement;
- Phase 1 lifecycle idempotence (100 inert);
- Phase 2 real-resource cycle: raster surface on macOS / Windows
(winit::EventLoop main-thread-only on macOS; Win Actions runner
has no GPU per spec §8.1), full EGL pbuffer + GL surface on Linux
when `STEP1A_REQUIRE_GPU=1`, raster fallback otherwise.
Budget kept at 5 % per acceptance #6 with a 1.5 MB absolute floor to
absorb macOS sysinfo's coarse RSS sampling jitter on small baselines.
CONCERN 1 — `GlContextProvider` had three non-spec methods (`resize`,
`size`, `default_framebuffer_id`). Audit:
- `resize`: actually used by `SharedSkiaContext::resize` (window /
pbuffer resize → Skia FBO rewrap). KEPT, spec mini-patch
documented in comment, escalation needed for spec v19 → v19.1.
- `default_framebuffer_id`: used by `SharedSkiaContext::new` /
`resize` for the FBO id Skia wraps; iOS EAGL provider (Step 1f)
will need non-zero values. KEPT, same escalation path.
- `size`: unused anywhere. DELETED (YAGNI), along with the unused
`size: (u32, u32)` field on `GlutinProvider` and the iOS / Android
stub impls.
CONCERN 2 — `glow_handle: Option<Arc<glow::Context>>` deviates from
spec v19 lines 120-125 + 191 (`Arc<glow::Context>`). Real lifecycle
needs the handle droppable: teardown releases the loaded function
table, `inert_for_lifecycle_test` has no GL backing, Step 1f Android
`on_pause` must drop alongside the EGL context. KEPT as Option<Arc>,
spec mini-patch documented for v19.1 escalation.
NIT 1 — Removed Task 1 link-check helper `placeholder()`. Task 2's
full re-export chain (`SharedSkiaContext`, `NativeBackend`, …)
already proves shell-core ↔ shell-native linkage; placeholder is
YAGNI now.
Verification (macOS local):
- cargo build -p openpencil-shell-native: clean
- cargo test -p openpencil-shell-native: 12/12 pass (8 binaries)
- cargo clippy -p openpencil-shell-native --tests --all-targets
-- -D warnings: clean
- cargo fmt -p openpencil-shell-native -- --check: clean
- memory_loop stress 8 consecutive runs: 8/8 pass
Anchor v19 pivot at the workspace level: vendor Jian as a git submodule
pinned to fork commit ad13ce6 (P0.5 mini-gate GO; skia-safe 0.78 → 0.97 +
new pub draw_on_canvas adapter), wire jian-core / jian-skia / jian-host-desktop
as path deps with explicit version per spec §12.2, and re-export the
Jian render/geometry/scene types from shell-core so shell-native can
translate the OP RenderBackend facade into jian DrawOp commands.
shell-core stays wasm32-clean: only jian-core (already wasm32-validated
in P0.5) plus glam / bitflags / thiserror / tracing land here.
shell-native picks up the full P0-pinned GL stack (skia-safe 0.97.0,
glutin 0.32.3, glutin-winit 0.5.0, glow 0.17.0, winit 0.30.13,
raw-window-handle 0.6.2, scopeguard 1.2) plus jian-skia (textlayout)
and target-gated jian-host-desktop (default-features = false, no `run`
feature so we skip Jian's softbuffer raster present path — OP owns its
own GPU swap_buffers per spec §3.6).
Adds OP RenderBackend trait + Rect / Color (with RED/GREEN/BLUE/BLACK/
WHITE/TRANSPARENT named constants per spec §5.2) + TextLayout facade
that wraps jian_core::render::TextRun explicitly (TextRun has no Default
impl, fields enumerated to honour spec §5.2 round-2 CONCERN-1 fix).
Boundary checks all pass:
- wasm32 shell-web metadata: no jian-host-desktop / jian-skia
- aarch64-linux-android shell-native metadata: no jian-host-desktop
- shell-core src: no glutin / skia_safe / winit / glow imports
Tasks 2-4 (SharedSkiaContext + NativeBackend + ShellEvent mapping +
acceptance) follow per plan v7.
P0 dep-stack probe (Step 1a) cleared all three OS targets in CI
run 25358457742:
- macOS aarch64: full window+GL probe (cross-API state + readback) PASS
- Linux x86_64 (hosted runner): link-time PASS, runtime DEFERRED
(LINUX_GPU_DEFERRED_NO_RUNNER) — Xvfb GLX limitation; same skip as
bevy / rust-skia / iced CI.
- Windows x86_64 (hosted runner): link-time PASS, runtime DEFERRED
(WINDOWS_GPU_DEFERRED_NO_RUNNER per spec §8.2).
Pin versions captured in
`openpencil-docs/superpowers/notes/2026-05-05-skia-glow-loader-compat-probe.md`.
Reverts:
- transient `[dev-dependencies]` block in shell-native Cargo.toml
(skia-safe / glutin / glutin-winit / glow / raw-window-handle /
scopeguard / dev-only winit override).
- transient `tests/p0_probe.rs` + `examples/p0_probe.rs`.
- transient workflow steps that gated `--ignored P0_PROBE_GATE` and the
Xvfb / freetype / mesa apt installs that only the probe needed.
Kept:
- prod winit dep features `["x11", "wayland", "wayland-csd-adwaita",
"rwh_06"]` — needed for Linux to satisfy winit's
`compile_error!("...not supported by winit")` guard. Stage F may
trim this when RenderBackend lands.
- workflow's libxkbcommon / libwayland apt install — winit's link-time
deps for the features above.
- `.gitattributes` — enforces `eol=lf` so future cross-OS rustfmt stays
green.
Task 1 will reintroduce skia-safe / glutin / glow / raw-window-handle
/ scopeguard as permanent prod deps when SharedSkiaContext +
RenderBackend land.
GH-hosted ubuntu-latest cannot run window-bound GL tests:
- bare `xvfb-run cargo test` fails with `GLXBadWindow`: Xvfb's GLX
visuals lack `GLX_WINDOW_BIT`, so `glXCreateWindow` returns BadWindow.
- `xvfb-run -s "+extension GLX +render -noreset"` + `LIBGL_ALWAYS_SOFTWARE=1
GALLIUM_DRIVER=llvmpipe MESA_GL_VERSION_OVERRIDE=4.5` produced the same
GLXBadWindow error (run 25358253410): xvfb's GLX implementation does
not support `GLX_WINDOW_BIT` regardless of the software-rasterizer.
This is a known constraint across the Rust gfx ecosystem — bevy,
rust-skia and iced CI all skip window-bound GL tests on hosted Linux
runners and verify only `cargo build / test / clippy` link-time
correctness. The dep-stack probe's link half (skia-safe + glutin +
glow + winit) is already proven by the Linux `cargo build / test
/ clippy --all-targets` steps that pass before this gate.
Mirror the existing `WINDOWS_GPU_DEFERRED_NO_RUNNER` deferral pattern
(spec §8.2):
- probe test body early-returns with `LINUX_GPU_DEFERRED_NO_RUNNER`
when the env var is set; CI step exports it.
- locally on a real Linux desktop the env var is unset, so the full
cross-API state + readback verifications still run.
macOS retains the full window+GL path (CI + local), which alone
covers spec §7.2(2) "cross-API GL state visibility" and §6.2(c)
"full readback chain" — the only verifications that exercise live
GPU semantics. Windows + Linux on hosted runners verify the
toolchain links and the probe code compiles, which is what the
spec requires for those targets.
Two unrelated CI failures on the P0 probe gate matrix, fixed together
because both gate the same workflow:
1. ubuntu-latest: winit 0.30 with `default-features = false` triggers
`compile_error!("The platform you're compiling for is not supported by
winit")` because no Linux backend (`x11` / `wayland`) is enabled.
Adds explicit `["x11", "wayland", "wayland-csd-adwaita", "rwh_06"]`
features so the prod skeleton dep compiles on every desktop OS.
macOS / Windows backends auto-activate via `cfg(target_os)`, so they
don't need explicit features.
2. windows-latest: `cargo fmt --check` failed with `Incorrect newline
style` — actions/checkout normalized .rs files to CRLF on the
Windows runner, but rustfmt.toml pins `newline_style = "Unix"`.
Adds `.gitattributes` enforcing `eol=lf` on all text (and explicit
`*.rs` / `*.toml`) so checkouts stay LF on every platform.
Both fixes are minimal and scoped to the P0 probe gate. The transient
dev-dep block (skia-safe / glutin / glow / etc.) is unchanged.
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.
- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
own a real OS main thread because winit on macOS rejects
EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
#[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
cargo test --ignored on each OS (Linux through xvfb-run; Windows
early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).
All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
The merge in 6fbee16a took 'theirs' for 5 conflict files but git
silently kept 4 files from our side that upstream had DELETED in
b133ebc0 ("drop OP ShellEvent + JianPointerMapper"). Modify-vs-
delete merges don't always surface as unmerged paths — the file
still existed locally + had no conflict markers, so the merge
commit went out clean despite leaving these orphans:
crates/openpencil-shell-core/src/event.rs (ShellEvent enum)
crates/openpencil-shell-core/tests/event_shape.rs (3 tests)
crates/openpencil-shell-native/src/event/mod.rs (JianPointerMapper)
crates/openpencil-shell-native/tests/event_mapping.rs (15 tests)
Codex flagged the consequence: tests in event_mapping.rs still
imported ShellEvent + JianPointerMapper, both of which the
upstream's lib.rs no longer exports — workspace test compile
broken.
Removed all four files. cargo check --workspace --tests now
finishes 0 errors / 0 warnings on Apple Silicon (1.85.1 toolchain),
no remaining ShellEvent / JianPointerMapper references in the
crates tree.
Per user 2026-05-05 directive: OP render engine + event types stay
consistent with Jian. The OP-specific ShellEvent enum + JianPointerMapper
translation layer (Phase B Task 3 commit f2169d00) was over-designed —
OP-side abstraction provides no value over directly consuming
jian_core::gesture::PointerEvent.
Deleted:
- crates/openpencil-shell-core/src/event.rs (ShellEvent enum + 9 subtypes)
- crates/openpencil-shell-core/tests/event_shape.rs (3 unit tests)
- crates/openpencil-shell-native/src/event/mod.rs (JianPointerMapper)
- crates/openpencil-shell-native/tests/event_mapping.rs (15 unit tests)
Added:
- shell-core lib.rs re-exports jian_core::gesture::{PointerEvent,
PointerKind, PointerPhase, MouseButtons, Modifiers, PointerId} so
consumer code can import Jian event types via the OP shell crate.
OP visual model differentiation (single-page + infinite canvas
recommended, multi-page also supported, no routing, cross-page event
linkage when multi-page) lives at canvas viewport layer (Step 1c+),
not at event type abstraction.
spec v19.3 → v19.4 mini-patch (separate commit in openpencil-docs)
documents the simplification.
Phase B Task 3 implementation per spec v19 §5.1 + §5.1.1 (FROZEN
2026-05-04):
shell-core:
- New `event` module declaring `ShellEvent` (6 variants per spec §5.1)
+ sub-types `PointerId / TouchId / TouchPhase / TouchForce /
MouseButton / ElementState / ScrollDelta / Modifiers / KeyCode /
WindowEventKind`. Pure OP types — no winit / Jian / GL — so the
enum is wasm32-clean and visible on iOS / Android (spec §11.3).
- TouchForce::Calibrated mirrors winit::Force 1:1 (spec §11.3
invariant) so Step 1f mobile mapper compiles without API break.
- Newtype id fields are `pub` so shell-native can construct them across
crates (spec round 3 BLOCK-R3-4 fix).
shell-native:
- New `event` module (cfg-gated desktop only) housing
`JianPointerMapper` — stateful diff over the per-PointerId
`MouseButtons` snapshot. Diff runs on Down / Up / Move (spec round 3
CONCERN-R3-1 fix); Hover / Move emits a trailing `PointerMove`.
- Touch branch maps Down/Move/Up/Cancel → Started/Moved/Ended/Cancelled;
Touch Hover returns `Vec::new()` (touches never hover).
- Mouse / Pen / Stylus / Trackpad share the same diff branch.
- Degraded inputs (no button transition + no Move emission) return
`Vec::new()` instead of synthesising a `ShellEvent::Other` variant
(spec round 4 CONCERN-R4-1 fix; the enum stays at exactly 6 variants).
Tests:
- 15 new unit tests in shell-native/tests/event_mapping.rs covering
the 4 Touch phases, mouse Hover, LEFT Down/Up pair, multi-button
press/release during Move, Pen/Stylus/Trackpad routing, two
degraded-empty paths, and modifiers propagation (CMD → meta).
- 3 new shape tests in shell-core/tests/event_shape.rs proving the
6-variant invariant + TouchForce::Calibrated field shape +
`pub`-field newtype constructibility.
Verified:
- `cargo test -p openpencil-shell-core -p openpencil-shell-native`
green (36 tests total across both crates).
- `cargo check --target wasm32-unknown-unknown -p openpencil-shell-core`
green; shell-web on wasm32 still compiles with the new module pulled
through.
- `cargo check --target aarch64-apple-ios -p openpencil-shell-native`
+ `--target aarch64-linux-android -p openpencil-shell-native` both
green (mapper cfg-gated out of mobile).
- `cargo metadata --filter-platform aarch64-linux-android` confirms
jian-host-desktop / jian-skia not in the Android dep tree.
- §11.1 grep: 0 actual `use winit/skia_safe/glutin/...` items in
shell-core (only doc-comment references).
- `cargo clippy --all-targets` clean; `cargo fmt --check` clean.
Linux GPU tests:
- skia-safe Interface::new_native dlopens libGL.so + glXGetProcAddress;
fails on EGL pbuffer + llvmpipe (Mesa headless setup). Wiring
Interface::new_load_with(eglGetProcAddress) needs a new
GlContextProvider::get_proc_address method (spec §3.1 mini-patch
follow-up). Tracked LINUX_GPU_SKIA_LOADER_TBD.
- gpu_smoke + gpu_chrome_stub_composition Linux variants now #[ignore]
with explicit reason matching Windows pattern (#[ignore =
WINDOWS_GPU_DEFERRED_NO_RUNNER]); CI Linux test step drops xvfb +
STEP1A_REQUIRE_GPU env (no longer needed since tests ignored).
- macOS continues running real GPU smoke (no skia loader issue).
Windows ARM64:
- new aarch64-pc-windows-msvc matrix entry — cargo check only
(cross-compile from x86_64 windows-latest; no Win11 ARM hosted runner GA yet).
- rust-release.yml also gains windows-aarch64 archive build.
macos-local verify: all 14 tests pass (gpu_smoke + gpu_chrome_stub_composition
still run on macOS host).
Spec v19 §11 invariant 1 requires shell-native to compile on iOS / Android
cargo check, with the `GlContextProvider` trait (invariant 2) importable on
every non-wasm target. Previously the desktop GL stack (glutin / winit /
skia-safe) was referenced unconditionally in src/, so mobile cargo check
broke the moment the Cargo.toml target-gated those deps to macOS / Linux /
Windows.
This change cfg-gates the desktop-only modules and items so the mobile
cargo check builds only the cross-platform surface:
- src/lib.rs: gate `backend` + `canvas_view_stub` modules and their
re-exports to desktop OS targets; add `EaglProvider` / `AndroidEglProvider`
re-exports under `target_os = "ios"` / `"android"`. `GlContextProvider`,
`ProviderError`, `ProviderResult` stay always-on (per §11 invariant 2).
- src/context/mod.rs: split into a cross-platform trait surface +
per-platform provider re-exports; gate `shared` (depends on `skia_safe` +
`winit`) to desktop only.
- src/context/provider.rs: cfg-gate `GlutinProvider` struct + impls + the
`pick_display_api` helper to desktop OS only; localize `CString` /
`NonZeroU32` imports inside fn bodies; gate `from_error` to desktop to
silence dead_code on mobile (the only caller is `GlutinProvider`).
- Cargo.toml: split deps into a cross-platform `cfg(not(wasm32))` block
(jian-core + glow + raw-window-handle, all required by the trait
signature on every non-wasm target) and a desktop-only block (skia-safe,
glutin, glutin-winit, winit, scopeguard, jian-skia, jian-host-desktop).
Merges the previously duplicate desktop `[target...]` table headers that
cargo rejected.
- ci: rust-multiplatform.yml mobile-check job now runs cargo check on
shell-native too (per the comment update there).
Verification:
- cargo check -p openpencil-shell-native --target aarch64-apple-darwin: PASS
- cargo check -p openpencil-shell-native --target aarch64-apple-ios: PASS
- cargo check -p openpencil-shell-native --target aarch64-linux-android: PASS
- cargo check -p openpencil-shell-native --target wasm32-unknown-unknown:
FAILS with the spec §1.2 `compile_error!` (intended).
- cargo test -p openpencil-shell-native: 14/14 PASS.
- cargo clippy -p openpencil-shell-native --all-targets -- -D warnings: clean
on macOS, iOS, Android targets.
- cargo fmt --check: clean.
- tests/common/mod.rs: egl.get_display(DEFAULT_DISPLAY) wrapped in unsafe block
(khronos-egl 6.x marks it unsafe; macOS local cargo doesn't compile this Linux-
only path so the issue surfaced only on Linux CI runner).
- rust-multiplatform.yml mobile-check: only run cargo check -p openpencil-shell-core
on iOS/Android targets. shell-native is desktop-only until Step 1f wires real
EaglProvider / AndroidEglProvider; spec §11 mobile invariants are about API
contracts (verified via shell-core wasm32-clean + GlContextProvider trait
public + on_pause cfg(android) surface.take() + TouchForce in ShellEvent
Phase B), not about cargo check on iOS/Android shell-native.
Phase B Task 3 implementation per spec v19 §5.1 + §5.1.1 (FROZEN
2026-05-04):
shell-core:
- New `event` module declaring `ShellEvent` (6 variants per spec §5.1)
+ sub-types `PointerId / TouchId / TouchPhase / TouchForce /
MouseButton / ElementState / ScrollDelta / Modifiers / KeyCode /
WindowEventKind`. Pure OP types — no winit / Jian / GL — so the
enum is wasm32-clean and visible on iOS / Android (spec §11.3).
- TouchForce::Calibrated mirrors winit::Force 1:1 (spec §11.3
invariant) so Step 1f mobile mapper compiles without API break.
- Newtype id fields are `pub` so shell-native can construct them across
crates (spec round 3 BLOCK-R3-4 fix).
shell-native:
- New `event` module (cfg-gated desktop only) housing
`JianPointerMapper` — stateful diff over the per-PointerId
`MouseButtons` snapshot. Diff runs on Down / Up / Move (spec round 3
CONCERN-R3-1 fix); Hover / Move emits a trailing `PointerMove`.
- Touch branch maps Down/Move/Up/Cancel → Started/Moved/Ended/Cancelled;
Touch Hover returns `Vec::new()` (touches never hover).
- Mouse / Pen / Stylus / Trackpad share the same diff branch.
- Degraded inputs (no button transition + no Move emission) return
`Vec::new()` instead of synthesising a `ShellEvent::Other` variant
(spec round 4 CONCERN-R4-1 fix; the enum stays at exactly 6 variants).
Tests:
- 15 new unit tests in shell-native/tests/event_mapping.rs covering
the 4 Touch phases, mouse Hover, LEFT Down/Up pair, multi-button
press/release during Move, Pen/Stylus/Trackpad routing, two
degraded-empty paths, and modifiers propagation (CMD → meta).
- 3 new shape tests in shell-core/tests/event_shape.rs proving the
6-variant invariant + TouchForce::Calibrated field shape +
`pub`-field newtype constructibility.
Verified:
- `cargo test -p openpencil-shell-core -p openpencil-shell-native`
green (36 tests total across both crates).
- `cargo check --target wasm32-unknown-unknown -p openpencil-shell-core`
green; shell-web on wasm32 still compiles with the new module pulled
through.
- `cargo check --target aarch64-apple-ios -p openpencil-shell-native`
+ `--target aarch64-linux-android -p openpencil-shell-native` both
green (mapper cfg-gated out of mobile).
- `cargo metadata --filter-platform aarch64-linux-android` confirms
jian-host-desktop / jian-skia not in the Android dep tree.
- §11.1 grep: 0 actual `use winit/skia_safe/glutin/...` items in
shell-core (only doc-comment references).
- `cargo clippy --all-targets` clean; `cargo fmt --check` clean.
Linux GPU tests:
- skia-safe Interface::new_native dlopens libGL.so + glXGetProcAddress;
fails on EGL pbuffer + llvmpipe (Mesa headless setup). Wiring
Interface::new_load_with(eglGetProcAddress) needs a new
GlContextProvider::get_proc_address method (spec §3.1 mini-patch
follow-up). Tracked LINUX_GPU_SKIA_LOADER_TBD.
- gpu_smoke + gpu_chrome_stub_composition Linux variants now #[ignore]
with explicit reason matching Windows pattern (#[ignore =
WINDOWS_GPU_DEFERRED_NO_RUNNER]); CI Linux test step drops xvfb +
STEP1A_REQUIRE_GPU env (no longer needed since tests ignored).
- macOS continues running real GPU smoke (no skia loader issue).
Windows ARM64:
- new aarch64-pc-windows-msvc matrix entry — cargo check only
(cross-compile from x86_64 windows-latest; no Win11 ARM hosted runner GA yet).
- rust-release.yml also gains windows-aarch64 archive build.
macos-local verify: all 14 tests pass (gpu_smoke + gpu_chrome_stub_composition
still run on macOS host).