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(
459 self.recent_colors
460 .iter()
461 .map(|c| {
462 let initial_srgb = palette::Srgb::from(*c);
463 let hsv = palette::Hsv::from_color(initial_srgb);
464 color_button(
465 Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
466 Some(*c),
467 Length::FillPortion(12),
468 )
469 .into()
470 })
471 .collect::<Vec<_>>(),
472 )
473 .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
474 .spacing(spacing.space_xxs),
475 )
476 .width(self.width)
477 .direction(iced_widget::scrollable::Direction::Horizontal(
478 scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
479 ))
480 }]
481 .spacing(spacing.space_xxs),
482 );
483 }
484
485 if let Some(reset_to_default) = self.reset_label.take() {
486 inner = inner.push(
487 column![
488 horizontal::light().width(self.width),
489 button::custom(
490 text(reset_to_default)
491 .width(self.width)
492 .align_x(iced_core::Alignment::Center)
493 )
494 .width(self.width)
495 .on_press(on_update(ColorPickerUpdate::Reset))
496 ]
497 .spacing(spacing.space_xs)
498 .width(self.width),
499 );
500 }
501 if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) {
502 inner = inner.push(
503 column![
504 horizontal::light().width(self.width),
505 button::custom(
506 text(cancel)
507 .width(self.width)
508 .align_x(iced_core::Alignment::Center)
509 )
510 .width(self.width)
511 .on_press(on_update(ColorPickerUpdate::Cancel)),
512 button::custom(
513 text(save)
514 .width(self.width)
515 .align_x(iced_core::Alignment::Center)
516 )
517 .width(self.width)
518 .on_press(on_update(ColorPickerUpdate::AppliedColor))
519 .class(Button::Suggested)
520 ]
521 .spacing(spacing.space_xs)
522 .width(self.width),
523 );
524 }
525
526 ColorPicker {
527 on_update,
528 inner: inner.into(),
529 width: self.width,
530 active_color: self.active_color,
531 must_clear_cache: self.must_clear_cache,
532 }
533 }
534}
535
536#[must_use]
537pub struct ColorPicker<'a, Message> {
538 pub(crate) on_update: fn(ColorPickerUpdate) -> Message,
539 width: Length,
540 active_color: palette::Hsv,
541 inner: Element<'a, Message>,
542 must_clear_cache: Rc<AtomicBool>,
543}
544
545impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
546where
547 Message: Clone + 'static,
548{
549 fn tag(&self) -> tree::Tag {
550 tree::Tag::of::<State>()
551 }
552
553 fn state(&self) -> tree::State {
554 tree::State::new(State::new())
555 }
556
557 fn diff(&mut self, tree: &mut Tree) {
558 tree.diff_children(std::slice::from_mut(&mut self.inner));
559 }
560
561 fn children(&self) -> Vec<Tree> {
562 vec![Tree::new(&self.inner)]
563 }
564
565 fn layout(
566 &self,
567 tree: &mut Tree,
568 renderer: &crate::Renderer,
569 limits: &layout::Limits,
570 ) -> layout::Node {
571 self.inner
572 .as_widget()
573 .layout(&mut tree.children[0], renderer, limits)
574 }
575
576 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
577 fn draw(
578 &self,
579 tree: &Tree,
580 renderer: &mut crate::Renderer,
581 theme: &crate::Theme,
582 style: &renderer::Style,
583 layout: Layout<'_>,
584 cursor: mouse::Cursor,
585 viewport: &Rectangle,
586 ) {
587 let column_layout = layout;
588 self.inner.as_widget().draw(
590 &tree.children[0],
591 renderer,
592 theme,
593 style,
594 layout,
595 cursor,
596 viewport,
597 );
598 let state: &State = tree.state.downcast_ref();
600
601 let active_color = self.active_color;
602 let canvas_layout = column_layout.children().nth(1).unwrap();
603
604 if self
605 .must_clear_cache
606 .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
607 .unwrap_or_default()
608 {
609 state.canvas_cache.clear();
610 }
611 let geo = state
612 .canvas_cache
613 .draw(renderer, canvas_layout.bounds().size(), move |frame| {
614 let column_count = frame.width() as u16;
615 let row_count = frame.height() as u16;
616
617 for column in 0..column_count {
618 for row in 0..row_count {
619 let saturation = f32::from(column) / frame.width();
620 let value = 1.0 - f32::from(row) / frame.height();
621
622 let mut c = active_color;
623 c.saturation = saturation;
624 c.value = value;
625 frame.fill_rectangle(
626 iced::Point::new(f32::from(column), f32::from(row)),
627 iced::Size::new(1.0, 1.0),
628 Color::from(palette::Srgb::from_color(c)),
629 );
630 }
631 }
632 });
633
634 let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
635 iced_core::Renderer::with_translation(renderer, translation, |renderer| {
636 iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
637 });
638
639 let bounds = canvas_layout.bounds();
640 let t = THEME.lock().unwrap().clone();
643 let t = t.cosmic();
644 let handle_radius = f32::from(t.space_xs()) / 2.0;
645 let (x, y) = (
646 self.active_color
647 .saturation
648 .mul_add(bounds.width, bounds.position().x)
649 - handle_radius,
650 (1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y)
651 - handle_radius,
652 );
653 renderer.with_layer(
654 Rectangle {
655 x,
656 y,
657 width: handle_radius.mul_add(2.0, 1.0),
658 height: handle_radius.mul_add(2.0, 1.0),
659 },
660 |renderer| {
661 renderer.fill_quad(
662 Quad {
663 bounds: Rectangle {
664 x,
665 y,
666 width: handle_radius.mul_add(2.0, 1.0),
667 height: handle_radius.mul_add(2.0, 1.0),
668 },
669 border: Border {
670 width: 1.0,
671 color: t.palette.neutral_5.into(),
672 radius: (1.0 + handle_radius).into(),
673 },
674 shadow: Shadow::default(),
675 },
676 Color::TRANSPARENT,
677 );
678 renderer.fill_quad(
679 Quad {
680 bounds: Rectangle {
681 x,
682 y,
683 width: handle_radius * 2.0,
684 height: handle_radius * 2.0,
685 },
686 border: Border {
687 width: 1.0,
688 color: t.palette.neutral_10.into(),
689 radius: handle_radius.into(),
690 },
691 shadow: Shadow::default(),
692 },
693 Color::TRANSPARENT,
694 );
695 },
696 );
697 }
698
699 fn overlay<'b>(
700 &'b mut self,
701 state: &'b mut Tree,
702 layout: Layout<'_>,
703 renderer: &crate::Renderer,
704 translation: Vector,
705 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
706 self.inner
707 .as_widget_mut()
708 .overlay(&mut state.children[0], layout, renderer, translation)
709 }
710
711 fn on_event(
712 &mut self,
713 tree: &mut Tree,
714 event: Event,
715 layout: Layout<'_>,
716 cursor: mouse::Cursor,
717 renderer: &crate::Renderer,
718 clipboard: &mut dyn Clipboard,
719 shell: &mut Shell<'_, Message>,
720 viewport: &Rectangle,
721 ) -> event::Status {
722 let state: &mut State = tree.state.downcast_mut();
726 let column_layout = layout;
727 if state.dragging {
728 let bounds = column_layout.children().nth(1).unwrap().bounds();
729 match event {
730 Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => {
731 if let Some(mut clamped) = cursor.position() {
732 clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width);
733 clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height);
734 let relative_pos = clamped - bounds.position();
735 let (s, v) = (
736 relative_pos.x / bounds.width,
737 1.0 - relative_pos.y / bounds.height,
738 );
739
740 let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
741 shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
742 }
743 }
744 Event::Mouse(
745 mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft,
746 ) => {
747 shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
748 state.dragging = false;
749 }
750 _ => return event::Status::Ignored,
751 };
752 return event::Status::Captured;
753 }
754
755 let column_tree = &mut tree.children[0];
756 if self.inner.as_widget_mut().on_event(
757 column_tree,
758 event.clone(),
759 column_layout,
760 cursor,
761 renderer,
762 clipboard,
763 shell,
764 viewport,
765 ) == event::Status::Captured
766 {
767 return event::Status::Captured;
768 }
769
770 match event {
771 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
772 let bounds = column_layout.children().nth(1).unwrap().bounds();
773 if let Some(point) = cursor.position_over(bounds) {
774 let relative_pos = point - bounds.position();
775 let (s, v) = (
776 relative_pos.x / bounds.width,
777 1.0 - relative_pos.y / bounds.height,
778 );
779 state.dragging = true;
780 let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
781 shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
782 event::Status::Captured
783 } else {
784 event::Status::Ignored
785 }
786 }
787 _ => event::Status::Ignored,
788 }
789 }
790
791 fn size(&self) -> Size<Length> {
792 Size::new(self.width, Length::Shrink)
793 }
794}
795
796#[derive(Debug, Default)]
797pub struct State {
798 canvas_cache: canvas::Cache,
799 dragging: bool,
800}
801
802impl State {
803 fn new() -> Self {
804 Self::default()
805 }
806}
807
808impl<Message> ColorPicker<'_, Message> where Message: Clone + 'static {}
809fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
811 let srgb = palette::Srgb::from_color(c);
812 let hex = srgb.into_format::<u8>();
813 if is_hex {
814 format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue)
815 } else {
816 format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue)
817 }
818}
819
820#[allow(clippy::too_many_lines)]
821pub fn color_button<'a, Message: Clone + 'static>(
823 on_press: Option<Message>,
824 color: Option<Color>,
825 icon_portion: Length,
826) -> crate::widget::Button<'a, Message> {
827 let spacing = THEME.lock().unwrap().cosmic().spacing;
828
829 button::custom(if color.is_some() {
830 Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
831 } else {
832 Element::from(column![
833 vertical_space().height(Length::FillPortion(6)),
834 row![
835 horizontal_space().width(Length::FillPortion(6)),
836 Icon::from(
837 icon::from_name("list-add-symbolic")
838 .prefer_svg(true)
839 .symbolic(true)
840 .size(64)
841 )
842 .width(icon_portion)
843 .height(Length::Fill)
844 .content_fit(iced_core::ContentFit::Contain),
845 horizontal_space().width(Length::FillPortion(6)),
846 ]
847 .height(icon_portion)
848 .width(Length::Fill),
849 vertical_space().height(Length::FillPortion(6)),
850 ])
851 })
852 .width(Length::Fixed(f32::from(spacing.space_s)))
853 .height(Length::Fixed(f32::from(spacing.space_s)))
854 .on_press_maybe(on_press)
855 .class(crate::theme::Button::Custom {
856 active: Box::new(move |focused, theme| {
857 let cosmic = theme.cosmic();
858
859 let (outline_width, outline_color) = if focused {
860 (1.0, cosmic.accent_color().into())
861 } else {
862 (0.0, Color::TRANSPARENT)
863 };
864 let standard = theme.active(focused, false, &Button::Standard);
865 button::Style {
866 shadow_offset: Vector::default(),
867 background: color.map(Background::from).or(standard.background),
868 border_radius: cosmic.radius_xs().into(),
869 border_width: 1.0,
870 border_color: cosmic.palette.neutral_8.into(),
871 outline_width,
872 outline_color,
873 icon_color: None,
874 text_color: None,
875 overlay: None,
876 }
877 }),
878 disabled: Box::new(move |theme| {
879 let cosmic = theme.cosmic();
880
881 let standard = theme.disabled(&Button::Standard);
882 button::Style {
883 shadow_offset: Vector::default(),
884 background: color.map(Background::from).or(standard.background),
885 border_radius: cosmic.radius_xs().into(),
886 border_width: 1.0,
887 border_color: cosmic.palette.neutral_8.into(),
888 outline_width: 0.0,
889 outline_color: Color::TRANSPARENT,
890 icon_color: None,
891 text_color: None,
892 overlay: None,
893 }
894 }),
895 hovered: Box::new(move |focused, theme| {
896 let cosmic = theme.cosmic();
897
898 let (outline_width, outline_color) = if focused {
899 (1.0, cosmic.accent_color().into())
900 } else {
901 (0.0, Color::TRANSPARENT)
902 };
903
904 let standard = theme.hovered(focused, false, &Button::Standard);
905 button::Style {
906 shadow_offset: Vector::default(),
907 background: color.map(Background::from).or(standard.background),
908 border_radius: cosmic.radius_xs().into(),
909 border_width: 1.0,
910 border_color: cosmic.palette.neutral_8.into(),
911 outline_width,
912 outline_color,
913 icon_color: None,
914 text_color: None,
915 overlay: None,
916 }
917 }),
918 pressed: Box::new(move |focused, theme| {
919 let cosmic = theme.cosmic();
920
921 let (outline_width, outline_color) = if focused {
922 (1.0, cosmic.accent_color().into())
923 } else {
924 (0.0, Color::TRANSPARENT)
925 };
926
927 let standard = theme.pressed(focused, false, &Button::Standard);
928 button::Style {
929 shadow_offset: Vector::default(),
930 background: color.map(Background::from).or(standard.background),
931 border_radius: cosmic.radius_xs().into(),
932 border_width: 1.0,
933 border_color: cosmic.palette.neutral_8.into(),
934 outline_width,
935 outline_color,
936 icon_color: None,
937 text_color: None,
938 overlay: None,
939 }
940 }),
941 })
942}
943
944impl<'a, Message> From<ColorPicker<'a, Message>>
945 for iced::Element<'a, Message, crate::Theme, crate::Renderer>
946where
947 Message: 'static + Clone,
948{
949 fn from(
950 picker: ColorPicker<'a, Message>,
951 ) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> {
952 Element::new(picker)
953 }
954}