1use 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
42pub 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(), 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 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 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 #[must_use]
234 pub fn get_is_active(&self) -> bool {
235 self.active
236 }
237
238 #[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 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_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 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 .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 .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 .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 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 self.inner.as_widget().draw(
570 &tree.children[0],
571 renderer,
572 theme,
573 style,
574 layout,
575 cursor,
576 viewport,
577 );
578 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 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 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 {}
796fn 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)]
808pub 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}