From 0c40a456e806017a1b341408c068d0d6c26703ec Mon Sep 17 00:00:00 2001 From: Akiba So Date: Sun, 21 Jun 2026 22:33:18 +0800 Subject: [PATCH] fix(canvas): persist strokes, pressure, slider Strokes vanished on pen-up: StaticInkPainter aliased the same mutable list so shouldRepaint saw no change. Commit/erase now replace the list. Finger-drawing toggle wins over palm-rejection; pressure surfaces even when the pen reports no min/max range; pages recenter after a flip; the keyboard page-jump (unreliable on Windows) is now a drag slider. Also register the dynamic_color plugin in generated registrants. --- lib/editor/canvas/pen_canvas.dart | 25 +-- lib/editor/canvas/pen_editor_screen.dart | 149 ++++++++++-------- linux/flutter/generated_plugin_registrant.cc | 4 + linux/flutter/generated_plugins.cmake | 1 + macos/Flutter/GeneratedPluginRegistrant.swift | 2 + .../flutter/generated_plugin_registrant.cc | 3 + windows/flutter/generated_plugins.cmake | 1 + 7 files changed, 109 insertions(+), 76 deletions(-) diff --git a/lib/editor/canvas/pen_canvas.dart b/lib/editor/canvas/pen_canvas.dart index 9891d54..3e81849 100644 --- a/lib/editor/canvas/pen_canvas.dart +++ b/lib/editor/canvas/pen_canvas.dart @@ -93,16 +93,15 @@ class _PenCanvasState extends State { /// Live stroke snapshot handed to the LiveInkPainter; null when idle. PenStroke? _liveStroke; - /// True once any stylus event is seen this session → finger-drawing forced - /// off so a resting palm pans instead of marking. - bool _stylusSeen = false; - /// True when the active stylus reports the eraser signal (barrel button or /// inverted stylus), detected on hover/down. bool _eraserActive = false; - bool get _fingerDrawingEnabled => - widget.allowFingerDrawing && !_stylusSeen; + // The explicit user toggle wins: if finger-drawing is ON, a single finger + // draws even after a stylus has been seen. (Palm rejection when the toggle is + // OFF is automatic — fingers simply never draw — and a 2nd pointer always + // cancels an in-progress stroke regardless.) + bool get _fingerDrawingEnabled => widget.allowFingerDrawing; bool _isStylus(PointerDeviceKind kind) => kind == PointerDeviceKind.stylus || @@ -112,9 +111,17 @@ class _PenCanvasState extends State { /// usable pressure range (then perfect_freehand simulates pressure). double? _normalizedPressure(PointerEvent event) { if (!_isStylus(event.kind)) return null; - if (event.pressureMin == event.pressureMax) return null; final range = event.pressureMax - event.pressureMin; - return ((event.pressure - event.pressureMin) / range).clamp(0.0, 1.0); + if (range > 0.0001) { + return ((event.pressure - event.pressureMin) / range).clamp(0.0, 1.0); + } + // No advertised range (some Windows pen stacks): use the raw normalized + // pressure directly if it's a usable non-degenerate value, so we still get + // real force instead of falling back to velocity simulation. + if (event.pressure > 0.0 && event.pressure < 1.0) { + return event.pressure; + } + return null; } /// The eraser signal: barrel/secondary button held, or an inverted stylus. @@ -244,7 +251,6 @@ class _PenCanvasState extends State { void _onPointerHover(PointerHoverEvent event) { if (_isStylus(event.kind)) { - _stylusSeen = true; // Detect eraser (barrel button / inverted) while hovering. _eraserActive = _isEraserSignal(event); } @@ -252,7 +258,6 @@ class _PenCanvasState extends State { void _onPointerDown(PointerDownEvent event) { if (event.kind == PointerDeviceKind.trackpad) return; - if (_isStylus(event.kind)) _stylusSeen = true; _activePointers[event.pointer] = event.kind; diff --git a/lib/editor/canvas/pen_editor_screen.dart b/lib/editor/canvas/pen_editor_screen.dart index 57eeabe..ef300d6 100644 --- a/lib/editor/canvas/pen_editor_screen.dart +++ b/lib/editor/canvas/pen_editor_screen.dart @@ -30,10 +30,16 @@ class _PenEditorScreenState extends State { /// Strokes per page, keyed by 0-based page index (normalized coords). final Map> _strokesByPage = {}; - /// One shared transform for the current page; reset on page change so each - /// page opens fit-to-view. + /// One shared transform for the current page; recentred on page change so + /// each page opens fit-to-view and centered. final TransformationController _transform = TransformationController(); + /// Set when the page must be (re)centered on the next layout pass. + bool _needsCenter = true; + + /// Live page value while dragging the page slider (null when not dragging). + double? _scrub; + // Tool state. CanvasTool _tool = CanvasTool.pen; Color _color = Colors.black; @@ -82,7 +88,13 @@ class _PenEditorScreenState extends State { void _commitStroke(PenStroke stroke) { setState(() { - _strokesByPage.putIfAbsent(_pageIndex, () => []).add(stroke); + // Replace with a NEW list so StaticInkPainter sees a fresh identity and + // actually repaints (mutating in place would alias the old painter's list + // and shouldRepaint would see no change → committed strokes vanish). + _strokesByPage[_pageIndex] = [ + ...?_strokesByPage[_pageIndex], + stroke, + ]; }); } @@ -90,7 +102,8 @@ class _PenEditorScreenState extends State { setState(() { final list = _strokesByPage[_pageIndex]; if (list != null && index >= 0 && index < list.length) { - list.removeAt(index); + final next = List.of(list)..removeAt(index); + _strokesByPage[_pageIndex] = next; } }); } @@ -102,40 +115,15 @@ class _PenEditorScreenState extends State { if (clamped == _pageIndex) return; setState(() { _pageIndex = clamped; - _transform.value = Matrix4.identity(); + _needsCenter = true; // recenter the new page on next layout }); } - Future _promptJumpToPage() async { - final doc = _document; - if (doc == null) return; - final controller = TextEditingController(text: '${_pageIndex + 1}'); - final result = await showDialog( - context: context, - builder: (context) => AlertDialog( - title: const Text('Go to page'), - content: TextField( - controller: controller, - autofocus: true, - keyboardType: TextInputType.number, - decoration: InputDecoration(hintText: '1 – ${doc.pages.length}'), - onSubmitted: (v) => - Navigator.of(context).pop(int.tryParse(v.trim())), - ), - actions: [ - TextButton( - onPressed: () => Navigator.of(context).pop(), - child: const Text('Cancel'), - ), - TextButton( - onPressed: () => - Navigator.of(context).pop(int.tryParse(controller.text.trim())), - child: const Text('Go'), - ), - ], - ), - ); - if (result != null) _goToPage(result - 1); + /// Centre [pageSize] within [viewport] via the shared transform. + void _centerPage(Size viewport, Size pageSize) { + final tx = (viewport.width - pageSize.width) / 2; + final ty = (viewport.height - pageSize.height) / 2; + _transform.value = Matrix4.identity()..setTranslationRaw(tx, ty, 0); } @override @@ -206,8 +194,15 @@ class _PenEditorScreenState extends State { final scale = fit < fitH ? fit : fitH; final pageSize = Size(page.width * scale, page.height * scale); - return Center( - child: PenCanvas( + if (_needsCenter) { + WidgetsBinding.instance.addPostFrameCallback((_) { + if (!mounted) return; + _centerPage( + Size(constraints.maxWidth, constraints.maxHeight), pageSize); + setState(() => _needsCenter = false); + }); + } + return PenCanvas( key: ValueKey(_pageIndex), pageSize: pageSize, strokes: _currentStrokes, @@ -229,8 +224,7 @@ class _PenEditorScreenState extends State { decoration: const BoxDecoration(color: Colors.white), backgroundColor: Colors.white, ), - ), - ); + ); }, ); } @@ -305,42 +299,65 @@ class _PenEditorScreenState extends State { ); } - /// Floating page-control pill: prev / "n / total" / next. + /// Floating page-control pill: prev / drag-slider / next. No keyboard input + /// (Windows on-screen keyboard is unreliable) — the slider scrubs pages. Widget _buildPagePill() { final doc = _document!; final cs = Theme.of(context).colorScheme; + final total = doc.pages.length; + final shown = (_scrub ?? (_pageIndex + 1).toDouble()).round(); return Material( color: cs.surfaceContainerHigh, elevation: 3, borderRadius: BorderRadius.circular(28), - child: Padding( - padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - tooltip: 'Previous page', - icon: const Icon(Icons.chevron_left), - onPressed: _pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null, - ), - TextButton( - onPressed: _promptJumpToPage, - child: Text( - '${_pageIndex + 1} / ${doc.pages.length}', - style: TextStyle( - color: cs.onSurface, - fontWeight: FontWeight.w600, + child: ConstrainedBox( + constraints: const BoxConstraints(maxWidth: 560), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 4, vertical: 2), + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + tooltip: 'Previous page', + icon: const Icon(Icons.chevron_left), + onPressed: + _pageIndex > 0 ? () => _goToPage(_pageIndex - 1) : null, + ), + if (total > 1) + Flexible( + child: Slider( + min: 1, + max: total.toDouble(), + value: (_scrub ?? (_pageIndex + 1).toDouble()) + .clamp(1, total.toDouble()), + label: '$shown', + divisions: total - 1, + onChanged: (v) => setState(() => _scrub = v), + onChangeEnd: (v) { + setState(() => _scrub = null); + _goToPage(v.round() - 1); + }, + ), + ), + Padding( + padding: const EdgeInsets.symmetric(horizontal: 6), + child: Text( + '$shown / $total', + style: TextStyle( + color: cs.onSurface, + fontWeight: FontWeight.w600, + ), ), ), - ), - IconButton( - tooltip: 'Next page', - icon: const Icon(Icons.chevron_right), - onPressed: _pageIndex < doc.pages.length - 1 - ? () => _goToPage(_pageIndex + 1) - : null, - ), - ], + IconButton( + tooltip: 'Next page', + icon: const Icon(Icons.chevron_right), + onPressed: _pageIndex < total - 1 + ? () => _goToPage(_pageIndex + 1) + : null, + ), + ], + ), ), ), ); diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e30f888..f5d0050 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,11 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) dynamic_color_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "DynamicColorPlugin"); + dynamic_color_plugin_register_with_registrar(dynamic_color_registrar); g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); file_selector_plugin_register_with_registrar(file_selector_linux_registrar); diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 6f90a96..de6e0a3 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color file_selector_linux flutter_onnxruntime url_launcher_linux diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index bf51a80..0c64dd7 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -6,6 +6,7 @@ import FlutterMacOS import Foundation import device_info_plus +import dynamic_color import file_picker import file_selector_macos import flutter_onnxruntime @@ -16,6 +17,7 @@ import url_launcher_macos func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { DeviceInfoPlusMacosPlugin.register(with: registry.registrar(forPlugin: "DeviceInfoPlusMacosPlugin")) + DynamicColorPlugin.register(with: registry.registrar(forPlugin: "DynamicColorPlugin")) FilePickerPlugin.register(with: registry.registrar(forPlugin: "FilePickerPlugin")) FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) FlutterOnnxruntimePlugin.register(with: registry.registrar(forPlugin: "FlutterOnnxruntimePlugin")) diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index f75d9c3..a729e8c 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,12 +6,15 @@ #include "generated_plugin_registrant.h" +#include #include #include #include #include void RegisterPlugins(flutter::PluginRegistry* registry) { + DynamicColorPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("DynamicColorPluginCApi")); FileSelectorWindowsRegisterWithRegistrar( registry->GetRegistrarForPlugin("FileSelectorWindows")); FlutterOnnxruntimePluginRegisterWithRegistrar( diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index cf85c27..b71f148 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + dynamic_color file_selector_windows flutter_onnxruntime syncfusion_pdfviewer_windows