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