creek

The AI Image Editor of 2030
commit b717221932c9fb9f656b5a7856f3fce628264eb9
parent 2370c9b25426a8ca92d96c6ffcb6b18dcbb9199c
Author: ajcoder13 <avnijhalani@gmail.com>
Date:   Mon,  1 Dec 2025 20:19:45 +0530

Fixed Canvas, save functionality working properly, UI fixed

Diffstat:
Aassets/icons/save-3-line.svg | 3+++
Aassets/icons/select.svg | 3+++
Mlib/ui/pages/canvas_board_page.dart | 367++++++++++++++++++++++++++++++++++++++++++++++++++++---------------------------
Mlib/ui/pages/canvas_toolbar/text_tools_overlay.dart | 187+++++++++++++++++++++++++++++++++++++++++--------------------------------------
4 files changed, 342 insertions(+), 218 deletions(-)

diff --git a/assets/icons/save-3-line.svg b/assets/icons/save-3-line.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M18 19H19V6.82843L17.1716 5H16V9H7V5H5V19H6V12H18V19ZM4 3H18L20.7071 5.70711C20.8946 5.89464 21 6.149 21 6.41421V20C21 20.5523 20.5523 21 20 21H4C3.44772 21 3 20.5523 3 20V4C3 3.44772 3.44772 3 4 3ZM8 14V19H16V14H8Z" fill="black"/> +</svg> diff --git a/assets/icons/select.svg b/assets/icons/select.svg @@ -0,0 +1,3 @@ +<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg"> +<path d="M4 19H6.15039V21H3C2.44772 21 2 20.5523 2 20V17.3496H4V19ZM11.1504 21H7.84961V19H11.1504V21ZM16.1504 21H12.8496V19H16.1504V21ZM22 20C22 20.5523 21.5523 21 21 21H17.8496V19H20V17.3496H22V20ZM4 15.6504H2V12.3496H4V15.6504ZM22 15.6504H20V12.3496H22V15.6504ZM4 10.6504H2V7.34961H4V10.6504ZM22 10.6504H20V7.34961H22V10.6504ZM6.15039 5H4V5.65039H2V4C2 3.44772 2.44772 3 3 3H6.15039V5ZM21 3C21.5523 3 22 3.44772 22 4V5.65039H20V5H17.8496V3H21ZM11.1504 5H7.84961V3H11.1504V5ZM16.1504 5H12.8496V3H16.1504V5Z" fill="#27272A"/> +</svg> diff --git a/lib/ui/pages/canvas_board_page.dart b/lib/ui/pages/canvas_board_page.dart @@ -9,10 +9,13 @@ import 'package:path_provider/path_provider.dart'; import 'package:flutter_svg/flutter_svg.dart'; import 'package:image_picker/image_picker.dart'; import 'package:undo/undo.dart'; +import 'package:share_plus/share_plus.dart'; +import 'package:image/image.dart' as img; import './canvas_toolbar/magic_draw_overlay.dart'; import './canvas_toolbar/text_tools_overlay.dart'; import '../../data/repos/project_repo.dart'; import '../../services/stylesheet_service.dart'; +import 'project_file_page.dart'; import '../../services/file_service.dart'; import '../../data/models/file_model.dart'; @@ -127,6 +130,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { double _strokeWidth = 10.0; bool _isEraser = false; final GlobalKey _drawingKey = GlobalKey(); + final GlobalKey _canvasGlobalKey = GlobalKey(); // --- VIEWPORT --- final TransformationController _transformationController = @@ -320,6 +324,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { elements.removeWhere((e) => e['id'] == selectedId); selectedId = null; _isEditingText = false; + _isTextToolsActive = false; }); _textFocusNode.unfocus(); _recordChange(oldState); @@ -353,6 +358,62 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { } } + Future<void> _exportProject() async { + try { + // 1. Show loading or feedback + ScaffoldMessenger.of( + context, + ).showSnackBar(const SnackBar(content: Text("Generating image..."))); + + // 2. Capture the Full Canvas using the global key + final boundary = + _canvasGlobalKey.currentContext?.findRenderObject() + as RenderRepaintBoundary?; + if (boundary == null) return; + + // 3. Convert to Image (High pixel ratio for quality) + final ui.Image image = await boundary.toImage(pixelRatio: 3.0); + final ByteData? byteData = await image.toByteData( + format: ui.ImageByteFormat.png, + ); + + if (byteData == null) return; + + final Uint8List pngBytes = byteData.buffer.asUint8List(); + + // 4. Convert PNG to JPG using 'image' package + // We do this in a compute isolate ideally, but for simplicity here on main thread + final img.Image? decodedImage = img.decodePng(pngBytes); + + if (decodedImage == null) { + throw Exception("Failed to decode image"); + } + + // Encode to JPG (Quality 90) + final Uint8List jpgBytes = img.encodeJpg(decodedImage, quality: 90); + + // 5. Save to Temporary File + final directory = await getTemporaryDirectory(); + final String fileName = + "export_${DateTime.now().millisecondsSinceEpoch}.jpg"; + final String filePath = '${directory.path}/$fileName'; + + final File imgFile = File(filePath); + await imgFile.writeAsBytes(jpgBytes); + + // 6. Trigger System Share/Save Dialog + // This allows the user to "Save to Device", "Share to Instagram", etc. + await Share.shareXFiles([ + XFile(filePath, mimeType: 'image/jpeg'), + ], text: 'Check out my design created with Adobe Clone!'); + } catch (e) { + debugPrint("Export Error: $e"); + ScaffoldMessenger.of( + context, + ).showSnackBar(SnackBar(content: Text("Export failed: $e"))); + } + } + // =========================================================================== // SAVE & LOAD LOGIC // =========================================================================== @@ -392,7 +453,11 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { onPressed: () async { Navigator.pop(context); // Close dialog await _saveCanvas(); // Save - if (mounted) Navigator.pop(context); // Leave page + // Note: The redirect logic is handled in _saveCanvas for new files. + // For existing files, we pop here. + if (mounted && widget.existingFile != null) { + Navigator.pop(context); + } }, child: const Text("Save"), ), @@ -436,9 +501,10 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { Future<void> _saveCanvas() async { try { String fileName = "Canvas ${DateTime.now().toString().split(' ')[0]}"; + bool isNewFile = widget.existingFile == null; // 1. IF NEW FILE: Ask user for name - if (widget.existingFile == null) { + if (isNewFile) { final userFileName = await _showNameDialog(); if (userFileName == null || userFileName.isEmpty) return; // Cancelled fileName = userFileName; @@ -447,7 +513,14 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { // 2. Serialize Elements AND Paths (Drawing) to JSON final jsonList = _elementsToJson(elements); final pathsJson = _paths.map((p) => p.toMap()).toList(); - final saveData = {'elements': jsonList, 'paths': pathsJson}; + + // [FIX] SAVE CANVAS DIMENSIONS + final saveData = { + 'elements': jsonList, + 'paths': pathsJson, + 'width': _canvasSize.width, // Saving Width + 'height': _canvasSize.height, // Saving Height + }; final jsonString = jsonEncode(saveData); final directory = await getTemporaryDirectory(); @@ -479,6 +552,14 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { ScaffoldMessenger.of(context).showSnackBar( const SnackBar(content: Text("Canvas Saved Successfully")), ); + if (isNewFile) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => ProjectFilePage(projectId: widget.projectId), + ), + ); + } } } catch (e) { debugPrint("Save Error: $e"); @@ -493,17 +574,29 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { final dynamic decoded = jsonDecode(jsonString); setState(() { - if (decoded is List) { - // Legacy support (older files with just elements) + if (decoded is Map && decoded.containsKey('elements')) { + // New format with dimensions + elements = _jsonToElements(decoded['elements']); + if (decoded['paths'] != null) { + _paths = + (decoded['paths'] as List) + .map((p) => DrawingPath.fromMap(p)) + .toList(); + } else { + _paths = []; + } + // [FIX] LOAD CANVAS DIMENSIONS & RESET VIEW INIT + if (decoded['width'] != null && decoded['height'] != null) { + _canvasSize = Size( + (decoded['width'] as num).toDouble(), + (decoded['height'] as num).toDouble(), + ); + _hasInitializedView = false; // FORCE RE-CENTERING + } + } else if (decoded is List) { + // Legacy support elements = _jsonToElements(decoded); _paths = []; - } else { - // New support (Elements + Paths) - elements = _jsonToElements(decoded['elements']); - _paths = - (decoded['paths'] as List) - .map((p) => DrawingPath.fromMap(p)) - .toList(); } _hasUnsavedChanges = false; }); @@ -621,114 +714,131 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { height: _canvasSize.height, child: Stack( children: [ - Container( - width: double.infinity, - height: double.infinity, - decoration: BoxDecoration( - color: Colors.white, - boxShadow: [ - BoxShadow( - color: Colors.black.withOpacity(0.15), - blurRadius: 40, - offset: const Offset(0, 10), + // [NEW] WRAPPED IN REPAINT BOUNDARY FOR EXPORT + RepaintBoundary( + key: _canvasGlobalKey, + child: Stack( + children: [ + Container( + width: double.infinity, + height: double.infinity, + decoration: BoxDecoration( + color: Colors.white, + boxShadow: [ + BoxShadow( + color: Colors.black.withOpacity(0.15), + blurRadius: 40, + offset: const Offset(0, 10), + ), + ], + ), ), - ], - ), - ), - ...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(() { - // Bring to front - elements.remove(e); - elements.add(e); - }); - } - }, - onDoubleTap: () { - if (e['type'] == 'text') _enterEditMode(e); - }, - onDragStart: _handleGestureStart, - onUpdate: - (newPos, newSize, newRot) => + ...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(() { + // Bring to front + 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, + transformationController: + _transformationController, + ); + }), + // Drawing Layer inside Capture Zone + IgnorePointer( + ignoring: !_isMagicDrawActive, + child: RepaintBoundary( + key: _drawingKey, + child: GestureDetector( + onPanStart: (_) { + _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, + ), ), - onDragEnd: (newPos, newSize, newRot) { - _handleElementUpdate( - e['id'], - newPos, - newSize, - newRot, - ); - _handleGestureEnd(); - }, - textController: - isSelected ? _textEditingController : null, - focusNode: isSelected ? _textFocusNode : null, - transformationController: - _transformationController, - ); - }), - // Drawing Layer - Always Visible (ignoring touches when not active) - IgnorePointer( - ignoring: !_isMagicDrawActive, - child: RepaintBoundary( - key: _drawingKey, - child: GestureDetector( - onPanStart: (_) { - _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, + ), ), ), - ), + ], ), ), ], @@ -743,7 +853,8 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { strokeWidth: _strokeWidth, isEraser: _isEraser, brandColors: _brandColors, - onClose: _saveAndCloseMagicDraw, + onClose: + _saveAndCloseMagicDraw, // REMOVED as per previous request onColorChanged: (c) => setState(() => _selectedColor = c), onWidthChanged: (w) => setState(() => _strokeWidth = w), onEraserToggle: (e) => setState(() => _isEraser = e), @@ -765,7 +876,6 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { }); }, onAddText: _addTextElement, - onDelete: _deleteSelectedElement, onColorChanged: (c) => _updateSelectedTextProperty('style_color', c.value), @@ -838,7 +948,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { PreferredSizeWidget _buildAppBar() { return AppBar( - leadingWidth: 140, + leadingWidth: 160, leading: SafeArea( child: Row( children: [ @@ -905,7 +1015,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { ), IconButton( icon: SvgPicture.asset( - 'assets/icons/file-image-line.svg', + 'assets/icons/save-3-line.svg', width: 22, ), onPressed: _saveCanvas, @@ -975,7 +1085,6 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { } void _openLayers() => _showComingSoon('Layers'); - void _exportProject() => _showComingSoon('Export'); void _openSettings() => _showComingSoon('Settings'); void _showComingSoon([dynamic feature]) => ScaffoldMessenger.of( context, @@ -983,7 +1092,7 @@ class _CanvasBoardPageState extends State<CanvasBoardPage> { } // ============================================================================= -// MANIPULATING BOX WIDGET +//  MANIPULATING BOX WIDGET // ============================================================================= class _ManipulatingBox extends StatefulWidget { @@ -1084,7 +1193,6 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> { onPanStart: (_) => widget.onDragStart(), onPanUpdate: (details) { if (widget.isSelected && !widget.isEditing) { - // FIX: Do not divide by currentZoom final delta = details.delta; final globalDelta = _rotateVector(delta, _rot); setState(() => _pos += globalDelta); @@ -1128,15 +1236,17 @@ class _ManipulatingBoxState extends State<_ManipulatingBox> { icon: Icons.zoom_out_map, color: Colors.blue, onDrag: (delta) { - // FIX: Use delta directly, no un-rotation setState(() { + final localDelta = _rotateVector(delta, -_rot); _size = Size( - (_size.width + delta.dx).clamp(50.0, 10000.0), - (_size.height + delta.dy).clamp(30.0, 10000.0), + (_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, ); - // Fix anchor point calculation - final offset = Offset(delta.dx / 2, delta.dy / 2); - _pos += _rotateVector(offset, _rot) - offset; + _pos += _rotateVector(offset, _rot); }); widget.onUpdate(_pos, _size, _rot); }, @@ -1355,7 +1465,7 @@ class CanvasBottomBar extends StatelessWidget { ], ), child: SafeArea( - top: false, + top: false, // We don't need top SafeArea as it's a bottom bar child: Container( padding: const EdgeInsets.symmetric(vertical: 10), child: SingleChildScrollView( @@ -1439,7 +1549,7 @@ class _BottomBarItem extends StatelessWidget { iconPath, width: 24, colorFilter: ColorFilter.mode( - isActive ? Colors.blue : Colors.black87, + isActive ? const Color(0xFF27272A) : const Color(0xFF9F9FA9), BlendMode.srcIn, ), ), @@ -1449,7 +1559,10 @@ class _BottomBarItem extends StatelessWidget { style: TextStyle( fontSize: 11, fontWeight: FontWeight.w500, - color: isActive ? Colors.blue : Colors.black87, + color: + isActive + ? const Color(0xFF27272A) + : const Color(0xFF9F9FA9), ), ), ], diff --git a/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart b/lib/ui/pages/canvas_toolbar/text_tools_overlay.dart @@ -10,7 +10,6 @@ class TextToolsOverlay extends StatelessWidget { final VoidCallback onAddText; final Function(Color) onColorChanged; final Function(double) onFontSizeChanged; - final VoidCallback onDelete; const TextToolsOverlay({ super.key, @@ -22,7 +21,6 @@ class TextToolsOverlay extends StatelessWidget { required this.onAddText, required this.onColorChanged, required this.onFontSizeChanged, - required this.onDelete, }); @override @@ -30,7 +28,7 @@ class TextToolsOverlay extends StatelessWidget { if (!isActive) return const SizedBox.shrink(); return Positioned( - bottom: 100, + bottom: 100 + MediaQuery.of(context).padding.bottom, left: 0, right: 0, child: Center( @@ -42,86 +40,88 @@ class TextToolsOverlay extends StatelessWidget { borderRadius: BorderRadius.circular(30), boxShadow: [ BoxShadow( - color: Colors.black.withValues(alpha: 0.15), + color: Colors.black.withOpacity(0.15), blurRadius: 12, offset: const Offset(0, 6), ), ], ), - child: Row( - mainAxisSize: MainAxisSize.min, - children: [ - IconButton( - 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), + // Use SingleChildScrollView + Row to prevent overflow if screen is narrow + child: SingleChildScrollView( + scrollDirection: Axis.horizontal, + child: Row( + mainAxisSize: MainAxisSize.min, + children: [ + IconButton( + 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), - IconButton( - icon: const Icon( - Icons.add_circle_outline, - color: Colors.black87, + IconButton( + icon: const Icon( + Icons.add_circle_outline, + color: Colors.black87, + ), + onPressed: onAddText, + tooltip: "Add Text", ), - onPressed: onAddText, - tooltip: "Add Text", - ), - if (isTextSelected) ...[ - const SizedBox(width: 8), - Container(width: 1, height: 20, color: Colors.grey[300]), - const SizedBox(width: 12), + if (isTextSelected) ...[ + const SizedBox(width: 8), + Container(width: 1, height: 20, color: Colors.grey[300]), + const SizedBox(width: 12), - const Icon(Icons.text_fields, size: 18, color: Colors.black54), - SizedBox( - width: 100, - child: SliderTheme( - data: SliderThemeData( - trackHeight: 2, - thumbShape: const RoundSliderThumbShape( - enabledThumbRadius: 6, + const Icon( + Icons.text_fields, + size: 18, + color: Colors.black54, + ), + SizedBox( + width: 100, + child: SliderTheme( + data: SliderThemeData( + 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, 200.0), + min: 10.0, + max: 200.0, + onChanged: onFontSizeChanged, ), - activeTrackColor: Colors.black87, - inactiveTrackColor: Colors.grey[200], - thumbColor: Colors.black, - overlayShape: SliderComponentShape.noOverlay, - ), - child: Slider( - value: currentFontSize.clamp(10.0, 200.0), - min: 10.0, - max: 200.0, - onChanged: onFontSizeChanged, ), ), - ), - const SizedBox(width: 12), + const SizedBox(width: 12), - 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), + 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: 12), - Container(width: 1, height: 20, color: Colors.grey[300]), - const SizedBox(width: 8), - - IconButton( - icon: const Icon(Icons.delete_outline, color: Colors.red), - onPressed: onDelete, - tooltip: "Delete", - ), + const SizedBox(width: 12), + Container(width: 1, height: 20, color: Colors.grey[300]), + const SizedBox(width: 8), + ], ], - ], + ), ), ), ), @@ -139,33 +139,38 @@ class TextToolsOverlay extends StatelessWidget { 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)], + child: SafeArea( + // Added SafeArea here for the bottom sheet content + 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), + ], + ), ), - ), - ), - ], + ), + ], + ), ), ), );