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