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