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.
227 lines
6.6 KiB
Rust
227 lines
6.6 KiB
Rust
//! `ShellEvent` — OP widget-facing primitive event enum (spec v19 §5.1).
|
|
//!
|
|
//! Per spec §1.2 (FROZEN 2026-05-04) shell-core must compile on
|
|
//! `wasm32-unknown-unknown` and remain platform-neutral on iOS / Android.
|
|
//! This module therefore declares **only OP types** — no winit, no Jian,
|
|
//! no GL — so the enum is visible everywhere widgets compile (mobile +
|
|
//! WASM included). The desktop mapper that lifts Jian `PointerEvent` into
|
|
//! `ShellEvent` lives in `openpencil-shell-native::event` (target-gated to
|
|
//! macOS / Linux / Windows; Step 1f extends to mobile).
|
|
//!
|
|
//! ## Spec invariants (§11 mobile-readiness)
|
|
//! - 6 variants: `PointerMove / PointerButton / MouseWheel / Touch / Window / Key`.
|
|
//! - `Touch` carries `TouchForce` (`Calibrated` mirrors winit::Force 1:1
|
|
//! to avoid Step 1f mobile API break, plus `Normalized` for Android).
|
|
//! - Newtype id fields are `pub` (spec round 3 BLOCK-R3-4 fix) so callers
|
|
//! in shell-native can construct them across crate boundaries.
|
|
|
|
use crate::render_backend::Point2D;
|
|
|
|
/// Stable identity for a single pointer (mouse/pen/stylus/trackpad cursor).
|
|
///
|
|
/// Widened to `u64` here so OP can ingest mappers from platforms (iOS, Web)
|
|
/// whose finger ids exceed Jian's `u32` `PointerId`. Desktop mapper widens
|
|
/// `jian_core::gesture::PointerId(u32)` → `PointerId(u64)` losslessly.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct PointerId(pub u64);
|
|
|
|
/// Stable identity for a single touch/finger contact.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub struct TouchId(pub u64);
|
|
|
|
/// Lifecycle of a touch contact (spec §5.1).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum TouchPhase {
|
|
/// Finger landed.
|
|
Started,
|
|
/// Finger moved while held.
|
|
Moved,
|
|
/// Finger lifted normally.
|
|
Ended,
|
|
/// System cancelled tracking (focus loss / iOS face-proximity /
|
|
/// Android system-gesture intercept).
|
|
Cancelled,
|
|
}
|
|
|
|
/// Pressure / force for a touch contact (mirrors `winit::event::Force`
|
|
/// 1:1 to avoid Step 1f mobile API break, per spec §11.3 invariant).
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum TouchForce {
|
|
/// iOS 3D Touch / Apple Pencil. `force` is the raw force value;
|
|
/// `max_possible_force` is the touch sensor's max; `altitude_angle`
|
|
/// is the Pencil tilt angle in radians (π/2 = perpendicular).
|
|
Calibrated {
|
|
force: f64,
|
|
max_possible_force: f64,
|
|
altitude_angle: Option<f64>,
|
|
},
|
|
/// Android pressure (already normalized to [0.0, 1.0]).
|
|
Normalized(f64),
|
|
}
|
|
|
|
/// Mouse buttons (spec §5.1; mirrors winit::event::MouseButton).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum MouseButton {
|
|
Left,
|
|
Right,
|
|
Middle,
|
|
Back,
|
|
Forward,
|
|
Other(u16),
|
|
}
|
|
|
|
/// Pressed/released state for buttons + keys.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum ElementState {
|
|
Pressed,
|
|
Released,
|
|
}
|
|
|
|
/// Mouse wheel / two-finger trackpad scroll delta.
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum ScrollDelta {
|
|
/// Discrete scroll, in lines (mouse wheel notch).
|
|
LineDelta { x: f32, y: f32 },
|
|
/// Continuous scroll, in logical pixels (trackpad).
|
|
PixelDelta(Point2D),
|
|
}
|
|
|
|
/// Modifier-key state at the moment an event was raised.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
|
|
pub struct Modifiers {
|
|
pub shift: bool,
|
|
pub ctrl: bool,
|
|
pub alt: bool,
|
|
/// Cmd on macOS, Super/Win on Linux/Windows (mirrors Jian `Modifiers::CMD`).
|
|
pub meta: bool,
|
|
}
|
|
|
|
/// Subset of keys OP currently surfaces (spec §5.1; expanded as widgets
|
|
/// need them in Step 1c+). Variant names follow winit::keyboard::KeyCode
|
|
/// for easy mapping. `Other(u32)` carries the raw scancode so
|
|
/// shell-native can pass through unmapped keys without losing them.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
pub enum KeyCode {
|
|
// Letters
|
|
A,
|
|
B,
|
|
C,
|
|
D,
|
|
E,
|
|
F,
|
|
G,
|
|
H,
|
|
I,
|
|
J,
|
|
K,
|
|
L,
|
|
M,
|
|
N,
|
|
O,
|
|
P,
|
|
Q,
|
|
R,
|
|
S,
|
|
T,
|
|
U,
|
|
V,
|
|
W,
|
|
X,
|
|
Y,
|
|
Z,
|
|
// Digits
|
|
Digit0,
|
|
Digit1,
|
|
Digit2,
|
|
Digit3,
|
|
Digit4,
|
|
Digit5,
|
|
Digit6,
|
|
Digit7,
|
|
Digit8,
|
|
Digit9,
|
|
// Whitespace / control
|
|
Space,
|
|
Enter,
|
|
Tab,
|
|
Backspace,
|
|
Escape,
|
|
Delete,
|
|
// Arrows
|
|
ArrowLeft,
|
|
ArrowRight,
|
|
ArrowUp,
|
|
ArrowDown,
|
|
// Modifiers (released as standalone keys)
|
|
Shift,
|
|
Control,
|
|
Alt,
|
|
Meta,
|
|
/// Anything else — raw scancode. Step 1c+ widgets that need a
|
|
/// specific key add a named variant.
|
|
Other(u32),
|
|
}
|
|
|
|
/// Window-level event kinds (spec §5.1; the desktop mapper synthesizes
|
|
/// these directly from winit `WindowEvent`, never via Jian).
|
|
#[derive(Debug, Clone, Copy, PartialEq)]
|
|
pub enum WindowEventKind {
|
|
/// Window content area resized (logical pixels via `inner_size`).
|
|
Resized { width: u32, height: u32 },
|
|
/// HiDPI scale factor changed (Retina toggle, monitor switch).
|
|
ScaleFactorChanged(f64),
|
|
/// User clicked the close button / hit Cmd-W / etc.
|
|
CloseRequested,
|
|
/// Focus gained (`true`) or lost (`false`).
|
|
Focused(bool),
|
|
}
|
|
|
|
/// Widget-facing primitive event (spec v19 §5.1).
|
|
///
|
|
/// Layered on top of Jian `PointerEvent` (which only carries
|
|
/// pointer/touch primitives — pos/buttons-bitset/phase). Window / Key /
|
|
/// MouseWheel events do **not** route through Jian; they come straight
|
|
/// from winit on desktop. See [`crate::event`] module docs and spec
|
|
/// §5.1.1 for the full mapping contract.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub enum ShellEvent {
|
|
/// Pointer moved (cursor or pen) — no button state change.
|
|
PointerMove {
|
|
id: PointerId,
|
|
pos: Point2D,
|
|
modifiers: Modifiers,
|
|
},
|
|
/// One pointer button transitioned Pressed/Released.
|
|
/// Multi-button transitions (e.g. mid-gesture press of an additional
|
|
/// button) are emitted as one `PointerButton` per changed bit, plus
|
|
/// a trailing `PointerMove` if Jian phase was `Move` — see spec
|
|
/// §5.1.1 for the full diff contract.
|
|
PointerButton {
|
|
id: PointerId,
|
|
button: MouseButton,
|
|
state: ElementState,
|
|
pos: Point2D,
|
|
modifiers: Modifiers,
|
|
},
|
|
/// Mouse wheel / trackpad scroll.
|
|
MouseWheel {
|
|
delta: ScrollDelta,
|
|
modifiers: Modifiers,
|
|
},
|
|
/// Touch contact lifecycle event (mobile-ready; Step 1f).
|
|
Touch {
|
|
id: TouchId,
|
|
phase: TouchPhase,
|
|
pos: Point2D,
|
|
force: Option<TouchForce>,
|
|
},
|
|
/// Window-level event (resize, scale change, close, focus).
|
|
Window { kind: WindowEventKind },
|
|
/// Keyboard event.
|
|
Key {
|
|
key: KeyCode,
|
|
state: ElementState,
|
|
modifiers: Modifiers,
|
|
},
|
|
}
|