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