diff --git a/crates/openpencil-shell-core/src/event.rs b/crates/openpencil-shell-core/src/event.rs deleted file mode 100644 index e176c3a7e..000000000 --- a/crates/openpencil-shell-core/src/event.rs +++ /dev/null @@ -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, - }, - /// 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, - }, - /// Window-level event (resize, scale change, close, focus). - Window { kind: WindowEventKind }, - /// Keyboard event. - Key { - key: KeyCode, - state: ElementState, - modifiers: Modifiers, - }, -} diff --git a/crates/openpencil-shell-core/src/lib.rs b/crates/openpencil-shell-core/src/lib.rs index 66a881739..fe8d836e0 100644 --- a/crates/openpencil-shell-core/src/lib.rs +++ b/crates/openpencil-shell-core/src/lib.rs @@ -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, }; diff --git a/crates/openpencil-shell-core/tests/event_shape.rs b/crates/openpencil-shell-core/tests/event_shape.rs deleted file mode 100644 index 3ac5f00cc..000000000 --- a/crates/openpencil-shell-core/tests/event_shape.rs +++ /dev/null @@ -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); -} diff --git a/crates/openpencil-shell-core/tests/gesture_re_export.rs b/crates/openpencil-shell-core/tests/gesture_re_export.rs new file mode 100644 index 000000000..fe61f751f --- /dev/null +++ b/crates/openpencil-shell-core/tests/gesture_re_export.rs @@ -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); +}