creek

The AI Image Editor of 2030

lib/ui/pages/canvas_toolbar/magic_draw_overlay.dart (17819B)


  1import 'package:flutter/material.dart';
  2import 'package:flutter_svg/flutter_svg.dart';
  3import '../../widgets/canvas/advanced_color_picker.dart';
  4import '../../styles/variables.dart';
  5
  6// Dropdown
  7class AIModelOption {
  8  final String id;
  9  final String name;
 10  final String? badge;
 11
 12  AIModelOption({required this.id, required this.name, this.badge});
 13}
 14
 15class MagicDrawTools extends StatefulWidget {
 16  final bool isActive;
 17  final Color selectedColor;
 18  final double strokeWidth;
 19  final bool isEraser;
 20  final Function(Color) onColorChanged;
 21  final Function(double) onWidthChanged;
 22  final Function(bool) onEraserToggle;
 23  final VoidCallback onClose;
 24  final Function(String prompt, String modelId) onPromptSubmit;
 25  final bool isProcessing;
 26  final List<Color> brandColors;
 27  final Function(bool) onViewModeToggle;
 28  final Function(bool) onMagicPanelActivityToggle;
 29  final bool isMagicPanelDisabled;
 30
 31  // To decide which dropdown list to show
 32  final bool hasImageLayers;
 33
 34  const MagicDrawTools({
 35    super.key,
 36    required this.isActive,
 37    required this.selectedColor,
 38    required this.strokeWidth,
 39    required this.isEraser,
 40    required this.onColorChanged,
 41    required this.onWidthChanged,
 42    required this.onEraserToggle,
 43    required this.onClose,
 44    required this.onPromptSubmit,
 45    required this.isProcessing,
 46    required this.brandColors,
 47    required this.onViewModeToggle,
 48    required this.onMagicPanelActivityToggle,
 49    required this.isMagicPanelDisabled,
 50    required this.hasImageLayers,
 51  });
 52
 53  @override
 54  State<MagicDrawTools> createState() => _MagicDrawToolsState();
 55}
 56
 57class _MagicDrawToolsState extends State<MagicDrawTools> {
 58  bool _showStrokeSlider = false;
 59  final TextEditingController _promptController = TextEditingController();
 60
 61  final List<Color> _recentColors = [
 62    Colors.blue,
 63    Colors.purple,
 64    const Color(0xFFD81B60),
 65    Colors.pinkAccent,
 66    Colors.amber,
 67  ];
 68
 69  bool _isViewMode = false;
 70  bool _showModelMenu = false;
 71  late AIModelOption _selectedModel;
 72
 73  // 1. List for inpainting (When Image Exists)
 74  final List<AIModelOption> _inpaintingModels = [
 75    AIModelOption(
 76      id: 'inpaint_standard',
 77      name: 'Stable Diffusion Inpainting',
 78      badge: "Recommended",
 79    ),
 80    AIModelOption(id: 'inpaint_api', name: 'FLUX LoRa Fill', badge: null),
 81  ];
 82
 83  // 2. List for sketch to image (When No Image Exists)
 84  final List<AIModelOption> _sketchModels = [
 85    AIModelOption(id: 'sketch_advanced', name: 'Nano Banana', badge: 'Premium'),
 86    AIModelOption(id: 'sketch_creative', name: 'FLUX Dev', badge: 'Fast'),
 87    AIModelOption(
 88      id: 'sketch_fusion',
 89      name: 'Stable Diffusion v1.5',
 90      badge: null,
 91    ),
 92  ];
 93
 94  @override
 95  void initState() {
 96    super.initState();
 97    _updateSelectedModelGroup();
 98  }
 99
100  @override
101  void didUpdateWidget(MagicDrawTools oldWidget) {
102    super.didUpdateWidget(oldWidget);
103    // If the layer state changes (image added/removed), reset selection to correct group
104    if (oldWidget.hasImageLayers != widget.hasImageLayers) {
105      _updateSelectedModelGroup();
106    }
107
108    // Clear prompt when opening the tool
109    if (!oldWidget.isActive && widget.isActive) {
110      _promptController.clear();
111    }
112  }
113
114  void _updateSelectedModelGroup() {
115    if (widget.hasImageLayers) {
116      _selectedModel = _inpaintingModels.first;
117    } else {
118      _selectedModel = _sketchModels.first;
119    }
120  }
121
122  List<AIModelOption> get _currentModelList {
123    return widget.hasImageLayers ? _inpaintingModels : _sketchModels;
124  }
125
126  @override
127  void dispose() {
128    if (_isViewMode) {
129      widget.onViewModeToggle(false);
130    }
131    _promptController.dispose();
132    super.dispose();
133  }
134
135  void _handleSubmit() {
136    if (!widget.isProcessing && !widget.isMagicPanelDisabled) {
137      widget.onPromptSubmit(_promptController.text.trim(), _selectedModel.id);
138    }
139  }
140
141  void _toggleViewMode() {
142    final newViewMode = !_isViewMode;
143    setState(() {
144      _isViewMode = newViewMode;
145      _showStrokeSlider = false;
146      _showModelMenu = false;
147    });
148    widget.onViewModeToggle(newViewMode);
149  }
150
151  void _toggleModelMenu() {
152    setState(() {
153      if (!_showModelMenu) _showStrokeSlider = false;
154      _showModelMenu = !_showModelMenu;
155    });
156  }
157
158  @override
159  Widget build(BuildContext context) {
160    if (!widget.isActive) return const SizedBox.shrink();
161
162    return Positioned(
163      bottom: 140,
164      left: 16,
165      right: 16,
166      child: Column(
167        mainAxisSize: MainAxisSize.min,
168        children: [
169          if (_showStrokeSlider) _buildTaperedStrokeSlider(),
170          const SizedBox(height: 8),
171
172          Container(
173            decoration: BoxDecoration(
174              color: Colors.white,
175              borderRadius: BorderRadius.circular(16),
176              boxShadow: const [
177                BoxShadow(
178                  color: Colors.black12,
179                  blurRadius: 12,
180                  offset: Offset(0, 4),
181                ),
182              ],
183            ),
184            child: Column(
185              mainAxisSize: MainAxisSize.min,
186              children: [
187                if (_showModelMenu) _buildModelDropdown(),
188                _buildMagicDrawPanel(),
189              ],
190            ),
191          ),
192        ],
193      ),
194    );
195  }
196
197  Widget _buildToolIcon(dynamic icon, bool isActive, VoidCallback onTap) {
198    final Color activeColor = Variables.textPrimary;
199    final Color inactiveColor = Variables.textSecondary;
200    final Color iconColor = isActive ? activeColor : inactiveColor;
201
202    final Widget iconWidget =
203        (icon is IconData)
204            ? Icon(icon, size: 20, color: iconColor)
205            : SvgPicture.asset(
206              icon,
207              width: 20,
208              height: 20,
209              colorFilter: ColorFilter.mode(iconColor, BlendMode.srcIn),
210            );
211
212    return GestureDetector(
213      onTap: onTap,
214      child: Container(
215        padding: const EdgeInsets.all(10),
216        decoration: BoxDecoration(
217          color: isActive ? Variables.surfaceSubtle : Colors.transparent,
218          shape: BoxShape.circle,
219        ),
220        child: iconWidget,
221      ),
222    );
223  }
224
225  Widget _buildModelDropdown() {
226    return Container(
227      padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
228      child: Column(
229        crossAxisAlignment: CrossAxisAlignment.start,
230        children: [
231          Text(
232            widget.hasImageLayers ? 'Inpainting Models' : 'Sketch Models',
233            style: Variables.captionStyle.copyWith(fontSize: 12),
234          ),
235          const SizedBox(height: 4),
236
237          ..._currentModelList.map((model) {
238            final isSelected = model.id == _selectedModel.id;
239            return GestureDetector(
240              onTap: () {
241                setState(() {
242                  _selectedModel = model;
243                  _showModelMenu = false;
244                });
245              },
246              child: Padding(
247                padding: const EdgeInsets.symmetric(vertical: 8.0),
248                child: Row(
249                  children: [
250                    Icon(
251                      Icons.star_half,
252                      color: isSelected ? Variables.accentMagic : Colors.grey,
253                      size: 18,
254                    ),
255                    const SizedBox(width: 8),
256                    Text(
257                      model.name,
258                      style: Variables.bodyStyle.copyWith(
259                        fontWeight:
260                            isSelected ? FontWeight.bold : FontWeight.normal,
261                      ),
262                    ),
263                    const Spacer(),
264                    if (model.badge != null)
265                      Container(
266                        padding: const EdgeInsets.symmetric(
267                          horizontal: 6,
268                          vertical: 2,
269                        ),
270                        decoration: BoxDecoration(
271                          color: Colors.blue.shade50,
272                          borderRadius: BorderRadius.circular(4),
273                        ),
274                        child: Text(
275                          model.badge!,
276                          style: const TextStyle(
277                            color: Color(0xFF0D47A1), // Dark blue
278                            fontSize: 10,
279                            fontWeight: FontWeight.bold,
280                            fontFamily: 'GeneralSans',
281                          ),
282                        ),
283                      ),
284                  ],
285                ),
286              ),
287            );
288          }).toList(),
289          const Divider(height: 1, color: Variables.borderSubtle),
290        ],
291      ),
292    );
293  }
294
295  Widget _buildMagicDrawPanel() {
296    return Column(
297      mainAxisSize: MainAxisSize.min,
298      children: [
299        Padding(
300          padding: const EdgeInsets.all(8.0),
301          child: Row(
302            children: [
303              GestureDetector(
304                onTap: _toggleModelMenu,
305                child: Container(
306                  padding: const EdgeInsets.all(8),
307                  decoration: BoxDecoration(
308                    color:
309                        _showModelMenu ? Variables.surfaceSubtle : Colors.white,
310                    border: Border.all(color: Variables.borderSubtle),
311                    borderRadius: BorderRadius.circular(8),
312                  ),
313                  child: const Icon(
314                    Icons.star_half,
315                    color: Variables.accentMagic,
316                    size: 20,
317                  ),
318                ),
319              ),
320
321              const SizedBox(width: 12),
322              Expanded(
323                child: TextField(
324                  controller: _promptController,
325                  enabled: !widget.isProcessing,
326                  onSubmitted: (_) => _handleSubmit(),
327                  style: Variables.bodyStyle,
328                  decoration: InputDecoration.collapsed(
329                    hintText: "tap imagination...",
330                    hintStyle: Variables.captionStyle.copyWith(fontSize: 14),
331                  ),
332                ),
333              ),
334              const Icon(Icons.mic_none, color: Colors.grey),
335              const SizedBox(width: 8),
336              GestureDetector(
337                onTap: _handleSubmit,
338                child: Container(
339                  width: 44,
340                  height: 44,
341                  decoration: const BoxDecoration(
342                    color: Variables.textPrimary,
343                    shape: BoxShape.circle,
344                  ),
345                  child:
346                      widget.isProcessing
347                          ? const Padding(
348                            padding: EdgeInsets.all(12.0),
349                            child: CircularProgressIndicator(
350                              color: Colors.white,
351                              strokeWidth: 2,
352                            ),
353                          )
354                          : const Icon(
355                            Icons.auto_awesome,
356                            color: Colors.white,
357                            size: 20,
358                          ),
359                ),
360              ),
361            ],
362          ),
363        ),
364        const Divider(height: 1, color: Variables.borderSubtle),
365        Padding(
366          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
367          child: Row(
368            mainAxisAlignment: MainAxisAlignment.spaceBetween,
369            children: [
370              _buildToolIcon(Icons.pan_tool, widget.isMagicPanelDisabled, () {
371                setState(() => _showStrokeSlider = false);
372                _showModelMenu = false;
373
374                if (!widget.isMagicPanelDisabled) {
375                  widget.onEraserToggle(false);
376                }
377
378                widget.onMagicPanelActivityToggle(!widget.isMagicPanelDisabled);
379              }),
380              _buildToolIcon(
381                'assets/icons/brush-line.svg',
382                !widget.isEraser &&
383                    !_isViewMode &&
384                    !widget.isMagicPanelDisabled,
385                () {
386                  setState(() {
387                    _showStrokeSlider = false;
388                    _showModelMenu = false;
389                  });
390
391                  if (_isViewMode) {
392                    setState(() => _isViewMode = false);
393                    widget.onViewModeToggle(false);
394                  }
395                  if (widget.isMagicPanelDisabled) {
396                    widget.onMagicPanelActivityToggle(false);
397                  }
398
399                  widget.onEraserToggle(false);
400                },
401              ),
402
403              GestureDetector(
404                onTap: () {
405                  setState(() {
406                    _showStrokeSlider = false;
407                    _showModelMenu = false;
408                  });
409
410                  if (_isViewMode) {
411                    setState(() => _isViewMode = false);
412                    widget.onViewModeToggle(false);
413                  }
414
415                  _showAdvancedColorPicker(context);
416                },
417                child: Container(
418                  width: 28,
419                  height: 28,
420                  decoration: BoxDecoration(
421                    color: widget.selectedColor,
422                    shape: BoxShape.circle,
423                    border: Border.all(color: Colors.white, width: 2),
424                    boxShadow: [
425                      BoxShadow(
426                        color: Colors.black.withOpacity(0.1),
427                        blurRadius: 4,
428                      ),
429                    ],
430                  ),
431                ),
432              ),
433
434              GestureDetector(
435                onTap: () {
436                  if (_isViewMode) {
437                    setState(() => _isViewMode = false);
438                    widget.onViewModeToggle(false);
439                  }
440
441                  setState(() {
442                    _showStrokeSlider = !_showStrokeSlider;
443                    _showModelMenu = false;
444                  });
445                },
446                child: Container(
447                  padding: const EdgeInsets.all(8),
448                  decoration: BoxDecoration(
449                    color:
450                        _showStrokeSlider
451                            ? Variables.surfaceSubtle
452                            : Colors.transparent,
453                    shape: BoxShape.circle,
454                  ),
455                  child: Container(
456                    width: 10,
457                    height: 10,
458                    decoration: const BoxDecoration(
459                      color: Variables.textPrimary,
460                      shape: BoxShape.circle,
461                    ),
462                  ),
463                ),
464              ),
465
466              _buildToolIcon(
467                'assets/icons/eraser-line.svg',
468                widget.isEraser && !_isViewMode && !widget.isMagicPanelDisabled,
469                () {
470                  setState(() {
471                    _showStrokeSlider = false;
472                    _showModelMenu = false;
473                  });
474
475                  if (_isViewMode) {
476                    setState(() => _isViewMode = false);
477                    widget.onViewModeToggle(false);
478                  }
479                  if (widget.isMagicPanelDisabled) {
480                    widget.onMagicPanelActivityToggle(false);
481                  }
482
483                  widget.onEraserToggle(true);
484                },
485              ),
486            ],
487          ),
488        ),
489      ],
490    );
491  }
492
493  Widget _buildTaperedStrokeSlider() {
494    return Container(
495      width: 250,
496      height: 50,
497      decoration: BoxDecoration(
498        color: Colors.white,
499        borderRadius: BorderRadius.circular(30),
500        boxShadow: const [BoxShadow(color: Colors.black12, blurRadius: 8)],
501      ),
502      child: Stack(
503        alignment: Alignment.center,
504        children: [
505          CustomPaint(
506            size: const Size(200, 30),
507            painter: _TaperedSliderPainter(),
508          ),
509          SliderTheme(
510            data: SliderThemeData(
511              trackHeight: 0,
512              thumbColor: Colors.white,
513              thumbShape: const RoundSliderThumbShape(
514                enabledThumbRadius: 10,
515                elevation: 3,
516              ),
517              overlayShape: SliderComponentShape.noOverlay,
518              activeTrackColor: Colors.transparent,
519              inactiveTrackColor: Colors.transparent,
520            ),
521            child: SizedBox(
522              width: 210,
523              child: Slider(
524                value: widget.strokeWidth,
525                min: 5.0,
526                max: 60.0,
527                onChanged: widget.onWidthChanged,
528              ),
529            ),
530          ),
531        ],
532      ),
533    );
534  }
535
536  void _showAdvancedColorPicker(BuildContext context) {
537    showModalBottomSheet(
538      context: context,
539      isScrollControlled: true,
540      backgroundColor: Colors.transparent,
541      builder: (context) {
542        return AdvancedColorPickerSheet(
543          initialColor: widget.selectedColor,
544          recentColors: _recentColors,
545          onColorChanged: widget.onColorChanged,
546          brandColors: widget.brandColors,
547        );
548      },
549    );
550  }
551}
552
553class _TaperedSliderPainter extends CustomPainter {
554  @override
555  void paint(Canvas canvas, Size size) {
556    final paint =
557        Paint()
558          ..color = const Color(0xFF2B2B2B)
559          ..style = PaintingStyle.fill;
560    final path = Path();
561    path.moveTo(10, size.height / 2 - 2);
562    path.lineTo(size.width - 10, size.height / 2 - 10);
563    path.lineTo(size.width - 10, size.height / 2 + 10);
564    path.lineTo(10, size.height / 2 + 2);
565    path.close();
566    canvas.drawPath(path, paint);
567  }
568
569  @override
570  bool shouldRepaint(covariant CustomPainter oldDelegate) => false;
571}