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::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
40// TODO is this going to look correct enough?
41pub 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(), // 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        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 buttons
339            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            // canvas with gradient for the current color
346            // still needs the canvas and the handle to be drawn on it
347            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                // TODO copy paste input contents
415                .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        // Should we ensure the side padding is at least half the width of the handle?
443        .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                    // TODO get global colors from some cache?
456                    // TODO how to handle overflow? should this use a grid widget for the list or a horizontal scroll and a limit for the max?
457                    crate::widget::scrollable(
458                        Row::with_children(
459                            self.recent_colors
460                                .iter()
461                                .map(|c| {
462                                    let initial_srgb = palette::Srgb::from(*c);
463                                    let hsv = palette::Hsv::from_color(initial_srgb);
464                                    color_button(
465                                        Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
466                                        Some(*c),
467                                        Length::FillPortion(12),
468                                    )
469                                    .into()
470                                })
471                                .collect::<Vec<_>>(),
472                        )
473                        .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
474                        .spacing(spacing.space_xxs),
475                    )
476                    .width(self.width)
477                    .direction(iced_widget::scrollable::Direction::Horizontal(
478                        scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
479                    ))
480                }]
481                .spacing(spacing.space_xxs),
482            );
483        }
484
485        if let Some(reset_to_default) = self.reset_label.take() {
486            inner = inner.push(
487                column![
488                    horizontal::light().width(self.width),
489                    button::custom(
490                        text(reset_to_default)
491                            .width(self.width)
492                            .align_x(iced_core::Alignment::Center)
493                    )
494                    .width(self.width)
495                    .on_press(on_update(ColorPickerUpdate::Reset))
496                ]
497                .spacing(spacing.space_xs)
498                .width(self.width),
499            );
500        }
501        if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) {
502            inner = inner.push(
503                column![
504                    horizontal::light().width(self.width),
505                    button::custom(
506                        text(cancel)
507                            .width(self.width)
508                            .align_x(iced_core::Alignment::Center)
509                    )
510                    .width(self.width)
511                    .on_press(on_update(ColorPickerUpdate::Cancel)),
512                    button::custom(
513                        text(save)
514                            .width(self.width)
515                            .align_x(iced_core::Alignment::Center)
516                    )
517                    .width(self.width)
518                    .on_press(on_update(ColorPickerUpdate::AppliedColor))
519                    .class(Button::Suggested)
520                ]
521                .spacing(spacing.space_xs)
522                .width(self.width),
523            );
524        }
525
526        ColorPicker {
527            on_update,
528            inner: inner.into(),
529            width: self.width,
530            active_color: self.active_color,
531            must_clear_cache: self.must_clear_cache,
532        }
533    }
534}
535
536#[must_use]
537pub struct ColorPicker<'a, Message> {
538    pub(crate) on_update: fn(ColorPickerUpdate) -> Message,
539    width: Length,
540    active_color: palette::Hsv,
541    inner: Element<'a, Message>,
542    must_clear_cache: Rc<AtomicBool>,
543}
544
545impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
546where
547    Message: Clone + 'static,
548{
549    fn tag(&self) -> tree::Tag {
550        tree::Tag::of::<State>()
551    }
552
553    fn state(&self) -> tree::State {
554        tree::State::new(State::new())
555    }
556
557    fn diff(&mut self, tree: &mut Tree) {
558        tree.diff_children(std::slice::from_mut(&mut self.inner));
559    }
560
561    fn children(&self) -> Vec<Tree> {
562        vec![Tree::new(&self.inner)]
563    }
564
565    fn layout(
566        &self,
567        tree: &mut Tree,
568        renderer: &crate::Renderer,
569        limits: &layout::Limits,
570    ) -> layout::Node {
571        self.inner
572            .as_widget()
573            .layout(&mut tree.children[0], renderer, limits)
574    }
575
576    #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
577    fn draw(
578        &self,
579        tree: &Tree,
580        renderer: &mut crate::Renderer,
581        theme: &crate::Theme,
582        style: &renderer::Style,
583        layout: Layout<'_>,
584        cursor: mouse::Cursor,
585        viewport: &Rectangle,
586    ) {
587        let column_layout = layout;
588        // First draw children
589        self.inner.as_widget().draw(
590            &tree.children[0],
591            renderer,
592            theme,
593            style,
594            layout,
595            cursor,
596            viewport,
597        );
598        // Draw saturation value canvas
599        let state: &State = tree.state.downcast_ref();
600
601        let active_color = self.active_color;
602        let canvas_layout = column_layout.children().nth(1).unwrap();
603
604        if self
605            .must_clear_cache
606            .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
607            .unwrap_or_default()
608        {
609            state.canvas_cache.clear();
610        }
611        let geo = state
612            .canvas_cache
613            .draw(renderer, canvas_layout.bounds().size(), move |frame| {
614                let column_count = frame.width() as u16;
615                let row_count = frame.height() as u16;
616
617                for column in 0..column_count {
618                    for row in 0..row_count {
619                        let saturation = f32::from(column) / frame.width();
620                        let value = 1.0 - f32::from(row) / frame.height();
621
622                        let mut c = active_color;
623                        c.saturation = saturation;
624                        c.value = value;
625                        frame.fill_rectangle(
626                            iced::Point::new(f32::from(column), f32::from(row)),
627                            iced::Size::new(1.0, 1.0),
628                            Color::from(palette::Srgb::from_color(c)),
629                        );
630                    }
631                }
632            });
633
634        let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
635        iced_core::Renderer::with_translation(renderer, translation, |renderer| {
636            iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
637        });
638
639        let bounds = canvas_layout.bounds();
640        // Draw the handle on the saturation value canvas
641
642        let t = THEME.lock().unwrap().clone();
643        let t = t.cosmic();
644        let handle_radius = f32::from(t.space_xs()) / 2.0;
645        let (x, y) = (
646            self.active_color
647                .saturation
648                .mul_add(bounds.width, bounds.position().x)
649                - handle_radius,
650            (1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y)
651                - handle_radius,
652        );
653        renderer.with_layer(
654            Rectangle {
655                x,
656                y,
657                width: handle_radius.mul_add(2.0, 1.0),
658                height: handle_radius.mul_add(2.0, 1.0),
659            },
660            |renderer| {
661                renderer.fill_quad(
662                    Quad {
663                        bounds: Rectangle {
664                            x,
665                            y,
666                            width: handle_radius.mul_add(2.0, 1.0),
667                            height: handle_radius.mul_add(2.0, 1.0),
668                        },
669                        border: Border {
670                            width: 1.0,
671                            color: t.palette.neutral_5.into(),
672                            radius: (1.0 + handle_radius).into(),
673                        },
674                        shadow: Shadow::default(),
675                    },
676                    Color::TRANSPARENT,
677                );
678                renderer.fill_quad(
679                    Quad {
680                        bounds: Rectangle {
681                            x,
682                            y,
683                            width: handle_radius * 2.0,
684                            height: handle_radius * 2.0,
685                        },
686                        border: Border {
687                            width: 1.0,
688                            color: t.palette.neutral_10.into(),
689                            radius: handle_radius.into(),
690                        },
691                        shadow: Shadow::default(),
692                    },
693                    Color::TRANSPARENT,
694                );
695            },
696        );
697    }
698
699    fn overlay<'b>(
700        &'b mut self,
701        state: &'b mut Tree,
702        layout: Layout<'_>,
703        renderer: &crate::Renderer,
704        translation: Vector,
705    ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
706        self.inner
707            .as_widget_mut()
708            .overlay(&mut state.children[0], layout, renderer, translation)
709    }
710
711    fn on_event(
712        &mut self,
713        tree: &mut Tree,
714        event: Event,
715        layout: Layout<'_>,
716        cursor: mouse::Cursor,
717        renderer: &crate::Renderer,
718        clipboard: &mut dyn Clipboard,
719        shell: &mut Shell<'_, Message>,
720        viewport: &Rectangle,
721    ) -> event::Status {
722        // if the pointer is performing a drag, intercept pointer motion and button events
723        // else check if event is handled by child elements
724        // if the event is not handled by a child element, check if it is over the canvas when pressing a button
725        let state: &mut State = tree.state.downcast_mut();
726        let column_layout = layout;
727        if state.dragging {
728            let bounds = column_layout.children().nth(1).unwrap().bounds();
729            match event {
730                Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => {
731                    if let Some(mut clamped) = cursor.position() {
732                        clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width);
733                        clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height);
734                        let relative_pos = clamped - bounds.position();
735                        let (s, v) = (
736                            relative_pos.x / bounds.width,
737                            1.0 - relative_pos.y / bounds.height,
738                        );
739
740                        let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
741                        shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
742                    }
743                }
744                Event::Mouse(
745                    mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft,
746                ) => {
747                    shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
748                    state.dragging = false;
749                }
750                _ => return event::Status::Ignored,
751            };
752            return event::Status::Captured;
753        }
754
755        let column_tree = &mut tree.children[0];
756        if self.inner.as_widget_mut().on_event(
757            column_tree,
758            event.clone(),
759            column_layout,
760            cursor,
761            renderer,
762            clipboard,
763            shell,
764            viewport,
765        ) == event::Status::Captured
766        {
767            return event::Status::Captured;
768        }
769
770        match event {
771            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
772                let bounds = column_layout.children().nth(1).unwrap().bounds();
773                if let Some(point) = cursor.position_over(bounds) {
774                    let relative_pos = point - bounds.position();
775                    let (s, v) = (
776                        relative_pos.x / bounds.width,
777                        1.0 - relative_pos.y / bounds.height,
778                    );
779                    state.dragging = true;
780                    let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
781                    shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
782                    event::Status::Captured
783                } else {
784                    event::Status::Ignored
785                }
786            }
787            _ => event::Status::Ignored,
788        }
789    }
790
791    fn size(&self) -> Size<Length> {
792        Size::new(self.width, Length::Shrink)
793    }
794}
795
796#[derive(Debug, Default)]
797pub struct State {
798    canvas_cache: canvas::Cache,
799    dragging: bool,
800}
801
802impl State {
803    fn new() -> Self {
804        Self::default()
805    }
806}
807
808impl<Message> ColorPicker<'_, Message> where Message: Clone + 'static {}
809// TODO convert active color to hex or rgba
810fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
811    let srgb = palette::Srgb::from_color(c);
812    let hex = srgb.into_format::<u8>();
813    if is_hex {
814        format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue)
815    } else {
816        format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue)
817    }
818}
819
820#[allow(clippy::too_many_lines)]
821/// A button for selecting a color from a color picker.
822pub fn color_button<'a, Message: Clone + 'static>(
823    on_press: Option<Message>,
824    color: Option<Color>,
825    icon_portion: Length,
826) -> crate::widget::Button<'a, Message> {
827    let spacing = THEME.lock().unwrap().cosmic().spacing;
828
829    button::custom(if color.is_some() {
830        Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
831    } else {
832        Element::from(column![
833            vertical_space().height(Length::FillPortion(6)),
834            row![
835                horizontal_space().width(Length::FillPortion(6)),
836                Icon::from(
837                    icon::from_name("list-add-symbolic")
838                        .prefer_svg(true)
839                        .symbolic(true)
840                        .size(64)
841                )
842                .width(icon_portion)
843                .height(Length::Fill)
844                .content_fit(iced_core::ContentFit::Contain),
845                horizontal_space().width(Length::FillPortion(6)),
846            ]
847            .height(icon_portion)
848            .width(Length::Fill),
849            vertical_space().height(Length::FillPortion(6)),
850        ])
851    })
852    .width(Length::Fixed(f32::from(spacing.space_s)))
853    .height(Length::Fixed(f32::from(spacing.space_s)))
854    .on_press_maybe(on_press)
855    .class(crate::theme::Button::Custom {
856        active: Box::new(move |focused, theme| {
857            let cosmic = theme.cosmic();
858
859            let (outline_width, outline_color) = if focused {
860                (1.0, cosmic.accent_color().into())
861            } else {
862                (0.0, Color::TRANSPARENT)
863            };
864            let standard = theme.active(focused, false, &Button::Standard);
865            button::Style {
866                shadow_offset: Vector::default(),
867                background: color.map(Background::from).or(standard.background),
868                border_radius: cosmic.radius_xs().into(),
869                border_width: 1.0,
870                border_color: cosmic.palette.neutral_8.into(),
871                outline_width,
872                outline_color,
873                icon_color: None,
874                text_color: None,
875                overlay: None,
876            }
877        }),
878        disabled: Box::new(move |theme| {
879            let cosmic = theme.cosmic();
880
881            let standard = theme.disabled(&Button::Standard);
882            button::Style {
883                shadow_offset: Vector::default(),
884                background: color.map(Background::from).or(standard.background),
885                border_radius: cosmic.radius_xs().into(),
886                border_width: 1.0,
887                border_color: cosmic.palette.neutral_8.into(),
888                outline_width: 0.0,
889                outline_color: Color::TRANSPARENT,
890                icon_color: None,
891                text_color: None,
892                overlay: None,
893            }
894        }),
895        hovered: Box::new(move |focused, theme| {
896            let cosmic = theme.cosmic();
897
898            let (outline_width, outline_color) = if focused {
899                (1.0, cosmic.accent_color().into())
900            } else {
901                (0.0, Color::TRANSPARENT)
902            };
903
904            let standard = theme.hovered(focused, false, &Button::Standard);
905            button::Style {
906                shadow_offset: Vector::default(),
907                background: color.map(Background::from).or(standard.background),
908                border_radius: cosmic.radius_xs().into(),
909                border_width: 1.0,
910                border_color: cosmic.palette.neutral_8.into(),
911                outline_width,
912                outline_color,
913                icon_color: None,
914                text_color: None,
915                overlay: None,
916            }
917        }),
918        pressed: Box::new(move |focused, theme| {
919            let cosmic = theme.cosmic();
920
921            let (outline_width, outline_color) = if focused {
922                (1.0, cosmic.accent_color().into())
923            } else {
924                (0.0, Color::TRANSPARENT)
925            };
926
927            let standard = theme.pressed(focused, false, &Button::Standard);
928            button::Style {
929                shadow_offset: Vector::default(),
930                background: color.map(Background::from).or(standard.background),
931                border_radius: cosmic.radius_xs().into(),
932                border_width: 1.0,
933                border_color: cosmic.palette.neutral_8.into(),
934                outline_width,
935                outline_color,
936                icon_color: None,
937                text_color: None,
938                overlay: None,
939            }
940        }),
941    })
942}
943
944impl<'a, Message> From<ColorPicker<'a, Message>>
945    for iced::Element<'a, Message, crate::Theme, crate::Renderer>
946where
947    Message: 'static + Clone,
948{
949    fn from(
950        picker: ColorPicker<'a, Message>,
951    ) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> {
952        Element::new(picker)
953    }
954}