import 'package:flutter/material.dart'; import 'drawing_canvas.dart'; import 'drawing_controller.dart'; /// A drawing board that overlays a child widget with a drawing canvas. /// /// Supports: /// - Drawing with normalized coordinates that scale correctly /// - Zooming and panning via InteractiveViewer /// - Toggle between view-only and drawing modes class DrawingBoard extends StatefulWidget { /// The widget to display behind the drawing (e.g., PDF page) final Widget child; /// Size of the drawing area (should match child size) final Size boardSize; /// Controller for managing drawing state final DrawingController controller; /// Whether drawing is enabled (false = view only) final bool drawingEnabled; /// Minimum zoom scale final double minScale; /// Maximum zoom scale final double maxScale; /// Alignment of the board within available space final Alignment alignment; const DrawingBoard({ super.key, required this.child, required this.boardSize, required this.controller, this.drawingEnabled = true, this.minScale = 1.0, this.maxScale = 3.0, this.alignment = Alignment.topCenter, }); @override State createState() => _DrawingBoardState(); } class _DrawingBoardState extends State { final TransformationController _transformationController = TransformationController(); /// Tracks whether we're currently in a drawing gesture bool _isDrawing = false; /// Tracks the number of active pointers for gesture disambiguation int _pointerCount = 0; @override void dispose() { _transformationController.dispose(); super.dispose(); } @override Widget build(BuildContext context) { // The content that will be transformed (zoomed/panned) final content = SizedBox( width: widget.boardSize.width, height: widget.boardSize.height, child: Stack( children: [ // Background child (e.g., PDF) Positioned.fill(child: widget.child), // Drawing overlay Positioned.fill( child: widget.drawingEnabled ? _buildDrawingLayer() : DrawingOverlay( controller: widget.controller, canvasSize: widget.boardSize, ), ), ], ), ); if (!widget.drawingEnabled) { // View-only mode: just show the content (no zoom/pan here, // let parent handle navigation) return Align( alignment: widget.alignment, child: content, ); } // Drawing mode: wrap with InteractiveViewer for zoom/pan return Align( alignment: widget.alignment, child: InteractiveViewer( transformationController: _transformationController, minScale: widget.minScale, maxScale: widget.maxScale, boundaryMargin: EdgeInsets.zero, constrained: true, panEnabled: !_isDrawing, scaleEnabled: !_isDrawing, child: content, ), ); } /// Builds the drawing layer with gesture handling. /// /// Uses Listener for drawing and distinguishes between: /// - Single finger: Draw /// - Two+ fingers: Pan/Zoom (handled by InteractiveViewer) Widget _buildDrawingLayer() { return Listener( onPointerDown: _onPointerDown, onPointerMove: _onPointerMove, onPointerUp: _onPointerUp, onPointerCancel: _onPointerCancel, behavior: HitTestBehavior.translucent, child: DrawingOverlay( controller: widget.controller, canvasSize: widget.boardSize, ), ); } void _onPointerDown(PointerDownEvent event) { _pointerCount++; // Only start drawing with single finger touch if (_pointerCount == 1) { _isDrawing = true; final normalized = _toNormalized(event.localPosition); widget.controller.startLine(normalized); setState(() {}); // Update to disable pan/scale } else { // Multiple fingers: cancel drawing, enable pan/zoom if (_isDrawing) { widget.controller.endLine(); _isDrawing = false; setState(() {}); } } } void _onPointerMove(PointerMoveEvent event) { if (_isDrawing && _pointerCount == 1) { final normalized = _toNormalized(event.localPosition); widget.controller.addPoint(normalized); } } void _onPointerUp(PointerUpEvent event) { _pointerCount = (_pointerCount - 1).clamp(0, 10); if (_isDrawing) { widget.controller.endLine(); _isDrawing = false; setState(() {}); // Re-enable pan/scale } } void _onPointerCancel(PointerCancelEvent event) { _pointerCount = (_pointerCount - 1).clamp(0, 10); if (_isDrawing) { widget.controller.endLine(); _isDrawing = false; setState(() {}); } } /// Converts local position to normalized coordinates (0-1). Offset _toNormalized(Offset localPosition) { return Offset( (localPosition.dx / widget.boardSize.width).clamp(0.0, 1.0), (localPosition.dy / widget.boardSize.height).clamp(0.0, 1.0), ); } }