cosmic/widget/color_picker/
mod.rs

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