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