creek
The AI Image Editor of 2030git clone https://github.com/maydayv7/creek
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}