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.
This commit is contained in:
Kayshen-X 2026-05-08 22:03:23 +08:00
parent 136274a3ec
commit ca2308a0d5
4 changed files with 102 additions and 319 deletions

View file

@ -1,226 +0,0 @@
//! `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,
},
}

View file

@ -28,6 +28,18 @@ pub use render_backend::{Color, Point2D, Rect, RenderBackend, TextLayout};
/// consistent with Jian; OP-specific differentiation is at the canvas
/// viewport / chrome layer (single-page + infinite canvas recommended,
/// multi-page also supported), not at event-type abstraction.
///
/// Step 1b §2.4 additions (P0.5A jian PR):
/// - `KeyEvent` + supporting `KeyValue` / `NamedKey` / `KeyCode` /
/// `KeyLocation` / `KeyState` for browser + winit keyboard input.
/// - `ImeEvent` + `ImeKind` for IME composition (UTF-8 byte-indexed
/// selection per spec §2.4).
/// - `FocusEvent` for window-level / widget-level focus transitions
/// (separate from `FocusManager`'s internal Tab-ring chain).
/// - `WheelEvent` + `ScrollMode` for W3C `WheelEvent.deltaMode` parity
/// on browser + native winit hosts.
pub use jian_core::gesture::{
Modifiers, MouseButtons, PointerEvent, PointerId, PointerKind, PointerPhase,
FocusEvent, ImeEvent, ImeKind, KeyCode, KeyEvent, KeyLocation, KeyState, KeyValue, Modifiers,
MouseButtons, NamedKey, PointerEvent, PointerId, PointerKind, PointerPhase, ScrollMode,
WheelEvent,
};

View file

@ -1,92 +0,0 @@
//! Plan v7 Task 3 Step 12 — proves the `ShellEvent` enum shape (spec
//! §5.1) is reachable through the public re-export path and matches the
//! 6-variant invariant. Constructed via the cross-platform OP types
//! only — no Jian / winit / GL imports — so this test compiles on
//! wasm32 and mobile too (verified by `cargo check
//! --target wasm32-unknown-unknown -p openpencil-shell-core`).
use openpencil_shell_core::event::{
ElementState, KeyCode, Modifiers, MouseButton, PointerId, ScrollDelta, ShellEvent, TouchForce,
TouchId, TouchPhase, WindowEventKind,
};
use openpencil_shell_core::render_backend::Point2D;
#[test]
fn six_variants_constructible_via_re_export() {
let mods = Modifiers {
shift: true,
ctrl: false,
alt: false,
meta: false,
};
let pos = Point2D::new(1.0, 2.0);
let events = [
ShellEvent::PointerMove {
id: PointerId(1),
pos,
modifiers: mods,
},
ShellEvent::PointerButton {
id: PointerId(1),
button: MouseButton::Left,
state: ElementState::Pressed,
pos,
modifiers: mods,
},
ShellEvent::MouseWheel {
delta: ScrollDelta::LineDelta { x: 0.0, y: 1.0 },
modifiers: mods,
},
ShellEvent::Touch {
id: TouchId(7),
phase: TouchPhase::Started,
pos,
force: Some(TouchForce::Normalized(0.5)),
},
ShellEvent::Window {
kind: WindowEventKind::Resized {
width: 800,
height: 600,
},
},
ShellEvent::Key {
key: KeyCode::Escape,
state: ElementState::Released,
modifiers: mods,
},
];
assert_eq!(events.len(), 6, "spec §5.1 declares exactly 6 variants");
}
#[test]
fn touch_force_calibrated_mirrors_winit() {
// Spec §11.3 invariant: `TouchForce::Calibrated` mirrors
// `winit::event::Force::Calibrated` 1:1 — fields exist with the
// declared names so a Step 1f mobile mapper compiles.
let f = TouchForce::Calibrated {
force: 0.4,
max_possible_force: 1.0,
altitude_angle: Some(std::f64::consts::FRAC_PI_2),
};
if let TouchForce::Calibrated {
force,
max_possible_force,
altitude_angle,
} = f
{
assert_eq!(force, 0.4);
assert_eq!(max_possible_force, 1.0);
assert_eq!(altitude_angle, Some(std::f64::consts::FRAC_PI_2));
} else {
panic!("expected Calibrated variant");
}
}
#[test]
fn newtype_id_fields_pub_constructible() {
// Round 3 BLOCK-R3-4 fix: `TouchId(pub u64)` + `PointerId(pub u64)`
// constructible across crates so shell-native's mapper works.
let _ = TouchId(42);
let _ = PointerId(42);
}

View file

@ -0,0 +1,89 @@
use openpencil_shell_core::{
FocusEvent, ImeEvent, ImeKind, KeyCode, KeyEvent, KeyLocation, KeyState, KeyValue, Modifiers,
MouseButtons, NamedKey, PointerEvent, PointerId, PointerKind, PointerPhase, ScrollMode,
WheelEvent,
};
#[test]
fn pointer_event_is_re_exported_from_jian() {
let event = PointerEvent::simple(
7,
PointerPhase::Down,
jian_core::geometry::Point::new(12.0, 34.0),
);
let _: PointerEvent = event.clone();
let _: jian_core::gesture::PointerEvent = event.clone();
assert_eq!(event.id, PointerId(7));
assert_eq!(event.kind, PointerKind::Touch);
assert_eq!(event.phase, PointerPhase::Down);
assert_eq!(event.position.x, 12.0);
assert_eq!(event.position.y, 34.0);
assert!(event.buttons.contains(MouseButtons::LEFT));
assert!(event.modifiers.is_empty());
}
#[test]
fn pointer_modifier_and_button_flags_keep_jian_names() {
let mods = Modifiers::SHIFT | Modifiers::CMD;
assert!(mods.contains(Modifiers::SHIFT));
assert!(mods.contains(Modifiers::CMD));
assert!(!mods.contains(Modifiers::CTRL));
let buttons = MouseButtons::LEFT | MouseButtons::RIGHT;
assert!(buttons.contains(MouseButtons::LEFT));
assert!(buttons.contains(MouseButtons::RIGHT));
assert!(!buttons.contains(MouseButtons::MIDDLE));
}
#[test]
fn key_event_is_re_exported_from_jian() {
let event = KeyEvent {
key: KeyValue::Named(NamedKey::Enter),
code: KeyCode::Enter,
location: KeyLocation::Standard,
modifiers: Modifiers::empty(),
state: KeyState::Pressed,
repeat: false,
is_composing: false,
};
let _: jian_core::gesture::KeyEvent = event.clone();
assert_eq!(event.key, KeyValue::Named(NamedKey::Enter));
}
#[test]
fn ime_event_is_re_exported_from_jian() {
let event = ImeEvent {
kind: ImeKind::CompositionUpdate {
selection: Some(0..6),
},
text: "你好".to_string(),
};
let _: jian_core::gesture::ImeEvent = event.clone();
match event.kind {
ImeKind::CompositionUpdate { selection } => assert_eq!(selection, Some(0..6)),
_ => panic!("expected CompositionUpdate"),
}
}
#[test]
fn focus_event_is_re_exported_from_jian() {
let event = FocusEvent {
gained: true,
node_id_hint: Some(11),
related_node_id_hint: Some(7),
};
let _: jian_core::gesture::FocusEvent = event;
assert!(event.gained);
}
#[test]
fn wheel_event_is_re_exported_from_jian() {
let event = WheelEvent::simple(
jian_core::geometry::Point::new(0.0, 0.0),
jian_core::geometry::Point::new(0.0, 120.0),
);
assert_eq!(event.mode, ScrollMode::Pixel);
assert_eq!(event.delta_z, 0.0);
}