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