Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
# Changelog

## 13.4.0
- **FEAT**(paint-editor): Navigate while a drawing tool is active via `PaintEditorConfigs.enableZoomWhileDrawing` (opt-in). Pinch, trackpad and mouse wheel zoom; secondary/middle mouse buttons pan.

## 13.3.1
- **FIX**(crop-rotate): Crop handles no longer jump onto the finger when a drag starts; the gesture slop and the distance to the grabbed handle are compensated.
- **FIX**(crop-rotate): Corner drags with a fixed aspect ratio now follow the finger diagonally instead of tracking horizontal movement only, and stay inside the image.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ class PaintEditorConfigs extends ZoomConfigs
this.isInitiallyFilled = false,
this.showLayers = true,
this.enableShareZoomMatrix = true,
this.enableZoomWhileDrawing = false,
this.polygonConnectionThreshold = 20,
this.minStrokeWidth = 1.0,
this.maxStrokeWidth = 40.0,
Expand Down Expand Up @@ -174,6 +175,19 @@ class PaintEditorConfigs extends ZoomConfigs
/// Shares the zoom matrix between the main and paint editor.
final bool enableShareZoomMatrix;

/// Whether pinch, trackpad, pointer-scroll and secondary/middle mouse
/// buttons may navigate while a drawing tool is active.
///
/// Primary click-drag still draws. Secondary and middle mouse buttons pan
/// instead of drawing. If the view moves while a stroke is in progress,
/// that stroke is discarded so path coordinates stay in one transform
/// space. Has no effect when [enableZoom] is `false`.
///
/// Defaults to `false` (viewport stays frozen until
/// [PaintMode.moveAndZoom] is selected). Set to `true` to navigate while
/// drawing.
final bool enableZoomWhileDrawing;

/// Indicates the initial paint mode.
final PaintMode initialPaintMode;

Expand Down Expand Up @@ -313,6 +327,7 @@ class PaintEditorConfigs extends ZoomConfigs
bool? isInitiallyFilled,
bool? showLayers,
bool? enableShareZoomMatrix,
bool? enableZoomWhileDrawing,
PaintMode? initialPaintMode,
EraserMode? eraserMode,
double? eraserSize,
Expand Down Expand Up @@ -363,6 +378,8 @@ class PaintEditorConfigs extends ZoomConfigs
showLayers: showLayers ?? this.showLayers,
enableShareZoomMatrix:
enableShareZoomMatrix ?? this.enableShareZoomMatrix,
enableZoomWhileDrawing:
enableZoomWhileDrawing ?? this.enableZoomWhileDrawing,
initialPaintMode: initialPaintMode ?? this.initialPaintMode,
eraserMode: eraserMode ?? this.eraserMode,
eraserSize: eraserSize ?? this.eraserSize,
Expand Down
95 changes: 77 additions & 18 deletions lib/features/paint_editor/paint_editor.dart
Original file line number Diff line number Diff line change
Expand Up @@ -626,13 +626,73 @@ class PaintEditorState extends State<PaintEditor>
paintCtrl.setMode(mode);
paintEditorCallbacks?.handlePaintModeChanged(mode);
rebuildController.add(null);
interactiveViewer.currentState?.setEnableInteraction(
mode == PaintMode.moveAndZoom,
);
_syncViewerInteraction();
_paintCanvas.currentState?.setState(() {});
setState(() {});
}

bool get _viewerPanEnabled =>
_enableZoom && paintMode == PaintMode.moveAndZoom;

bool get _viewerScaleEnabled {
if (!_enableZoom) return false;
if (paintMode == PaintMode.moveAndZoom) return true;
return paintEditorConfigs.enableZoomWhileDrawing;
}

void _syncViewerInteraction() {
interactiveViewer.currentState?.setPanAndScaleEnabled(
pan: _viewerPanEnabled,
scale: _viewerScaleEnabled,
);
}

void _onPaintViewerMatrixChanged(Matrix4 value) {
_paintCanvas.currentState?.cancelActiveDrawing();
callbacks.paintEditorCallbacks?.onEditorZoomMatrix4Change?.call(value);
}

/// Whether the gesture the viewer is currently reporting can move the view.
bool _viewerGestureNavigates = false;

/// While a drawing tool is active the viewer keeps its gesture detector
/// alive so pinch may still zoom, but it reports every one-finger drag too -
/// and those drags are strokes, not navigation. Only gestures the viewer can
/// act on count: any gesture while panning is enabled, or a real multi-finger
/// pinch.
bool _isNavigationGesture(int pointerCount) =>
_viewerPanEnabled || pointerCount > 1;

void _onPaintViewerInteractionStart(ScaleStartDetails details) {
_viewerGestureNavigates = _isNavigationGesture(details.pointerCount);
if (!_viewerGestureNavigates) return;
callbacks.paintEditorCallbacks?.onEditorZoomScaleStart?.call(details);
setState(() {});
}

void _onPaintViewerInteractionUpdate(ScaleUpdateDetails details) {
if (!_viewerGestureNavigates) {
// A second finger joined a stroke, so the viewer takes the gesture over
// from here. Report the start it never got.
if (!_isNavigationGesture(details.pointerCount)) return;
_onPaintViewerInteractionStart(
ScaleStartDetails(
focalPoint: details.focalPoint,
localFocalPoint: details.localFocalPoint,
pointerCount: details.pointerCount,
),
);
}
callbacks.paintEditorCallbacks?.onEditorZoomScaleUpdate?.call(details);
}

void _onPaintViewerInteractionEnd(ScaleEndDetails details) {
if (!_viewerGestureNavigates) return;
_viewerGestureNavigates = false;
callbacks.paintEditorCallbacks?.onEditorZoomScaleEnd?.call(details);
setState(() {});
}

/// Undoes the last action performed in the paint editor.
void undoAction() {
if (canUndo) {
Expand Down Expand Up @@ -1009,21 +1069,13 @@ class PaintEditorState extends State<PaintEditor>
? initConfigs.initialZoomMatrix
: null,
zoomConfigs: paintEditorConfigs,
enableInteraction: paintMode == PaintMode.moveAndZoom,
onInteractionStart: (details) {
callbacks.paintEditorCallbacks?.onEditorZoomScaleStart?.call(
details,
);
setState(() {});
},
onInteractionUpdate:
callbacks.paintEditorCallbacks?.onEditorZoomScaleUpdate,
onInteractionEnd: (details) {
callbacks.paintEditorCallbacks?.onEditorZoomScaleEnd?.call(details);
setState(() {});
},
onMatrix4Change:
callbacks.paintEditorCallbacks?.onEditorZoomMatrix4Change,
enableInteraction: _viewerPanEnabled || _viewerScaleEnabled,
panEnabled: _viewerPanEnabled,
scaleEnabled: _viewerScaleEnabled,
onInteractionStart: _onPaintViewerInteractionStart,
onInteractionUpdate: _onPaintViewerInteractionUpdate,
onInteractionEnd: _onPaintViewerInteractionEnd,
onMatrix4Change: _onPaintViewerMatrixChanged,
child: Stack(
alignment: Alignment.center,
fit: StackFit.expand,
Expand Down Expand Up @@ -1279,6 +1331,13 @@ class PaintEditorState extends State<PaintEditor>
..add(
FlagProperty('_enableZoom', value: _enableZoom, ifTrue: 'zoom enabled'),
)
..add(
FlagProperty(
'enableZoomWhileDrawing',
value: paintEditorConfigs.enableZoomWhileDrawing,
ifTrue: 'zoom while drawing',
),
)
..add(
FlagProperty(
'hasFakeHeroBytes',
Expand Down
75 changes: 62 additions & 13 deletions lib/features/paint_editor/widgets/paint_canvas.dart
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import 'dart:async';
import 'dart:math';

// Flutter imports:
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart';

import '/core/models/editor_configs/paint_editor/paint_editor_configs.dart';
Expand Down Expand Up @@ -137,6 +138,8 @@ class PaintCanvasState extends State<PaintCanvas> {
/// for the interaction to be considered a tap rather than a drag gesture.
static const double _tapDistanceThreshold = 10.0;

final Set<int> _navigationPointers = <int>{};

bool get _isPartialEraser => widget.eraserMode == EraserMode.partial;
bool get _isFreeStyleMode =>
_paintCtrl.mode == PaintMode.freeStyle ||
Expand All @@ -156,24 +159,64 @@ class PaintCanvasState extends State<PaintCanvas> {
super.dispose();
}

/// Discards the in-progress stroke so a viewport change cannot mix
/// coordinate spaces in a single path.
///
/// Remaining pointers are ignored until they are released, matching the
/// multi-touch pinch behavior.
void cancelActiveDrawing() {
_discardActiveStroke();
if (_activePointerCount > 0) {
_isMultiTouch = true;
}
}

void _discardActiveStroke() {
if (_paintCtrl.busy || _paintCtrl.start != null) {
_paintCtrl
..setInProgress(false)
..reset();
_activePaintStreamCtrl.add(null);
}
}

bool get _auxiliaryMousePansView =>
widget.paintEditorConfigs.enableZoom &&
widget.paintEditorConfigs.enableZoomWhileDrawing;

bool _isAuxiliaryMousePan(PointerEvent event) {
if (!_auxiliaryMousePansView) return false;
if (event.kind != PointerDeviceKind.mouse) return false;
return (event.buttons & kSecondaryMouseButton) != 0 ||
(event.buttons & kMiddleMouseButton) != 0;
}

/// Handles the pointer down event for immediate response to touch/stylus
/// input.
///
/// This uses the low-level [Listener] widget instead of [GestureDetector]
/// to eliminate gesture disambiguation delays, significantly reducing drawing
/// latency on devices like iPad with Apple Pencil.
void _onPointerDown(PointerDownEvent event) {
if (_isAuxiliaryMousePan(event)) {
_navigationPointers.add(event.pointer);
_discardActiveStroke();
if (_activePointerCount > 0) {
_isMultiTouch = true;
}
return;
}
if (_navigationPointers.isNotEmpty) {
_activePointerCount++;
_isMultiTouch = true;
_discardActiveStroke();
return;
}
_activePointerCount++;
if (_activePointerCount > 1) {
// Multi-touch detected - disable drawing to allow pinch-to-zoom
_isMultiTouch = true;
// Cancel any ongoing drawing
if (_paintCtrl.busy) {
_paintCtrl
..setInProgress(false)
..reset();
_activePaintStreamCtrl.add(null);
}
_discardActiveStroke();
return;
}

Expand Down Expand Up @@ -207,6 +250,15 @@ class PaintCanvasState extends State<PaintCanvas> {
/// This provides immediate response to pointer movement without the
/// gesture disambiguation delay that occurs with [GestureDetector].
void _onPointerMove(PointerMoveEvent event) {
if (_navigationPointers.contains(event.pointer)) return;
if (_isAuxiliaryMousePan(event)) {
// A mouse button pressed mid-stroke arrives as a move event, not a new
// pointer. Drop the stroke instead of pausing it, which would otherwise
// resume with a straight jump once the button is released again.
_isMultiTouch = true;
_discardActiveStroke();
return;
}
// Skip if multi-touch gesture is active (pinch-to-zoom)
if (_isMultiTouch || _activePointerCount > 1) return;

Expand Down Expand Up @@ -241,6 +293,7 @@ class PaintCanvasState extends State<PaintCanvas> {

/// Handles the pointer up event to finalize drawing.
void _onPointerUp(PointerUpEvent event) {
if (_navigationPointers.remove(event.pointer)) return;
_activePointerCount = max(0, _activePointerCount - 1);

// If this was part of a multi-touch gesture, reset and return
Expand Down Expand Up @@ -298,6 +351,7 @@ class PaintCanvasState extends State<PaintCanvas> {

/// Handles the pointer cancel event to clean up state.
void _onPointerCancel(PointerCancelEvent event) {
if (_navigationPointers.remove(event.pointer)) return;
_activePointerCount = max(0, _activePointerCount - 1);
_pointerDownPosition = null;

Expand All @@ -306,12 +360,7 @@ class PaintCanvasState extends State<PaintCanvas> {
}

// Reset any ongoing drawing
if (_paintCtrl.busy) {
_paintCtrl
..setInProgress(false)
..reset();
_activePaintStreamCtrl.add(null);
}
_discardActiveStroke();
}

Offset _rotatePoint(Offset point, Offset center, double angle) {
Expand Down
Loading
Loading