diff --git a/lib/editor/canvas/pen_interactive_viewer.dart b/lib/editor/canvas/pen_interactive_viewer.dart index a320acf..66940d9 100644 --- a/lib/editor/canvas/pen_interactive_viewer.dart +++ b/lib/editor/canvas/pen_interactive_viewer.dart @@ -91,6 +91,11 @@ class _PenInteractiveViewerState extends State double? _scaleStart; _GestureType? _gestureType; + /// Number of pointers in the active gesture. When it changes (a finger lands + /// or lifts, or a Windows touch dropout/re-acquire), we re-baseline instead of + /// applying a frame whose scale/focal still refer to the old finger set. + int _lastPointerCount = 0; + // --- Matrix helpers (infinite boundary → no clamping to bounds) ----------- Matrix4 _matrixTranslate(Matrix4 matrix, Offset translation) { @@ -139,6 +144,7 @@ class _PenInteractiveViewerState extends State _scaleAnimation = null; } _gestureType = null; + _lastPointerCount = details.pointerCount; _scaleStart = _transformer.value.getMaxScaleOnAxis(); _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); } @@ -146,6 +152,17 @@ class _PenInteractiveViewerState extends State void _onScaleUpdate(ScaleUpdateDetails details) { final double scale = _transformer.value.getMaxScaleOnAxis(); _scaleAnimationFocalPoint = details.localFocalPoint; + + // Re-baseline on any pointer-count change so a finger landing/lifting (or a + // Windows touch dropout) can't make scale/focal jump from the stale set. + // The transitional frame itself is skipped. + if (details.pointerCount != _lastPointerCount) { + _lastPointerCount = details.pointerCount; + _scaleStart = _transformer.value.getMaxScaleOnAxis(); + _referenceFocalPoint = _transformer.toScene(details.localFocalPoint); + return; + } + final Offset focalPointScene = _transformer.toScene(details.localFocalPoint); if (_gestureType == _GestureType.pan) { @@ -200,6 +217,7 @@ class _PenInteractiveViewerState extends State void _onScaleEnd(ScaleEndDetails details) { _scaleStart = null; _referenceFocalPoint = null; + _lastPointerCount = 0; _animation?.removeListener(_handleInertiaAnimation); _scaleAnimation?.removeListener(_handleScaleAnimation); _controller.reset(); diff --git a/windows/runner/CMakeLists.txt b/windows/runner/CMakeLists.txt index 3319b72..90bf01f 100644 --- a/windows/runner/CMakeLists.txt +++ b/windows/runner/CMakeLists.txt @@ -36,6 +36,9 @@ target_compile_definitions(${BINARY_NAME} PRIVATE "NOMINMAX") # dependencies here. target_link_libraries(${BINARY_NAME} PRIVATE flutter flutter_wrapper_app) target_link_libraries(${BINARY_NAME} PRIVATE "dwmapi.lib" "windowsapp.lib") +# comctl32 provides SetWindowSubclass/DefSubclassProc for the pen WM_POINTER +# observer installed on the Flutter child window (flutter_window.cpp). +target_link_libraries(${BINARY_NAME} PRIVATE "comctl32.lib") target_compile_options(${BINARY_NAME} PRIVATE /await) target_include_directories(${BINARY_NAME} PRIVATE "${CMAKE_SOURCE_DIR}") diff --git a/windows/runner/flutter_window.cpp b/windows/runner/flutter_window.cpp index b6cddf4..4e20694 100644 --- a/windows/runner/flutter_window.cpp +++ b/windows/runner/flutter_window.cpp @@ -1,11 +1,36 @@ #include "flutter_window.h" +#include + #include #include "flutter/generated_plugin_registrant.h" #include "ocr_channel.h" #include "pen_channel.h" +// SetWindowSubclass / DefSubclassProc / RemoveWindowSubclass live in comctl32. +#pragma comment(lib, "comctl32.lib") + +namespace { + +// Id for the pen observer subclass installed on the Flutter child view window. +constexpr UINT_PTR kPenSubclassId = 1; + +// Subclass proc on the FLUTTER CHILD window. WM_POINTER messages are delivered +// to the window under the pointer — that is the Flutter view's child HWND, not +// the top-level window — so this is where the pen's barrel/eraser/tilt actually +// arrive. We observe (non-consuming) and pass every message straight through to +// Flutter's own handler via DefSubclassProc, so input behavior is unchanged. +LRESULT CALLBACK FlutterChildSubclassProc(HWND hwnd, UINT message, + WPARAM wparam, LPARAM lparam, + UINT_PTR /*subclass_id*/, + DWORD_PTR /*ref_data*/) { + ObservePenMessage(message, wparam, lparam); + return DefSubclassProc(hwnd, message, wparam, lparam); +} + +} // namespace + FlutterWindow::FlutterWindow(const flutter::DartProject& project) : project_(project) {} @@ -29,7 +54,15 @@ bool FlutterWindow::OnCreate() { RegisterPlugins(flutter_controller_->engine()); RegisterOcrChannel(flutter_controller_->engine()); RegisterPenChannel(flutter_controller_->engine()); - SetChildContent(flutter_controller_->view()->GetNativeWindow()); + + HWND flutter_child = flutter_controller_->view()->GetNativeWindow(); + SetChildContent(flutter_child); + + // Observe WM_POINTER on the Flutter child window (where pen messages land) so + // the pen barrel/eraser/inverted flags + tilt reach the badnote/pen channel. + if (flutter_child) { + SetWindowSubclass(flutter_child, FlutterChildSubclassProc, kPenSubclassId, 0); + } flutter_controller_->engine()->SetNextFrameCallback([&]() { this->Show(); @@ -45,6 +78,11 @@ bool FlutterWindow::OnCreate() { void FlutterWindow::OnDestroy() { if (flutter_controller_) { + HWND flutter_child = flutter_controller_->view()->GetNativeWindow(); + if (flutter_child) { + RemoveWindowSubclass(flutter_child, FlutterChildSubclassProc, + kPenSubclassId); + } flutter_controller_ = nullptr; }