// lib/editor/input/pen_input_service.dart // // Dart side of the native Windows pen observer (`windows/runner/pen_channel.cpp`). // // WHY THIS EXISTS: Flutter 3.44 on Windows delivers stylus PRESSURE but drops // the pen's barrel button, eraser/inverted end, and tilt (it does not map // POINTER_PEN_FLAG_* into `PointerEvent.buttons`/`invertedStylus`/`tilt`). The // native plugin observes WM_POINTER + GetPointerPenInfo and streams the missing // hardware state over an EventChannel; this service latches the LATEST value. // // CORRELATION (plan M2): we do NOT key state by Win32 pointerId joined to // Flutter's `event.pointer` — those are different id spaces. Only one pen is // active at a time, so a single latched "current" state is correct. The native // observer runs at the TOP of the window proc (BEFORE Flutter synthesizes its // pointer event, plan M1), so by the time Dart's pointer-down handler reads // [current], the latch already reflects that exact contact — no hover required. // // GRACEFUL DEGRADATION: on non-Windows (or if the channel is silent) the stream // simply never emits / errors are swallowed, and [current] stays [PenHardwareState.empty] // so the canvas falls back to its normal Flutter-pressure drawing. import 'dart:async'; import 'package:flutter/services.dart'; /// Latest hardware pen state delivered by the native observer. class PenHardwareState { const PenHardwareState({ this.barrel = false, this.inverted = false, this.eraser = false, this.tiltX = 0.0, this.tiltY = 0.0, }); /// Side barrel button held. final bool barrel; /// Pen flipped to the inverted (eraser) end. final bool inverted; /// Hardware eraser flag set. final bool eraser; /// Tilt in degrees along X / Y ([-90, 90]); 0 = perpendicular. final double tiltX; final double tiltY; /// Combined tilt magnitude in degrees (for [PenPoint.tilt]). double get tiltMagnitude { final t = tiltX * tiltX + tiltY * tiltY; return t <= 0 ? 0.0 : _sqrt(t); } static const empty = PenHardwareState(); } // Avoids importing dart:math for a single call. double _sqrt(double v) { if (v <= 0) return 0; var x = v; var last = 0.0; // Newton's method; converges fast for the small (<=~127) magnitudes here. for (var i = 0; i < 12 && x != last; i++) { last = x; x = 0.5 * (x + v / x); } return x; } /// Latches the most recent [PenHardwareState] streamed by the native pen plugin. /// /// Use the singleton [PenInputService.instance]. Call [start] once (e.g. in the /// editor's `initState`) and [stop] on dispose. class PenInputService { PenInputService._(); /// Process-wide singleton (one physical pen). static final PenInputService instance = PenInputService._(); /// Must match the native `EventChannel` name in `pen_channel.cpp`. static const EventChannel _channel = EventChannel('badnote/pen'); StreamSubscription? _sub; PenHardwareState _current = PenHardwareState.empty; /// The latest hardware pen state (or [PenHardwareState.empty] when no native /// data has arrived — non-Windows, plugin absent, or channel silent). PenHardwareState get current => _current; /// Whether the native channel has delivered at least one event (i.e. the /// native pen plugin is present and active). Used to prefer hardware signals /// over the Flutter fallback only when they are actually available. bool get isActive => _active; bool _active = false; /// Begins listening to the native channel. Idempotent; safe on any platform /// (no-ops where the channel has no handler). void start() { if (_sub != null) return; try { _sub = _channel.receiveBroadcastStream().listen( _onEvent, onError: (Object _) { // No native handler (e.g. Linux/macOS) or transient error — ignore // and keep the empty fallback state. }, cancelOnError: false, ); } catch (_) { // receiveBroadcastStream can throw synchronously if the platform side is // unavailable; degrade silently. } } void _onEvent(dynamic event) { if (event is! Map) return; final flags = (event['flags'] as num?)?.toInt() ?? 0; _current = PenHardwareState( barrel: flags & 0x1 != 0, inverted: flags & 0x2 != 0, eraser: flags & 0x4 != 0, tiltX: (event['tiltX'] as num?)?.toDouble() ?? 0.0, tiltY: (event['tiltY'] as num?)?.toDouble() ?? 0.0, ); _active = true; } /// Stops listening and resets state. void stop() { _sub?.cancel(); _sub = null; _active = false; _current = PenHardwareState.empty; } }