commit fafebf83417b68babfa39ec6002fb227ddfcf32a
parent b24e79ba524d30e8c3cc839d8aedc28617b75550
Author: maydayv7 <maydayv7@gmail.com>
Date: Mon, 1 Dec 2025 03:05:07 +0530
Fix magic draw, undo/redo, box resize/rotate for canvas
Diffstat:
3 files changed, 633 insertions(+), 665 deletions(-)
diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -31,6 +31,13 @@ class DrawingPath {
});
}
+// Helper to snapshot the entire canvas state for undo/redo
+class CanvasState {
+ final List<Map<String, dynamic>> elements;
+ final List<DrawingPath> paths;
+ CanvasState(this.elements, this.paths);
+}
+
class CanvasBoardPage extends StatefulWidget {
final String projectId;
final double width;
@@ -49,36 +56,39 @@ class CanvasBoardPage extends StatefulWidget {
}
class _CanvasBoardPageState extends State<CanvasBoardPage> {
- // --- EXISTING STATE ---
+ // --- STATE ---
final ChangeStack _changeStack = ChangeStack();
final ImagePicker _picker = ImagePicker();
+
List<Map<String, dynamic>> elements = [];
+ List<DrawingPath> _paths = [];
+
+ // Snapshots for undo grouping
+ CanvasState? _gestureStartSnapshot;
+
String? selectedId;
- Offset? _dragStartPosition;
late Size _canvasSize;
- // --- NEW TOOL STATE ---
+ // --- TOOLS ---
bool _isMagicDrawActive = false;
bool _isTextToolsActive = false;
- // --- INLINE EDITING STATE ---
+ // --- EDITING ---
bool _isEditingText = false;
final TextEditingController _textEditingController = TextEditingController();
final FocusNode _textFocusNode = FocusNode();
- // Drawing Data
- List<DrawingPath> _paths = [];
+ // --- DRAWING ---
List<DrawingPoint> _currentPoints = [];
Color _selectedColor = const Color(0xFFFF4081);
double _strokeWidth = 10.0;
bool _isEraser = false;
-
- // Key to capture the drawing
final GlobalKey _drawingKey = GlobalKey();
- // --- VIEWPORT CONTROLLER ---
- final TransformationController _transformationController = TransformationController();
- bool _hasInitializedView = false; // To ensure auto-zoom happens only once
+ // --- VIEWPORT ---
+ final TransformationController _transformationController =
+ TransformationController();
+ bool _hasInitializedView = false;
@override
void initState() {
@@ -105,13 +115,47 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
super.dispose();
}
- // --- TEXT FEATURE LOGIC ---
+ // --- UNDO/REDO ---
+
+ // Robust undo function: call this AFTER a change is made, passing the OLD state
+ void _recordChange(CanvasState oldState) {
+ // Capture NEW state
+ final newState = CanvasState(
+ _deepCopyElements(elements),
+ List.from(_paths),
+ );
+
+ _changeStack.add(
+ Change(
+ oldState,
+ () {
+ // REDO
+ setState(() {
+ elements = _deepCopyElements(newState.elements);
+ _paths = List.from(newState.paths);
+ });
+ },
+ (val) {
+ // UNDO
+ setState(() {
+ elements = _deepCopyElements(val.elements);
+ _paths = List.from(val.paths);
+ });
+ },
+ ),
+ );
+ }
+
+ CanvasState _getCurrentState() {
+ return CanvasState(_deepCopyElements(elements), List.from(_paths));
+ }
+
+ // --- ACTIONS ---
void _toggleTextTools() {
setState(() {
_isTextToolsActive = !_isTextToolsActive;
- _isMagicDrawActive = false; // Disable drawing if text is active
- // If closing toolbar, finalize edits and deselect
+ _isMagicDrawActive = false;
if (!_isTextToolsActive) {
_exitEditMode();
selectedId = null;
@@ -120,24 +164,21 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
}
void _addTextElement() {
- final oldList = _deepCopy(elements);
+ final oldState = _getCurrentState();
final id = 'text_${DateTime.now().millisecondsSinceEpoch}';
- // Place roughly in visual center
+ final double defaultFontSize = (_canvasSize.width / 25).clamp(24.0, 96.0);
final initialPos = Offset(
- _canvasSize.width / 2 - 110,
- _canvasSize.height / 2 - 40
+ _canvasSize.width / 2 - 150,
+ _canvasSize.height / 2 - 50,
);
- // Scale font size so it's visible on large posters
- final double defaultFontSize = (_canvasSize.width / 20).clamp(24.0, 96.0);
-
final newElement = {
'id': id,
'type': 'text',
- 'content': 'Tap to edit',
+ 'content': 'Double tap to edit',
'position': initialPos,
- 'size': Size(220, defaultFontSize * 2),
+ 'size': Size(300, defaultFontSize * 2),
'rotation': 0.0,
'style_color': Colors.black.value,
'style_fontSize': defaultFontSize,
@@ -145,18 +186,14 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
setState(() {
elements.add(newElement);
- _bringToFront(id); // Ensure it's on top
- selectedId = id;
- _isTextToolsActive = true;
+ // Bring to front
+ elements.remove(newElement);
+ elements.add(newElement);
+ selectedId = id;
+ _isTextToolsActive = true;
});
- _changeStack.add(Change(
- oldList,
- () => setState(() => elements = _deepCopy(elements)),
- (val) => setState(() => elements = val),
- ));
-
- // Immediately start editing
+ _recordChange(oldState);
_enterEditMode(newElement);
}
@@ -166,11 +203,10 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
_isEditingText = true;
_textEditingController.text = element['content'];
_textEditingController.selection = TextSelection.fromPosition(
- TextPosition(offset: _textEditingController.text.length)
+ TextPosition(offset: _textEditingController.text.length),
);
});
- // Request focus after build
WidgetsBinding.instance.addPostFrameCallback((_) {
if (_textFocusNode.canRequestFocus) {
_textFocusNode.requestFocus();
@@ -182,17 +218,16 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
if (_isEditingText && selectedId != null) {
final index = elements.indexWhere((e) => e['id'] == selectedId);
if (index != -1) {
- // Save changes
- final oldList = _deepCopy(elements);
- setState(() {
- elements[index]['content'] = _textEditingController.text;
- _isEditingText = false;
- });
- _changeStack.add(Change(
- oldList,
- () => setState(() => elements = _deepCopy(elements)),
- (val) => setState(() => elements = val),
- ));
+ if (elements[index]['content'] != _textEditingController.text) {
+ final oldState = _getCurrentState();
+ setState(() {
+ elements[index]['content'] = _textEditingController.text;
+ _isEditingText = false;
+ });
+ _recordChange(oldState);
+ } else {
+ setState(() => _isEditingText = false);
+ }
} else {
setState(() => _isEditingText = false);
}
@@ -204,90 +239,105 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
if (selectedId == null) return;
final index = elements.indexWhere((e) => e['id'] == selectedId);
if (index == -1) return;
+
+ final oldState = _getCurrentState();
setState(() {
elements[index][key] = value;
});
+ _recordChange(oldState);
}
void _deleteSelectedElement() {
if (selectedId == null) return;
- final oldList = _deepCopy(elements);
+ final oldState = _getCurrentState();
setState(() {
elements.removeWhere((e) => e['id'] == selectedId);
selectedId = null;
_isEditingText = false;
});
_textFocusNode.unfocus();
- _changeStack.add(Change(
- oldList,
- () => setState(() => elements = _deepCopy(elements)),
- (val) => setState(() => elements = val)
- ));
+ _recordChange(oldState);
+ }
+
+ void _handleGestureStart() {
+ _gestureStartSnapshot = _getCurrentState();
}
- void _bringToFront(String id) {
+ void _handleElementUpdate(
+ String id,
+ Offset newPos,
+ Size newSize,
+ double newRotation,
+ ) {
final index = elements.indexWhere((e) => e['id'] == id);
- if (index != -1 && index != elements.length - 1) {
- setState(() {
- final item = elements.removeAt(index);
- elements.add(item);
- });
- }
+ if (index == -1) return;
+
+ setState(() {
+ elements[index]['position'] = newPos;
+ elements[index]['size'] = newSize;
+ elements[index]['rotation'] = newRotation;
+ });
}
- Map<String, dynamic>? get _selectedElementData {
- if (selectedId == null) return null;
- try {
- return elements.firstWhere((e) => e['id'] == selectedId);
- } catch (_) {
- return null;
+ void _handleGestureEnd() {
+ if (_gestureStartSnapshot != null) {
+ _recordChange(_gestureStartSnapshot!);
+ _gestureStartSnapshot = null;
}
}
+ // --- BUILD ---
+
+ @override
Widget build(BuildContext context) {
- // Determine overlay state
- final selectedEl = _selectedElementData;
- final bool isTextSelected = selectedEl != null && selectedEl['type'] == 'text';
+ Map<String, dynamic>? selectedEl;
+ try {
+ selectedEl = elements.firstWhere((e) => e['id'] == selectedId);
+ } catch (_) {}
+
+ final bool isTextSelected =
+ selectedEl != null && selectedEl['type'] == 'text';
final bool showTextOverlay = _isTextToolsActive || isTextSelected;
return Scaffold(
- backgroundColor: const Color(0xFFE0E0E0), // Darker BG to see canvas bounds
- appBar: !_isMagicDrawActive ? _buildAppBar() : null,
+ backgroundColor: const Color(0xFFE0E0E0),
+ appBar: _buildAppBar(),
body: LayoutBuilder(
builder: (context, constraints) {
- // --- AUTO-ZOOM LOGIC ---
- // Automatically fit large canvases (like Posters) to screen on load
if (!_hasInitializedView) {
_hasInitializedView = true;
- final double scaleX = (constraints.maxWidth - 40) / _canvasSize.width;
- final double scaleY = (constraints.maxHeight - 40) / _canvasSize.height;
- final double initialScale = math.min(scaleX, scaleY).clamp(0.01, 1.0);
-
- final double transX = (constraints.maxWidth - (_canvasSize.width * initialScale)) / 2;
- final double transY = (constraints.maxHeight - (_canvasSize.height * initialScale)) / 2;
-
- _transformationController.value = Matrix4.identity()
- ..translate(transX, transY)
- ..scale(initialScale);
+ final double scaleX =
+ (constraints.maxWidth - 40) / _canvasSize.width;
+ final double scaleY =
+ (constraints.maxHeight - 40) / _canvasSize.height;
+ final double initialScale = math
+ .min(scaleX, scaleY)
+ .clamp(0.01, 1.0);
+
+ final double transX =
+ (constraints.maxWidth - (_canvasSize.width * initialScale)) / 2;
+ final double transY =
+ (constraints.maxHeight - (_canvasSize.height * initialScale)) /
+ 2;
+
+ _transformationController.value =
+ Matrix4.identity()
+ ..translate(transX, transY)
+ ..scale(initialScale);
}
return Stack(
children: [
- // -------------------------------------------
- // LAYER 1: INTERACTIVE CANVAS + DRAWING LAYER
- // -------------------------------------------
GestureDetector(
- // Tap outside to deselect
onTap: () {
if (!_isMagicDrawActive) {
_exitEditMode();
setState(() => selectedId = null);
}
},
- behavior: HitTestBehavior.translucent, // Catches clicks on empty space
+ behavior: HitTestBehavior.translucent,
child: InteractiveViewer(
transformationController: _transformationController,
- // Disable constrained to allow full size posters (e.g. 3000px height)
constrained: false,
boundaryMargin: const EdgeInsets.all(double.infinity),
minScale: 0.01,
@@ -298,9 +348,8 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
width: _canvasSize.width,
height: _canvasSize.height,
child: Stack(
- alignment: Alignment.center,
children: [
- // THE WHITE BOARD CONTAINER
+ // Background
Container(
width: double.infinity,
height: double.infinity,
@@ -308,42 +357,109 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
color: Colors.white,
boxShadow: [
BoxShadow(
- color: Colors.black.withOpacity(0.1),
- blurRadius: 30,
+ color: Colors.black.withOpacity(0.15),
+ blurRadius: 40,
offset: const Offset(0, 10),
),
],
),
- child: ClipRect(
- child: Stack(
- children: [
- // 1. Existing Images/Elements
- ...elements.map((e) => _buildCanvasElement(e)),
-
- // 2. THE DRAWING LAYER
- if (_isMagicDrawActive)
- RepaintBoundary(
- key: _drawingKey,
- child: GestureDetector(
- onPanStart: _onPanStart,
- onPanUpdate: _onPanUpdate,
- onPanEnd: _onPanEnd,
- child: CustomPaint(
- size: Size.infinite,
- painter: CanvasPainter(
- paths: _paths,
- currentPoints: _currentPoints,
- currentColor:
- _isEraser
- ? Colors.transparent
- : _selectedColor,
- currentWidth: _strokeWidth,
- isEraser: _isEraser,
- ),
- ),
- ),
- ),
- ],
+ ),
+
+ // Elements
+ ...elements.map((e) {
+ final bool isSelected = selectedId == e['id'];
+ return _ManipulatingBox(
+ key: ValueKey(e['id']),
+ id: e['id'],
+ position: e['position'],
+ size: e['size'],
+ rotation: e['rotation'],
+ type: e['type'],
+ content: e['content'],
+ styleData: e,
+ isSelected: isSelected && !_isMagicDrawActive,
+ isEditing:
+ isSelected &&
+ _isEditingText &&
+ e['type'] == 'text',
+ viewScale:
+ _transformationController.value
+ .getMaxScaleOnAxis(),
+ onTap: () {
+ if (!_isMagicDrawActive) {
+ if (_isEditingText && selectedId != e['id'])
+ _exitEditMode();
+ setState(() {
+ selectedId = e['id'];
+ if (e['type'] == 'text')
+ _isTextToolsActive = true;
+ });
+ setState(() {
+ elements.remove(e);
+ elements.add(e);
+ });
+ }
+ },
+ onDoubleTap: () {
+ if (e['type'] == 'text') _enterEditMode(e);
+ },
+ onDragStart: _handleGestureStart,
+ onUpdate: (newPos, newSize, newRot) {
+ _handleElementUpdate(
+ e['id'],
+ newPos,
+ newSize,
+ newRot,
+ );
+ },
+ onDragEnd: (newPos, newSize, newRot) {
+ _handleElementUpdate(
+ e['id'],
+ newPos,
+ newSize,
+ newRot,
+ );
+ _handleGestureEnd();
+ },
+ textController:
+ isSelected ? _textEditingController : null,
+ focusNode: isSelected ? _textFocusNode : null,
+ // Fix: pass the controller to listen for zooms
+ transformationController: _transformationController,
+ );
+ }),
+
+ // Drawing Layer
+ IgnorePointer(
+ ignoring: !_isMagicDrawActive,
+ child: RepaintBoundary(
+ key: _drawingKey,
+ child: GestureDetector(
+ onPanStart: (details) {
+ // Save state before drawing stroke
+ _gestureStartSnapshot = _getCurrentState();
+ },
+ onPanUpdate: _onPanUpdate,
+ onPanEnd: (details) {
+ _onPanEnd(details);
+ if (_gestureStartSnapshot != null) {
+ _recordChange(_gestureStartSnapshot!);
+ _gestureStartSnapshot = null;
+ }
+ },
+ child: CustomPaint(
+ size: Size.infinite,
+ painter: CanvasPainter(
+ paths: _paths,
+ currentPoints: _currentPoints,
+ currentColor:
+ _isEraser
+ ? Colors.transparent
+ : _selectedColor,
+ currentWidth: _strokeWidth,
+ isEraser: _isEraser,
+ ),
+ ),
),
),
),
@@ -353,26 +469,24 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
),
),
- // -------------------------------------------
- // LAYER 2: OVERLAYS (Magic Draw & Text Tools)
- // -------------------------------------------
MagicDrawTools(
isActive: _isMagicDrawActive,
selectedColor: _selectedColor,
strokeWidth: _strokeWidth,
isEraser: _isEraser,
- onClose: _saveAndCloseMagicDraw,
onColorChanged: (c) => setState(() => _selectedColor = c),
onWidthChanged: (w) => setState(() => _strokeWidth = w),
onEraserToggle: (e) => setState(() => _isEraser = e),
),
- // Text Toolbar Overlay
TextToolsOverlay(
isActive: showTextOverlay && !_isMagicDrawActive,
isTextSelected: isTextSelected,
- currentColor: Color(selectedEl?['style_color'] ?? Colors.black.value),
- currentFontSize: (selectedEl?['style_fontSize'] ?? 24.0) as double,
+ currentColor: Color(
+ selectedEl?['style_color'] ?? Colors.black.value,
+ ),
+ currentFontSize:
+ (selectedEl?['style_fontSize'] ?? 24.0) as double,
onClose: () {
_exitEditMode();
setState(() {
@@ -380,32 +494,33 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
selectedId = null;
});
},
- onAddText: _addTextElement,
+ onAddText: _addTextElement,
onDelete: _deleteSelectedElement,
- onColorChanged: (c) => _updateSelectedTextProperty('style_color', c.value),
- onFontSizeChanged: (s) => _updateSelectedTextProperty('style_fontSize', s),
+ onColorChanged:
+ (c) => _updateSelectedTextProperty('style_color', c.value),
+ onFontSizeChanged:
+ (s) => _updateSelectedTextProperty('style_fontSize', s),
),
- // -------------------------------------------
- // LAYER 3: BOTTOM NAVIGATION
- // -------------------------------------------
Positioned(
bottom: 0,
left: 0,
right: 0,
child: CanvasBottomBar(
- activeItem: _isMagicDrawActive ? "Magic Draw" : (_isTextToolsActive ? "Text" : null),
+ activeItem:
+ _isMagicDrawActive
+ ? "Magic Draw"
+ : (_isTextToolsActive ? "Text" : null),
onMagicDraw:
- () =>
- setState(() {
- _isMagicDrawActive = !_isMagicDrawActive;
- _isTextToolsActive = false;
- _exitEditMode();
- }),
+ () => setState(() {
+ _isMagicDrawActive = !_isMagicDrawActive;
+ _isTextToolsActive = false;
+ _exitEditMode();
+ }),
onMedia: _pickImageFromGallery,
onStylesheet: () => _showComingSoon('Stylesheet'),
onTools: () => _showComingSoon('Tools'),
- onText: _toggleTextTools, // UPDATED: Toggles text tools
+ onText: _toggleTextTools,
onSelect: () => _showComingSoon('Select'),
onPlugins: () => _showComingSoon('Plugins'),
),
@@ -417,21 +532,12 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
);
}
- // --- DRAWING LOGIC ---
-
- void _onPanStart(DragStartDetails details) {
- // No logic needed here for now
- }
+ // --- BOILERPLATE HELPERS ---
void _onPanUpdate(DragUpdateDetails details) {
setState(() {
_currentPoints.add(
- DrawingPoint(
- offset:
- details
- .localPosition, // Uses local coordinates relative to White Board
- paint: Paint(),
- ),
+ DrawingPoint(offset: details.localPosition, paint: Paint()),
);
});
}
@@ -452,68 +558,10 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
});
}
- // --- SAVE & CLOSE LOGIC ---
-
Future<void> _saveAndCloseMagicDraw() async {
- // 1. If nothing was drawn, just close
- if (_paths.isEmpty && _currentPoints.isEmpty) {
- setState(() => _isMagicDrawActive = false);
- return;
- }
-
- try {
- // 2. Capture the Drawing Layer as an Image
- RenderRepaintBoundary boundary =
- _drawingKey.currentContext!.findRenderObject()
- as RenderRepaintBoundary;
- ui.Image image = await boundary.toImage(pixelRatio: 3.0); // High res
- ByteData? byteData = await image.toByteData(
- format: ui.ImageByteFormat.png,
- );
- Uint8List pngBytes = byteData!.buffer.asUint8List();
-
- // 3. Save to File
- final directory = await getApplicationDocumentsDirectory();
- final String filePath =
- '${directory.path}/drawing_${DateTime.now().millisecondsSinceEpoch}.png';
- File imgFile = File(filePath);
- await imgFile.writeAsBytes(pngBytes);
-
- // 4. Add as a new Element to the Board
- final oldList = _deepCopy(elements);
- setState(() {
- elements.add({
- 'id': 'drawing_${DateTime.now().millisecondsSinceEpoch}',
- 'type': 'file_image',
- 'content': filePath,
- // Position it over the whole canvas since that's where we drew it
- 'position': const Offset(0, 0),
- 'size': _canvasSize,
- 'rotation': 0.0,
- });
-
- // 5. Clear the drawing paths and close mode
- _paths.clear();
- _currentPoints.clear();
- _isMagicDrawActive = false;
- });
-
- // Add to Undo Stack
- _changeStack.add(
- Change(
- oldList,
- () => setState(() => elements = _deepCopy(elements)),
- (val) => setState(() => elements = val),
- ),
- );
- } catch (e) {
- debugPrint("Error saving drawing: $e");
- setState(() => _isMagicDrawActive = false);
- }
+ setState(() => _isMagicDrawActive = false);
}
- // --- EXISTING LOGIC ---
-
PreferredSizeWidget _buildAppBar() {
return AppBar(
leadingWidth: 140,
@@ -524,8 +572,15 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
icon: SvgPicture.asset(
'assets/icons/arrow-left-s-line.svg',
width: 22,
+ colorFilter: ColorFilter.mode(Colors.black, BlendMode.srcIn),
),
- onPressed: () => Navigator.of(context).maybePop(),
+ onPressed: () {
+ if (_isMagicDrawActive) {
+ _saveAndCloseMagicDraw();
+ } else {
+ Navigator.of(context).maybePop();
+ }
+ },
),
IconButton(
icon: SvgPicture.asset(
@@ -570,7 +625,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
'assets/icons/file-image-line.svg',
width: 22,
),
- onPressed: _openLayers,
+ onPressed: _pickImageFromGallery,
),
IconButton(
icon: SvgPicture.asset(
@@ -594,317 +649,346 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
);
}
- Widget _buildCanvasElement(Map<String, dynamic> e) {
- final isSelected = selectedId == e['id'];
- final position = e['position'] as Offset;
- final size = e['size'] as Size;
- final rotation = (e['rotation'] ?? 0.0) as double;
- final type = e['type'];
- final isEditingThis = isSelected && _isEditingText && type == 'text';
-
- return Positioned(
- left: position.dx,
- top: position.dy,
- child: Transform.rotate(
- angle: rotation,
- child: GestureDetector(
- behavior: HitTestBehavior.translucent, // Ensures taps work on transparent areas
- onTap: () {
- if (!_isMagicDrawActive) {
- if (_isEditingText && selectedId != e['id']) {
- _exitEditMode();
- }
- setState(() {
- selectedId = e['id'];
- if (type == 'text') _isTextToolsActive = true;
- });
- _bringToFront(e['id']);
- }
- },
- onDoubleTap: () {
- if (type == 'text') {
- _enterEditMode(e);
- }
- },
- child: Stack(
- clipBehavior: Clip.none, // Allow handles to be visible outside element bounds
- children: [
- // Main Element Container
- Container(
- width: size.width,
- height: size.height,
- decoration: BoxDecoration(
- border:
- isSelected
- // Fix border width scaling so it looks consistent at zoom levels
- ? Border.all(
- color: Colors.blue,
- width: 2.0 / (_transformationController.value.getMaxScaleOnAxis())
- )
- : type == 'text'
- // Dashed-like grey border for unselected text
- ? Border.all(color: Colors.grey.withOpacity(0.5), width: 1.0)
- : null,
- color: type == 'text' ? Colors.white.withOpacity(0.01) : null,
- ),
- child:
- e['type'] == 'file_image'
- ? Image.file(File(e['content']), fit: BoxFit.contain)
- : e['type'] == 'text'
- ? _buildTextContent(e, isEditingThis)
- : Container(),
- ),
+ Future<void> _pickImageFromGallery() async {
+ final List<XFile> images = await _picker.pickMultiImage();
+ if (images.isNotEmpty) {
+ final oldState = _getCurrentState();
+ setState(() {
+ for (int i = 0; i < images.length; i++) {
+ elements.add({
+ 'id': '${DateTime.now().millisecondsSinceEpoch}_$i',
+ 'type': 'file_image',
+ 'content': images[i].path,
+ 'position': const Offset(50, 50),
+ 'size': const Size(150, 150),
+ 'rotation': 0.0,
+ });
+ }
+ });
+ _recordChange(oldState);
+ }
+ }
- // Transform Controls (only when selected and not editing text)
- if (isSelected && !_isMagicDrawActive && !isEditingThis) ...[
- // Resize Handle - Bottom Right
- Positioned(
- right: -12,
- bottom: -12,
- child: GestureDetector(
- behavior: HitTestBehavior.opaque,
- onPanUpdate: (details) {
- setState(() {
- final newWidth = (size.width + details.delta.dx).clamp(
- 50.0,
- _canvasSize.width,
- );
- final newHeight = (size.height + details.delta.dy)
- .clamp(30.0, _canvasSize.height);
- e['size'] = Size(newWidth, newHeight);
- });
- },
- child: _buildHandle(Icons.zoom_out_map, Colors.blue),
+ List<Map<String, dynamic>> _deepCopyElements(
+ List<Map<String, dynamic>> source,
+ ) {
+ return source.map((e) {
+ final copy = Map<String, dynamic>.from(e);
+ if (e['position'] is Offset)
+ copy['position'] = Offset(
+ (e['position'] as Offset).dx,
+ (e['position'] as Offset).dy,
+ );
+ if (e['size'] is Size)
+ copy['size'] = Size(
+ (e['size'] as Size).width,
+ (e['size'] as Size).height,
+ );
+ return copy;
+ }).toList();
+ }
+
+ void _openLayers() => _showComingSoon('Layers');
+ void _exportProject() => _showComingSoon('Export');
+ void _openSettings() => _showComingSoon('Settings');
+ void _showComingSoon([dynamic feature]) => ScaffoldMessenger.of(
+ context,
+ ).showSnackBar(const SnackBar(content: Text('Coming soon')));
+}
+
+// =============================================================================
+// MANIPULATING BOX WIDGET
+// =============================================================================
+
+class _ManipulatingBox extends StatefulWidget {
+ final String id;
+ final Offset position;
+ final Size size;
+ final double rotation;
+ final String type;
+ final String content;
+ final Map<String, dynamic> styleData;
+ final bool isSelected;
+ final bool isEditing;
+ final double viewScale;
+
+ // Passed controller for real-time zoom updates
+ final TransformationController transformationController;
+
+ final VoidCallback onTap;
+ final VoidCallback onDoubleTap;
+ final VoidCallback onDragStart;
+ final Function(Offset, Size, double) onUpdate;
+ final Function(Offset, Size, double) onDragEnd;
+ final TextEditingController? textController;
+ final FocusNode? focusNode;
+
+ const _ManipulatingBox({
+ Key? key,
+ required this.id,
+ required this.position,
+ required this.size,
+ required this.rotation,
+ required this.type,
+ required this.content,
+ required this.styleData,
+ required this.isSelected,
+ required this.isEditing,
+ required this.viewScale,
+ required this.transformationController, // Receive controller
+ required this.onTap,
+ required this.onDoubleTap,
+ required this.onDragStart,
+ required this.onUpdate,
+ required this.onDragEnd,
+ this.textController,
+ this.focusNode,
+ }) : super(key: key);
+
+ @override
+ State<_ManipulatingBox> createState() => _ManipulatingBoxState();
+}
+
+class _ManipulatingBoxState extends State<_ManipulatingBox> {
+ late Offset _pos;
+ late Size _size;
+ late double _rot;
+
+ @override
+ void initState() {
+ super.initState();
+ _updateInternalState();
+ }
+
+ @override
+ void didUpdateWidget(_ManipulatingBox oldWidget) {
+ super.didUpdateWidget(oldWidget);
+ if (widget.position != oldWidget.position ||
+ widget.size != oldWidget.size ||
+ widget.rotation != oldWidget.rotation) {
+ _updateInternalState();
+ }
+ }
+
+ void _updateInternalState() {
+ _pos = widget.position;
+ _size = widget.size;
+ _rot = widget.rotation;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ // Listen to the transformation controller to get real-time zoom updates
+ return ValueListenableBuilder(
+ valueListenable: widget.transformationController,
+ builder: (context, matrix, child) {
+ final double currentZoom = matrix.getMaxScaleOnAxis();
+ // Calculate inverse scale to keep handles visually constant
+ final double handleScale = (1.0 / currentZoom).clamp(0.1, 5.0);
+ final double touchTargetSize = 40.0 * handleScale;
+ final double visualSize = 24.0 * handleScale;
+
+ return Positioned(
+ left: _pos.dx,
+ top: _pos.dy,
+ child: Transform.rotate(
+ angle: _rot,
+ child: Stack(
+ clipBehavior: Clip.none,
+ children: [
+ GestureDetector(
+ behavior: HitTestBehavior.translucent,
+ onTap: widget.onTap,
+ onDoubleTap: widget.onDoubleTap,
+ onPanStart: (_) => widget.onDragStart(),
+ onPanUpdate: (details) {
+ if (widget.isSelected && !widget.isEditing) {
+ // Use real-time zoom to normalize drag delta
+ final delta = details.delta / currentZoom;
+ final globalDelta = _rotateVector(delta, _rot);
+ setState(() => _pos += globalDelta);
+ widget.onUpdate(_pos, _size, _rot);
+ }
+ },
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: _size.width,
+ height: _size.height,
+ decoration: BoxDecoration(
+ border:
+ widget.isSelected
+ ? Border.all(
+ color: Colors.blue,
+ width: 2.0 * handleScale,
+ )
+ : widget.type == 'text'
+ ? Border.all(
+ color: Colors.grey.withOpacity(0.3),
+ width: 1.0 * handleScale,
+ )
+ : null,
+ ),
+ child:
+ widget.type == 'file_image'
+ ? Image.file(
+ File(widget.content),
+ fit: BoxFit.contain,
+ )
+ : _buildText(),
),
),
- // Rotate Handle - Top Right
- Positioned(
- right: -12,
- top: -12,
- child: GestureDetector(
- behavior: HitTestBehavior.opaque,
- onPanUpdate: (details) {
- setState(() {
- final center = Offset(
- position.dx + size.width / 2,
- position.dy + size.height / 2,
+ if (widget.isSelected && !widget.isEditing) ...[
+ Positioned(
+ right: -visualSize / 2,
+ bottom: -visualSize / 2,
+ child: _buildHandle(
+ touchSize: touchTargetSize,
+ visualSize: visualSize,
+ icon: Icons.zoom_out_map,
+ color: Colors.blue,
+ onDrag: (delta) {
+ final normalizedDelta = delta / currentZoom;
+ final localDelta = _rotateVector(
+ normalizedDelta,
+ -_rot,
);
- final angle =
- (details.globalPosition - center).direction;
- e['rotation'] = angle;
- });
- },
- child: _buildHandle(Icons.rotate_right, Colors.green),
+ setState(() {
+ _size = Size(
+ (_size.width + localDelta.dx).clamp(50.0, 10000.0),
+ (_size.height + localDelta.dy).clamp(30.0, 10000.0),
+ );
+ final offset = Offset(
+ localDelta.dx / 2,
+ localDelta.dy / 2,
+ );
+ _pos += _rotateVector(offset, _rot);
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
),
- ),
- // Move Handle - Full Overlay
- Positioned.fill(
- child: GestureDetector(
- behavior: HitTestBehavior.translucent,
- onPanStart: (details) {
- setState(() {
- _dragStartPosition = position;
- });
- },
- onPanUpdate: (details) {
- setState(() {
- // Apply zoom scaling to drag distance
- final scale = _transformationController.value.getMaxScaleOnAxis();
- final newPosition = position + (details.delta / scale);
- e['position'] = newPosition;
- });
- },
- onPanEnd: (details) {
- if (_dragStartPosition != null &&
- _dragStartPosition != position) {
- _addDragToUndoStack(
- e['id'],
- _dragStartPosition!,
- position,
- );
- }
- _dragStartPosition = null;
- },
- child: Container(color: Colors.transparent),
+ Positioned(
+ right: -visualSize / 2,
+ top: -visualSize / 2,
+ child: _buildHandle(
+ touchSize: touchTargetSize,
+ visualSize: visualSize,
+ icon: Icons.rotate_right,
+ color: Colors.green,
+ onDrag: (delta) {
+ setState(() {
+ _rot += (delta.dx + delta.dy) * 0.005;
+ });
+ widget.onUpdate(_pos, _size, _rot);
+ },
+ ),
),
- ),
+ ],
],
- ],
+ ),
),
- ),
- ),
+ );
+ },
);
}
- Widget _buildTextContent(Map<String, dynamic> e, bool isEditing) {
+ Widget _buildText() {
final style = TextStyle(
- fontSize: (e['style_fontSize'] ?? 24.0) as double,
- color: Color(e['style_color'] ?? Colors.black.value),
+ fontSize: (widget.styleData['style_fontSize'] ?? 24.0) as double,
+ color: Color(widget.styleData['style_color'] ?? Colors.black.value),
fontFamily: 'GeneralSans',
);
- if (isEditing) {
- // Auto-growing TextField
+ if (widget.isEditing) {
return Center(
child: IntrinsicWidth(
child: TextField(
- controller: _textEditingController,
- focusNode: _textFocusNode,
+ controller: widget.textController,
+ focusNode: widget.focusNode,
autofocus: true,
maxLines: null,
textAlign: TextAlign.center,
style: style,
decoration: const InputDecoration(
border: InputBorder.none,
- contentPadding: EdgeInsets.zero,
isDense: true,
+ contentPadding: EdgeInsets.zero,
),
onChanged: (text) {
final span = TextSpan(text: text, style: style);
- final tp = TextPainter(text: span, textDirection: TextDirection.ltr);
- tp.layout(maxWidth: _canvasSize.width);
+ final tp = TextPainter(
+ text: span,
+ textDirection: TextDirection.ltr,
+ );
+ tp.layout(maxWidth: 10000);
setState(() {
- // Resize element to fit text
- e['size'] = Size(tp.width + 40, tp.height + 40);
+ _size = Size(tp.width + 40, tp.height + 40);
});
+ widget.onUpdate(_pos, _size, _rot);
},
),
),
);
- } else {
- return Padding(
- padding: const EdgeInsets.all(8.0),
- child: Center(
- child: Text(
- e['content'],
- textAlign: TextAlign.center,
- style: style,
- ),
- ),
- );
}
- }
-
- Widget _buildHandle(IconData icon, Color color) {
- // Keep handles consistent size regardless of zoom
- final double scale = 1 / _transformationController.value.getMaxScaleOnAxis();
- return Transform.scale(
- scale: scale.clamp(1.0, 5.0),
- child: Container(
- width: 28,
- height: 28,
- decoration: BoxDecoration(
- color: color,
- shape: BoxShape.circle,
- border: Border.all(color: Colors.white, width: 2),
- boxShadow: const [BoxShadow(color: Colors.black26, blurRadius: 4)],
- ),
- child: Icon(
- icon,
- size: 14,
- color: Colors.white,
- ),
+ return Center(
+ child: Padding(
+ padding: const EdgeInsets.all(8.0),
+ child: Text(widget.content, textAlign: TextAlign.center, style: style),
),
);
}
- Future<void> _pickImageFromGallery() async {
- final List<XFile> images = await _picker.pickMultiImage();
- if (images.isNotEmpty) {
- final oldList = _deepCopy(elements);
-
- // Smart grid layout for multiple images
- final double imageSize = 150.0;
- final double padding = 20.0;
- final int columns = ((_canvasSize.width - padding * 2) /
- (imageSize + padding))
- .floor()
- .clamp(1, 100);
-
- setState(() {
- for (int i = 0; i < images.length; i++) {
- final int row = i ~/ columns;
- final int col = i % columns;
-
- final double x = padding + (col * (imageSize + padding));
- final double y = padding + (row * (imageSize + padding));
-
- elements.add({
- 'id': '${DateTime.now().millisecondsSinceEpoch}_$i',
- 'type': 'file_image',
- 'content': images[i].path,
- 'position': Offset(
- x.clamp(padding, _canvasSize.width - imageSize - padding),
- y.clamp(padding, _canvasSize.height - imageSize - padding),
- ),
- 'size': const Size(150, 150),
- 'rotation': 0.0,
- });
- }
- });
-
- _changeStack.add(
- Change(
- oldList,
- () => setState(() => elements = _deepCopy(elements)),
- (val) => setState(() => elements = val),
+ Widget _buildHandle({
+ required double touchSize,
+ required double visualSize,
+ required IconData icon,
+ required Color color,
+ required Function(Offset) onDrag,
+ }) {
+ return GestureDetector(
+ behavior: HitTestBehavior.opaque,
+ onPanStart: (_) => widget.onDragStart(),
+ onPanUpdate: (details) => onDrag(details.delta),
+ onPanEnd: (_) => widget.onDragEnd(_pos, _size, _rot),
+ child: Container(
+ width: touchSize,
+ height: touchSize,
+ alignment: Alignment.center,
+ color: Colors.transparent,
+ child: Container(
+ width: visualSize,
+ height: visualSize,
+ decoration: BoxDecoration(
+ color: color,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.white, width: 2),
+ boxShadow: [BoxShadow(color: Colors.black26, blurRadius: 4)],
+ ),
+ child: Icon(icon, size: visualSize * 0.6, color: Colors.white),
),
- );
- }
- }
-
- void _addDragToUndoStack(String id, Offset oldPos, Offset newPos) {
- final oldList = _deepCopy(elements);
- final oldItem = oldList.firstWhere((x) => x['id'] == id);
- oldItem['position'] = oldPos;
- final newList = _deepCopy(elements);
- _changeStack.add(
- Change(
- oldList,
- () => setState(() => elements = newList),
- (val) => setState(() => elements = val),
),
);
}
- List<Map<String, dynamic>> _deepCopy(List<Map<String, dynamic>> source) {
- return source.map((e) {
- final copy = Map<String, dynamic>.from(e);
- if (e['position'] is Offset) {
- copy['position'] = Offset(
- (e['position'] as Offset).dx,
- (e['position'] as Offset).dy,
- );
- }
- if (e['size'] is Size) {
- copy['size'] = Size(
- (e['size'] as Size).width,
- (e['size'] as Size).height,
- );
- }
- return copy;
- }).toList();
+ Offset _rotateVector(Offset vector, double angle) {
+ final cosA = math.cos(angle);
+ final sinA = math.sin(angle);
+ return Offset(
+ vector.dx * cosA - vector.dy * sinA,
+ vector.dx * sinA + vector.dy * cosA,
+ );
}
-
- void _openLayers() => _showComingSoon('Layers');
- void _exportProject() => _showComingSoon('Export');
- void _openSettings() => _showComingSoon('Settings');
- void _showComingSoon(String feature) => ScaffoldMessenger.of(
- context,
- ).showSnackBar(SnackBar(content: Text('$feature coming soon')));
}
// --- PAINTER ---
-
class CanvasPainter extends CustomPainter {
final List<DrawingPath> paths;
final List<DrawingPoint> currentPoints;
final Color currentColor;
final double currentWidth;
final bool isEraser;
-
CanvasPainter({
required this.paths,
required this.currentPoints,
@@ -912,12 +996,9 @@ class CanvasPainter extends CustomPainter {
required this.currentWidth,
required this.isEraser,
});
-
@override
void paint(Canvas canvas, Size size) {
canvas.saveLayer(Rect.fromLTWH(0, 0, size.width, size.height), Paint());
-
- // Draw committed paths
for (final path in paths) {
final paint =
Paint()
@@ -927,13 +1008,11 @@ class CanvasPainter extends CustomPainter {
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
-
if (path.points.length > 1) {
final Path p = Path();
p.moveTo(path.points.first.offset.dx, path.points.first.offset.dy);
- for (int i = 1; i < path.points.length; i++) {
+ for (int i = 1; i < path.points.length; i++)
p.lineTo(path.points[i].offset.dx, path.points[i].offset.dy);
- }
canvas.drawPath(p, paint);
} else if (path.points.isNotEmpty) {
canvas.drawPoints(ui.PointMode.points, [
@@ -941,8 +1020,6 @@ class CanvasPainter extends CustomPainter {
], paint);
}
}
-
- // Draw current stroke
if (currentPoints.isNotEmpty) {
final paint =
Paint()
@@ -952,12 +1029,10 @@ class CanvasPainter extends CustomPainter {
..strokeCap = StrokeCap.round
..strokeJoin = StrokeJoin.round
..style = PaintingStyle.stroke;
-
final Path p = Path();
p.moveTo(currentPoints.first.offset.dx, currentPoints.first.offset.dy);
- for (int i = 1; i < currentPoints.length; i++) {
+ for (int i = 1; i < currentPoints.length; i++)
p.lineTo(currentPoints[i].offset.dx, currentPoints[i].offset.dy);
- }
canvas.drawPath(p, paint);
}
canvas.restore();
@@ -967,8 +1042,6 @@ class CanvasPainter extends CustomPainter {
bool shouldRepaint(covariant CanvasPainter oldDelegate) => true;
}
-// --- BOTTOM BAR ---
-
class CanvasBottomBar extends StatelessWidget {
final String? activeItem;
final VoidCallback onMagicDraw;
@@ -978,7 +1051,6 @@ class CanvasBottomBar extends StatelessWidget {
final VoidCallback onText;
final VoidCallback onSelect;
final VoidCallback onPlugins;
-
const CanvasBottomBar({
super.key,
this.activeItem,
@@ -990,7 +1062,6 @@ class CanvasBottomBar extends StatelessWidget {
required this.onSelect,
required this.onPlugins,
});
-
@override
Widget build(BuildContext context) {
return Container(
@@ -1071,14 +1142,12 @@ class _BottomBarItem extends StatelessWidget {
final String iconPath;
final VoidCallback onTap;
final bool isActive;
-
const _BottomBarItem({
required this.label,
required this.iconPath,
required this.onTap,
this.isActive = false,
});
-
@override
Widget build(BuildContext context) {
return InkWell(
diff --git a/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart b/lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart
@@ -1,13 +1,11 @@
import 'package:flutter/material.dart';
import 'package:flutter_colorpicker/flutter_colorpicker.dart';
-// --- MAIN WIDGET: TOOLS OVERLAY ---
class MagicDrawTools extends StatefulWidget {
final bool isActive;
final Color selectedColor;
final double strokeWidth;
final bool isEraser;
- final VoidCallback onClose;
final Function(Color) onColorChanged;
final Function(double) onWidthChanged;
final Function(bool) onEraserToggle;
@@ -18,7 +16,6 @@ class MagicDrawTools extends StatefulWidget {
required this.selectedColor,
required this.strokeWidth,
required this.isEraser,
- required this.onClose,
required this.onColorChanged,
required this.onWidthChanged,
required this.onEraserToggle,
@@ -35,44 +32,20 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
Widget build(BuildContext context) {
if (!widget.isActive) return const SizedBox.shrink();
- return Stack(
- children: [
- // 1. Close Button (Top Left - Safe Area)
- Positioned(
- top: 0,
- left: 16,
- child: SafeArea(
- child: Padding(
- padding: const EdgeInsets.only(top: 10.0),
- child: Material(
- type: MaterialType.circle,
- color: Colors.white,
- elevation: 4,
- child: IconButton(
- icon: const Icon(Icons.close, color: Colors.black),
- onPressed: widget.onClose,
- ),
- ),
- ),
- ),
- ),
-
- // 2. Floating Tools UI (Bottom)
- // MOVED UP: Changed bottom from 100 to 140
- Positioned(
- bottom: 140,
- left: 16,
- right: 16,
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- if (_showStrokeSlider) _buildTaperedStrokeSlider(),
- const SizedBox(height: 8),
- _buildMagicDrawPanel(),
- ],
- ),
- ),
- ],
+ // Only displaying the bottom tool panel now.
+ // The top header is handled by the main Scaffold AppBar.
+ return Positioned(
+ bottom: 140,
+ left: 16,
+ right: 16,
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ if (_showStrokeSlider) _buildTaperedStrokeSlider(),
+ const SizedBox(height: 8),
+ _buildMagicDrawPanel(),
+ ],
+ ),
);
}
@@ -265,7 +238,6 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
);
}
- // --- NEW ADVANCED COLOR PICKER ---
void _showAdvancedColorPicker(BuildContext context) {
showModalBottomSheet(
context: context,
@@ -281,7 +253,6 @@ class _MagicDrawToolsState extends State<MagicDrawTools> {
}
}
-// --- PAINTER FOR SLIDER ---
class _TaperedSliderPainter extends CustomPainter {
@override
void paint(Canvas canvas, Size size) {
@@ -302,10 +273,6 @@ class _TaperedSliderPainter extends CustomPainter {
bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
}
-// ==========================================
-// ADVANCED COLOR PICKER WIDGET
-// ==========================================
-
class _AdvancedColorPickerSheet extends StatefulWidget {
final Color initialColor;
final ValueChanged<Color> onColorChanged;
@@ -325,12 +292,11 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
late TabController _tabController;
late Color _currentColor;
- // Dummy Brand Palette (Simulating StyleSheet Fetch)
final List<Color> _brandPalette = [
Colors.blue,
- const Color(0xFFCCFF00), // Neon Lime
+ const Color(0xFFCCFF00),
Colors.purpleAccent,
- const Color(0xFFF0F0F0), // Off White
+ const Color(0xFFF0F0F0),
Colors.black,
];
@@ -371,7 +337,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
child: SafeArea(
child: Column(
children: [
- // --- HEADER ---
const SizedBox(height: 8),
Container(
width: 40,
@@ -382,8 +347,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
),
),
const SizedBox(height: 16),
-
- // --- TABS ---
TabBar(
controller: _tabController,
labelColor: Colors.black,
@@ -396,70 +359,11 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
Tab(text: 'Sliders'),
],
),
-
- const SizedBox(height: 16),
-
- // --- HEX Preview ---
- Padding(
- padding: const EdgeInsets.symmetric(horizontal: 20),
- child: Row(
- children: [
- Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 8,
- vertical: 4,
- ),
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey.shade300),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Row(
- children: const [
- Text("Hex", style: TextStyle(fontSize: 12)),
- Icon(Icons.arrow_drop_down, size: 16),
- ],
- ),
- ),
- const SizedBox(width: 8),
- Container(
- width: 30,
- height: 30,
- decoration: BoxDecoration(
- color: _currentColor,
- borderRadius: BorderRadius.circular(6),
- ),
- ),
- const SizedBox(width: 8),
- Expanded(
- child: Container(
- padding: const EdgeInsets.symmetric(
- horizontal: 10,
- vertical: 8,
- ),
- decoration: BoxDecoration(
- border: Border.all(color: Colors.grey.shade300),
- borderRadius: BorderRadius.circular(6),
- ),
- child: Text(
- "#${_currentColor.value.toRadixString(16).toUpperCase().substring(2)}",
- style: const TextStyle(fontWeight: FontWeight.w500),
- ),
- ),
- ),
- const SizedBox(width: 8),
- const Icon(Icons.edit, size: 18, color: Colors.grey),
- ],
- ),
- ),
-
const SizedBox(height: 16),
-
- // --- TAB VIEWS ---
Expanded(
child: TabBarView(
controller: _tabController,
- physics:
- const NeverScrollableScrollPhysics(), // Prevent swipe conflict
+ physics: const NeverScrollableScrollPhysics(),
children: [
_buildGridTab(),
_buildSpectrumTab(),
@@ -467,8 +371,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
],
),
),
-
- // --- SHARED FOOTER (Recently Used, Brand, Gradients) ---
_buildSharedFooter(),
],
),
@@ -476,7 +378,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
);
}
- // --- TAB 1: GRID ---
Widget _buildGridTab() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -494,7 +395,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
),
itemCount: 100,
itemBuilder: (context, index) {
- // Generate a varied color grid
final double hue = (index % 10) * 36.0;
final double saturation = ((index ~/ 10) + 1) / 10.0;
final color =
@@ -516,7 +416,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
);
}
- // --- TAB 2: SPECTRUM ---
Widget _buildSpectrumTab() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -529,10 +428,9 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
enableAlpha: false,
displayThumbColor: true,
paletteType: PaletteType.hsvWithHue,
- labelTypes: const [], // Hide default text inputs
+ labelTypes: const [],
pickerAreaHeightPercent: 0.8,
pickerAreaBorderRadius: BorderRadius.circular(12),
- // We hide standard sliders to use our custom ones to match design
),
),
],
@@ -540,7 +438,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
);
}
- // --- TAB 3: SLIDERS ---
Widget _buildSlidersTab() {
return Padding(
padding: const EdgeInsets.symmetric(horizontal: 20),
@@ -579,8 +476,7 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
trackHeight: 36,
activeTrackColor: activeColor,
inactiveTrackColor: activeColor.withOpacity(0.2),
- thumbColor:
- Colors.transparent, // Hide knob, make it look like a bar
+ thumbColor: Colors.transparent,
thumbShape: const RoundSliderThumbShape(
enabledThumbRadius: 0,
),
@@ -618,8 +514,6 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
);
}
- // --- SHARED COMPONENTS ---
-
Widget _buildHueSlider() {
return SizedBox(
height: 15,
@@ -641,7 +535,7 @@ class _AdvancedColorPickerSheetState extends State<_AdvancedColorPickerSheet>
displayThumbColor: true,
paletteType: PaletteType.hsv,
labelTypes: const [],
- pickerAreaHeightPercent: 0.0, // Only sliders
+ pickerAreaHeightPercent: 0.0,
),
),
);
diff --git a/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart b/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart
@@ -30,11 +30,12 @@ class TextToolsOverlay extends StatelessWidget {
if (!isActive) return const SizedBox.shrink();
return Positioned(
- bottom: 120, // Positioned above the bottom bar
- left: 20,
- right: 20,
+ bottom: 100,
+ left: 0,
+ right: 0,
child: Center(
child: Container(
+ margin: const EdgeInsets.symmetric(horizontal: 20),
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
decoration: BoxDecoration(
color: Colors.white,
@@ -42,61 +43,61 @@ class TextToolsOverlay extends StatelessWidget {
boxShadow: [
BoxShadow(
color: Colors.black.withOpacity(0.15),
- blurRadius: 10,
- offset: const Offset(0, 4),
+ blurRadius: 12,
+ offset: const Offset(0, 6),
),
],
),
child: Row(
mainAxisSize: MainAxisSize.min,
children: [
- // --- Close / Done ---
IconButton(
- icon: const Icon(Icons.check, color: Colors.green),
+ icon: const Icon(Icons.check_circle, color: Colors.black87),
onPressed: onClose,
tooltip: "Done",
),
-
Container(width: 1, height: 20, color: Colors.grey[300]),
const SizedBox(width: 8),
- // --- Add Text Button ---
IconButton(
- icon: const Icon(Icons.add_circle_outline, color: Colors.black87),
+ icon: const Icon(
+ Icons.add_circle_outline,
+ color: Colors.black87,
+ ),
onPressed: onAddText,
tooltip: "Add Text",
),
- // --- Edit Tools (Only if text is selected) ---
if (isTextSelected) ...[
- Container(width: 1, height: 20, color: Colors.grey[300]),
const SizedBox(width: 8),
+ Container(width: 1, height: 20, color: Colors.grey[300]),
+ const SizedBox(width: 12),
- // Font Size
- const Icon(Icons.text_fields, size: 16, color: Colors.grey),
+ const Icon(Icons.text_fields, size: 18, color: Colors.black54),
SizedBox(
width: 100,
child: SliderTheme(
data: SliderThemeData(
- trackHeight: 3,
- thumbShape: const RoundSliderThumbShape(enabledThumbRadius: 6),
+ trackHeight: 2,
+ thumbShape: const RoundSliderThumbShape(
+ enabledThumbRadius: 6,
+ ),
activeTrackColor: Colors.black87,
inactiveTrackColor: Colors.grey[200],
thumbColor: Colors.black,
overlayShape: SliderComponentShape.noOverlay,
),
child: Slider(
- value: currentFontSize.clamp(10.0, 100.0),
+ value: currentFontSize.clamp(10.0, 200.0),
min: 10.0,
- max: 100.0,
+ max: 200.0,
onChanged: onFontSizeChanged,
),
),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 12),
- // Color Picker
GestureDetector(
onTap: () => _showColorPicker(context),
child: Container(
@@ -110,13 +111,12 @@ class TextToolsOverlay extends StatelessWidget {
),
),
- const SizedBox(width: 8),
+ const SizedBox(width: 12),
Container(width: 1, height: 20, color: Colors.grey[300]),
const SizedBox(width: 8),
- // Delete
IconButton(
- icon: const Icon(Icons.delete_outline, color: Colors.red, size: 20),
+ icon: const Icon(Icons.delete_outline, color: Colors.red),
onPressed: onDelete,
tooltip: "Delete",
),
@@ -132,37 +132,42 @@ class TextToolsOverlay extends StatelessWidget {
showModalBottomSheet(
context: context,
backgroundColor: Colors.transparent,
- builder: (ctx) => Container(
- padding: const EdgeInsets.all(24),
- decoration: const BoxDecoration(
- color: Colors.white,
- borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
- ),
- child: Column(
- mainAxisSize: MainAxisSize.min,
- children: [
- const Text("Text Color", style: TextStyle(fontWeight: FontWeight.bold)),
- const SizedBox(height: 20),
- BlockPicker(
- pickerColor: currentColor,
- onColorChanged: (c) {
- onColorChanged(c);
- Navigator.pop(ctx);
- },
- layoutBuilder: (context, colors, child) => SizedBox(
- width: 300,
- height: 160,
- child: GridView.count(
- crossAxisCount: 5,
- crossAxisSpacing: 10,
- mainAxisSpacing: 10,
- children: [for (Color color in colors) child(color)],
+ builder:
+ (ctx) => Container(
+ padding: const EdgeInsets.all(24),
+ decoration: const BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
+ ),
+ child: Column(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ const Text(
+ "Text Color",
+ style: TextStyle(fontWeight: FontWeight.bold),
+ ),
+ const SizedBox(height: 20),
+ BlockPicker(
+ pickerColor: currentColor,
+ onColorChanged: (c) {
+ onColorChanged(c);
+ Navigator.pop(ctx);
+ },
+ layoutBuilder:
+ (context, colors, child) => SizedBox(
+ width: 300,
+ height: 160,
+ child: GridView.count(
+ crossAxisCount: 5,
+ crossAxisSpacing: 10,
+ mainAxisSpacing: 10,
+ children: [for (Color color in colors) child(color)],
+ ),
+ ),
),
- ),
+ ],
),
- ],
- ),
- ),
+ ),
);
}
}