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