creek

The AI Image Editor of 2030

lib/ui/widgets/canvas/advanced_color_picker.dart (21441B)


  1import 'package:flutter/material.dart';
  2import 'package:flutter_colorpicker/flutter_colorpicker.dart';
  3import 'package:creekui/ui/styles/variables.dart';
  4
  5class AdvancedColorPickerSheet extends StatefulWidget {
  6  final Color initialColor;
  7  final ValueChanged<Color> onColorChanged;
  8  final List<Color> recentColors;
  9  final List<Color> brandColors;
 10
 11  const AdvancedColorPickerSheet({
 12    super.key,
 13    required this.initialColor,
 14    required this.onColorChanged,
 15    required this.recentColors,
 16    required this.brandColors,
 17  });
 18
 19  @override
 20  State<AdvancedColorPickerSheet> createState() =>
 21      _AdvancedColorPickerSheetState();
 22}
 23
 24class _AdvancedColorPickerSheetState extends State<AdvancedColorPickerSheet>
 25    with SingleTickerProviderStateMixin {
 26  late TabController _tabController;
 27  late Color _currentColor;
 28  late HSVColor _currentHsv;
 29  late List<Color> _brandPalette;
 30
 31  final TextEditingController _rController = TextEditingController();
 32  final TextEditingController _gController = TextEditingController();
 33  final TextEditingController _bController = TextEditingController();
 34
 35  @override
 36  void initState() {
 37    super.initState();
 38    _currentColor = widget.initialColor;
 39    _currentHsv = HSVColor.fromColor(widget.initialColor);
 40    _tabController = TabController(length: 3, vsync: this);
 41    _brandPalette = widget.brandColors;
 42    _updateControllers();
 43  }
 44
 45  void _updateControllers() {
 46    _rController.text = _currentColor.red.toString();
 47    _gController.text = _currentColor.green.toString();
 48    _bController.text = _currentColor.blue.toString();
 49  }
 50
 51  @override
 52  void dispose() {
 53    _tabController.dispose();
 54    _rController.dispose();
 55    _gController.dispose();
 56    _bController.dispose();
 57    super.dispose();
 58  }
 59
 60  // Updates the active color and canvas immediately (Live Preview)
 61  void _handleColorChange(Color color) {
 62    setState(() {
 63      _currentColor = color;
 64      _currentHsv = HSVColor.fromColor(color);
 65      _updateControllers();
 66    });
 67    widget.onColorChanged(color);
 68  }
 69
 70  // Handler for Spectrum Tab to avoid HSV->RGB->HSV conversion loss
 71  void _handleHsvChange(HSVColor hsv) {
 72    setState(() {
 73      _currentHsv = hsv;
 74      _currentColor = hsv.toColor();
 75      _updateControllers();
 76    });
 77    widget.onColorChanged(_currentColor);
 78  }
 79
 80  // Saves the current color to the "Recently Used" list
 81  void _saveToRecent() {
 82    setState(() {
 83      widget.recentColors.removeWhere((c) => c.value == _currentColor.value);
 84      widget.recentColors.insert(0, _currentColor);
 85      if (widget.recentColors.length > 5) {
 86        widget.recentColors.removeLast();
 87      }
 88    });
 89  }
 90
 91  @override
 92  Widget build(BuildContext context) {
 93    return Container(
 94      height: MediaQuery.of(context).size.height * 0.85,
 95      decoration: const BoxDecoration(
 96        color: Colors.white,
 97        borderRadius: BorderRadius.vertical(top: Radius.circular(24)),
 98      ),
 99      child: Column(
100        crossAxisAlignment: CrossAxisAlignment.stretch,
101        children: [
102          const SizedBox(height: 12),
103          Center(
104            child: Container(
105              width: 40,
106              height: 4,
107              decoration: BoxDecoration(
108                color: Colors.grey[300],
109                borderRadius: BorderRadius.circular(2),
110              ),
111            ),
112          ),
113          const SizedBox(height: 16),
114          TabBar(
115            controller: _tabController,
116            labelColor: Colors.black,
117            unselectedLabelColor: Colors.grey,
118            indicatorColor: Colors.black,
119            indicatorSize: TabBarIndicatorSize.label,
120            labelStyle: const TextStyle(
121              fontWeight: FontWeight.w600,
122              fontSize: 14,
123            ),
124            tabs: const [
125              Tab(text: 'Grid'),
126              Tab(text: 'Spectrum'),
127              Tab(text: 'Sliders'),
128            ],
129          ),
130          const SizedBox(height: 16),
131          // Header: Hex Code & Preview
132          Padding(
133            padding: const EdgeInsets.symmetric(horizontal: 16),
134            child: Row(
135              children: [
136                Container(
137                  padding: const EdgeInsets.symmetric(
138                    horizontal: 8,
139                    vertical: 6,
140                  ),
141                  decoration: BoxDecoration(
142                    border: Border.all(color: Colors.grey.shade300),
143                    borderRadius: BorderRadius.circular(8),
144                  ),
145                  child: Row(
146                    mainAxisSize: MainAxisSize.min,
147                    children: const [
148                      Text(
149                        "Hex",
150                        style: TextStyle(
151                          fontSize: 13,
152                          fontWeight: FontWeight.bold,
153                        ),
154                      ),
155                      SizedBox(width: 4),
156                      Icon(
157                        Icons.keyboard_arrow_down,
158                        size: 16,
159                        color: Colors.grey,
160                      ),
161                    ],
162                  ),
163                ),
164                const SizedBox(width: 8),
165                Container(
166                  width: 32,
167                  height: 32,
168                  decoration: BoxDecoration(
169                    color: _currentColor,
170                    borderRadius: BorderRadius.circular(6),
171                    border: Border.all(color: Colors.grey.shade200),
172                  ),
173                ),
174                const SizedBox(width: 8),
175                Expanded(
176                  child: Container(
177                    padding: const EdgeInsets.symmetric(
178                      horizontal: 10,
179                      vertical: 8,
180                    ),
181                    decoration: BoxDecoration(
182                      color: Colors.grey.shade50,
183                      borderRadius: BorderRadius.circular(8),
184                    ),
185                    child: Text(
186                      "#${_currentColor.value.toRadixString(16).toUpperCase().substring(2)}",
187                      style: const TextStyle(
188                        fontWeight: FontWeight.w600,
189                        fontSize: 14,
190                      ),
191                      overflow: TextOverflow.ellipsis,
192                    ),
193                  ),
194                ),
195                const SizedBox(width: 8),
196                const Icon(Icons.colorize, size: 20, color: Colors.black54),
197              ],
198            ),
199          ),
200          const SizedBox(height: 16),
201          const Divider(height: 1),
202          // Tab Views
203          Expanded(
204            child: TabBarView(
205              controller: _tabController,
206              physics: const NeverScrollableScrollPhysics(),
207              children: [
208                _buildGridTab(),
209                _buildSpectrumTab(),
210                _buildSlidersTab(),
211              ],
212            ),
213          ),
214          _buildSharedFooter(),
215        ],
216      ),
217    );
218  }
219
220  // Tab 1: Grid
221  List<Color> _generateColorGrid() {
222    List<Color> colors = [];
223    for (int i = 0; i < 9; i++) {
224      double lightness = 1.0 - (i / 8);
225      colors.add(HSLColor.fromAHSL(1.0, 0.0, 0.0, lightness).toColor());
226    }
227    final int hueSteps = 9;
228    final int shadeSteps = 7;
229    for (int shade = 0; shade < shadeSteps; shade++) {
230      for (int hueStep = 0; hueStep < hueSteps; hueStep++) {
231        double hue = (hueStep / hueSteps) * 360;
232        double saturation = 0.5 + (shade / shadeSteps) * 0.5;
233        double lightness = 0.8 - (shade / shadeSteps) * 0.5;
234        colors.add(
235          HSLColor.fromAHSL(1.0, hue, saturation, lightness).toColor(),
236        );
237      }
238    }
239    return colors;
240  }
241
242  Widget _buildGridTab() {
243    final gridColors = _generateColorGrid();
244
245    return Column(
246      children: [
247        Expanded(
248          child: Padding(
249            padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 16),
250            child: GridView.builder(
251              physics: const BouncingScrollPhysics(),
252              gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
253                crossAxisCount: 9,
254                crossAxisSpacing: 0,
255                mainAxisSpacing: 0,
256                childAspectRatio: 1.0,
257              ),
258              itemCount: gridColors.length,
259              itemBuilder: (context, index) {
260                final color = gridColors[index];
261                final isSelected = _currentColor.value == color.value;
262                return GestureDetector(
263                  onTap: () {
264                    _handleColorChange(color);
265                    _saveToRecent(); // Save immediately on tap
266                  },
267                  child: Stack(
268                    alignment: Alignment.center,
269                    children: [
270                      Container(
271                        decoration: BoxDecoration(
272                          color: color,
273                          border: Border.all(
274                            color: Colors.black.withOpacity(0.05),
275                            width: 0.5,
276                          ),
277                        ),
278                      ),
279                      if (isSelected)
280                        Container(
281                          width: 24,
282                          height: 24,
283                          decoration: BoxDecoration(
284                            color: color,
285                            shape: BoxShape.circle,
286                            border: Border.all(color: Colors.white, width: 3),
287                            boxShadow: [
288                              BoxShadow(
289                                color: Colors.black.withOpacity(0.2),
290                                blurRadius: 4,
291                                spreadRadius: 1,
292                              ),
293                            ],
294                          ),
295                        ),
296                    ],
297                  ),
298                );
299              },
300            ),
301          ),
302        ),
303      ],
304    );
305  }
306
307  // Tab 2: Spectrum
308  Widget _buildSpectrumTab() {
309    return Padding(
310      padding: const EdgeInsets.all(20),
311      child: Column(
312        children: [
313          // Saturation / Value Box
314          Expanded(
315            child: ClipRRect(
316              borderRadius: BorderRadius.circular(12),
317              child: LayoutBuilder(
318                builder: (context, constraints) {
319                  return GestureDetector(
320                    onPanUpdate: (details) {
321                      RenderBox box = context.findRenderObject() as RenderBox;
322                      Offset localOffset = box.globalToLocal(
323                        details.globalPosition,
324                      );
325
326                      double saturation = (localOffset.dx /
327                              constraints.maxWidth)
328                          .clamp(0.0, 1.0);
329                      double value =
330                          1.0 -
331                          (localOffset.dy / constraints.maxHeight).clamp(
332                            0.0,
333                            1.0,
334                          );
335
336                      _handleHsvChange(
337                        _currentHsv.withSaturation(saturation).withValue(value),
338                      );
339                    },
340                    onPanEnd: (_) => _saveToRecent(),
341                    child: Stack(
342                      children: [
343                        // Base Hue Color
344                        Container(
345                          color:
346                              HSVColor.fromAHSV(
347                                1.0,
348                                _currentHsv.hue,
349                                1.0,
350                                1.0,
351                              ).toColor(),
352                        ),
353                        // Gradient: White -> Transparent (Saturation)
354                        Container(
355                          decoration: const BoxDecoration(
356                            gradient: LinearGradient(
357                              colors: [Colors.white, Colors.transparent],
358                              begin: Alignment.centerLeft,
359                              end: Alignment.centerRight,
360                            ),
361                          ),
362                        ),
363                        // Gradient: Transparent -> Black (Value/Brightness)
364                        Container(
365                          decoration: const BoxDecoration(
366                            gradient: LinearGradient(
367                              colors: [Colors.transparent, Colors.black],
368                              begin: Alignment.topCenter,
369                              end: Alignment.bottomCenter,
370                            ),
371                          ),
372                        ),
373                        // Selector Circle
374                        Positioned(
375                          left:
376                              _currentHsv.saturation * constraints.maxWidth -
377                              10,
378                          top:
379                              (1 - _currentHsv.value) * constraints.maxHeight -
380                              10,
381                          child: Container(
382                            width: 20,
383                            height: 20,
384                            decoration: BoxDecoration(
385                              color: _currentColor,
386                              shape: BoxShape.circle,
387                              border: Border.all(color: Colors.white, width: 2),
388                              boxShadow: const [
389                                BoxShadow(color: Colors.black26, blurRadius: 4),
390                              ],
391                            ),
392                          ),
393                        ),
394                      ],
395                    ),
396                  );
397                },
398              ),
399            ),
400          ),
401          const SizedBox(height: 24),
402          // Hue Slider
403          SizedBox(
404            height: 40,
405            child: Stack(
406              alignment: Alignment.center,
407              children: [
408                // Rainbow Background
409                Container(
410                  height: 12,
411                  decoration: BoxDecoration(
412                    borderRadius: BorderRadius.circular(6),
413                    gradient: const LinearGradient(
414                      colors: [
415                        Color(0xFFFF0000),
416                        Color(0xFFFFFF00),
417                        Color(0xFF00FF00),
418                        Color(0xFF00FFFF),
419                        Color(0xFF0000FF),
420                        Color(0xFFFF00FF),
421                        Color(0xFFFF0000),
422                      ],
423                    ),
424                  ),
425                ),
426                SliderTheme(
427                  data: SliderThemeData(
428                    trackHeight: 12,
429                    thumbShape: const RoundSliderThumbShape(
430                      enabledThumbRadius: 14,
431                      elevation: 4,
432                    ),
433                    overlayColor: Colors.transparent,
434                    thumbColor: Colors.white,
435                    activeTrackColor: Colors.transparent,
436                    inactiveTrackColor: Colors.transparent,
437                  ),
438                  child: Slider(
439                    value: _currentHsv.hue,
440                    min: 0.0,
441                    max: 360.0,
442                    onChanged: (newHue) {
443                      _handleHsvChange(_currentHsv.withHue(newHue));
444                    },
445                    onChangeEnd: (_) => _saveToRecent(),
446                  ),
447                ),
448              ],
449            ),
450          ),
451          const SizedBox(height: 10),
452        ],
453      ),
454    );
455  }
456
457  // Tab 3: Sliders
458  Widget _buildSlidersTab() {
459    return Padding(
460      padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 24),
461      child: SingleChildScrollView(
462        child: Column(
463          children: [
464            _buildSingleRGBSlider("Red", Colors.red, _currentColor.red, (v) {
465              _handleColorChange(_currentColor.withRed(v.toInt()));
466            }),
467            const SizedBox(height: 24),
468            _buildSingleRGBSlider("Green", Colors.green, _currentColor.green, (
469              v,
470            ) {
471              _handleColorChange(_currentColor.withGreen(v.toInt()));
472            }),
473            const SizedBox(height: 24),
474            _buildSingleRGBSlider("Blue", Colors.blue, _currentColor.blue, (v) {
475              _handleColorChange(_currentColor.withBlue(v.toInt()));
476            }),
477          ],
478        ),
479      ),
480    );
481  }
482
483  Widget _buildSingleRGBSlider(
484    String label,
485    Color activeColor,
486    int value,
487    ValueChanged<double> onChanged,
488  ) {
489    return Row(
490      children: [
491        Expanded(
492          child: SizedBox(
493            height: 30,
494            child: SliderTheme(
495              data: SliderThemeData(
496                trackHeight: 6,
497                activeTrackColor: activeColor,
498                inactiveTrackColor: activeColor.withOpacity(0.15),
499                thumbColor: Colors.white,
500                thumbShape: const RoundSliderThumbShape(
501                  enabledThumbRadius: 14,
502                  elevation: 4,
503                ),
504                overlayShape: const RoundSliderOverlayShape(overlayRadius: 24),
505                trackShape: const RoundedRectSliderTrackShape(),
506              ),
507              child: Slider(
508                value: value.toDouble(),
509                min: 0,
510                max: 255,
511                onChanged: onChanged,
512                onChangeEnd: (_) => _saveToRecent(),
513              ),
514            ),
515          ),
516        ),
517        const SizedBox(width: 12),
518        Container(
519          width: 50,
520          height: 36,
521          decoration: BoxDecoration(
522            border: Border.all(color: Colors.grey.shade300),
523            borderRadius: BorderRadius.circular(8),
524            color: Colors.white,
525          ),
526          alignment: Alignment.center,
527          child: Text(
528            value.toString(),
529            style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 14),
530          ),
531        ),
532      ],
533    );
534  }
535
536  // Footer
537  Widget _buildSharedFooter() {
538    return SafeArea(
539      top: false,
540      child: Container(
541        padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
542        decoration: BoxDecoration(
543          color: Colors.white,
544          border: Border(top: BorderSide(color: Colors.grey.shade100)),
545        ),
546        child: Column(
547          mainAxisSize: MainAxisSize.min,
548          crossAxisAlignment: CrossAxisAlignment.start,
549          children: [
550            const Text(
551              "Recently used",
552              style: TextStyle(
553                fontSize: 12,
554                fontWeight: FontWeight.w600,
555                color: Colors.grey,
556              ),
557            ),
558            const SizedBox(height: 10),
559            SingleChildScrollView(
560              scrollDirection: Axis.horizontal,
561              child: Row(
562                children:
563                    widget.recentColors
564                        .map((c) => _buildColorCircle(c))
565                        .toList(),
566              ),
567            ),
568            const SizedBox(height: 16),
569            Row(
570              mainAxisAlignment: MainAxisAlignment.spaceBetween,
571              children: [
572                const Text(
573                  "Brand Palette",
574                  style: TextStyle(
575                    fontSize: 12,
576                    fontWeight: FontWeight.w600,
577                    color: Colors.grey,
578                  ),
579                ),
580                Text(
581                  "Edit",
582                  style: TextStyle(
583                    fontSize: 12,
584                    color: Colors.blue[700],
585                    fontWeight: FontWeight.bold,
586                  ),
587                ),
588              ],
589            ),
590            const SizedBox(height: 10),
591            SingleChildScrollView(
592              scrollDirection: Axis.horizontal,
593              child: Row(
594                children: [
595                  _buildAddButton(),
596                  const SizedBox(width: 12),
597                  ..._brandPalette.map((c) => _buildColorCircle(c)).toList(),
598                ],
599              ),
600            ),
601          ],
602        ),
603      ),
604    );
605  }
606
607  Widget _buildColorCircle(Color color) {
608    return GestureDetector(
609      onTap: () {
610        _handleColorChange(color);
611        _saveToRecent();
612      },
613      child: Container(
614        width: 36,
615        height: 36,
616        margin: const EdgeInsets.only(right: 12),
617        decoration: BoxDecoration(
618          color: color,
619          shape: BoxShape.circle,
620          border: Border.all(color: Colors.grey.shade200, width: 1),
621          boxShadow: [
622            BoxShadow(
623              color: Colors.black.withOpacity(0.05),
624              blurRadius: 4,
625              offset: const Offset(0, 2),
626            ),
627          ],
628        ),
629      ),
630    );
631  }
632
633  Widget _buildAddButton() {
634    return Container(
635      width: 36,
636      height: 36,
637      decoration: BoxDecoration(
638        color: Colors.grey[100],
639        shape: BoxShape.circle,
640        border: Border.all(color: Colors.grey.shade300),
641      ),
642      child: const Icon(Icons.add, size: 20, color: Colors.black54),
643    );
644  }
645}