commit b24e79ba524d30e8c3cc839d8aedc28617b75550
parent 1e16c94c95d703ee0e32af21f22c4a1d119ee289
Author: maydayv7 <maydayv7@gmail.com>
Date: Mon, 1 Dec 2025 01:48:26 +0530
Add canvas text feature
Also try to fix canvas sizing
Diffstat:
2 files changed, 608 insertions(+), 156 deletions(-)
diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart
@@ -1,4 +1,5 @@
import 'dart:io';
+import 'dart:math' as math;
import 'dart:ui' as ui;
import 'package:flutter/foundation.dart';
import 'package:flutter/material.dart';
@@ -8,6 +9,7 @@ import 'package:image_picker/image_picker.dart';
import 'package:path_provider/path_provider.dart';
import 'package:undo/undo.dart';
import './canvas_toolbar/magic_draw_overlay.dart';
+import './canvas_toolbar/text_tools_overlay.dart';
// --- MODELS ---
class DrawingPoint {
@@ -55,8 +57,14 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
Offset? _dragStartPosition;
late Size _canvasSize;
- // --- NEW MAGIC DRAW STATE ---
+ // --- NEW TOOL STATE ---
bool _isMagicDrawActive = false;
+ bool _isTextToolsActive = false;
+
+ // --- INLINE EDITING STATE ---
+ bool _isEditingText = false;
+ final TextEditingController _textEditingController = TextEditingController();
+ final FocusNode _textFocusNode = FocusNode();
// Drawing Data
List<DrawingPath> _paths = [];
@@ -68,6 +76,10 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
// 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
+
@override
void initState() {
super.initState();
@@ -85,121 +97,322 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
}
}
+ @override
+ void dispose() {
+ _textEditingController.dispose();
+ _textFocusNode.dispose();
+ _transformationController.dispose();
+ super.dispose();
+ }
+
+ // --- TEXT FEATURE LOGIC ---
+
+ void _toggleTextTools() {
+ setState(() {
+ _isTextToolsActive = !_isTextToolsActive;
+ _isMagicDrawActive = false; // Disable drawing if text is active
+ // If closing toolbar, finalize edits and deselect
+ if (!_isTextToolsActive) {
+ _exitEditMode();
+ selectedId = null;
+ }
+ });
+ }
+
+ void _addTextElement() {
+ final oldList = _deepCopy(elements);
+ final id = 'text_${DateTime.now().millisecondsSinceEpoch}';
+
+ // Place roughly in visual center
+ final initialPos = Offset(
+ _canvasSize.width / 2 - 110,
+ _canvasSize.height / 2 - 40
+ );
+
+ // 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',
+ 'position': initialPos,
+ 'size': Size(220, defaultFontSize * 2),
+ 'rotation': 0.0,
+ 'style_color': Colors.black.value,
+ 'style_fontSize': defaultFontSize,
+ };
+
+ setState(() {
+ elements.add(newElement);
+ _bringToFront(id); // Ensure it's on top
+ selectedId = id;
+ _isTextToolsActive = true;
+ });
+
+ _changeStack.add(Change(
+ oldList,
+ () => setState(() => elements = _deepCopy(elements)),
+ (val) => setState(() => elements = val),
+ ));
+
+ // Immediately start editing
+ _enterEditMode(newElement);
+ }
+
+ void _enterEditMode(Map<String, dynamic> element) {
+ setState(() {
+ selectedId = element['id'];
+ _isEditingText = true;
+ _textEditingController.text = element['content'];
+ _textEditingController.selection = TextSelection.fromPosition(
+ TextPosition(offset: _textEditingController.text.length)
+ );
+ });
+
+ // Request focus after build
+ WidgetsBinding.instance.addPostFrameCallback((_) {
+ if (_textFocusNode.canRequestFocus) {
+ _textFocusNode.requestFocus();
+ }
+ });
+ }
+
+ void _exitEditMode() {
+ 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),
+ ));
+ } else {
+ setState(() => _isEditingText = false);
+ }
+ _textFocusNode.unfocus();
+ }
+ }
+
+ void _updateSelectedTextProperty(String key, dynamic value) {
+ if (selectedId == null) return;
+ final index = elements.indexWhere((e) => e['id'] == selectedId);
+ if (index == -1) return;
+ setState(() {
+ elements[index][key] = value;
+ });
+ }
+
+ void _deleteSelectedElement() {
+ if (selectedId == null) return;
+ final oldList = _deepCopy(elements);
+ 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)
+ ));
+ }
+
+ void _bringToFront(String id) {
+ final index = elements.indexWhere((e) => e['id'] == id);
+ if (index != -1 && index != elements.length - 1) {
+ setState(() {
+ final item = elements.removeAt(index);
+ elements.add(item);
+ });
+ }
+ }
+
+ Map<String, dynamic>? get _selectedElementData {
+ if (selectedId == null) return null;
+ try {
+ return elements.firstWhere((e) => e['id'] == selectedId);
+ } catch (_) {
+ return null;
+ }
+ }
+
Widget build(BuildContext context) {
+ // Determine overlay state
+ final selectedEl = _selectedElementData;
+ final bool isTextSelected = selectedEl != null && selectedEl['type'] == 'text';
+ final bool showTextOverlay = _isTextToolsActive || isTextSelected;
+
return Scaffold(
- backgroundColor: const Color(0xFFF5F5F5),
+ backgroundColor: const Color(0xFFE0E0E0), // Darker BG to see canvas bounds
appBar: !_isMagicDrawActive ? _buildAppBar() : null,
- body: Stack(
- children: [
- // -------------------------------------------
- // LAYER 1: INTERACTIVE CANVAS + DRAWING LAYER
- // -------------------------------------------
- InteractiveViewer(
- // Disable panning/zooming when drawing
- scaleEnabled: !_isMagicDrawActive,
- panEnabled: !_isMagicDrawActive,
- boundaryMargin: const EdgeInsets.all(double.infinity),
- minScale: 0.1,
- maxScale: 5.0,
- transformationController: TransformationController(
- Matrix4.identity()
- ..scale(0.85)
- ..translate(40.0, 40.0),
- ),
- child: SizedBox(
- width: 3000,
- height: 3000,
- child: Stack(
- alignment: Alignment.center,
- children: [
- // THE WHITE BOARD CONTAINER
- Container(
+ 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);
+ }
+
+ 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
+ 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,
+ maxScale: 10.0,
+ scaleEnabled: !_isMagicDrawActive,
+ panEnabled: !_isMagicDrawActive,
+ child: SizedBox(
width: _canvasSize.width,
height: _canvasSize.height,
- decoration: BoxDecoration(
- color: Colors.white,
- boxShadow: [
- BoxShadow(
- color: Colors.black.withOpacity(0.08),
- blurRadius: 24,
- offset: const Offset(0, 4),
- ),
- ],
- ),
- child: ClipRect(
- child: Stack(
- children: [
- // 1. Existing Images/Elements
- ...elements.map((e) => _buildCanvasElement(e)),
-
- // 2. THE DRAWING LAYER (Only visible when active)
- // Positioned exactly on top, clipped to the board
- 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,
- ),
- ),
+ child: Stack(
+ alignment: Alignment.center,
+ children: [
+ // THE WHITE BOARD CONTAINER
+ Container(
+ width: double.infinity,
+ height: double.infinity,
+ decoration: BoxDecoration(
+ color: Colors.white,
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.1),
+ blurRadius: 30,
+ 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,
+ ),
+ ),
+ ),
+ ),
+ ],
),
- ],
- ),
+ ),
+ ),
+ ],
),
),
- ],
+ ),
),
- ),
- ),
- // -------------------------------------------
- // LAYER 2: MAGIC DRAW TOOLS (Floating UI)
- // -------------------------------------------
- MagicDrawTools(
- isActive: _isMagicDrawActive,
- selectedColor: _selectedColor,
- strokeWidth: _strokeWidth,
- isEraser: _isEraser,
- onClose: _saveAndCloseMagicDraw, // This saves the drawing!
- onColorChanged: (c) => setState(() => _selectedColor = c),
- onWidthChanged: (w) => setState(() => _strokeWidth = w),
- onEraserToggle: (e) => setState(() => _isEraser = e),
- ),
+ // -------------------------------------------
+ // 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),
+ ),
- // -------------------------------------------
- // LAYER 3: BOTTOM NAVIGATION
- // -------------------------------------------
- Positioned(
- bottom: 0,
- left: 0,
- right: 0,
- child: CanvasBottomBar(
- activeItem: _isMagicDrawActive ? "Magic Draw" : null,
- onMagicDraw:
- () =>
- setState(() => _isMagicDrawActive = !_isMagicDrawActive),
- onMedia: _pickImageFromGallery,
- onStylesheet: () => _showComingSoon('Stylesheet'),
- onTools: () => _showComingSoon('Tools'),
- onText: () => _showComingSoon('Text'),
- onSelect: () => _showComingSoon('Select'),
- onPlugins: () => _showComingSoon('Plugins'),
- ),
- ),
- ],
+ // 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,
+ onClose: () {
+ _exitEditMode();
+ setState(() {
+ _isTextToolsActive = false;
+ selectedId = null;
+ });
+ },
+ onAddText: _addTextElement,
+ onDelete: _deleteSelectedElement,
+ 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),
+ onMagicDraw:
+ () =>
+ setState(() {
+ _isMagicDrawActive = !_isMagicDrawActive;
+ _isTextToolsActive = false;
+ _exitEditMode();
+ }),
+ onMedia: _pickImageFromGallery,
+ onStylesheet: () => _showComingSoon('Stylesheet'),
+ onTools: () => _showComingSoon('Tools'),
+ onText: _toggleTextTools, // UPDATED: Toggles text tools
+ onSelect: () => _showComingSoon('Select'),
+ onPlugins: () => _showComingSoon('Plugins'),
+ ),
+ ),
+ ],
+ );
+ },
),
);
}
@@ -386,6 +599,8 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
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,
@@ -393,34 +608,61 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
child: Transform.rotate(
angle: rotation,
child: GestureDetector(
+ behavior: HitTestBehavior.translucent, // Ensures taps work on transparent areas
onTap: () {
- if (!_isMagicDrawActive) setState(() => selectedId = e['id']);
+ 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 Image Container
+ // Main Element Container
Container(
width: size.width,
height: size.height,
decoration: BoxDecoration(
border:
isSelected
- ? Border.all(color: Colors.blue, width: 2)
- : null,
+ // 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)
- : Container(),
+ : e['type'] == 'text'
+ ? _buildTextContent(e, isEditingThis)
+ : Container(),
),
- // Transform Controls (only when selected)
- if (isSelected && !_isMagicDrawActive) ...[
+ // Transform Controls (only when selected and not editing text)
+ if (isSelected && !_isMagicDrawActive && !isEditingThis) ...[
// Resize Handle - Bottom Right
Positioned(
- right: -8,
- bottom: -8,
+ right: -12,
+ bottom: -12,
child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
setState(() {
final newWidth = (size.width + details.delta.dx).clamp(
@@ -428,32 +670,20 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
_canvasSize.width,
);
final newHeight = (size.height + details.delta.dy)
- .clamp(50.0, _canvasSize.height);
+ .clamp(30.0, _canvasSize.height);
e['size'] = Size(newWidth, newHeight);
});
},
- child: Container(
- width: 24,
- height: 24,
- decoration: BoxDecoration(
- color: Colors.blue,
- shape: BoxShape.circle,
- border: Border.all(color: Colors.white, width: 2),
- ),
- child: const Icon(
- Icons.zoom_out_map,
- size: 12,
- color: Colors.white,
- ),
- ),
+ child: _buildHandle(Icons.zoom_out_map, Colors.blue),
),
),
// Rotate Handle - Top Right
Positioned(
- right: -8,
- top: -8,
+ right: -12,
+ top: -12,
child: GestureDetector(
+ behavior: HitTestBehavior.opaque,
onPanUpdate: (details) {
setState(() {
final center = Offset(
@@ -465,26 +695,14 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
e['rotation'] = angle;
});
},
- child: Container(
- width: 24,
- height: 24,
- decoration: BoxDecoration(
- color: Colors.green,
- shape: BoxShape.circle,
- border: Border.all(color: Colors.white, width: 2),
- ),
- child: const Icon(
- Icons.rotate_right,
- size: 12,
- color: Colors.white,
- ),
- ),
+ child: _buildHandle(Icons.rotate_right, Colors.green),
),
),
- // Move Handle - Center
+ // Move Handle - Full Overlay
Positioned.fill(
child: GestureDetector(
+ behavior: HitTestBehavior.translucent,
onPanStart: (details) {
setState(() {
_dragStartPosition = position;
@@ -492,17 +710,10 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
},
onPanUpdate: (details) {
setState(() {
- final newPosition = position + details.delta;
- e['position'] = Offset(
- newPosition.dx.clamp(
- 0,
- _canvasSize.width - size.width,
- ),
- newPosition.dy.clamp(
- 0,
- _canvasSize.height - size.height,
- ),
- );
+ // Apply zoom scaling to drag distance
+ final scale = _transformationController.value.getMaxScaleOnAxis();
+ final newPosition = position + (details.delta / scale);
+ e['position'] = newPosition;
});
},
onPanEnd: (details) {
@@ -527,6 +738,78 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> {
);
}
+ Widget _buildTextContent(Map<String, dynamic> e, bool isEditing) {
+ final style = TextStyle(
+ fontSize: (e['style_fontSize'] ?? 24.0) as double,
+ color: Color(e['style_color'] ?? Colors.black.value),
+ fontFamily: 'GeneralSans',
+ );
+
+ if (isEditing) {
+ // Auto-growing TextField
+ return Center(
+ child: IntrinsicWidth(
+ child: TextField(
+ controller: _textEditingController,
+ focusNode: _textFocusNode,
+ autofocus: true,
+ maxLines: null,
+ textAlign: TextAlign.center,
+ style: style,
+ decoration: const InputDecoration(
+ border: InputBorder.none,
+ contentPadding: EdgeInsets.zero,
+ isDense: true,
+ ),
+ onChanged: (text) {
+ final span = TextSpan(text: text, style: style);
+ final tp = TextPainter(text: span, textDirection: TextDirection.ltr);
+ tp.layout(maxWidth: _canvasSize.width);
+ setState(() {
+ // Resize element to fit text
+ e['size'] = Size(tp.width + 40, tp.height + 40);
+ });
+ },
+ ),
+ ),
+ );
+ } 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,
+ ),
+ ),
+ );
+ }
+
Future<void> _pickImageFromGallery() async {
final List<XFile> images = await _picker.pickMultiImage();
if (images.isNotEmpty) {
@@ -760,6 +1043,7 @@ class CanvasBottomBar extends StatelessWidget {
label: "Text",
iconPath: "assets/icons/text.svg",
onTap: onText,
+ isActive: activeItem == "Text",
),
const SizedBox(width: 24),
_BottomBarItem(
diff --git a/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart b/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart
@@ -0,0 +1,168 @@
+import 'package:flutter/material.dart';
+import 'package:flutter_colorpicker/flutter_colorpicker.dart';
+
+class TextToolsOverlay extends StatelessWidget {
+ final bool isActive;
+ final bool isTextSelected;
+ final Color currentColor;
+ final double currentFontSize;
+ final VoidCallback onClose;
+ final VoidCallback onAddText;
+ final Function(Color) onColorChanged;
+ final Function(double) onFontSizeChanged;
+ final VoidCallback onDelete;
+
+ const TextToolsOverlay({
+ super.key,
+ required this.isActive,
+ required this.isTextSelected,
+ required this.currentColor,
+ required this.currentFontSize,
+ required this.onClose,
+ required this.onAddText,
+ required this.onColorChanged,
+ required this.onFontSizeChanged,
+ required this.onDelete,
+ });
+
+ @override
+ Widget build(BuildContext context) {
+ if (!isActive) return const SizedBox.shrink();
+
+ return Positioned(
+ bottom: 120, // Positioned above the bottom bar
+ left: 20,
+ right: 20,
+ child: Center(
+ child: Container(
+ padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
+ decoration: BoxDecoration(
+ color: Colors.white,
+ borderRadius: BorderRadius.circular(30),
+ boxShadow: [
+ BoxShadow(
+ color: Colors.black.withOpacity(0.15),
+ blurRadius: 10,
+ offset: const Offset(0, 4),
+ ),
+ ],
+ ),
+ child: Row(
+ mainAxisSize: MainAxisSize.min,
+ children: [
+ // --- Close / Done ---
+ IconButton(
+ icon: const Icon(Icons.check, color: Colors.green),
+ 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),
+ 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),
+
+ // Font Size
+ const Icon(Icons.text_fields, size: 16, color: Colors.grey),
+ SizedBox(
+ width: 100,
+ child: SliderTheme(
+ data: SliderThemeData(
+ trackHeight: 3,
+ 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),
+ min: 10.0,
+ max: 100.0,
+ onChanged: onFontSizeChanged,
+ ),
+ ),
+ ),
+
+ const SizedBox(width: 8),
+
+ // Color Picker
+ GestureDetector(
+ onTap: () => _showColorPicker(context),
+ child: Container(
+ width: 24,
+ height: 24,
+ decoration: BoxDecoration(
+ color: currentColor,
+ shape: BoxShape.circle,
+ border: Border.all(color: Colors.grey[300]!, width: 1),
+ ),
+ ),
+ ),
+
+ const SizedBox(width: 8),
+ 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),
+ onPressed: onDelete,
+ tooltip: "Delete",
+ ),
+ ],
+ ],
+ ),
+ ),
+ ),
+ );
+ }
+
+ void _showColorPicker(BuildContext context) {
+ 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)],
+ ),
+ ),
+ ),
+ ],
+ ),
+ ),
+ );
+ }
+}