feat(diag): full input logging + read barrel from pointerFlags; focal-jump reject
All checks were successful
CI / Windows build (push) Successful in 12m44s

Buttons (likely fix + ground truth): device diag showed ptr=12577 pen=10056 —
WM_POINTER reaches the observer and GetPointerPenInfo succeeds, so the buttons
were just read from the wrong field. Native now resolves the barrel from BOTH
penFlags(PEN_FLAG_BARREL) AND pointerInfo.pointerFlags(POINTER_FLAG_SECONDBUTTON)
— many pens use the latter. It also emits the full raw set (pointerFlags,
penFlags, penMask, ButtonChangeType, tilt) plus OR-accumulated flags so a single
session reveals exactly which field each button sets.

Comprehensive logging (per user request "用好用的log库 / 我手动开启日志再记录"):
new DiagnosticLogger emits through dart:developer log(name 'badnote.input') —
capturable via `flutter run` / DevTools / `flutter logs` — AND mirrors to a file
(path shown in the overlay) for the packaged GUI build that has no console.
Manually enabled by the toolbar diagnostic toggle; off by default. PEN lines log
on raw-field change; ZOOM lines log every scale frame + rebaselines.

Zoom: scale-only glitch rejection didn't stop the jumping, so add focal/position
glitch rejection — drop a 2-finger frame whose focal jumps >250px (a touch
misread). The full per-frame trace (raw scale, pointerCount, applied change,
focal jump, drops) is now logged so the residual cause is unambiguous.

InputDiagnostics singleton accumulates the stats; the overlay shows summary +
last trace lines + log path + reset. Removed the ad-hoc inline zoom min/max.

Dart: analyze clean, 66/66 tests, linux build green. Native compiles on CI.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-23 01:10:17 +08:00
parent ae9e070b46
commit f9ec04fe86
6 changed files with 379 additions and 55 deletions

View File

@@ -0,0 +1,90 @@
// lib/editor/canvas/input_diagnostics.dart
//
// Live zoom/pan diagnostics for the pen canvas. PenInteractiveViewer records one
// entry per scale-update frame; the editor's diagnostic overlay displays the
// accumulated summary + a rolling trace so a SINGLE device session reveals the
// nature of any "跳变" (is it a raw-scale spike, a focal/position jump, or a
// pointer-count oscillation?). All numbers reset via [reset].
import 'package:flutter/foundation.dart';
import '../input/diagnostic_logger.dart';
class InputDiagnostics extends ChangeNotifier {
InputDiagnostics._();
static final InputDiagnostics instance = InputDiagnostics._();
int frames = 0;
int scaleDropped = 0; // frames rejected as a scale glitch
int focalDropped = 0; // frames rejected as a focal/position glitch
int rebaselines = 0; // pointer-count re-baselines
int pointerCountMax = 0;
double rawScaleMin = double.infinity, rawScaleMax = 0;
double scaleMin = double.infinity, scaleMax = 0;
double maxFocalJumpPx = 0; // largest single-frame local focal delta
double maxAppliedScaleJump = 1; // largest single-frame applied scale ratio
final List<String> _trace = <String>[];
List<String> get trace => List.unmodifiable(_trace);
void recordRebaseline() {
rebaselines++;
DiagnosticLogger.instance.log('ZOOM rebaseline');
notifyListeners();
}
void recordScaleFrame({
required double rawScale,
required int pointerCount,
required double currentScale,
required double appliedChange, // 1.0 when the frame was dropped
required double focalJumpPx,
required bool scaleDrop,
required bool focalDrop,
}) {
frames++;
if (scaleDrop) scaleDropped++;
if (focalDrop) focalDropped++;
if (rawScale < rawScaleMin) rawScaleMin = rawScale;
if (rawScale > rawScaleMax) rawScaleMax = rawScale;
final double resulting = currentScale * appliedChange;
if (resulting < scaleMin) scaleMin = resulting;
if (resulting > scaleMax) scaleMax = resulting;
if (pointerCount > pointerCountMax) pointerCountMax = pointerCount;
if (focalJumpPx > maxFocalJumpPx) maxFocalJumpPx = focalJumpPx;
final double jump = appliedChange >= 1 ? appliedChange : 1 / appliedChange;
if (jump > maxAppliedScaleJump) maxAppliedScaleJump = jump;
final String line = 'p$pointerCount raw=${rawScale.toStringAsFixed(3)} '
'ch=${appliedChange.toStringAsFixed(3)} '
'cur=${currentScale.toStringAsFixed(3)} '
'fj=${focalJumpPx.toStringAsFixed(0)}'
'${scaleDrop ? " SDROP" : ""}${focalDrop ? " FDROP" : ""}';
_trace.add(line);
if (_trace.length > 24) _trace.removeAt(0);
DiagnosticLogger.instance.log('ZOOM $line');
notifyListeners();
}
void reset() {
frames = scaleDropped = focalDropped = rebaselines = pointerCountMax = 0;
rawScaleMin = scaleMin = double.infinity;
rawScaleMax = scaleMax = 0;
maxFocalJumpPx = 0;
maxAppliedScaleJump = 1;
_trace.clear();
notifyListeners();
}
String _f(double v) => v.isFinite ? v.toStringAsFixed(2) : '-';
String summary() {
if (frames == 0) return 'zoom: (pinch to record)';
return 'zoom f=$frames sDrop=$scaleDropped fDrop=$focalDropped '
'rebase=$rebaselines pMax=$pointerCountMax\n'
' raw=${_f(rawScaleMin)}..${_f(rawScaleMax)} '
'scale=${_f(scaleMin)}..${_f(scaleMax)}\n'
' maxFocalJump=${maxFocalJumpPx.toStringAsFixed(0)}px '
'maxScaleJump=${_f(maxAppliedScaleJump)}';
}
}

View File

@@ -12,12 +12,14 @@ import '../../services/database_service.dart';
import '../engine/stroke_geometry.dart' show kDefaultPenThinning;
import '../engine/stroke_model.dart';
import '../engine/undo_stack.dart';
import '../input/diagnostic_logger.dart';
import '../input/pen_config.dart';
import '../input/pen_input_service.dart';
import '../persistence/editor_repository.dart';
import '../persistence/save_scheduler.dart';
import '../ui/pen_settings_page.dart';
import '../ui/thumbnail_grid.dart';
import 'input_diagnostics.dart';
import 'pen_canvas.dart';
import 'pen_stroke.dart';
@@ -95,13 +97,6 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
String _penDebug = '';
bool _showPenDebug = false;
/// Live zoom-scale diagnostic (only tracked while the overlay is on), so we
/// can capture the value the pinch flash jumps to. `_zoomMin/_zoomMax` record
/// the extremes seen since the overlay was last enabled.
double _zoomNow = 1.0;
double _zoomMin = double.infinity;
double _zoomMax = 0.0;
// Tool state.
CanvasTool _tool = CanvasTool.pen;
Color _color = Colors.black;
@@ -126,23 +121,11 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
// Begin listening to the native Windows pen plugin (barrel/eraser/tilt).
// No-op on platforms without the plugin (W3).
PenInputService.instance.start();
_transform.addListener(_onTransformDebug);
_initPersistence();
_initPenConfig();
_open();
}
/// Track the live zoom scale for the diagnostic overlay (no-op when off).
void _onTransformDebug() {
if (!_showPenDebug) return;
final s = _transform.value.getMaxScaleOnAxis();
setState(() {
_zoomNow = s;
if (s < _zoomMin) _zoomMin = s;
if (s > _zoomMax) _zoomMax = s;
});
}
Future<void> _initPenConfig() async {
final controller = await PenConfigController.load();
if (!mounted) {
@@ -240,10 +223,10 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
scheduler.dispose();
}
_document?.dispose();
_transform.removeListener(_onTransformDebug);
_transform.dispose();
_penConfig?.dispose();
PenInputService.instance.stop();
DiagnosticLogger.instance.stop();
super.dispose();
}
@@ -453,18 +436,59 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
child: Material(
color: Theme.of(context).colorScheme.inverseSurface,
borderRadius: BorderRadius.circular(8),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
child: Text(
'${_penDebug.isEmpty ? 'hover / draw with the pen…' : _penDebug}'
'\nzoom=${_zoomNow.toStringAsFixed(2)} '
'min=${_zoomMin.isFinite ? _zoomMin.toStringAsFixed(2) : '-'} '
'max=${_zoomMax.toStringAsFixed(2)}',
style: TextStyle(
fontFamily: 'monospace',
fontSize: 12,
color: Theme.of(context).colorScheme.onInverseSurface,
child: ConstrainedBox(
constraints: const BoxConstraints(maxWidth: 380),
child: Padding(
padding: const EdgeInsets.symmetric(
horizontal: 10, vertical: 6),
child: ListenableBuilder(
listenable: InputDiagnostics.instance,
builder: (context, _) {
final cs = Theme.of(context).colorScheme;
final d = InputDiagnostics.instance;
final tail = d.trace.length > 6
? d.trace.sublist(d.trace.length - 6)
: d.trace;
final mono = TextStyle(
fontFamily: 'monospace',
fontSize: 11,
color: cs.onInverseSurface);
final monoFaint = mono.copyWith(
fontSize: 10,
color:
cs.onInverseSurface.withValues(alpha: 0.75));
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
mainAxisSize: MainAxisSize.min,
children: [
Text(
_penDebug.isEmpty
? 'hover / draw with the pen…'
: _penDebug,
style: mono),
const SizedBox(height: 4),
Text(d.summary(), style: mono),
if (tail.isNotEmpty) ...[
const SizedBox(height: 4),
Text(tail.join('\n'), style: monoFaint),
],
const SizedBox(height: 4),
Text(
'log: ${DiagnosticLogger.instance.path ?? "(developer.log only)"}',
style: monoFaint),
Align(
alignment: Alignment.centerRight,
child: TextButton(
onPressed: () =>
InputDiagnostics.instance.reset(),
child: Text('Reset stats',
style:
TextStyle(color: cs.inversePrimary)),
),
),
],
);
},
),
),
),
@@ -621,15 +645,17 @@ class _PenEditorScreenState extends State<PenEditorScreen> {
_ToolButton(
icon: Icons.bug_report_outlined,
selected: _showPenDebug,
tooltip: 'Pen + zoom diagnostic',
onPressed: () => setState(() {
_showPenDebug = !_showPenDebug;
if (_showPenDebug) {
_zoomMin = double.infinity;
_zoomMax = 0.0;
_zoomNow = _transform.value.getMaxScaleOnAxis();
tooltip: 'Input diagnostic (writes a log file)',
onPressed: () {
final on = !_showPenDebug;
setState(() => _showPenDebug = on);
if (on) {
InputDiagnostics.instance.reset();
DiagnosticLogger.instance.start();
} else {
DiagnosticLogger.instance.stop();
}
}),
},
),
],
),

View File

@@ -31,6 +31,8 @@ import 'package:flutter/gestures.dart';
import 'package:flutter/physics.dart';
import 'package:flutter/widgets.dart';
import 'input_diagnostics.dart';
/// Devices allowed to pan/zoom. Stylus + invertedStylus are excluded so the pen
/// is owned exclusively by the drawing `Listener`.
const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
@@ -46,6 +48,11 @@ const Set<PointerDeviceKind> _kPanZoomDevices = <PointerDeviceKind>{
const double _kScaleGlitchHi = 1.4;
const double _kScaleGlitchLo = 1 / _kScaleGlitchHi;
/// During a 2-finger gesture the focal point (finger midpoint) should move
/// smoothly. A single-frame local jump beyond this is a Windows touch misread,
/// and the frame is dropped (position-jump guard).
const double _kFocalGlitchPx = 250.0;
const double _kDrag = 0.0000135;
enum _GestureType { pan, scale }
@@ -162,9 +169,11 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
_lastPointerCount = details.pointerCount;
_scaleStart = _transformer.value.getMaxScaleOnAxis();
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
InputDiagnostics.instance.recordRebaseline();
return;
}
final double focalJumpPx = details.focalPointDelta.distance;
final Offset focalPointScene = _transformer.toScene(details.localFocalPoint);
if (_gestureType == _GestureType.pan) {
@@ -175,16 +184,36 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
}
if (!_gestureIsSupported(_gestureType)) return;
// Position-jump guard: during a pinch the focal midpoint should move
// smoothly; a big single-frame jump is a touch misread → drop the frame.
final bool focalDrop =
details.pointerCount >= 2 && focalJumpPx > _kFocalGlitchPx;
void record(double appliedChange, bool scaleDrop, bool focalDropped) {
InputDiagnostics.instance.recordScaleFrame(
rawScale: details.scale,
pointerCount: details.pointerCount,
currentScale: scale,
appliedChange: appliedChange,
focalJumpPx: focalJumpPx,
scaleDrop: scaleDrop,
focalDrop: focalDropped,
);
}
switch (_gestureType!) {
case _GestureType.scale:
assert(_scaleStart != null);
final double desiredScale = _scaleStart! * details.scale;
final double scaleChange = desiredScale / scale;
// Glitch rejection: drop a frame that demands an implausible per-frame
// scale jump (a Windows multi-touch position glitch). The next good
// frame resumes from the true finger positions, so the spike never
// shows — unlike clamping, which still applied a visible partial jump.
if (scaleChange > _kScaleGlitchHi || scaleChange < _kScaleGlitchLo) {
// Drop a frame demanding an implausible per-frame scale jump (Windows
// multi-touch glitch) OR an implausible focal jump. Absolute tracking
// means the next good frame resumes from the true finger positions, so
// the spike never shows.
final bool scaleDrop =
scaleChange > _kScaleGlitchHi || scaleChange < _kScaleGlitchLo;
if (scaleDrop || focalDrop) {
record(1.0, scaleDrop, focalDrop);
return;
}
_transformer.value = _matrixScale(_transformer.value, scaleChange);
@@ -203,16 +232,23 @@ class _PenInteractiveViewerState extends State<PenInteractiveViewer>
if (_round(_referenceFocalPoint!) != _round(focalPointSceneCheck)) {
_referenceFocalPoint = focalPointSceneCheck;
}
record(scaleChange, false, false);
case _GestureType.pan:
assert(_referenceFocalPoint != null);
// Throw away near-scale frames so a stale reference can't jump the pan.
if (details.scale != 1.0) return;
if (focalDrop) {
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
record(1.0, false, true);
return;
}
final Offset translationChange =
focalPointScene - _referenceFocalPoint!;
_transformer.value =
_matrixTranslate(_transformer.value, translationChange);
_referenceFocalPoint = _transformer.toScene(details.localFocalPoint);
record(1.0, false, false);
}
}

View File

@@ -0,0 +1,90 @@
// lib/editor/input/diagnostic_logger.dart
//
// On-device input diagnostics. When the user manually enables the diagnostic
// (the toolbar toggle), every native pen event (raw button/flag/tilt fields)
// and every zoom frame is emitted through the standard `dart:developer` log
// channel (name 'badnote.input') — capturable via `flutter run`, DevTools, or
// any log tool — AND mirrored to a text file as a fallback for the packaged
// GUI build, which has no attached console. Disabled by default (no overhead).
import 'dart:async';
import 'dart:developer' as developer;
import 'dart:io';
import 'package:path_provider/path_provider.dart';
class DiagnosticLogger {
DiagnosticLogger._();
static final DiagnosticLogger instance = DiagnosticLogger._();
final List<String> _buffer = <String>[];
File? _file;
Timer? _timer;
int _epochMs = 0;
bool _active = false;
bool get isActive => _active;
/// Absolute path of the current log file (shown in the overlay), or null.
String? path;
/// Begin a session. Enables the `dart:developer` log channel immediately and
/// opens the fallback file (best-effort). Safe to call repeatedly.
Future<void> start() async {
if (_active) return;
_active = true; // developer.log works even if the file can't be opened
_epochMs = DateTime.now().millisecondsSinceEpoch;
developer.log('--- session start ${DateTime.now().toIso8601String()} ---',
name: 'badnote.input');
try {
Directory dir;
try {
dir = await getApplicationDocumentsDirectory();
} catch (_) {
dir = await getTemporaryDirectory();
}
final file = File('${dir.path}${Platform.pathSeparator}badnote_input_log.txt');
await file.writeAsString(
'# BadNote input diagnostic log\n'
'# started ${DateTime.now().toIso8601String()}\n'
'# columns: <ms> <kind> <fields...>\n',
flush: true,
);
_file = file;
path = file.path;
_buffer.clear();
_timer = Timer.periodic(const Duration(seconds: 1), (_) => _flush());
} catch (_) {
// File is a fallback; never break the app over it.
}
}
/// Emit one diagnostic line through the standard log channel and the file.
void log(String line) {
if (!_active) return;
developer.log(line, name: 'badnote.input');
if (_file == null) return;
final t = DateTime.now().millisecondsSinceEpoch - _epochMs;
_buffer.add('$t $line');
if (_buffer.length >= 1000) _flush();
}
Future<void> _flush() async {
final file = _file;
if (file == null || _buffer.isEmpty) return;
final chunk = '${_buffer.join('\n')}\n';
_buffer.clear();
try {
await file.writeAsString(chunk, mode: FileMode.append, flush: true);
} catch (_) {}
}
/// Flush and stop. The file remains on disk for retrieval.
Future<void> stop() async {
if (!_active) return;
_active = false;
_timer?.cancel();
_timer = null;
await _flush();
}
}

View File

@@ -23,6 +23,8 @@ import 'dart:async';
import 'package:flutter/services.dart';
import 'diagnostic_logger.dart';
/// Latest hardware pen state delivered by the native observer.
class PenHardwareState {
const PenHardwareState({
@@ -94,17 +96,27 @@ class PenInputService {
bool get isActive => _active;
bool _active = false;
// Native-side diagnostics (see windows/runner/pen_channel.cpp): how many
// WM_POINTER / PT_PEN / legacy-mouse messages the observer has seen. Lets the
// on-device overlay tell us WHICH layer is failing for buttons/tilt.
// Native-side diagnostics (see windows/runner/pen_channel.cpp).
int _diagPtr = 0;
int _diagPen = 0;
int _diagMouse = 0;
int _diagMsg = 0;
int _orPtrFlags = 0;
int _orPenFlags = 0;
int _orPenMask = 0;
int _btnChangeLast = 0;
int _tiltAbsMax = 0;
/// One-line native readout for the diagnostic overlay.
String _hex(int v) => '0x${v.toRadixString(16)}';
/// Multi-line native readout for the diagnostic overlay. The OR-accumulated
/// flag fields are the ground truth for which field carries the side/eraser
/// button: e.g. orPtrFlags with bit 0x20 (POINTER_FLAG_SECONDBUTTON) set means
/// the barrel button IS detectable.
String get debugSummary => _active
? 'native ptr=$_diagPtr pen=$_diagPen mouse=$_diagMouse msg=0x${_diagMsg.toRadixString(16)}'
? 'native ptr=$_diagPtr pen=$_diagPen mouse=$_diagMouse msg=${_hex(_diagMsg)}'
'\n orPtrFlags=${_hex(_orPtrFlags)} orPenFlags=${_hex(_orPenFlags)}'
' mask=${_hex(_orPenMask)} btnChg=$_btnChangeLast tiltMax=$_tiltAbsMax'
: 'native: channel silent (no events)';
/// Begins listening to the native channel. Idempotent; safe on any platform
@@ -140,9 +152,35 @@ class PenInputService {
_diagPen = (event['diagPen'] as num?)?.toInt() ?? _diagPen;
_diagMouse = (event['diagMouse'] as num?)?.toInt() ?? _diagMouse;
_diagMsg = (event['diagMsg'] as num?)?.toInt() ?? _diagMsg;
_orPtrFlags = (event['orPtrFlags'] as num?)?.toInt() ?? _orPtrFlags;
_orPenFlags = (event['orPenFlags'] as num?)?.toInt() ?? _orPenFlags;
_orPenMask = (event['orPenMask'] as num?)?.toInt() ?? _orPenMask;
_btnChangeLast = (event['btnChangeLast'] as num?)?.toInt() ?? _btnChangeLast;
_tiltAbsMax = (event['tiltAbsMax'] as num?)?.toInt() ?? _tiltAbsMax;
_active = true;
// Log a PEN line whenever the raw per-event button/flag fields change, so
// the file captures exactly which field a button press sets (without
// flooding on every high-rate WM_POINTERUPDATE).
final rawPtr = (event['rawPtrFlags'] as num?)?.toInt() ?? 0;
final rawPen = (event['rawPenFlags'] as num?)?.toInt() ?? 0;
final rawMask = (event['rawPenMask'] as num?)?.toInt() ?? 0;
final btnChange = (event['btnChange'] as num?)?.toInt() ?? 0;
final key = '$rawPtr,$rawPen,$rawMask,$btnChange,${_current.tiltX},${_current.tiltY}';
if (key != _lastPenLogKey) {
_lastPenLogKey = key;
DiagnosticLogger.instance.log(
'PEN ptrFlags=0x${rawPtr.toRadixString(16)} '
'penFlags=0x${rawPen.toRadixString(16)} '
'mask=0x${rawMask.toRadixString(16)} btnChg=$btnChange '
'tilt=${_current.tiltX.toStringAsFixed(0)},${_current.tiltY.toStringAsFixed(0)} '
'msg=0x${_diagMsg.toRadixString(16)} resolved=0x${flags.toRadixString(16)}',
);
}
}
String _lastPenLogKey = '';
/// Stops listening and resets state.
void stop() {
_sub?.cancel();

View File

@@ -23,6 +23,15 @@ int g_pen_msgs = 0;
int g_mouse_msgs = 0;
int g_last_msg = 0;
// OR-accumulated raw flag fields, so a momentary button press is CAPTURED and
// held (a live readout would miss it). These are the ground truth for "which
// field/bit does the side button / eraser set?".
int g_ptr_flags_or = 0; // POINTER_INFO.pointerFlags (POINTER_FLAG_SECONDBUTTON = barrel on many pens)
int g_pen_flags_or = 0; // POINTER_PEN_INFO.penFlags (PEN_FLAG_BARREL/INVERTED/ERASER)
int g_pen_mask_or = 0; // POINTER_PEN_INFO.penMask
int g_btn_change_last = 0; // last non-zero POINTER_INFO.ButtonChangeType
int g_tilt_abs_max = 0; // max |tiltX|,|tiltY| seen
} // namespace
void RegisterPenChannel(flutter::FlutterEngine* engine) {
@@ -76,6 +85,10 @@ void ObservePenMessage(UINT message, WPARAM wparam, LPARAM lparam) {
int flags = 0;
double tilt_x = 0.0;
double tilt_y = 0.0;
int raw_ptr_flags = 0;
int raw_pen_flags = 0;
int raw_pen_mask = 0;
int btn_change = 0;
if (is_pointer) {
++g_ptr_msgs;
@@ -85,11 +98,32 @@ void ObservePenMessage(UINT message, WPARAM wparam, LPARAM lparam) {
POINTER_PEN_INFO ppi{};
if (GetPointerPenInfo(pointerId, &ppi)) {
++g_pen_msgs;
if (ppi.penFlags & PEN_FLAG_BARREL) flags |= 1;
if (ppi.penFlags & PEN_FLAG_INVERTED) flags |= 2;
if (ppi.penFlags & PEN_FLAG_ERASER) flags |= 4;
raw_pen_flags = static_cast<int>(ppi.penFlags);
raw_pen_mask = static_cast<int>(ppi.penMask);
raw_ptr_flags = static_cast<int>(ppi.pointerInfo.pointerFlags);
btn_change = static_cast<int>(ppi.pointerInfo.ButtonChangeType);
tilt_x = static_cast<double>(ppi.tiltX);
tilt_y = static_cast<double>(ppi.tiltY);
// Barrel/side button can arrive in EITHER penFlags (PEN_FLAG_BARREL) or
// pointerFlags (POINTER_FLAG_SECONDBUTTON) depending on the pen/driver,
// so check both. Eraser end = inverted/eraser pen flags.
const bool barrel = (ppi.penFlags & PEN_FLAG_BARREL) ||
(ppi.pointerInfo.pointerFlags & POINTER_FLAG_SECONDBUTTON);
const bool inverted = (ppi.penFlags & PEN_FLAG_INVERTED) != 0;
const bool eraser = (ppi.penFlags & PEN_FLAG_ERASER) != 0;
if (barrel) flags |= 1;
if (inverted) flags |= 2;
if (eraser) flags |= 4;
g_ptr_flags_or |= raw_ptr_flags;
g_pen_flags_or |= raw_pen_flags;
g_pen_mask_or |= raw_pen_mask;
if (btn_change != 0) g_btn_change_last = btn_change;
const int ax = ppi.tiltX < 0 ? -ppi.tiltX : ppi.tiltX;
const int ay = ppi.tiltY < 0 ? -ppi.tiltY : ppi.tiltY;
if (ax > g_tilt_abs_max) g_tilt_abs_max = ax;
if (ay > g_tilt_abs_max) g_tilt_abs_max = ay;
}
}
if (message == WM_POINTERUP) {
@@ -97,8 +131,9 @@ void ObservePenMessage(UINT message, WPARAM wparam, LPARAM lparam) {
}
}
// Always emit the diagnostic counters so the Dart overlay can show whether
// WM_POINTER / PT_PEN ever reach this observer.
// Emit the resolved flags/tilt PLUS the full raw + OR-accumulated diagnostic
// set, so a single device session reveals exactly which field carries the
// button and what tilt/mask the pen reports.
flutter::EncodableMap payload{
{flutter::EncodableValue("flags"), flutter::EncodableValue(flags)},
{flutter::EncodableValue("tiltX"), flutter::EncodableValue(tilt_x)},
@@ -107,6 +142,15 @@ void ObservePenMessage(UINT message, WPARAM wparam, LPARAM lparam) {
{flutter::EncodableValue("diagPen"), flutter::EncodableValue(g_pen_msgs)},
{flutter::EncodableValue("diagMouse"), flutter::EncodableValue(g_mouse_msgs)},
{flutter::EncodableValue("diagMsg"), flutter::EncodableValue(g_last_msg)},
{flutter::EncodableValue("rawPtrFlags"), flutter::EncodableValue(raw_ptr_flags)},
{flutter::EncodableValue("rawPenFlags"), flutter::EncodableValue(raw_pen_flags)},
{flutter::EncodableValue("rawPenMask"), flutter::EncodableValue(raw_pen_mask)},
{flutter::EncodableValue("btnChange"), flutter::EncodableValue(btn_change)},
{flutter::EncodableValue("orPtrFlags"), flutter::EncodableValue(g_ptr_flags_or)},
{flutter::EncodableValue("orPenFlags"), flutter::EncodableValue(g_pen_flags_or)},
{flutter::EncodableValue("orPenMask"), flutter::EncodableValue(g_pen_mask_or)},
{flutter::EncodableValue("btnChangeLast"), flutter::EncodableValue(g_btn_change_last)},
{flutter::EncodableValue("tiltAbsMax"), flutter::EncodableValue(g_tilt_abs_max)},
};
g_pen_sink->Success(flutter::EncodableValue(payload));
}