Skip to content
Open
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
55 changes: 54 additions & 1 deletion lib/src/view/notifier/scribble_notifier.dart
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,19 @@ abstract class ScribbleNotifierBase extends ValueNotifier<ScribbleState> {
/// access it from the [renderImage] method.
GlobalKey get repaintBoundaryKey;

/// Attaches a [GlobalKey] representing a [RepaintBoundary] used by a
/// [Scribble] widget instance.
///
/// Default implementation is a no-op so external implementations of the
/// notifier don't need to override this method.
void attachRepaintBoundaryKey(GlobalKey key) {}

/// Detaches a previously attached [GlobalKey].
///
/// Default implementation is a no-op so external implementations of the
/// notifier don't need to override this method.
void detachRepaintBoundaryKey(GlobalKey key) {}

/// Should be called when the pointer hovers over the canvas with the
/// corresponding [event].
void onPointerHover(PointerHoverEvent event);
Expand Down Expand Up @@ -130,10 +143,50 @@ class ScribbleNotifier extends ScribbleNotifierBase
/// receive a map.
Sketch get currentSketch => value.sketch;

// Fallback key for backward compatibility when no widget-attached key exists.
final GlobalKey _repaintBoundaryKey = GlobalKey();

// Tracks keys of mounted Scribble widgets using this notifier.
// The last attached key is preferred for image rendering to avoid collisions
// when the same notifier is used by multiple widgets concurrently.
final List<GlobalKey> _attachedRepaintBoundaryKeys = <GlobalKey>[];

@override
GlobalKey get repaintBoundaryKey => _attachedRepaintBoundaryKeys.isNotEmpty
? _attachedRepaintBoundaryKeys.last
: _repaintBoundaryKey;

@override
void attachRepaintBoundaryKey(GlobalKey key) {
// Avoid duplicates while preserving order semantics
if (!_attachedRepaintBoundaryKeys.contains(key)) {
_attachedRepaintBoundaryKeys.add(key);
}
}

@override
GlobalKey get repaintBoundaryKey => _repaintBoundaryKey;
void detachRepaintBoundaryKey(GlobalKey key) {
_attachedRepaintBoundaryKeys.remove(key);
}

@override
Future<ByteData> renderImage({
double pixelRatio = 1.0,
ui.ImageByteFormat format = ui.ImageByteFormat.png,
}) {
assert(() {
if (_attachedRepaintBoundaryKeys.isEmpty) {
debugPrint(
'[scribble] renderImage() is falling back to an internal GlobalKey. '
'If you implement ScribbleNotifierBase yourself, override '
'attachRepaintBoundaryKey/detachRepaintBoundaryKey so Scribble widgets '
'can register their RepaintBoundary. This fallback will be removed in a future release.',
);
}
return true;
}());
return super.renderImage(pixelRatio: pixelRatio, format: format);
}

/// The [SketchSimplifier] that is used to simplify the lines of the sketch.
///
Expand Down
59 changes: 45 additions & 14 deletions lib/src/view/scribble.dart
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import 'package:scribble/src/view/state/scribble.state.dart';
/// You can control its behavior from code using the [notifier] instance you
/// pass in.
/// {@endtemplate}
class Scribble extends StatelessWidget {
class Scribble extends StatefulWidget {
/// {@macro scribble}
const Scribble({
/// The notifier that controls this canvas.
Expand Down Expand Up @@ -46,28 +46,59 @@ class Scribble extends StatelessWidget {
/// {@endtemplate}
final bool simulatePressure;

@override
State<Scribble> createState() => _ScribbleState();
}

class _ScribbleState extends State<Scribble> {
late final GlobalKey _localRepaintBoundaryKey;

@override
void initState() {
super.initState();
_localRepaintBoundaryKey = GlobalKey(
debugLabel: 'scribble_${identityHashCode(this)}',
);
widget.notifier.attachRepaintBoundaryKey(_localRepaintBoundaryKey);
}

@override
void didUpdateWidget(covariant Scribble oldWidget) {
super.didUpdateWidget(oldWidget);
if (!identical(oldWidget.notifier, widget.notifier)) {
oldWidget.notifier.detachRepaintBoundaryKey(_localRepaintBoundaryKey);
widget.notifier.attachRepaintBoundaryKey(_localRepaintBoundaryKey);
}
}

@override
void dispose() {
widget.notifier.detachRepaintBoundaryKey(_localRepaintBoundaryKey);
super.dispose();
}

@override
Widget build(BuildContext context) {
return ValueListenableBuilder<ScribbleState>(
valueListenable: notifier,
valueListenable: widget.notifier,
builder: (context, state, _) {
final drawCurrentTool =
drawPen && state is Drawing || drawEraser && state is Erasing;
widget.drawPen && state is Drawing || widget.drawEraser && state is Erasing;
final child = SizedBox.expand(
child: CustomPaint(
foregroundPainter: ScribbleEditingPainter(
state: state,
drawPointer: drawPen,
drawEraser: drawEraser,
simulatePressure: simulatePressure,
drawPointer: widget.drawPen,
drawEraser: widget.drawEraser,
simulatePressure: widget.simulatePressure,
),
child: RepaintBoundary(
key: notifier.repaintBoundaryKey,
key: _localRepaintBoundaryKey,
child: CustomPaint(
painter: ScribblePainter(
sketch: state.sketch,
scaleFactor: state.scaleFactor,
simulatePressure: simulatePressure,
simulatePressure: widget.simulatePressure,
),
),
),
Expand All @@ -83,13 +114,13 @@ class Scribble extends StatelessWidget {
.contains(PointerDeviceKind.mouse)
? SystemMouseCursors.none
: MouseCursor.defer,
onExit: notifier.onPointerExit,
onExit: widget.notifier.onPointerExit,
child: Listener(
onPointerDown: notifier.onPointerDown,
onPointerMove: notifier.onPointerUpdate,
onPointerUp: notifier.onPointerUp,
onPointerHover: notifier.onPointerHover,
onPointerCancel: notifier.onPointerCancel,
onPointerDown: widget.notifier.onPointerDown,
onPointerMove: widget.notifier.onPointerUpdate,
onPointerUp: widget.notifier.onPointerUp,
onPointerHover: widget.notifier.onPointerHover,
onPointerCancel: widget.notifier.onPointerCancel,
child: child,
),
),
Expand Down
25 changes: 25 additions & 0 deletions test/src/view/scribble_test.dart
Original file line number Diff line number Diff line change
Expand Up @@ -44,5 +44,30 @@ void main() {
},
);
});

testWidgets(
'does not throw when sharing a notifier across multiple instances',
(WidgetTester tester) async {
final notifier = ScribbleNotifier();
addTearDown(notifier.dispose);

await tester.pumpWidget(
MaterialApp(
home: Scaffold(
body: Column(
children: [
Expanded(child: Scribble(notifier: notifier)),
Expanded(child: Scribble(notifier: notifier)),
],
),
),
),
);

await tester.pumpAndSettle();

expect(tester.takeException(), isNull);
},
);
});
}