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