Commit graph

425 commits

Author SHA1 Message Date
Kayshen-X f214f4b4e3 feat(shell-core): Phase B2 — four static inspector widgets
Lands the four Step 1b inspector widgets in shell-core (per spec
§1.4 — widget logic lives here so shell-native + shell-web reuse
it; only the RenderBackend impl + DOM event mapping + accesskit
DOM mirror are platform-owned).

Widgets:
- `widgets::TreeWidget` (Role::Tree, label "Layers") — sample
  3-item tree (Frame / Title / Button) with selection-aware
  blue-row paint and depth-indented labels. WidgetIds 100-103.
- `widgets::PropertyRow` (Role::Group, label "{label} {value}")
  — single-row label/value pair. PropertyRow uses Role::Group
  rather than Role::GenericContainer so the row label survives
  ARIA filtering on the way to VoiceOver / NVDA (codex B2 R1
  CONCERN). WidgetIds 200-299.
- `widgets::Dropdown` (Role::ComboBox, label "Blend") — sample
  blend-mode picker with 3 options. Phase B static slice does
  NOT yet pop a menu when `state.open == true`; Phase C wires
  click + keyboard handling. WidgetIds 300-399.
- `widgets::TextInput` (Role::TextInput) — single-line input
  with CJK IME preview. Paints `state.preedit` (in-progress
  composition) when present, else `state.value`; non-empty
  preedit also draws an 80px underline. Phase C lands
  compositionstart / update / end → state mutation in shell-web.
  WidgetIds 400-499.

State separation:
- `DropdownState { selected, open }` and `TextInputState { value,
  preedit }` live as their own structs so Phase C event handlers
  can swap them without taking ownership of the surrounding
  widget. `TextInputState::default()` returns the empty state.

Tests (`tests/widgets_static.rs`):
- `four_inspector_widgets_paint_static_content` — paints all
  four widgets through one RecordingBackend, asserts
  ≥5 fills / ≥3 strokes / ≥7 text dispatches (each tightened
  vs the plan sketch to actually catch per-widget regressions).
- Per-widget role + label assertions (Tree / Group / ComboBox /
  TextInput).
- `text_input_paints_preedit_underline_when_composing` —
  drives `state.preedit = "你好"`, paints, asserts the IME
  branch emits 1 fill + 2 strokes (border + underline) + 1
  text run.
- `dropdown_state_independent_state_struct` +
  `text_input_state_default_is_empty` — verify state structs
  are independent + default-constructible.

Plan-vs-implementation deviations (deliberate):
- `WidgetId::new(N)` instead of the plan's `WidgetId(N)` tuple
  literal so the sample/new constructors exercise the B1
  debug_assert non-zero check. Tuple stays public for pattern
  matching + `const` contexts.
- WidgetId range conventions per widget kind (Tree=100s /
  PropertyRow=200s / Dropdown=300s / TextInput=400s) added as
  doc-only comments. Real Phase C host will allocate from a
  counter; the conventions just keep the B-phase fixtures
  predictable.
- Per-widget paint counts in `four_inspector_widgets_…` test
  tightened to ≥5/≥3/≥7 (plan sketch had ≥4/≥3/≥4 which
  wouldn't catch a regression in Tree's selection-row fill).

Verification:
- `cargo test -p openpencil-shell-core` — 10/10 passing
- `cargo check -p openpencil-shell-core --target
  wasm32-unknown-unknown` — green (shell-core stays
  wasm32-clean per spec §1.2 — no platform deps creeping in)
- `cargo check -p openpencil-shell-native` — green

Codex iterate review: 2 rounds → GO.
2026-05-09 21:46:00 +08:00
Kayshen-X b6efa324d1 feat(shell-core): Phase B1 — Widget trait + recording test harness
Adds the widget facade that B2 inspector widgets and Phase C event
handling will plug into. Logic-bearing widget code lives in
shell-core (per spec §1.4); shell-native + shell-web only own their
RenderBackend impls + DOM event mapping + accesskit DOM mirror.

What's added:
- `widgets::Widget` trait with `id` / `layout` / `paint(&self,...)` /
  `access_node` methods. Phase B widgets are static — `paint` is
  `&self`, mutable per-widget state lives in `*State` structs that
  B2 lands. Phase C will extend the trait with a `&mut self` event
  method for input handling.
- `widgets::WidgetId(pub u64)` plus a `pub const ROOT_WIDGET_ID =
  WidgetId(0)` and a `WidgetId::new(id)` constructor with
  `debug_assert!(id != 0)`. The tuple constructor stays public so
  pattern matching + `const` contexts keep working; `::new` is the
  conventional path that surfaces the root-id reservation in debug
  builds. (Codex B1 R1 NIT-7 — make the convention compiler-visible
  before Phase C tree routing lands.)
- `widgets::PaintCx<'a> { backend: &'a mut dyn RenderBackend }` and
  `widgets::LayoutCx { available_width, dpi }` — frame-scoped paint
  context + layout-time context. The `&mut dyn` indirection lets
  shell-native + shell-web reuse the widget code without
  monomorphising over the concrete backend.
- `widgets::LayoutBox { rect: Rect }` with `Debug + Clone + Copy +
  PartialEq` derives.
- A `rect(x, y, w, h)` constructor convenience used by tests + B2.

Test harness (`tests/widgets_static.rs`):
- `RecordingBackend` impl `RenderBackend` counting each call.
- `paint_cx_dispatches_through_dyn_backend` — verifies fill_rect /
  stroke_rect / save / translate / clip_rect / restore all dispatch
  via `&mut dyn RenderBackend`.
- `widget_trait_dispatches_layout_and_paint` — minimal `StubWidget`
  proves the trait shape compiles; asserts layout result, paint
  dispatch count, `WidgetId::new(7)` round-trip, `ROOT_WIDGET_ID.0
  == 0`, and `access_node().role() == Role::GenericContainer`. Real
  semantic roles (TreeItem / EditableText / etc) land with B2.

Plumbing:
- `accesskit = "0.24"` added to shell-core deps to match shell-web's
  pin (the version compatible with shell-native's accesskit_winit
  Step 1a usage). Codex B1 R1 Q3 flagged that shell-native does not
  yet pull accesskit; this is acknowledged as a Phase C tracked item
  — verify the same version when DOM mirror / native a11y wires up.
- `Rect` now derives `PartialEq` so `LayoutBox` can use the same
  derive. `Eq` is intentionally NOT derived (Vec2 carries floats);
  comment in render_backend.rs explains.

Plan-vs-implementation deviations (deliberate, all kept narrow):
- Plan B1 step 2 declares `pub mod {dropdown, prop_row, text_input,
  tree};` + re-exports inside widgets/mod.rs. Omitted here because
  those modules don't exist until B2; declaring them now would
  break the B1 standalone build. Top-block plan mini-patch
  convention applies (override sketches in body).
- Plan didn't enumerate the accesskit dep + `Rect: PartialEq`
  deltas — added with rationale comments.

Verification:
- `cargo test -p openpencil-shell-core` — green
- `cargo check -p openpencil-shell-core --target
  wasm32-unknown-unknown` — green (shell-core stays wasm32-clean
  per spec §1.2)
- `cargo check -p openpencil-shell-native` — green (no regression)

Codex iterate review: 4 rounds → GO. Round 1 CONCERN (3 items),
Round 2 CONCERN (1 stale comment), Round 3 CONCERN (comment vs
test body mismatch), Round 4 GO clean. Q3 (accesskit_winit
alignment) carries to Phase C as informational.
2026-05-09 21:45:00 +08:00
Kayshen-X 5af1674f6b feat(shell): switch openpencil-shell-web to wasm32-unknown-unknown C-hard pipeline
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.
2026-05-09 21:08:00 +08:00
Kayshen-X 8ac685ff84 feat(shell): add wasm-libc-shim crate for wasm32-unknown-unknown skia bundle
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.
2026-05-09 21:07:00 +08:00
Kayshen-X 5a027a4f4e test(shell-core): assert W3C field readback in gesture re-export tests
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.
2026-05-09 21:03:00 +08:00
Fini af9292d8f5 fix(ai): classify Type 0 components as non-mobile to skip phone chrome
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.
2026-05-09 21:00:00 +08:00
Kayshen-X c078b2a0e0 feat(shell): switch openpencil-shell-web to wasm32-unknown-unknown C-hard pipeline
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.
2026-05-09 20:59:22 +08:00
Kayshen-X 40b03c9075 feat(shell): add wasm-libc-shim crate for wasm32-unknown-unknown skia bundle
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.
2026-05-09 20:59:21 +08:00
Kayshen-X 3389ab456d test(shell-core): assert W3C field readback in gesture re-export tests
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.
2026-05-09 09:29:52 +08:00
Fini d582a715e4 fix(shell): drop orphan pub mod event refs after Step 1b §3.2 merge
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.
2026-05-08 22:35:00 +08:00
Fini b2f6ae99ff Merge remote-tracking branch 'origin/v0.8.0' into v0.8.0 2026-05-08 22:33:12 +08:00
Kayshen-X e759bededd refactor(shell-native): finish v19.4 cleanup — drop orphan event/ + stale test
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.
2026-05-08 22:03:34 +08:00
Kayshen-X ca2308a0d5 refactor(shell-core): drop stale ShellEvent + re-export Jian Step 1b events
Step 1b Phase 0 + §3.2 P0.5A landing on the OP side:

(1) Phase 0 cleanup (spec v19.4 / OP commit f9dd4b56 was reverted by
    merge cf61bcc1 ↔ origin/v0.8.0): delete src/event.rs (226-line OP
    ShellEvent enum) and tests/event_shape.rs (3 stale unit tests).
    Replace with tests/gesture_re_export.rs, which asserts the contract
    that shell-core exposes Jian gesture types directly without an
    OP-side wrapper.

(2) §3.2 P0.5A re-exports: lib.rs now also re-exports the new Jian
    keyboard / IME / focus / wheel event types so widget code can
    `use openpencil_shell_core::{KeyEvent, ImeEvent, FocusEvent,
    WheelEvent, ScrollMode, ...};` without naming jian_core directly.
    Full export list: FocusEvent, ImeEvent, ImeKind, KeyCode, KeyEvent,
    KeyLocation, KeyState, KeyValue, NamedKey, Modifiers, MouseButtons,
    PointerEvent, PointerId, PointerKind, PointerPhase, ScrollMode,
    WheelEvent.

Tests (gesture_re_export.rs, 6 tests):
- pointer_event_is_re_exported_from_jian
- pointer_modifier_and_button_flags_keep_jian_names
- key_event_is_re_exported_from_jian
- ime_event_is_re_exported_from_jian
- focus_event_is_re_exported_from_jian
- wheel_event_is_re_exported_from_jian

cargo test -p openpencil-shell-core --test gesture_re_export → 6/6 PASS.
2026-05-08 22:03:23 +08:00
Kayshen-X 136274a3ec chore(vendor): bump jian submodule to d5d358e (Step 1b §3.2 P0.5A)
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.
2026-05-08 22:03:08 +08:00
Fini b5219a2c17 fix(shell): wire ShellEvent + JianPointerMapper through lib.rs after merge
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).
2026-05-08 07:30:00 +08:00
Fini c81cfe82a4 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0
# Conflicts:
#	.github/workflows/rust-multiplatform.yml
#	README.md
#	crates/openpencil-shell-core/src/lib.rs
#	crates/openpencil-shell-native/examples/basic_window.rs
#	crates/openpencil-shell-native/src/lib.rs
2026-05-05 22:51:00 +08:00
Kayshen-X e706454235 refactor(shell): drop OP ShellEvent + JianPointerMapper — re-export Jian events directly
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.
2026-05-05 22:42:00 +08:00
Kayshen-X 543f556cdf feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate
Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0:

- crates/openpencil-shell-native/examples/basic_window.rs:
  winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp)
  + JianPointerMapper integration. Paints chrome rect + "Hello 你好"
  + box outline; close → idempotent teardown. Demonstrates Phase B
  Task 3 winit → Jian PointerTranslator → JianPointerMapper →
  ShellEvent pipeline end-to-end.
- crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md:
  manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS
  recorded; Linux/Windows pending real-hardware run, deferred per
  CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER).
- tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants.
  Verifies that openpencil-app has no direct jian-* dep, mobile
  (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures
  exclude jian-host-desktop / jian-skia, and openpencil-shell-web
  declares no jian-host-desktop dep at the manifest level.
- .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh
  on Linux runner with mobile + wasm32 targets installed.
- README.md: roadmap entry for the Step 1a milestone.

Verified locally on macOS aarch64:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --examples --workspace
- cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED)
- cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios}
- bash tools/check-jian-boundaries.sh (4 invariants PASS)
- spec §11 invariants 1–4 grep checks PASS

Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN.
vendor/jian pinned at c4a794dc.
2026-05-05 22:39:00 +08:00
Kayshen-X c3e935ccbb feat(shell-core,shell-native): map Jian PointerEvent to ShellEvent (Step 1a Task 3)
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.
2026-05-05 22:36:00 +08:00
Kayshen-X c6c59d95c1 ci: defer Linux GPU smoke + add Windows arm64 matrix
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).
2026-05-05 22:33:00 +08:00
Kayshen-X 919a1381f2 fix(shell-native): cfg-gate desktop GL stack so iOS/Android cargo check passes
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.
2026-05-05 22:24:00 +08:00
Kayshen-X d699cb89bd ci+test: fix Linux EGL unsafe wrap + drop shell-native from mobile cargo check
- 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.
2026-05-05 22:21:00 +08:00
Kayshen-X cf50616db1 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 22:12:00 +08:00
Kayshen-X c55807e432 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 22:09:00 +08:00
Kayshen-X c9aaa0efc4 feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate
Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0:

- crates/openpencil-shell-native/examples/basic_window.rs:
  winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp)
  + JianPointerMapper integration. Paints chrome rect + "Hello 你好"
  + box outline; close → idempotent teardown. Demonstrates Phase B
  Task 3 winit → Jian PointerTranslator → JianPointerMapper →
  ShellEvent pipeline end-to-end.
- crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md:
  manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS
  recorded; Linux/Windows pending real-hardware run, deferred per
  CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER).
- tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants.
  Verifies that openpencil-app has no direct jian-* dep, mobile
  (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures
  exclude jian-host-desktop / jian-skia, and openpencil-shell-web
  declares no jian-host-desktop dep at the manifest level.
- .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh
  on Linux runner with mobile + wasm32 targets installed.
- README.md: roadmap entry for the Step 1a milestone.

Verified locally on macOS aarch64:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --examples --workspace
- cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED)
- cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios}
- bash tools/check-jian-boundaries.sh (4 invariants PASS)
- spec §11 invariants 1–4 grep checks PASS

Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN.
vendor/jian pinned at c4a794dc.
2026-05-05 21:51:00 +08:00
Kayshen-X bb321f115c feat(shell-core,shell-native): map Jian PointerEvent to ShellEvent (Step 1a Task 3)
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.
2026-05-05 21:48:00 +08:00
Kayshen-X 133202c5b7 ci: defer Linux GPU smoke + add Windows arm64 matrix
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).
2026-05-05 21:45:00 +08:00
Kayshen-X c1a394439a fix(shell-native): cfg-gate desktop GL stack so iOS/Android cargo check passes
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.
2026-05-05 21:36:00 +08:00
Kayshen-X 61f16acdd3 ci+test: fix Linux EGL unsafe wrap + drop shell-native from mobile cargo check
- 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.
2026-05-05 21:33:00 +08:00
Kayshen-X 7fb674d928 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 21:24:00 +08:00
Kayshen-X b066a1c057 feat(shell-native): Phase A Gate round 3 fixes
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.
2026-05-05 21:15:00 +08:00
Kayshen-X 656b57a024 style(shell): convert all comments to English
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.
2026-05-05 21:12:00 +08:00
Kayshen-X d26b53c06d fix(shell-native): Phase A Gate round 1 fixes (Task 2 patches)
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
2026-05-05 21:09:00 +08:00
Kayshen-X c1ce879582 feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp
Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7).

- `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface,
  `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)`
  callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with
  Android surface drop contract; tracing spans + events on every
  per-frame entry point.
- `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS /
  Android stubs; trait carries no `Send` bound (per spec §3.1).
- `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes
  STENCIL_TEST + blend func to verify chrome-paint isolation.
- `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend`
  trait surface (no direct trait impl in 1a; Step 1c+ wraps via
  `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect /
  draw_text / clip_rect / save / restore / translate` to
  `jian_core::render::DrawOp` and submits via
  `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper +
  `to_jian_color` / `to_jian_rect` converters.
- Tests:
  - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence.
  - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3
    with sysinfo RSS budget < 5 %.
  - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches
    begin_frame / with_frame / present / resize / teardown / on_pause /
    on_resume / on_low_memory events.
  - `raster_composition.rs` — chrome-only fill_rect on raster surface,
    pixel-asserts red + black + untouched-bg.
  - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature,
    asserts visible glyph rasterisation.
  - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible
    winit window (graceful inconclusive when off main thread; full
    path runs from `cargo run --example basic_window`) + Windows
    `#[ignore]` per spec §8.1.
  - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL
    surface, asserts chrome pixels survive stub's GL pollution.
- Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror`
  workspace deps; dev-deps `sysinfo`, `tracing-test` (with
  `no-env-filter`), Linux-only `khronos-egl` + `libloading`.

`cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --all -- --check` all green on macOS.
2026-05-05 21:06:00 +08:00
Kayshen-X 2f60bd49f8 feat(workspace): pin Jian submodule and shell wrapper deps (Step 1a Task 1)
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.
2026-05-05 21:00:00 +08:00
Kayshen-X ce4d78c42e chore(shell-native): revert P0 probe gate transients
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.
2026-05-05 12:55:11 +08:00
Kayshen-X fb65a0c96e ci(shell-native): add LINUX_GPU_DEFERRED_NO_RUNNER for hosted Linux runner
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.
2026-05-05 12:50:18 +08:00
Kayshen-X 89a5bb8a37 fix(shell-native): enable winit Linux backends + LF line endings for cross-OS CI
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.
2026-05-05 12:32:03 +08:00
Kayshen-X 22003f9b0c chore(shell-native): add transient P0 probe gate (Step 1a)
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.
2026-05-05 12:23:49 +08:00
Fini f5ac6a494b fix(shell): remove orphan rust files merge missed (modify-vs-delete)
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.
2026-05-05 12:23:48 +08:00
Fini 90cd86fc11 Merge branch 'v0.8.0' of github.com:ZSeven-W/openpencil into v0.8.0
# Conflicts:
#	.github/workflows/rust-multiplatform.yml
#	README.md
#	crates/openpencil-shell-core/src/lib.rs
#	crates/openpencil-shell-native/examples/basic_window.rs
#	crates/openpencil-shell-native/src/lib.rs
2026-05-05 12:23:47 +08:00
Kayshen-X 92cf0378e8 refactor(shell): drop OP ShellEvent + JianPointerMapper — re-export Jian events directly
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.
2026-05-05 12:23:44 +08:00
Kayshen-X e4dd3d7b37 feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate
Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0:

- crates/openpencil-shell-native/examples/basic_window.rs:
  winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp)
  + JianPointerMapper integration. Paints chrome rect + "Hello 你好"
  + box outline; close → idempotent teardown. Demonstrates Phase B
  Task 3 winit → Jian PointerTranslator → JianPointerMapper →
  ShellEvent pipeline end-to-end.
- crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md:
  manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS
  recorded; Linux/Windows pending real-hardware run, deferred per
  CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER).
- tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants.
  Verifies that openpencil-app has no direct jian-* dep, mobile
  (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures
  exclude jian-host-desktop / jian-skia, and openpencil-shell-web
  declares no jian-host-desktop dep at the manifest level.
- .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh
  on Linux runner with mobile + wasm32 targets installed.
- README.md: roadmap entry for the Step 1a milestone.

Verified locally on macOS aarch64:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --examples --workspace
- cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED)
- cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios}
- bash tools/check-jian-boundaries.sh (4 invariants PASS)
- spec §11 invariants 1–4 grep checks PASS

Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN.
vendor/jian pinned at c4a794dc.
2026-05-05 12:23:43 +08:00
Kayshen-X 8d720dbc83 feat(shell-core,shell-native): map Jian PointerEvent to ShellEvent (Step 1a Task 3)
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.
2026-05-05 12:23:42 +08:00
Kayshen-X 303859fdcd ci: defer Linux GPU smoke + add Windows arm64 matrix
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).
2026-05-05 12:23:41 +08:00
Kayshen-X a9fcb26096 fix(shell-native): cfg-gate desktop GL stack so iOS/Android cargo check passes
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.
2026-05-05 12:23:38 +08:00
Kayshen-X 371c874cf0 ci+test: fix Linux EGL unsafe wrap + drop shell-native from mobile cargo check
- 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.
2026-05-05 12:23:37 +08:00
Kayshen-X db0b50f7b5 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 12:23:34 +08:00
Kayshen-X d5011547f1 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 12:23:33 +08:00
Kayshen-X e66e8aefcc feat(shell-native): Step 1a Task 4 — basic_window demo + acceptance + Phase C Gate
Phase C Task 4 closes Step 1a (G1 shared Skia context) on v0.8.0:

- crates/openpencil-shell-native/examples/basic_window.rs:
  winit + SharedSkiaContext::new_desktop + NativeBackend (Jian DrawOp)
  + JianPointerMapper integration. Paints chrome rect + "Hello 你好"
  + box outline; close → idempotent teardown. Demonstrates Phase B
  Task 3 winit → Jian PointerTranslator → JianPointerMapper →
  ShellEvent pipeline end-to-end.
- crates/openpencil-shell-native/notes/step-1a-{macos,linux,windows}-manual-smoke.md:
  manual GPU smoke runbooks for spec §1.2 acceptance #1 (macOS PASS
  recorded; Linux/Windows pending real-hardware run, deferred per
  CONCERN-R5-1 + WINDOWS_GPU_DEFERRED_NO_RUNNER).
- tools/check-jian-boundaries.sh: spec §11 + §12.3 invariants.
  Verifies that openpencil-app has no direct jian-* dep, mobile
  (aarch64-linux-android, aarch64-apple-ios) and wasm32 closures
  exclude jian-host-desktop / jian-skia, and openpencil-shell-web
  declares no jian-host-desktop dep at the manifest level.
- .github/workflows/rust-check.yml: wires bash tools/check-jian-boundaries.sh
  on Linux runner with mobile + wasm32 targets installed.
- README.md: roadmap entry for the Step 1a milestone.

Verified locally on macOS aarch64:
- cargo fmt --all -- --check
- cargo clippy --workspace --all-targets -- -D warnings
- cargo build --examples --workspace
- cargo test --workspace (38 PASS, 0 FAIL, 0 IGNORED)
- cargo check -p openpencil-shell-native --target {aarch64-linux-android, aarch64-apple-ios}
- bash tools/check-jian-boundaries.sh (4 invariants PASS)
- spec §11 invariants 1–4 grep checks PASS

Spec v19.3 FROZEN (openpencil-docs 651090d); Plan v7 FROZEN.
vendor/jian pinned at c4a794dc.
2026-05-05 12:23:27 +08:00
Kayshen-X 786d10a1c6 feat(shell-core,shell-native): map Jian PointerEvent to ShellEvent (Step 1a Task 3)
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.
2026-05-05 12:23:26 +08:00
Kayshen-X 46238d36b5 ci: defer Linux GPU smoke + add Windows arm64 matrix
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).
2026-05-05 12:23:25 +08:00
Kayshen-X 968f1fda88 fix(shell-native): cfg-gate desktop GL stack so iOS/Android cargo check passes
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.
2026-05-05 12:23:22 +08:00
Kayshen-X e35174ff00 ci+test: fix Linux EGL unsafe wrap + drop shell-native from mobile cargo check
- 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.
2026-05-05 12:23:21 +08:00
Kayshen-X e6d6b1bd6f style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 12:23:18 +08:00
Kayshen-X 006646057d feat(shell-native): Phase A Gate round 3 fixes
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.
2026-05-05 12:23:15 +08:00
Kayshen-X b649143667 style(shell): convert all comments to English
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.
2026-05-05 12:23:14 +08:00
Kayshen-X 3b4b7a6f36 fix(shell-native): Phase A Gate round 1 fixes (Task 2 patches)
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
2026-05-05 12:23:13 +08:00
Kayshen-X ad079e7662 feat(shell-native): SharedSkiaContext + NativeBackend over Jian DrawOp
Step 1a Task 2 (spec v19 §3 / §5.2.1, plan v7).

- `SharedSkiaContext`: own GL stack + Skia DirectContext + Surface,
  `Option<>`-field idempotent teardown, `with_frame(|canvas, glow|)`
  callback, lifecycle hooks (on_pause/on_resume/on_low_memory) with
  Android surface drop contract; tracing spans + events on every
  per-frame entry point.
- `GlContextProvider` trait + `GlutinProvider` desktop impl + iOS /
  Android stubs; trait carries no `Send` bound (per spec §3.1).
- `CanvasViewportStub::render_into(&Canvas)` deliberately pollutes
  STENCIL_TEST + blend func to verify chrome-paint isolation.
- `NativeBackend`: frame-scoped methods mirroring OP `RenderBackend`
  trait surface (no direct trait impl in 1a; Step 1c+ wraps via
  `WithCanvas<'a>` newtype). Translates `fill_rect / stroke_rect /
  draw_text / clip_rect / save / restore / translate` to
  `jian_core::render::DrawOp` and submits via
  `jian_skia::SkiaBackend::draw_on_canvas`. Public `draw_op` helper +
  `to_jian_color` / `to_jian_rect` converters.
- Tests:
  - `teardown_idempotent.rs` — teardown × 3 + lifecycle hook idempotence.
  - `memory_loop.rs` — 100 × create/begin_frame/present/teardown × 3
    with sysinfo RSS budget < 5 %.
  - `tracing_spans.rs` — `tracing-test` (no-env-filter) catches
    begin_frame / with_frame / present / resize / teardown / on_pause /
    on_resume / on_low_memory events.
  - `raster_composition.rs` — chrome-only fill_rect on raster surface,
    pixel-asserts red + black + untouched-bg.
  - `raster_text_smoke.rs` — "Hello 你好" through textlayout feature,
    asserts visible glyph rasterisation.
  - `gpu_smoke.rs` — Linux EGL pbuffer (non-ignored) + macOS invisible
    winit window (graceful inconclusive when off main thread; full
    path runs from `cargo run --example basic_window`) + Windows
    `#[ignore]` per spec §8.1.
  - `gpu_chrome_stub_composition.rs` — chrome+stub on the same GL
    surface, asserts chrome pixels survive stub's GL pollution.
- Cargo.toml: add `jian-core` direct dep + `tracing` / `thiserror`
  workspace deps; dev-deps `sysinfo`, `tracing-test` (with
  `no-env-filter`), Linux-only `khronos-egl` + `libloading`.

`cargo build`, `cargo test`, `cargo clippy --all-targets -- -D warnings`,
`cargo fmt --all -- --check` all green on macOS.
2026-05-05 12:23:12 +08:00
Kayshen-X 2dcc8a96d3 feat(workspace): pin Jian submodule and shell wrapper deps (Step 1a Task 1)
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.
2026-05-05 12:23:10 +08:00
Kayshen-X ae13dc9bef chore(shell-native): revert P0 probe gate transients
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.
2026-05-05 12:23:09 +08:00
Kayshen-X c75eba9964 ci(shell-native): add LINUX_GPU_DEFERRED_NO_RUNNER for hosted Linux runner
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.
2026-05-05 12:23:08 +08:00
Kayshen-X 8901767c0e fix(shell-native): enable winit Linux backends + LF line endings for cross-OS CI
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.
2026-05-05 12:23:05 +08:00
Kayshen-X 9b7d96c60e chore(shell-native): add transient P0 probe gate (Step 1a)
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.
2026-05-05 12:23:03 +08:00
Kayshen-X c54a5facee chore(workspace): cargo-deny 0.18 activation (Phase 1 Task 1.8 Step 6)
- deny.toml: add [graph].targets to limit metadata to native+wasm32
  (avoid Android/iOS edition-2024 deps that fail rustc 1.82 cargo metadata)
- deny.toml: [bans] allow-wildcard-paths = true for workspace path deps
- crates/*/Cargo.toml: add explicit version="0.1.0" alongside path = "..."
  (cargo-deny rejects wildcard-path deps for publishable crates)

cargo-deny 0.16.4 hits a CVSS 4.0 parse error AND lacks edition-2024 cargo
metadata support; bumped to 0.18.9 (installed via stable toolchain). Run
cargo-deny with RUSTUP_TOOLCHAIN=stable so it uses cargo 1.95 for metadata
parsing while project itself still builds on 1.82.

Verified: advisories ok, bans ok, licenses ok, sources ok (exit 0)
on both native and wasm32-unknown-unknown targets.
2026-05-03 23:05:00 +08:00
Kayshen-X 4764be8dc5 style: rustfmt placeholder format! macros (Phase 1 Task 1.8 Step 3) 2026-05-03 23:00:00 +08:00
Kayshen-X 701c7670e2 feat(pen-figma): skeleton crate (bucket A) 2026-05-03 22:55:00 +08:00
Kayshen-X d2554eaa4e feat(pen-codegen): skeleton crate (bucket A) 2026-05-03 22:50:00 +08:00
Kayshen-X aabd681444 feat(pen-engine): skeleton crate (bucket A) 2026-05-03 22:45:00 +08:00
Kayshen-X fbeb66324c feat(pen-core): skeleton crate (bucket A) 2026-05-03 22:40:00 +08:00
Kayshen-X 05da632559 feat(pen-types): skeleton crate (bucket A) 2026-05-03 22:35:00 +08:00
Kayshen-X 059a7f3d73 feat(openpencil-shell-native): skeleton crate (kickoff §1.2 native-only)
Phase 1 skeleton: declare crate, add compile_error! wasm32 guard so accidental
inclusion in the web bundle fails at compile time (kickoff spec §1.2 invariant).

Native deps intentionally minimal (just winit, no default features). skia-safe /
accesskit / accesskit_winit deferred to Stage F when RenderBackend is actually
implemented. Reason: current top-tier versions of these crates pull transitive
deps (home 0.5.12, litemap 0.7.5, hashbrown 0.17) that require Rust 1.81+ /
edition2024, but our pinned toolchain is 1.80. Pinning to spec versions
(skia-safe=0.74) also fails since 0.74 was never published. Will revisit when
either the toolchain bumps or upstream stabilizes around an MSRV-1.80 line.

Verified:
- cargo build -p openpencil-shell-native        PASS
- cargo test  -p openpencil-shell-native        PASS (1 test)
- cargo check --target wasm32-unknown-unknown -p openpencil-shell-native
  fails with the compile_error! guard text (NOT a winit/skia build error).
2026-05-03 22:25:00 +08:00
Kayshen-X 79b2a766af feat(openpencil-shell-web): skeleton crate (kickoff §1.2 wasm bundle entry) 2026-05-03 22:20:00 +08:00
Kayshen-X 1a6b28698d feat(openpencil-shell-core): skeleton crate (kickoff §1.2 three-crate split) 2026-05-03 22:10:00 +08:00
Kayshen-X fcaf791b18 feat(openpencil-app): skeleton crate (Stage F entry placeholder) 2026-05-03 22:05:00 +08:00