1use std::borrow::Cow;
7use std::iter;
8use std::rc::Rc;
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::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space};
29use lazy_static::lazy_static;
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
40lazy_static! {
42 pub static ref HSV_RAINBOW: Vec<iced::Color> = (0u16..8)
43 .map(
44 |h| iced::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 let on_update = self.on_update;
293 let spacing = THEME.lock().unwrap().cosmic().spacing;
294 let mut inner =
295 column![
296 segmented_control::horizontal(self.model)
298 .on_activate(Box::new(move |e| on_update(
299 ColorPickerUpdate::ActivateSegmented(e)
300 )))
301 .minimum_button_width(0)
302 .width(self.width),
303 container(vertical_space().height(self.height))
306 .width(self.width)
307 .height(self.height),
308 slider(
309 0.0..=359.99,
310 self.active_color.hue.into_positive_degrees(),
311 move |v| {
312 let mut new = self.active_color;
313 new.hue = v.into();
314 on_update(ColorPickerUpdate::ActiveColor(new))
315 }
316 )
317 .on_release(on_update(ColorPickerUpdate::ActionFinished))
318 .class(Slider::Custom {
319 active: Rc::new(move |t| {
320 let cosmic = t.cosmic();
321 let mut a =
322 slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
323
324 let hue = self.active_color.hue.into_positive_degrees();
325 let pivot = hue * 7.0 / 360.;
326
327 let low_end = pivot.floor() as usize;
328 let high_start = pivot.ceil() as usize;
329 let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
330 let low_range = HSV_RAINBOW[0..=low_end]
331 .iter()
332 .enumerate()
333 .map(|(i, color)| ColorStop {
334 color: *color,
335 offset: i as f32 / pivot.max(0.0001),
336 })
337 .chain(iter::once(ColorStop {
338 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
339 offset: 1.,
340 }))
341 .collect::<Vec<_>>();
342 let high_range =
343 iter::once(ColorStop {
344 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
345 offset: 0.,
346 })
347 .chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
348 |(i, color)| ColorStop {
349 color: *color,
350 offset: (i as f32 + (1. - pivot.fract()))
351 / (7. - pivot).max(0.0001),
352 },
353 ))
354 .collect::<Vec<_>>();
355 a.rail.backgrounds = (
356 Background::Gradient(iced::Gradient::Linear(
357 Linear::new(Radians(90.0)).add_stops(low_range),
358 )),
359 Background::Gradient(iced::Gradient::Linear(
360 Linear::new(Radians(90.0)).add_stops(high_range),
361 )),
362 );
363
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 hovered: 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 let pivot = hue * 7.0 / 360.;
377
378 let low_end = pivot.floor() as usize;
379 let high_start = pivot.ceil() as usize;
380 let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
381 let low_range = HSV_RAINBOW[0..=low_end]
382 .iter()
383 .enumerate()
384 .map(|(i, color)| ColorStop {
385 color: *color,
386 offset: i as f32 / pivot.max(0.0001),
387 })
388 .chain(iter::once(ColorStop {
389 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
390 offset: 1.,
391 }))
392 .collect::<Vec<_>>();
393 let high_range =
394 iter::once(ColorStop {
395 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
396 offset: 0.,
397 })
398 .chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
399 |(i, color)| ColorStop {
400 color: *color,
401 offset: (i as f32 + (1. - pivot.fract()))
402 / (7. - pivot).max(0.0001),
403 },
404 ))
405 .collect::<Vec<_>>();
406 a.rail.backgrounds = (
407 Background::Gradient(iced::Gradient::Linear(
408 Linear::new(Radians(90.0)).add_stops(low_range),
409 )),
410 Background::Gradient(iced::Gradient::Linear(
411 Linear::new(Radians(90.0)).add_stops(high_range),
412 )),
413 );
414 a.rail.width = 8.0;
415 a.handle.background = Color::TRANSPARENT.into();
416 a.handle.shape = HandleShape::Circle { radius: 8.0 };
417 a.handle.border_color = cosmic.palette.neutral_10.into();
418 a.handle.border_width = 4.0;
419 a
420 }),
421 dragging: Rc::new(move |t| {
422 let cosmic = t.cosmic();
423 let mut a =
424 slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
425 let hue = self.active_color.hue.into_positive_degrees();
426 let pivot = hue * 7.0 / 360.;
427
428 let low_end = pivot.floor() as usize;
429 let high_start = pivot.ceil() as usize;
430 let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
431 let low_range = HSV_RAINBOW[0..=low_end]
432 .iter()
433 .enumerate()
434 .map(|(i, color)| ColorStop {
435 color: *color,
436 offset: i as f32 / pivot.max(0.0001),
437 })
438 .chain(iter::once(ColorStop {
439 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
440 offset: 1.,
441 }))
442 .collect::<Vec<_>>();
443 let high_range =
444 iter::once(ColorStop {
445 color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
446 offset: 0.,
447 })
448 .chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
449 |(i, color)| ColorStop {
450 color: *color,
451 offset: (i as f32 + (1. - pivot.fract()))
452 / (7. - pivot).max(0.0001),
453 },
454 ))
455 .collect::<Vec<_>>();
456 a.rail.backgrounds = (
457 Background::Gradient(iced::Gradient::Linear(
458 Linear::new(Radians(90.0)).add_stops(low_range),
459 )),
460 Background::Gradient(iced::Gradient::Linear(
461 Linear::new(Radians(90.0)).add_stops(high_range),
462 )),
463 );
464 a.rail.width = 8.0;
465 a.handle.background = Color::TRANSPARENT.into();
466 a.handle.shape = HandleShape::Circle { radius: 8.0 };
467 a.handle.border_color = cosmic.palette.neutral_10.into();
468 a.handle.border_width = 4.0;
469 a
470 }),
471 })
472 .width(self.width),
473 text_input("", self.input_color)
474 .on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
475 .on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
476 .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor))
477 .leading_icon(
478 color_button(
479 None,
480 Some(Color::from(palette::Srgb::from_color(self.active_color))),
481 Length::FillPortion(12)
482 )
483 .into()
484 )
485 .trailing_icon({
487 let button = button::custom(crate::widget::icon(
488 from_name("edit-copy-symbolic").size(spacing.space_s).into(),
489 ))
490 .on_press(on_update(ColorPickerUpdate::Copied(Instant::now())))
491 .class(Button::Text);
492
493 match self.copied_at.take() {
494 Some(t)
495 if Instant::now().duration_since(t) > Duration::from_secs(2) =>
496 {
497 button.into()
498 }
499 Some(_) => tooltip(
500 button,
501 text(copied_to_clipboard_label),
502 iced_widget::tooltip::Position::Bottom,
503 )
504 .into(),
505 None => tooltip(
506 button,
507 text(copy_to_clipboard_label),
508 iced_widget::tooltip::Position::Bottom,
509 )
510 .into(),
511 }
512 })
513 .width(self.width),
514 ]
515 .padding([
517 spacing.space_none,
518 spacing.space_s,
519 spacing.space_s,
520 spacing.space_s,
521 ])
522 .spacing(spacing.space_s);
523
524 if !self.recent_colors.is_empty() {
525 inner = inner.push(horizontal::light().width(self.width));
526 inner = inner.push(
527 column![text(recent_colors_label), {
528 crate::widget::scrollable(
531 Row::with_children(
532 self.recent_colors
533 .iter()
534 .map(|c| {
535 let initial_srgb = palette::Srgb::from(*c);
536 let hsv = palette::Hsv::from_color(initial_srgb);
537 color_button(
538 Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
539 Some(*c),
540 Length::FillPortion(12),
541 )
542 .into()
543 })
544 .collect::<Vec<_>>(),
545 )
546 .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
547 .spacing(spacing.space_xxs),
548 )
549 .width(self.width)
550 .direction(iced_widget::scrollable::Direction::Horizontal(
551 scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
552 ))
553 }]
554 .spacing(spacing.space_xxs),
555 );
556 }
557
558 if let Some(reset_to_default) = self.reset_label.take() {
559 inner = inner.push(
560 column![
561 horizontal::light().width(self.width),
562 button::custom(
563 text(reset_to_default)
564 .width(self.width)
565 .align_x(iced_core::Alignment::Center)
566 )
567 .width(self.width)
568 .on_press(on_update(ColorPickerUpdate::Reset))
569 ]
570 .spacing(spacing.space_xs)
571 .width(self.width),
572 );
573 }
574 if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) {
575 inner = inner.push(
576 column![
577 horizontal::light().width(self.width),
578 button::custom(
579 text(cancel)
580 .width(self.width)
581 .align_x(iced_core::Alignment::Center)
582 )
583 .width(self.width)
584 .on_press(on_update(ColorPickerUpdate::Cancel)),
585 button::custom(
586 text(save)
587 .width(self.width)
588 .align_x(iced_core::Alignment::Center)
589 )
590 .width(self.width)
591 .on_press(on_update(ColorPickerUpdate::AppliedColor))
592 .class(Button::Suggested)
593 ]
594 .spacing(spacing.space_xs)
595 .width(self.width),
596 );
597 }
598
599 ColorPicker {
600 on_update,
601 inner: inner.into(),
602 width: self.width,
603 active_color: self.active_color,
604 must_clear_cache: self.must_clear_cache,
605 }
606 }
607}
608
609#[must_use]
610pub struct ColorPicker<'a, Message> {
611 pub(crate) on_update: fn(ColorPickerUpdate) -> Message,
612 width: Length,
613 active_color: palette::Hsv,
614 inner: Element<'a, Message>,
615 must_clear_cache: Rc<AtomicBool>,
616}
617
618impl<Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'_, Message>
619where
620 Message: Clone + 'static,
621{
622 fn tag(&self) -> tree::Tag {
623 tree::Tag::of::<State>()
624 }
625
626 fn state(&self) -> tree::State {
627 tree::State::new(State::new())
628 }
629
630 fn diff(&mut self, tree: &mut Tree) {
631 tree.diff_children(std::slice::from_mut(&mut self.inner));
632 }
633
634 fn children(&self) -> Vec<Tree> {
635 vec![Tree::new(&self.inner)]
636 }
637
638 fn layout(
639 &self,
640 tree: &mut Tree,
641 renderer: &crate::Renderer,
642 limits: &layout::Limits,
643 ) -> layout::Node {
644 self.inner
645 .as_widget()
646 .layout(&mut tree.children[0], renderer, limits)
647 }
648
649 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
650 fn draw(
651 &self,
652 tree: &Tree,
653 renderer: &mut crate::Renderer,
654 theme: &crate::Theme,
655 style: &renderer::Style,
656 layout: Layout<'_>,
657 cursor: mouse::Cursor,
658 viewport: &Rectangle,
659 ) {
660 let column_layout = layout;
661 self.inner.as_widget().draw(
663 &tree.children[0],
664 renderer,
665 theme,
666 style,
667 layout,
668 cursor,
669 viewport,
670 );
671 let state: &State = tree.state.downcast_ref();
673
674 let active_color = self.active_color;
675 let canvas_layout = column_layout.children().nth(1).unwrap();
676
677 if self
678 .must_clear_cache
679 .compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
680 .unwrap_or_default()
681 {
682 state.canvas_cache.clear();
683 }
684 let geo = state
685 .canvas_cache
686 .draw(renderer, canvas_layout.bounds().size(), move |frame| {
687 let column_count = frame.width() as u16;
688 let row_count = frame.height() as u16;
689
690 for column in 0..column_count {
691 for row in 0..row_count {
692 let saturation = f32::from(column) / frame.width();
693 let value = 1.0 - f32::from(row) / frame.height();
694
695 let mut c = active_color;
696 c.saturation = saturation;
697 c.value = value;
698 frame.fill_rectangle(
699 iced::Point::new(f32::from(column), f32::from(row)),
700 iced::Size::new(1.0, 1.0),
701 Color::from(palette::Srgb::from_color(c)),
702 );
703 }
704 }
705 });
706
707 let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
708 iced_core::Renderer::with_translation(renderer, translation, |renderer| {
709 iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
710 });
711
712 let bounds = canvas_layout.bounds();
713 let t = THEME.lock().unwrap().clone();
716 let t = t.cosmic();
717 let handle_radius = f32::from(t.space_xs()) / 2.0;
718 let (x, y) = (
719 self.active_color
720 .saturation
721 .mul_add(bounds.width, bounds.position().x)
722 - handle_radius,
723 (1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y)
724 - handle_radius,
725 );
726 renderer.with_layer(
727 Rectangle {
728 x,
729 y,
730 width: handle_radius.mul_add(2.0, 1.0),
731 height: handle_radius.mul_add(2.0, 1.0),
732 },
733 |renderer| {
734 renderer.fill_quad(
735 Quad {
736 bounds: Rectangle {
737 x,
738 y,
739 width: handle_radius.mul_add(2.0, 1.0),
740 height: handle_radius.mul_add(2.0, 1.0),
741 },
742 border: Border {
743 width: 1.0,
744 color: t.palette.neutral_5.into(),
745 radius: (1.0 + handle_radius).into(),
746 },
747 shadow: Shadow::default(),
748 },
749 Color::TRANSPARENT,
750 );
751 renderer.fill_quad(
752 Quad {
753 bounds: Rectangle {
754 x,
755 y,
756 width: handle_radius * 2.0,
757 height: handle_radius * 2.0,
758 },
759 border: Border {
760 width: 1.0,
761 color: t.palette.neutral_10.into(),
762 radius: handle_radius.into(),
763 },
764 shadow: Shadow::default(),
765 },
766 Color::TRANSPARENT,
767 );
768 },
769 );
770 }
771
772 fn overlay<'b>(
773 &'b mut self,
774 state: &'b mut Tree,
775 layout: Layout<'_>,
776 renderer: &crate::Renderer,
777 translation: Vector,
778 ) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
779 self.inner
780 .as_widget_mut()
781 .overlay(&mut state.children[0], layout, renderer, translation)
782 }
783
784 fn on_event(
785 &mut self,
786 tree: &mut Tree,
787 event: Event,
788 layout: Layout<'_>,
789 cursor: mouse::Cursor,
790 renderer: &crate::Renderer,
791 clipboard: &mut dyn Clipboard,
792 shell: &mut Shell<'_, Message>,
793 viewport: &Rectangle,
794 ) -> event::Status {
795 let state: &mut State = tree.state.downcast_mut();
799 let column_layout = layout;
800 if state.dragging {
801 let bounds = column_layout.children().nth(1).unwrap().bounds();
802 match event {
803 Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => {
804 if let Some(mut clamped) = cursor.position() {
805 clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width);
806 clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height);
807 let relative_pos = clamped - bounds.position();
808 let (s, v) = (
809 relative_pos.x / bounds.width,
810 1.0 - relative_pos.y / bounds.height,
811 );
812
813 let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
814 shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
815 }
816 }
817 Event::Mouse(
818 mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft,
819 ) => {
820 shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
821 state.dragging = false;
822 }
823 _ => return event::Status::Ignored,
824 };
825 return event::Status::Captured;
826 }
827
828 let column_tree = &mut tree.children[0];
829 if self.inner.as_widget_mut().on_event(
830 column_tree,
831 event.clone(),
832 column_layout,
833 cursor,
834 renderer,
835 clipboard,
836 shell,
837 viewport,
838 ) == event::Status::Captured
839 {
840 return event::Status::Captured;
841 }
842
843 match event {
844 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
845 let bounds = column_layout.children().nth(1).unwrap().bounds();
846 if let Some(point) = cursor.position_over(bounds) {
847 let relative_pos = point - bounds.position();
848 let (s, v) = (
849 relative_pos.x / bounds.width,
850 1.0 - relative_pos.y / bounds.height,
851 );
852 state.dragging = true;
853 let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
854 shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
855 event::Status::Captured
856 } else {
857 event::Status::Ignored
858 }
859 }
860 _ => event::Status::Ignored,
861 }
862 }
863
864 fn size(&self) -> Size<Length> {
865 Size::new(self.width, Length::Shrink)
866 }
867}
868
869#[derive(Debug, Default)]
870pub struct State {
871 canvas_cache: canvas::Cache,
872 dragging: bool,
873}
874
875impl State {
876 fn new() -> Self {
877 Self::default()
878 }
879}
880
881impl<Message> ColorPicker<'_, Message> where Message: Clone + 'static {}
882fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
884 let srgb = palette::Srgb::from_color(c);
885 let hex = srgb.into_format::<u8>();
886 if is_hex {
887 format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue)
888 } else {
889 format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue)
890 }
891}
892
893#[allow(clippy::too_many_lines)]
894pub fn color_button<'a, Message: Clone + 'static>(
896 on_press: Option<Message>,
897 color: Option<Color>,
898 icon_portion: Length,
899) -> crate::widget::Button<'a, Message> {
900 let spacing = THEME.lock().unwrap().cosmic().spacing;
901
902 button::custom(if color.is_some() {
903 Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
904 } else {
905 Element::from(column![
906 vertical_space().height(Length::FillPortion(6)),
907 row![
908 horizontal_space().width(Length::FillPortion(6)),
909 Icon::from(
910 icon::from_name("list-add-symbolic")
911 .prefer_svg(true)
912 .symbolic(true)
913 .size(64)
914 )
915 .width(icon_portion)
916 .height(Length::Fill)
917 .content_fit(iced_core::ContentFit::Contain),
918 horizontal_space().width(Length::FillPortion(6)),
919 ]
920 .height(icon_portion)
921 .width(Length::Fill),
922 vertical_space().height(Length::FillPortion(6)),
923 ])
924 })
925 .width(Length::Fixed(f32::from(spacing.space_s)))
926 .height(Length::Fixed(f32::from(spacing.space_s)))
927 .on_press_maybe(on_press)
928 .class(crate::theme::Button::Custom {
929 active: Box::new(move |focused, theme| {
930 let cosmic = theme.cosmic();
931
932 let (outline_width, outline_color) = if focused {
933 (1.0, cosmic.accent_color().into())
934 } else {
935 (0.0, Color::TRANSPARENT)
936 };
937 let standard = theme.active(focused, false, &Button::Standard);
938 button::Style {
939 shadow_offset: Vector::default(),
940 background: color.map(Background::from).or(standard.background),
941 border_radius: cosmic.radius_xs().into(),
942 border_width: 1.0,
943 border_color: cosmic.on_bg_color().into(),
944 outline_width,
945 outline_color,
946 icon_color: None,
947 text_color: None,
948 overlay: None,
949 }
950 }),
951 disabled: Box::new(move |theme| {
952 let cosmic = theme.cosmic();
953
954 let standard = theme.disabled(&Button::Standard);
955 button::Style {
956 shadow_offset: Vector::default(),
957 background: color.map(Background::from).or(standard.background),
958 border_radius: cosmic.radius_xs().into(),
959 border_width: 1.0,
960 border_color: cosmic.on_bg_color().into(),
961 outline_width: 0.0,
962 outline_color: Color::TRANSPARENT,
963 icon_color: None,
964 text_color: None,
965 overlay: None,
966 }
967 }),
968 hovered: Box::new(move |focused, theme| {
969 let cosmic = theme.cosmic();
970
971 let (outline_width, outline_color) = if focused {
972 (1.0, cosmic.accent_color().into())
973 } else {
974 (0.0, Color::TRANSPARENT)
975 };
976
977 let standard = theme.hovered(focused, false, &Button::Standard);
978 button::Style {
979 shadow_offset: Vector::default(),
980 background: color.map(Background::from).or(standard.background),
981 border_radius: cosmic.radius_xs().into(),
982 border_width: 1.0,
983 border_color: cosmic.on_bg_color().into(),
984 outline_width,
985 outline_color,
986 icon_color: None,
987 text_color: None,
988 overlay: None,
989 }
990 }),
991 pressed: Box::new(move |focused, theme| {
992 let cosmic = theme.cosmic();
993
994 let (outline_width, outline_color) = if focused {
995 (1.0, cosmic.accent_color().into())
996 } else {
997 (0.0, Color::TRANSPARENT)
998 };
999
1000 let standard = theme.pressed(focused, false, &Button::Standard);
1001 button::Style {
1002 shadow_offset: Vector::default(),
1003 background: color.map(Background::from).or(standard.background),
1004 border_radius: cosmic.radius_xs().into(),
1005 border_width: 1.0,
1006 border_color: cosmic.on_bg_color().into(),
1007 outline_width,
1008 outline_color,
1009 icon_color: None,
1010 text_color: None,
1011 overlay: None,
1012 }
1013 }),
1014 })
1015}
1016
1017impl<'a, Message> From<ColorPicker<'a, Message>>
1018 for iced::Element<'a, Message, crate::Theme, crate::Renderer>
1019where
1020 Message: 'static + Clone,
1021{
1022 fn from(
1023 picker: ColorPicker<'a, Message>,
1024 ) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> {
1025 Element::new(picker)
1026 }
1027}