creek

The AI Image Editor of 2030

lib/ui/widgets/canvas/layers_panel.dart (8212B)


  1import 'dart:io';
  2import 'package:flutter/material.dart';
  3import 'package:creekui/data/models/canvas_models.dart';
  4
  5class LayersPanel extends StatefulWidget {
  6  final List<CanvasLayer> layers;
  7  final String? activeLayerId;
  8  final bool isOpen;
  9  final VoidCallback onToggle;
 10  final Function(int oldIndex, int newIndex) onReorder;
 11  final Function(String id) onDelete;
 12  final Function(String id) onToggleVisibility;
 13  final Function(String id) onLayerTap;
 14  final VoidCallback? onAddLayer;
 15
 16  const LayersPanel({
 17    Key? key,
 18    required this.layers,
 19    required this.activeLayerId,
 20    required this.isOpen,
 21    required this.onToggle,
 22    required this.onReorder,
 23    required this.onDelete,
 24    required this.onToggleVisibility,
 25    required this.onLayerTap,
 26    this.onAddLayer,
 27  }) : super(key: key);
 28
 29  @override
 30  State<LayersPanel> createState() => _LayersPanelState();
 31}
 32
 33class _LayersPanelState extends State<LayersPanel> {
 34  @override
 35  Widget build(BuildContext context) {
 36    // Reverse list for UI so Top Layer is index 0
 37    final displayLayers = widget.layers.reversed.toList();
 38    return AnimatedContainer(
 39      duration: const Duration(milliseconds: 300),
 40      curve: Curves.easeOutCubic,
 41      width: widget.isOpen ? 260 : 56,
 42      // Use constrained height calculation based on state
 43      height: widget.isOpen ? 400 : 56,
 44      decoration: BoxDecoration(
 45        color: Colors.white,
 46        borderRadius: BorderRadius.circular(16),
 47        boxShadow: [
 48          BoxShadow(
 49            color: Colors.black.withOpacity(0.15),
 50            blurRadius: 10,
 51            offset: const Offset(0, 4),
 52          ),
 53        ],
 54      ),
 55      clipBehavior: Clip.antiAlias,
 56      child: Material(
 57        color: Colors.transparent,
 58        child:
 59            widget.isOpen
 60                ? _buildPanelContent(displayLayers)
 61                : _buildCollapsedButton(),
 62      ),
 63    );
 64  }
 65
 66  Widget _buildCollapsedButton() {
 67    return InkWell(
 68      onTap: widget.onToggle,
 69      borderRadius: BorderRadius.circular(16),
 70      child: const Center(
 71        child: Icon(Icons.layers_outlined, color: Colors.black),
 72      ),
 73    );
 74  }
 75
 76  Widget _buildPanelContent(List<CanvasLayer> displayLayers) {
 77    return AnimatedSize(
 78      duration: const Duration(milliseconds: 200),
 79      curve: Curves.easeOutCubic,
 80      alignment: Alignment.topRight,
 81      child: Container(
 82        constraints: const BoxConstraints(maxHeight: 400),
 83        child: Column(
 84          mainAxisSize: MainAxisSize.min, // Shrink to fit content
 85          crossAxisAlignment: CrossAxisAlignment.start,
 86          children: [
 87            // Header
 88            Padding(
 89              padding: const EdgeInsets.all(12),
 90              child: Row(
 91                children: [
 92                  // Add Layer Button
 93                  if (widget.onAddLayer != null) ...[
 94                    InkWell(
 95                      onTap: widget.onAddLayer,
 96                      borderRadius: BorderRadius.circular(8),
 97                      child: Container(
 98                        padding: const EdgeInsets.all(6),
 99                        decoration: BoxDecoration(
100                          color: Colors.blue.shade50,
101                          borderRadius: BorderRadius.circular(8),
102                        ),
103                        child: const Icon(
104                          Icons.add,
105                          size: 20,
106                          color: Colors.blue,
107                        ),
108                      ),
109                    ),
110                  ],
111
112                  const Spacer(),
113
114                  // Layers Icon
115                  InkWell(
116                    onTap: widget.onToggle,
117                    borderRadius: BorderRadius.circular(8),
118                    child: Container(
119                      padding: const EdgeInsets.all(6),
120                      decoration: BoxDecoration(
121                        color: Colors.grey.shade100,
122                        borderRadius: BorderRadius.circular(8),
123                      ),
124                      child: const Icon(
125                        Icons.layers,
126                        size: 20,
127                        color: Colors.black87,
128                      ),
129                    ),
130                  ),
131                ],
132              ),
133            ),
134            const Divider(height: 1, thickness: 0.5),
135
136            // List
137            Flexible(
138              child: ReorderableListView.builder(
139                shrinkWrap: true,
140                padding: EdgeInsets.zero,
141                buildDefaultDragHandles: false,
142                itemCount: displayLayers.length,
143                onReorder: widget.onReorder,
144                itemBuilder: (context, index) {
145                  final layer = displayLayers[index];
146                  return _buildLayerItem(context, layer, index);
147                },
148              ),
149            ),
150          ],
151        ),
152      ),
153    );
154  }
155
156  Widget _buildLayerItem(BuildContext context, CanvasLayer layer, int index) {
157    final bool isActive = layer.id == widget.activeLayerId;
158
159    Widget preview;
160    String label = "";
161
162    if (layer is ImageLayer) {
163      final type = layer.data['type'];
164      final content = layer.data['content'];
165
166      if (type == 'text') {
167        preview = const Icon(
168          Icons.text_fields,
169          size: 18,
170          color: Colors.black54,
171        );
172        label = content.toString();
173      } else {
174        preview = Container(
175          width: 28,
176          height: 28,
177          decoration: BoxDecoration(
178            borderRadius: BorderRadius.circular(4),
179            border: Border.all(color: Colors.grey[300]!),
180            image: DecorationImage(
181              image: FileImage(File(content)),
182              fit: BoxFit.cover,
183            ),
184          ),
185        );
186        label = "Image";
187      }
188    } else if (layer is SketchLayer) {
189      preview = const Icon(Icons.brush, size: 18, color: Colors.purpleAccent);
190      label = layer.isMagicDraw ? "Magic Draw" : "Sketch";
191    } else {
192      preview = const SizedBox();
193    }
194
195    return Dismissible(
196      key: ValueKey(layer.id),
197      direction: DismissDirection.endToStart,
198      onDismissed: (_) => widget.onDelete(layer.id),
199      background: Container(
200        color: Colors.redAccent,
201        alignment: Alignment.centerRight,
202        padding: const EdgeInsets.only(right: 12),
203        child: const Icon(Icons.delete, color: Colors.white, size: 20),
204      ),
205      child: Material(
206        color: isActive ? Colors.grey.withOpacity(0.1) : Colors.transparent,
207        child: InkWell(
208          onTap: () => widget.onLayerTap(layer.id),
209          child: Padding(
210            padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 4),
211            child: Row(
212              children: [
213                const SizedBox(width: 12),
214                preview,
215                const SizedBox(width: 12),
216                Expanded(
217                  child: Text(
218                    label,
219                    style: const TextStyle(fontSize: 14),
220                    maxLines: 1,
221                    overflow: TextOverflow.ellipsis,
222                  ),
223                ),
224
225                IconButton(
226                  padding: const EdgeInsets.all(4),
227                  constraints: const BoxConstraints(),
228                  icon: Icon(
229                    layer.isVisible ? Icons.visibility : Icons.visibility_off,
230                    size: 18,
231                    color: layer.isVisible ? Colors.black54 : Colors.grey,
232                  ),
233                  onPressed: () => widget.onToggleVisibility(layer.id),
234                ),
235
236                const SizedBox(width: 4),
237
238                ReorderableDragStartListener(
239                  index: index,
240                  child: Container(
241                    padding: const EdgeInsets.fromLTRB(12, 12, 16, 12),
242                    color: Colors.transparent,
243                    child: const Icon(
244                      Icons.drag_handle,
245                      size: 20,
246                      color: Colors.grey,
247                    ),
248                  ),
249                ),
250              ],
251            ),
252          ),
253        ),
254      ),
255    );
256  }
257}