1use std::borrow::Cow;
7use std::iter;
8use std::rc::Rc;
9use std::sync::LazyLock;
10use std::sync::atomic::{AtomicBool, Ordering};
11use std::time::{Duration, Instant};
12
13use crate::Element;
14use crate::theme::iced::Slider;
15use crate::theme::{Button, THEME};
16use crate::widget::{button::Catalog, container, segmented_button::Entity, slider};
17use derive_setters::Setters;
18use iced::Task;
19use iced_core::event::{self, Event};
20use iced_core::gradient::{ColorStop, Linear};
21use iced_core::renderer::Quad;
22use iced_core::widget::{Tree, tree};
23use iced_core::{
24    Background, Border, Clipboard, Color, Layout, Length, Radians, Rectangle, Renderer, Shadow,
25    Shell, Size, Vector, Widget, layout, mouse, renderer,
26};
27
28use iced_widget::slider::HandleShape;
29use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space};
30use palette::{FromColor, RgbHue};
31
32use super::divider::horizontal;
33use super::icon::{self, from_name};
34use super::segmented_button::{self, SingleSelect};
35use super::{Icon, button, segmented_control, text, text_input, tooltip};
36
37#[doc(inline)]
38pub use ColorPickerModel as Model;
39
40pub static HSV_RAINBOW: LazyLock<Vec<Color>> = LazyLock::new(|| {
42    (0u16..8)
43        .map(|h| {
44            Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const(
45                RgbHue::new(f32::from(h) * 360.0 / 7.0),
46                1.0,
47                1.0,
48            )))
49        })
50        .collect()
51});
52
53const MAX_RECENT: usize = 20;
54
55#[derive(Debug, Clone)]
56pub enum ColorPickerUpdate {
57    ActiveColor(palette::Hsv),
58    ActionFinished,
59    Input(String),
60    AppliedColor,
61    Reset,
62    ActivateSegmented(Entity),
63    Copied(Instant),
64    Cancel,
65    ToggleColorPicker,
66}
67
68#[derive(Setters)]
69pub struct ColorPickerModel {
70    #[setters(skip)]
71    segmented_model: segmented_button::Model<SingleSelect>,
72    #[setters(skip)]
73    active_color: palette::Hsv,
74    #[setters(skip)]
75    save_next: Option<Color>,
76    #[setters(skip)]
77    input_color: String,
78    #[setters(skip)]
79    applied_color: Option<Color>,
80    #[setters(skip)]
81    fallback_color: Option<Color>,
82    #[setters(skip)]
83    recent_colors: Vec<Color>,
84    active: bool,
85    width: Length,
86    height: Length,
87    #[setters(skip)]
88    must_clear_cache: Rc<AtomicBool>,
89    #[setters(skip)]
90    copied_at: Option<Instant>,
91}
92
93impl ColorPickerModel {
94    #[must_use]
95    pub fn new(
96        hex: impl Into<Cow<'static, str>> + Clone,
97        rgb: impl Into<Cow<'static, str>> + Clone,
98        fallback_color: Option<Color>,
99        initial_color: Option<Color>,
100    ) -> Self {
101        let initial = initial_color.or(fallback_color);
102        let initial_srgb = palette::Srgb::from(initial.unwrap_or(Color::BLACK));
103        let hsv = palette::Hsv::from_color(initial_srgb);
104        Self {
105            segmented_model: segmented_button::Model::builder()
106                .insert(move |b| b.text(hex.clone()).activate())
107                .insert(move |b| b.text(rgb.clone()))
108                .build(),
109            active_color: hsv,
110            save_next: None,
111            input_color: color_to_string(hsv, true),
112            applied_color: initial,
113            fallback_color,
114            recent_colors: Vec::new(), active: false,
116            width: Length::Fixed(300.0),
117            height: Length::Fixed(200.0),
118            must_clear_cache: Rc::new(AtomicBool::new(false)),
119            copied_at: None,
120        }
121    }
122
123    pub fn picker_button<
126        'a,
127        Message: 'static + std::clone::Clone,
128        T: Fn(ColorPickerUpdate) -> Message,
129    >(
130        &self,
131        f: T,
132        icon_portion: Option<u16>,
133    ) -> crate::widget::Button<'a, Message> {
134        color_button(
135            Some(f(ColorPickerUpdate::ToggleColorPicker)),
136            self.applied_color,
137            Length::FillPortion(icon_portion.unwrap_or(12)),
138        )
139    }
140
141    pub fn update<Message>(&mut self, update: ColorPickerUpdate) -> Task<Message> {
142        match update {
143            ColorPickerUpdate::ActiveColor(c) => {
144                self.must_clear_cache.store(true, Ordering::SeqCst);
145                self.input_color = color_to_string(c, self.is_hex());
146                if let Some(to_save) = self.save_next.take() {
147                    self.recent_colors.insert(0, to_save);
148                    self.recent_colors.truncate(MAX_RECENT);
149                }
150                self.active_color = c;
151                self.copied_at = None;
152            }
153            ColorPickerUpdate::AppliedColor => {
154                let srgb = palette::Srgb::from_color(self.active_color);
155                if let Some(applied_color) = self.applied_color.take() {
156                    self.recent_colors.push(applied_color);
157                }
158                self.applied_color = Some(Color::from(srgb));
159                self.active = false;
160            }
161            ColorPickerUpdate::ActivateSegmented(e) => {
162                self.segmented_model.activate(e);
163                self.input_color = color_to_string(self.active_color, self.is_hex());
164                self.copied_at = None;
165            }
166            ColorPickerUpdate::Copied(t) => {
167                self.copied_at = Some(t);
168
169                return iced::clipboard::write(self.input_color.clone());
170            }
171            ColorPickerUpdate::Reset => {
172                self.must_clear_cache.store(true, Ordering::SeqCst);
173
174                let initial_srgb = palette::Srgb::from(self.fallback_color.unwrap_or(Color::BLACK));
175                let hsv = palette::Hsv::from_color(initial_srgb);
176                self.active_color = hsv;
177                self.applied_color = self.fallback_color;
178                self.copied_at = None;
179            }
180            ColorPickerUpdate::Cancel => {
181                self.must_clear_cache.store(true, Ordering::SeqCst);
182
183                self.active = false;
184                self.copied_at = None;
185            }
186            ColorPickerUpdate::Input(c) => {
187                self.must_clear_cache.store(true, Ordering::SeqCst);
188
189                self.input_color = c;
190                self.copied_at = None;
191                if let Ok(c) = self.input_color.parse::<css_color::Srgb>() {
193                    self.active_color =
194                        palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue));
195                }
196            }
197            ColorPickerUpdate::ActionFinished => {
198                let srgb = palette::Srgb::from_color(self.active_color);
199                if let Some(applied_color) = self.applied_color.take() {
200                    self.recent_colors.push(applied_color);
201                }
202                self.applied_color = Some(Color::from(srgb));
203                self.active = false;
204                self.save_next = Some(Color::from(srgb));
205            }
206            ColorPickerUpdate::ToggleColorPicker => {
207                self.must_clear_cache.store(true, Ordering::SeqCst);
208                self.active = !self.active;
209                self.copied_at = None;
210            }
211        };
212        Task::none()
213    }
214
215    #[must_use]
216    pub fn is_hex(&self) -> bool {
217        self.segmented_model.position(self.segmented_model.active()) == Some(0)
218    }
219
220    #[must_use]
222    pub fn get_is_active(&self) -> bool {
223        self.active
224    }
225
226    #[must_use]
228    pub fn get_applied_color(&self) -> Option<Color> {
229        self.applied_color
230    }
231
232    #[must_use]
233    pub fn builder<Message>(
234        &self,
235        on_update: fn(ColorPickerUpdate) -> Message,
236    ) -> ColorPickerBuilder<'_, Message> {
237        ColorPickerBuilder {
238            model: &self.segmented_model,
239            active_color: self.active_color,
240            recent_colors: &self.recent_colors,
241            on_update,
242            width: self.width,
243            height: self.height,
244            must_clear_cache: self.must_clear_cache.clone(),
245            input_color: &self.input_color,
246            reset_label: None,
247            save_label: None,
248            cancel_label: None,
249            copied_at: self.copied_at,
250        }
251    }
252}
253
254#[derive(Setters, Clone)]
255pub struct ColorPickerBuilder<'a, Message> {
256    #[setters(skip)]
257    model: &'a segmented_button::Model<SingleSelect>,
258    #[setters(skip)]
259    active_color: palette::Hsv,
260    #[setters(skip)]
261    input_color: &'a str,
262    #[setters(skip)]
263    on_update: fn(ColorPickerUpdate) -> Message,
264    #[setters(skip)]
265    recent_colors: &'a Vec<Color>,
266    #[setters(skip)]
267    must_clear_cache: Rc<AtomicBool>,
268    #[setters(skip)]
269    copied_at: Option<Instant>,
270    width: Length,
272    height: Length,
273    #[setters(strip_option, into)]
274    reset_label: Option<Cow<'a, str>>,
275    #[setters(strip_option, into)]
276    save_label: Option<Cow<'a, str>>,
277    #[setters(strip_option, into)]
278    cancel_label: Option<Cow<'a, str>>,
279}
280
281impl<'a, Message> ColorPickerBuilder<'a, Message>
282where
283    Message: Clone + 'static,
284{
285    #[allow(clippy::too_many_lines)]
286    pub fn build<T: Into<Cow<'a, str>> + 'a>(
287        mut self,
288        recent_colors_label: T,
289        copy_to_clipboard_label: T,
290        copied_to_clipboard_label: T,
291    ) -> ColorPicker<'a, Message> {
292        fn rail_backgrounds(hue: f32) -> (Background, Background) {
293            let pivot = hue * 7.0 / 360.;
294
295            let low_end = pivot.floor() as usize;
296            let high_start = pivot.ceil() as usize;
297            let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
298            let low_range = HSV_RAINBOW[0..=low_end]
299                .iter()
300                .enumerate()
301                .map(|(i, color)| ColorStop {
302                    color: *color,
303                    offset: i as f32 / pivot.max(0.0001),
304                })
305                .chain(iter::once(ColorStop {
306                    color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
307                    offset: 1.,
308                }))
309                .collect::<Vec<_>>();
310            let high_range = iter::once(ColorStop {
311                color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
312                offset: 0.,
313            })
314            .chain(
315                HSV_RAINBOW[high_start..]
316                    .iter()
317                    .enumerate()
318                    .map(|(i, color)| ColorStop {
319                        color: *color,
320                        offset: (i as f32 + (1. - pivot.fract())) / (7. - pivot).max(0.0001),
321                    }),
322            )
323            .collect::<Vec<_>>();
324            (
325                Background::Gradient(iced::Gradient::Linear(
326                    Linear::new(Radians(90.0)).add_stops(low_range),
327                )),
328                Background::Gradient(iced::Gradient::Linear(
329                    Linear::new(Radians(90.0)).add_stops(high_range),
330                )),
331            )
332        }
333
334        let on_update = self.on_update;
335        let spacing = THEME.lock().unwrap().cosmic().spacing;
336
337        let mut inner = column![
338            segmented_control::horizontal(self.model)
340                .on_activate(Box::new(move |e| on_update(
341                    ColorPickerUpdate::ActivateSegmented(e)
342                )))
343                .minimum_button_width(0)
344                .width(self.width),
345            container(vertical_space().height(self.height))
348                .width(self.width)
349                .height(self.height),
350            slider(
351                0.001..=359.99,
352                self.active_color.hue.into_positive_degrees(),
353                move |v| {
354                    let mut new = self.active_color;
355                    new.hue = v.into();
356                    on_update(ColorPickerUpdate::ActiveColor(new))
357                }
358            )
359            .on_release(on_update(ColorPickerUpdate::ActionFinished))
360            .class(Slider::Custom {
361                active: Rc::new(move |t| {
362                    let cosmic = t.cosmic();
363                    let mut a =
364                        slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
365                    let hue = self.active_color.hue.into_positive_degrees();
366                    a.rail.backgrounds = rail_backgrounds(hue);
367                    a.rail.width = 8.0;
368                    a.handle.background = Color::TRANSPARENT.into();
369                    a.handle.shape = HandleShape::Circle { radius: 8.0 };
370                    a.handle.border_color = cosmic.palette.neutral_10.into();
371                    a.handle.border_width = 4.0;
372                    a
373                }),
374                hovered: Rc::new(move |t| {
375                    let cosmic = t.cosmic();
376                    let mut a =
377                        slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
378                    let hue = self.active_color.hue.into_positive_degrees();
379                    a.rail.backgrounds = rail_backgrounds(hue);
380                    a.rail.width = 8.0;
381                    a.handle.background = Color::TRANSPARENT.into();
382                    a.handle.shape = HandleShape::Circle { radius: 8.0 };
383                    a.handle.border_color = cosmic.palette.neutral_10.into();
384                    a.handle.border_width = 4.0;
385                    a
386                }),
387                dragging: Rc::new(move |t| {
388                    let cosmic = t.cosmic();
389                    let mut a =
390                        slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
391                    let hue = self.active_color.hue.into_positive_degrees();
392                    a.rail.backgrounds = rail_backgrounds(hue);
393                    a.rail.width = 8.0;
394                    a.handle.background = Color::TRANSPARENT.into();
395                    a.handle.shape = HandleShape::Circle { radius: 8.0 };
396                    a.handle.border_color = cosmic.palette.neutral_10.into();
397                    a.handle.border_width = 4.0;
398                    a
399                }),
400            })
401            .width(self.width),
402            text_input("", self.input_color)
403                .on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
404                .on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
405                .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor))
406                .leading_icon(
407                    color_button(
408                        None,
409                        Some(Color::from(palette::Srgb::from_color(self.active_color))),
410                        Length::FillPortion(12)
411                    )
412                    .into()
413                )
414                .trailing_icon({
416                    let button = button::custom(crate::widget::icon(
417                        from_name("edit-copy-symbolic").size(spacing.space_s).into(),
418                    ))
419                    .on_press(on_update(ColorPickerUpdate::Copied(Instant::now())))
420                    .class(Button::Text);
421
422                    match self.copied_at.take() {
423                        Some(t) if Instant::now().duration_since(t) > Duration::from_secs(2) => {
424                            button.into()
425                        }
426                        Some(_) => tooltip(
427                            button,
428                            text(copied_to_clipboard_label),
429                            iced_widget::tooltip::Position::Bottom,
430                        )
431                        .into(),
432                        None => tooltip(
433                            button,
434                            text(copy_to_clipboard_label),
435                            iced_widget::tooltip::Position::Bottom,
436                        )
437                        .into(),
438                    }
439                })
440                .width(self.width),
441        ]
442        .padding([
444            spacing.space_none,
445            spacing.space_s,
446            spacing.space_s,
447            spacing.space_s,
448        ])
449        .spacing(spacing.space_s);
450
451        if !self.recent_colors.is_empty() {
452            inner = inner.push(horizontal::light().width(self.width));
453            inner = inner.push(
454                column![text(recent_colors_label), {
455                    crate::widget::scrollable(
458                        Row::with_children(self.recent_colors.iter().map(|c| {
459                            let initial_srgb = palette::Srgb::from(*c);
460                            let hsv = palette::Hsv::from_color(initial_srgb);
461                            color_button(
462                                Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
463                                Some(*c),
464                                Length::FillPortion(12),
465                            )
466                            .into()
467                        }))
468                        .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
469                        .spacing(spacing.space_xxs),
470                    )
471                    .width(self.width)
472                    .direction(iced_widget::scrollable::Direction::Horizontal(
473                        scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
474                    ))
475                }]
476                .spacing(spacing.space_xxs),
477            );
478        }
479
480        if let Some(reset_to_default) = self.reset_label.take() {
481            inner = inner.push(
482                column![
483                    horizontal::light().width(self.width),
484                    button::custom(
485                        text(reset_to_default)
486                            .width(self.width)
487                            .align_x(iced_core::Alignment::Center)
488                    )
489                    .width(self.width)
490                    .on_press(on_update(ColorPickerUpdate::Reset))
491                ]
492                .spacing(spacing.space_xs)
493                .width(self.width),
494            );
495        }
496        if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) {
497            inner = inner.push(
498                column![
499                    horizontal::light().width(self.width),
500                    button::custom(
501                        text(cancel)
502                            .width(self.width)
503                            .align_x(iced_core::Alignment::Center)
504                    )
505                    .width(self.width)
506                    .on_press(on_update(ColorPickerUpdate::Cancel)),
507                    button::custom(
508                        text(save)
509                            .width(self.width)
510                            .align_x(iced_core::Alignment::Center)
511                    )
512                    .width(self.width)
513                    .on_press(on_update(ColorPickerUpdate::AppliedColor))
514                    .class(Button::Suggested)
515                ]
516                .spacing(spacing.space_xs)
517                .width(self.width),
518            );
519        }
520
521        ColorPicker {
522            on_update,
523            inner: inner.into(),
524            width: self.width,
525            active_color: self.active_color,
526            must_clear_cache: self.must_clear_cache,
527        }
528    }
529}
530
531#[must_use]
532pub struct ColorPicker<'a, Message> {
533    pub(crate) on_update: fn(ColorPickerUpdate) -> Message,
534    width: Length,
535    active_color: palette::Hsv,
536    inner: Element<'a, Message>,
537    must_clear_cache: Rc<AtomicBool>,
538}
539
540impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
541where
542    Message: Clone + 'static,
543{
544    fn tag(&self) -> tree::Tag {
545        tree::Tag::of::<State>()
546    }
547
548    fn state(&self) -> tree::State {
549        tree::State::new(State::new())
550    }
551
552    fn diff(&mut self, tree: &mut Tree) {
553        tree.diff_children(std::slice::from_mut(&mut self.inner));
554    }
555
556    fn children(&self) -> Vec<Tree> {
557        vec![Tree::new(&self.inner)]
558    }
559
560    fn layout(
561        &self,
562        tree: &mut Tree,
563        renderer: &crate::Renderer,
564        limits: &layout::Limits,
565    ) -> layout::Node {
566        self.inner
567            .as_widget()
568            .layout(&mut tree.children[0], renderer, limits)
569    }
570
571    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
572    fn draw(
573        &self,
574        tree: &Tree,
575        renderer: &mut crate::Renderer,
576        theme: &crate::Theme,
577        style: &renderer::Style,
578        layout: Layout<'_>,
579        cursor: mouse::Cursor,
580        viewport: &Rectangle,
581    ) {
582        let column_layout = layout;
583        self.inner.as_widget().draw(
585            &tree.children[0],
586            renderer,
587            theme,
588            style,
589            layout,
590            cursor,
591            viewport,
592        );
593        let state: &State = tree.state.downcast_ref();
595
596        let active_color = self.active_color;
597        let canvas_layout = column_layout.children().nth(1).unwrap();
598
599        if self
600            .must_clear_cache
601            .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
602            .unwrap_or_default()
603        {
604            state.canvas_cache.clear();
605        }
606        let geo = state
607            .canvas_cache
608            .draw(renderer, canvas_layout.bounds().size(), move |frame| {
609                let column_count = frame.width() as u16;
610                let row_count = frame.height() as u16;
611
612                for column in 0..column_count {
613                    for row in 0..row_count {
614                        let saturation = f32::from(column) / frame.width();
615                        let value = 1.0 - f32::from(row) / frame.height();
616
617                        let mut c = active_color;
618                        c.saturation = saturation;
619                        c.value = value;
620                        frame.fill_rectangle(
621                            iced::Point::new(f32::from(column), f32::from(row)),
622                            iced::Size::new(1.0, 1.0),
623                            Color::from(palette::Srgb::from_color(c)),
624                        );
625                    }
626                }
627            });
628
629        let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
630        iced_core::Renderer::with_translation(renderer, translation, |renderer| {
631            iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
632        });
633
634        let bounds = canvas_layout.bounds();
635        let t = THEME.lock().unwrap().clone();
638        let t = t.cosmic();
639        let handle_radius = f32::from(t.space_xs()) / 2.0;
640        let (x, y) = (
641            self.active_color
642                .saturation
643                .mul_add(bounds.width, bounds.position().x)
644                - handle_radius,
645            (1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y)
646                - handle_radius,
647        );
648        renderer.with_layer(
649            Rectangle {
650                x,
651                y,
652                width: handle_radius.mul_add(2.0, 1.0),
653                height: handle_radius.mul_add(2.0, 1.0),
654            },
655            |renderer| {
656                renderer.fill_quad(
657                    Quad {
658                        bounds: Rectangle {
659                            x,
660                            y,
661                            width: handle_radius.mul_add(2.0, 1.0),
662                            height: handle_radius.mul_add(2.0, 1.0),
663                        },
664                        border: Border {
665                            width: 1.0,
666                            color: t.palette.neutral_5.into(),
667                            radius: (1.0 + handle_radius).into(),
668                        },
669                        shadow: Shadow::default(),
670                    },
671                    Color::TRANSPARENT,
672                );
673                renderer.fill_quad(
674                    Quad {
675                        bounds: Rectangle {
676                            x,
677                            y,
678                            width: handle_radius * 2.0,
679                            height: handle_radius * 2.0,
680                        },
681                        border: Border {
682                            width: 1.0,
683                            color: t.palette.neutral_10.into(),
684                            radius: handle_radius.into(),
685                        },
686                        shadow: Shadow::default(),
687                    },
688                    Color::TRANSPARENT,
689                );
690            },
691        );
692    }
693
694    fn overlay<'b>(
695        &'b mut self,
696        state: &'b mut Tree,
697        layout: Layout<'_>,
698        renderer: &crate::Renderer,
699        translation: Vector,
700    ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
701        self.inner
702            .as_widget_mut()
703            .overlay(&mut state.children[0], layout, renderer, translation)
704    }
705
706    fn on_event(
707        &mut self,
708        tree: &mut Tree,
709        event: Event,
710        layout: Layout<'_>,
711        cursor: mouse::Cursor,
712        renderer: &crate::Renderer,
713        clipboard: &mut dyn Clipboard,
714        shell: &mut Shell<'_, Message>,
715        viewport: &Rectangle,
716    ) -> event::Status {
717        let state: &mut State = tree.state.downcast_mut();
721        let column_layout = layout;
722        if state.dragging {
723            let bounds = column_layout.children().nth(1).unwrap().bounds();
724            match event {
725                Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => {
726                    if let Some(mut clamped) = cursor.position() {
727                        clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width);
728                        clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height);
729                        let relative_pos = clamped - bounds.position();
730                        let (s, v) = (
731                            relative_pos.x / bounds.width,
732                            1.0 - relative_pos.y / bounds.height,
733                        );
734
735                        let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
736                        shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
737                    }
738                }
739                Event::Mouse(
740                    mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft,
741                ) => {
742                    shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
743                    state.dragging = false;
744                }
745                _ => return event::Status::Ignored,
746            };
747            return event::Status::Captured;
748        }
749
750        let column_tree = &mut tree.children[0];
751        if self.inner.as_widget_mut().on_event(
752            column_tree,
753            event.clone(),
754            column_layout,
755            cursor,
756            renderer,
757            clipboard,
758            shell,
759            viewport,
760        ) == event::Status::Captured
761        {
762            return event::Status::Captured;
763        }
764
765        match event {
766            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
767                let bounds = column_layout.children().nth(1).unwrap().bounds();
768                if let Some(point) = cursor.position_over(bounds) {
769                    let relative_pos = point - bounds.position();
770                    let (s, v) = (
771                        relative_pos.x / bounds.width,
772                        1.0 - relative_pos.y / bounds.height,
773                    );
774                    state.dragging = true;
775                    let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
776                    shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
777                    event::Status::Captured
778                } else {
779                    event::Status::Ignored
780                }
781            }
782            _ => event::Status::Ignored,
783        }
784    }
785
786    fn size(&self) -> Size<Length> {
787        Size::new(self.width, Length::Shrink)
788    }
789}
790
791#[derive(Debug, Default)]
792pub struct State {
793    canvas_cache: canvas::Cache,
794    dragging: bool,
795}
796
797impl State {
798    fn new() -> Self {
799        Self::default()
800    }
801}
802
803impl<Message> ColorPicker<'_, Message> where Message: Clone + 'static {}
804fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
806    let srgb = palette::Srgb::from_color(c);
807    let hex = srgb.into_format::<u8>();
808    if is_hex {
809        format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue)
810    } else {
811        format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue)
812    }
813}
814
815#[allow(clippy::too_many_lines)]
816pub fn color_button<'a, Message: Clone + 'static>(
818    on_press: Option<Message>,
819    color: Option<Color>,
820    icon_portion: Length,
821) -> crate::widget::Button<'a, Message> {
822    let spacing = THEME.lock().unwrap().cosmic().spacing;
823
824    button::custom(if color.is_some() {
825        Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
826    } else {
827        Element::from(column![
828            vertical_space().height(Length::FillPortion(6)),
829            row![
830                horizontal_space().width(Length::FillPortion(6)),
831                Icon::from(
832                    icon::from_name("list-add-symbolic")
833                        .prefer_svg(true)
834                        .symbolic(true)
835                        .size(64)
836                )
837                .width(icon_portion)
838                .height(Length::Fill)
839                .content_fit(iced_core::ContentFit::Contain),
840                horizontal_space().width(Length::FillPortion(6)),
841            ]
842            .height(icon_portion)
843            .width(Length::Fill),
844            vertical_space().height(Length::FillPortion(6)),
845        ])
846    })
847    .width(Length::Fixed(f32::from(spacing.space_s)))
848    .height(Length::Fixed(f32::from(spacing.space_s)))
849    .on_press_maybe(on_press)
850    .class(crate::theme::Button::Custom {
851        active: Box::new(move |focused, theme| {
852            let cosmic = theme.cosmic();
853
854            let (outline_width, outline_color) = if focused {
855                (1.0, cosmic.accent_color().into())
856            } else {
857                (0.0, Color::TRANSPARENT)
858            };
859            let standard = theme.active(focused, false, &Button::Standard);
860            button::Style {
861                shadow_offset: Vector::default(),
862                background: color.map(Background::from).or(standard.background),
863                border_radius: cosmic.radius_xs().into(),
864                border_width: 1.0,
865                border_color: cosmic.palette.neutral_8.into(),
866                outline_width,
867                outline_color,
868                icon_color: None,
869                text_color: None,
870                overlay: None,
871            }
872        }),
873        disabled: Box::new(move |theme| {
874            let cosmic = theme.cosmic();
875
876            let standard = theme.disabled(&Button::Standard);
877            button::Style {
878                shadow_offset: Vector::default(),
879                background: color.map(Background::from).or(standard.background),
880                border_radius: cosmic.radius_xs().into(),
881                border_width: 1.0,
882                border_color: cosmic.palette.neutral_8.into(),
883                outline_width: 0.0,
884                outline_color: Color::TRANSPARENT,
885                icon_color: None,
886                text_color: None,
887                overlay: None,
888            }
889        }),
890        hovered: Box::new(move |focused, theme| {
891            let cosmic = theme.cosmic();
892
893            let (outline_width, outline_color) = if focused {
894                (1.0, cosmic.accent_color().into())
895            } else {
896                (0.0, Color::TRANSPARENT)
897            };
898
899            let standard = theme.hovered(focused, false, &Button::Standard);
900            button::Style {
901                shadow_offset: Vector::default(),
902                background: color.map(Background::from).or(standard.background),
903                border_radius: cosmic.radius_xs().into(),
904                border_width: 1.0,
905                border_color: cosmic.palette.neutral_8.into(),
906                outline_width,
907                outline_color,
908                icon_color: None,
909                text_color: None,
910                overlay: None,
911            }
912        }),
913        pressed: Box::new(move |focused, theme| {
914            let cosmic = theme.cosmic();
915
916            let (outline_width, outline_color) = if focused {
917                (1.0, cosmic.accent_color().into())
918            } else {
919                (0.0, Color::TRANSPARENT)
920            };
921
922            let standard = theme.pressed(focused, false, &Button::Standard);
923            button::Style {
924                shadow_offset: Vector::default(),
925                background: color.map(Background::from).or(standard.background),
926                border_radius: cosmic.radius_xs().into(),
927                border_width: 1.0,
928                border_color: cosmic.palette.neutral_8.into(),
929                outline_width,
930                outline_color,
931                icon_color: None,
932                text_color: None,
933                overlay: None,
934            }
935        }),
936    })
937}
938
939impl<'a, Message> From<ColorPicker<'a, Message>>
940    for iced::Element<'a, Message, crate::Theme, crate::Renderer>
941where
942    Message: 'static + Clone,
943{
944    fn from(
945        picker: ColorPicker<'a, Message>,
946    ) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> {
947        Element::new(picker)
948    }
949}