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/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-native/src/event/mod.rs b/crates/openpencil-shell-native/src/event/mod.rs deleted file mode 100644 index f91ac269a..000000000 --- a/crates/openpencil-shell-native/src/event/mod.rs +++ /dev/null @@ -1,163 +0,0 @@ -//! Desktop event mapping (spec v19 §5.1.1). -//! -//! `winit::event::WindowEvent` → -//! ([`PointerTranslator`] from `jian_host_desktop`) → -//! `jian_core::gesture::PointerEvent` → -//! ([`JianPointerMapper`]) → -//! `openpencil_shell_core::ShellEvent`. -//! -//! Window / Key / MouseWheel events do **not** go through Jian — they -//! map straight from winit. Pointer / Touch primitives go through Jian -//! so OP reuses Jian's button-set + cursor-cache state machine -//! (`jian-host-desktop/src/pointer.rs`). -//! -//! This module is **desktop-only** (target-gated in `lib.rs`). Mobile -//! pointer/touch mapping lands in Step 1f. - -use std::collections::HashMap; - -use jian_core::gesture::{ - Modifiers as JianModifiers, MouseButtons as JianMouseButtons, PointerEvent as JianPointerEvent, - PointerId as JianPointerId, PointerKind as JianPointerKind, PointerPhase as JianPointerPhase, -}; -use openpencil_shell_core::event::{ - ElementState, Modifiers, MouseButton, PointerId, ShellEvent, TouchForce, TouchId, TouchPhase, -}; -use openpencil_shell_core::render_backend::Point2D; - -/// Stateful mapper from Jian `PointerEvent` to OP `ShellEvent` (spec -/// §5.1.1). Owns a per-`PointerId` snapshot of the last-seen -/// `MouseButtons` bitset so it can diff button transitions across -/// `Down / Up / Move` phases — Jian carries the **current** bitset, not -/// added/removed deltas (see `jian-core/src/gesture/pointer.rs:48-59` -/// + `jian-host-desktop/src/pointer.rs:104-134` for why all three -/// phases must diff). -#[derive(Debug, Clone, Default)] -pub struct JianPointerMapper { - previous_buttons: HashMap, -} - -impl JianPointerMapper { - pub fn new() -> Self { - Self::default() - } - - /// Translate one Jian `PointerEvent` into zero, one, or many - /// `ShellEvent`s. Empty `Vec` = degraded input the caller should - /// ignore (no `ShellEvent::Other` variant exists; see spec §5.1). - /// - /// Multi-button transitions during `Move` produce a `PointerButton` - /// per changed bit followed by a trailing `PointerMove` (spec - /// §5.1.1 round 3 CONCERN-R3-1 fix). - pub fn from_jian_pointer(&mut self, p: &JianPointerEvent) -> Vec { - let mut out = Vec::new(); - match p.kind { - JianPointerKind::Touch => { - let phase = match p.phase { - JianPointerPhase::Down => TouchPhase::Started, - JianPointerPhase::Move => TouchPhase::Moved, - JianPointerPhase::Up => TouchPhase::Ended, - JianPointerPhase::Cancel => TouchPhase::Cancelled, - // Touches never `Hover`: return empty so callers - // ignore the event rather than fabricate a phase. - JianPointerPhase::Hover => return out, - }; - out.push(ShellEvent::Touch { - id: TouchId(u64::from(p.id.0)), - phase, - pos: jian_point_to_point2d(p.position), - force: Some(TouchForce::Normalized(f64::from(p.pressure))), - }); - } - JianPointerKind::Mouse - | JianPointerKind::Pen - | JianPointerKind::Stylus - | JianPointerKind::Trackpad => { - let prev = self - .previous_buttons - .get(&p.id) - .copied() - .unwrap_or_default(); - let added = p.buttons.difference(prev); - let removed = prev.difference(p.buttons); - self.previous_buttons.insert(p.id, p.buttons); - - let pos = jian_point_to_point2d(p.position); - let modifiers = from_jian_modifiers(p.modifiers); - let id = PointerId(u64::from(p.id.0)); - - // 1. Emit one `PointerButton{Pressed}` per added bit. - for bit in added.iter() { - if let Some(button) = mouse_buttons_bit_to_button(bit) { - out.push(ShellEvent::PointerButton { - id, - button, - state: ElementState::Pressed, - pos, - modifiers, - }); - } - } - // 2. Emit one `PointerButton{Released}` per removed bit. - for bit in removed.iter() { - if let Some(button) = mouse_buttons_bit_to_button(bit) { - out.push(ShellEvent::PointerButton { - id, - button, - state: ElementState::Released, - pos, - modifiers, - }); - } - } - // 3. Hover / Move add a trailing `PointerMove` so motion - // is preserved alongside the button transitions. - // Down / Up don't emit Move (the PointerButton event - // carries the current pos). Cancel doesn't appear - // here — Jian only raises `Cancel` on Touch, which - // was branched out above; an unexpected - // `Mouse|Pen|Stylus|Trackpad + Cancel` is treated as - // degraded and ignored. - match p.phase { - JianPointerPhase::Hover | JianPointerPhase::Move => { - out.push(ShellEvent::PointerMove { id, pos, modifiers }); - } - JianPointerPhase::Down | JianPointerPhase::Up | JianPointerPhase::Cancel => {} - } - } - } - out - } -} - -/// Convert one `JianMouseButtons` single-bit flag to OP `MouseButton`. -/// Returns `None` if the input has zero or more than one bit set — -/// callers iterate the bitset one bit at a time so `None` should never -/// happen in practice; treating it as a no-op keeps the mapper -/// degradation contract intact (empty `Vec` rather than panic). -fn mouse_buttons_bit_to_button(bit: JianMouseButtons) -> Option { - match bit { - JianMouseButtons::LEFT => Some(MouseButton::Left), - JianMouseButtons::RIGHT => Some(MouseButton::Right), - JianMouseButtons::MIDDLE => Some(MouseButton::Middle), - JianMouseButtons::BACK => Some(MouseButton::Back), - JianMouseButtons::FORWARD => Some(MouseButton::Forward), - _ => None, - } -} - -/// Convert Jian `Modifiers` bitflags to OP `Modifiers` struct. -/// Jian's `CMD` bit maps to OP's `meta` (Cmd on macOS, Super/Win -/// elsewhere — same convention as `winit::keyboard::ModifiersState::super_key`). -fn from_jian_modifiers(m: JianModifiers) -> Modifiers { - Modifiers { - shift: m.contains(JianModifiers::SHIFT), - ctrl: m.contains(JianModifiers::CTRL), - alt: m.contains(JianModifiers::ALT), - meta: m.contains(JianModifiers::CMD), - } -} - -fn jian_point_to_point2d(p: jian_core::geometry::Point) -> Point2D { - Point2D::new(p.x, p.y) -} diff --git a/crates/openpencil-shell-native/tests/event_mapping.rs b/crates/openpencil-shell-native/tests/event_mapping.rs deleted file mode 100644 index eec5e5187..000000000 --- a/crates/openpencil-shell-native/tests/event_mapping.rs +++ /dev/null @@ -1,467 +0,0 @@ -//! Spec v19 §5.1.1 unit tests — Jian `PointerEvent` → OP `ShellEvent` -//! mapper (`JianPointerMapper`). -//! -//! Coverage target (plan v7 Task 3 Step 11 + spec §5.1.1 fixture list): -//! - 4 Touch phases (Started / Moved / Ended / Cancelled). -//! - Mouse Hover → `[PointerMove]`; LEFT Down/Up pair → Pressed/Released. -//! - Multi-button Move (press / release mid-gesture) — diff each bit -//! then trailing PointerMove (CONCERN-R3-1). -//! - Pen / Stylus / Trackpad route through the Mouse branch. -//! - Degraded inputs (no button change on Down/Up, Touch Hover) → -//! `Vec::new()` (spec round 4 CONCERN-R4-1; no `ShellEvent::Other`). -//! - Modifiers propagate (shift/ctrl/alt/meta/CMD). - -use std::time::Instant; - -use jian_core::geometry::point; -use jian_core::gesture::{ - Modifiers as JianModifiers, MouseButtons as JianMouseButtons, PointerEvent as JianPointerEvent, - PointerId as JianPointerId, PointerKind, PointerPhase, -}; -use openpencil_shell_core::event::{ - ElementState, Modifiers, MouseButton, PointerId, ShellEvent, TouchForce, TouchId, TouchPhase, -}; -use openpencil_shell_native::JianPointerMapper; - -/// Build a `JianPointerEvent` with default tilt / pressure / timestamp, -/// overriding the fields each test cares about. -fn jian_event( - id: u32, - kind: PointerKind, - phase: PointerPhase, - buttons: JianMouseButtons, - modifiers: JianModifiers, - pos_x: f32, - pos_y: f32, -) -> JianPointerEvent { - JianPointerEvent { - id: JianPointerId(id), - kind, - phase, - position: point(pos_x, pos_y), - pressure: 1.0, - buttons, - modifiers, - tilt: None, - timestamp: Instant::now(), - } -} - -// ---------------------------------------------------------------- Touch ---- - -#[test] -fn touch_down_emits_started_phase() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 7, - PointerKind::Touch, - PointerPhase::Down, - JianMouseButtons::empty(), - JianModifiers::empty(), - 10.0, - 20.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert_eq!(out.len(), 1); - match &out[0] { - ShellEvent::Touch { - id, - phase, - pos, - force, - } => { - assert_eq!(*id, TouchId(7)); - assert_eq!(*phase, TouchPhase::Started); - assert_eq!(pos.x, 10.0); - assert_eq!(pos.y, 20.0); - assert_eq!(*force, Some(TouchForce::Normalized(1.0))); - } - other => panic!("expected ShellEvent::Touch, got {other:?}"), - } -} - -#[test] -fn touch_move_emits_moved_phase() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 1, - PointerKind::Touch, - PointerPhase::Move, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(matches!( - out.as_slice(), - [ShellEvent::Touch { - phase: TouchPhase::Moved, - .. - }] - )); -} - -#[test] -fn touch_up_emits_ended_phase() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 1, - PointerKind::Touch, - PointerPhase::Up, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(matches!( - out.as_slice(), - [ShellEvent::Touch { - phase: TouchPhase::Ended, - .. - }] - )); -} - -#[test] -fn touch_cancel_emits_cancelled_phase() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 1, - PointerKind::Touch, - PointerPhase::Cancel, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(matches!( - out.as_slice(), - [ShellEvent::Touch { - phase: TouchPhase::Cancelled, - .. - }] - )); -} - -#[test] -fn touch_hover_returns_empty_vec() { - // Touches never `Hover`; mapper drops the event so callers don't - // synthesize a fake `Moved` phase. - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 1, - PointerKind::Touch, - PointerPhase::Hover, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(out.is_empty(), "expected empty Vec for touch Hover"); -} - -// ---------------------------------------------------------------- Mouse ---- - -#[test] -fn mouse_hover_emits_pointer_move() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 3, - PointerKind::Mouse, - PointerPhase::Hover, - JianMouseButtons::empty(), - JianModifiers::empty(), - 100.0, - 50.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert_eq!(out.len(), 1); - match &out[0] { - ShellEvent::PointerMove { id, pos, modifiers } => { - assert_eq!(*id, PointerId(3)); - assert_eq!(pos.x, 100.0); - assert_eq!(pos.y, 50.0); - assert_eq!(*modifiers, Modifiers::default()); - } - other => panic!("expected PointerMove, got {other:?}"), - } -} - -#[test] -fn mouse_left_down_then_up() { - let mut mapper = JianPointerMapper::new(); - - // Down: previous = empty, current = LEFT → emit Pressed. - let down = jian_event( - 9, - PointerKind::Mouse, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&down); - assert_eq!(out.len(), 1); - match &out[0] { - ShellEvent::PointerButton { button, state, .. } => { - assert_eq!(*button, MouseButton::Left); - assert_eq!(*state, ElementState::Pressed); - } - other => panic!("expected PointerButton{{Pressed}}, got {other:?}"), - } - - // Up: previous = LEFT, current = empty → emit Released. - let up = jian_event( - 9, - PointerKind::Mouse, - PointerPhase::Up, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&up); - assert_eq!(out.len(), 1); - match &out[0] { - ShellEvent::PointerButton { button, state, .. } => { - assert_eq!(*button, MouseButton::Left); - assert_eq!(*state, ElementState::Released); - } - other => panic!("expected PointerButton{{Released}}, got {other:?}"), - } -} - -#[test] -fn multi_button_press_during_move() { - // CONCERN-R3-1 fixture (a): LEFT held + Move with LEFT|RIGHT → - // [PointerButton{RIGHT, Pressed}, PointerMove]. - let mut mapper = JianPointerMapper::new(); - - // Prime previous = LEFT via a Down. - let prime = jian_event( - 4, - PointerKind::Mouse, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - ); - let _ = mapper.from_jian_pointer(&prime); - - // Now Move with LEFT|RIGHT. - let mid = jian_event( - 4, - PointerKind::Mouse, - PointerPhase::Move, - JianMouseButtons::LEFT | JianMouseButtons::RIGHT, - JianModifiers::empty(), - 5.0, - 7.0, - ); - let out = mapper.from_jian_pointer(&mid); - assert_eq!(out.len(), 2, "expected button Pressed + PointerMove"); - match &out[0] { - ShellEvent::PointerButton { button, state, .. } => { - assert_eq!(*button, MouseButton::Right); - assert_eq!(*state, ElementState::Pressed); - } - other => panic!("expected PointerButton{{Right,Pressed}}, got {other:?}"), - } - assert!(matches!(out[1], ShellEvent::PointerMove { .. })); -} - -#[test] -fn multi_button_release_during_move() { - // CONCERN-R3-1 fixture (b): LEFT|RIGHT held + Move with LEFT only → - // [PointerButton{RIGHT, Released}, PointerMove]. - let mut mapper = JianPointerMapper::new(); - - // Prime previous = LEFT|RIGHT via two Downs. - let _ = mapper.from_jian_pointer(&jian_event( - 2, - PointerKind::Mouse, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - )); - let _ = mapper.from_jian_pointer(&jian_event( - 2, - PointerKind::Mouse, - PointerPhase::Move, - JianMouseButtons::LEFT | JianMouseButtons::RIGHT, - JianModifiers::empty(), - 0.0, - 0.0, - )); - - let release = jian_event( - 2, - PointerKind::Mouse, - PointerPhase::Move, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 9.0, - 9.0, - ); - let out = mapper.from_jian_pointer(&release); - assert_eq!(out.len(), 2, "expected button Released + PointerMove"); - match &out[0] { - ShellEvent::PointerButton { button, state, .. } => { - assert_eq!(*button, MouseButton::Right); - assert_eq!(*state, ElementState::Released); - } - other => panic!("expected PointerButton{{Right,Released}}, got {other:?}"), - } - assert!(matches!(out[1], ShellEvent::PointerMove { .. })); -} - -// ---------------------------------------------------- Pen / Stylus / Trackpad - -#[test] -fn pen_phase_routes_through_mouse_branch() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 11, - PointerKind::Pen, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert_eq!(out.len(), 1); - assert!(matches!( - out[0], - ShellEvent::PointerButton { - button: MouseButton::Left, - state: ElementState::Pressed, - .. - } - )); -} - -#[test] -fn stylus_phase_routes_through_mouse_branch() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 12, - PointerKind::Stylus, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(matches!( - out.as_slice(), - [ShellEvent::PointerButton { - button: MouseButton::Left, - state: ElementState::Pressed, - .. - }] - )); -} - -#[test] -fn trackpad_phase_routes_through_mouse_branch() { - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 13, - PointerKind::Trackpad, - PointerPhase::Move, - JianMouseButtons::empty(), - JianModifiers::empty(), - 1.0, - 2.0, - ); - let out = mapper.from_jian_pointer(&ev); - // No buttons changed → only a PointerMove. - assert!(matches!(out.as_slice(), [ShellEvent::PointerMove { .. }])); -} - -// ---------------------------------------------------------- Degraded inputs - -#[test] -fn degraded_down_no_button_change_returns_empty() { - // Round 4 CONCERN-R4-1 fix: empty buttons on Down with empty - // previous → mapper returns `Vec::new()` (no `ShellEvent::Other`). - let mut mapper = JianPointerMapper::new(); - let ev = jian_event( - 5, - PointerKind::Mouse, - PointerPhase::Down, - JianMouseButtons::empty(), - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(out.is_empty(), "expected empty Vec, got {out:?}"); -} - -#[test] -fn degraded_up_no_button_change_returns_empty() { - // Up where current buttons match previous → no diff, no Move - // (Up phase doesn't emit Move) → empty Vec. - let mut mapper = JianPointerMapper::new(); - // Prime previous = LEFT. - let _ = mapper.from_jian_pointer(&jian_event( - 6, - PointerKind::Mouse, - PointerPhase::Down, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - )); - // Now Up but buttons still LEFT (e.g. host-side bookkeeping bug). - let ev = jian_event( - 6, - PointerKind::Mouse, - PointerPhase::Up, - JianMouseButtons::LEFT, - JianModifiers::empty(), - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert!(out.is_empty(), "expected empty Vec, got {out:?}"); -} - -// ---------------------------------------------------------------- Modifiers - -#[test] -fn modifiers_propagate() { - let mut mapper = JianPointerMapper::new(); - let mods = JianModifiers::SHIFT | JianModifiers::CTRL | JianModifiers::ALT | JianModifiers::CMD; - let ev = jian_event( - 8, - PointerKind::Mouse, - PointerPhase::Hover, - JianMouseButtons::empty(), - mods, - 0.0, - 0.0, - ); - let out = mapper.from_jian_pointer(&ev); - assert_eq!(out.len(), 1); - match &out[0] { - ShellEvent::PointerMove { modifiers, .. } => { - assert!(modifiers.shift); - assert!(modifiers.ctrl); - assert!(modifiers.alt); - assert!(modifiers.meta, "Jian CMD must map to OP meta"); - } - other => panic!("expected PointerMove, got {other:?}"), - } -}