Skip to main content

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