use std::borrow::Cow;
use std::iter;
use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering};
use std::time::{Duration, Instant};
use crate::theme::iced::Slider;
use crate::theme::{Button, THEME};
use crate::widget::{button::Catalog, container, segmented_button::Entity, slider};
use crate::Element;
use derive_setters::Setters;
use iced::Task;
use iced_core::event::{self, Event};
use iced_core::gradient::{ColorStop, Linear};
use iced_core::renderer::Quad;
use iced_core::widget::{tree, Tree};
use iced_core::{
layout, mouse, renderer, Background, Border, Clipboard, Color, Layout, Length, Radians,
Rectangle, Renderer, Shadow, Shell, Size, Vector, Widget,
};
use iced_widget::slider::HandleShape;
use iced_widget::{canvas, column, horizontal_space, row, scrollable, vertical_space, Row};
use lazy_static::lazy_static;
use palette::{FromColor, RgbHue};
use super::divider::horizontal;
use super::icon::{self, from_name};
use super::segmented_button::{self, SingleSelect};
use super::{button, segmented_control, text, text_input, tooltip, Icon};
#[doc(inline)]
pub use ColorPickerModel as Model;
lazy_static! {
pub static ref HSV_RAINBOW: Vec<iced::Color> = (0u16..8)
.map(
|h| iced::Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const(
RgbHue::new(f32::from(h) * 360.0 / 7.0),
1.0,
1.0
)))
)
.collect();
}
const MAX_RECENT: usize = 20;
#[derive(Debug, Clone)]
pub enum ColorPickerUpdate {
ActiveColor(palette::Hsv),
ActionFinished,
Input(String),
AppliedColor,
Reset,
ActivateSegmented(Entity),
Copied(Instant),
Cancel,
ToggleColorPicker,
}
#[derive(Setters)]
pub struct ColorPickerModel {
#[setters(skip)]
segmented_model: segmented_button::Model<SingleSelect>,
#[setters(skip)]
active_color: palette::Hsv,
#[setters(skip)]
save_next: Option<Color>,
#[setters(skip)]
input_color: String,
#[setters(skip)]
applied_color: Option<Color>,
#[setters(skip)]
fallback_color: Option<Color>,
#[setters(skip)]
recent_colors: Vec<Color>,
active: bool,
width: Length,
height: Length,
#[setters(skip)]
must_clear_cache: Rc<AtomicBool>,
#[setters(skip)]
copied_at: Option<Instant>,
}
impl ColorPickerModel {
#[must_use]
pub fn new(
hex: impl Into<Cow<'static, str>> + Clone,
rgb: impl Into<Cow<'static, str>> + Clone,
fallback_color: Option<Color>,
initial_color: Option<Color>,
) -> Self {
let initial = initial_color.or(fallback_color);
let initial_srgb = palette::Srgb::from(initial.unwrap_or(Color::BLACK));
let hsv = palette::Hsv::from_color(initial_srgb);
Self {
segmented_model: segmented_button::Model::builder()
.insert(move |b| b.text(hex.clone()).activate())
.insert(move |b| b.text(rgb.clone()))
.build(),
active_color: hsv,
save_next: None,
input_color: color_to_string(hsv, true),
applied_color: initial,
fallback_color,
recent_colors: Vec::new(), active: false,
width: Length::Fixed(300.0),
height: Length::Fixed(200.0),
must_clear_cache: Rc::new(AtomicBool::new(false)),
copied_at: None,
}
}
pub fn picker_button<'a, Message: 'static, T: Fn(ColorPickerUpdate) -> Message>(
&self,
f: T,
icon_portion: Option<u16>,
) -> crate::widget::Button<'a, Message> {
color_button(
Some(f(ColorPickerUpdate::ToggleColorPicker)),
self.applied_color,
Length::FillPortion(icon_portion.unwrap_or(12)),
)
}
pub fn update<Message>(&mut self, update: ColorPickerUpdate) -> Task<Message> {
match update {
ColorPickerUpdate::ActiveColor(c) => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.input_color = color_to_string(c, self.is_hex());
if let Some(to_save) = self.save_next.take() {
self.recent_colors.insert(0, to_save);
self.recent_colors.truncate(MAX_RECENT);
}
self.active_color = c;
self.copied_at = None;
}
ColorPickerUpdate::AppliedColor => {
let srgb = palette::Srgb::from_color(self.active_color);
if let Some(applied_color) = self.applied_color.take() {
self.recent_colors.push(applied_color);
}
self.applied_color = Some(Color::from(srgb));
self.active = false;
}
ColorPickerUpdate::ActivateSegmented(e) => {
self.segmented_model.activate(e);
self.input_color = color_to_string(self.active_color, self.is_hex());
self.copied_at = None;
}
ColorPickerUpdate::Copied(t) => {
self.copied_at = Some(t);
return iced::clipboard::write(self.input_color.clone());
}
ColorPickerUpdate::Reset => {
self.must_clear_cache.store(true, Ordering::SeqCst);
let initial_srgb = palette::Srgb::from(self.fallback_color.unwrap_or(Color::BLACK));
let hsv = palette::Hsv::from_color(initial_srgb);
self.active_color = hsv;
self.applied_color = self.fallback_color;
self.copied_at = None;
}
ColorPickerUpdate::Cancel => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.active = false;
self.copied_at = None;
}
ColorPickerUpdate::Input(c) => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.input_color = c;
self.copied_at = None;
if let Ok(c) = self.input_color.parse::<css_color::Srgb>() {
self.active_color =
palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue));
}
}
ColorPickerUpdate::ActionFinished => {
let srgb = palette::Srgb::from_color(self.active_color);
if let Some(applied_color) = self.applied_color.take() {
self.recent_colors.push(applied_color);
}
self.applied_color = Some(Color::from(srgb));
self.active = false;
self.save_next = Some(Color::from(srgb));
}
ColorPickerUpdate::ToggleColorPicker => {
self.must_clear_cache.store(true, Ordering::SeqCst);
self.active = !self.active;
self.copied_at = None;
}
};
Task::none()
}
#[must_use]
pub fn is_hex(&self) -> bool {
self.segmented_model.position(self.segmented_model.active()) == Some(0)
}
#[must_use]
pub fn get_is_active(&self) -> bool {
self.active
}
#[must_use]
pub fn get_applied_color(&self) -> Option<Color> {
self.applied_color
}
#[must_use]
pub fn builder<Message>(
&self,
on_update: fn(ColorPickerUpdate) -> Message,
) -> ColorPickerBuilder<Message> {
ColorPickerBuilder {
model: &self.segmented_model,
active_color: self.active_color,
recent_colors: &self.recent_colors,
on_update,
width: self.width,
height: self.height,
must_clear_cache: self.must_clear_cache.clone(),
input_color: &self.input_color,
reset_label: None,
save_label: None,
cancel_label: None,
copied_at: self.copied_at,
}
}
}
#[derive(Setters, Clone)]
pub struct ColorPickerBuilder<'a, Message> {
#[setters(skip)]
model: &'a segmented_button::Model<SingleSelect>,
#[setters(skip)]
active_color: palette::Hsv,
#[setters(skip)]
input_color: &'a str,
#[setters(skip)]
on_update: fn(ColorPickerUpdate) -> Message,
#[setters(skip)]
recent_colors: &'a Vec<Color>,
#[setters(skip)]
must_clear_cache: Rc<AtomicBool>,
#[setters(skip)]
copied_at: Option<Instant>,
width: Length,
height: Length,
#[setters(strip_option, into)]
reset_label: Option<Cow<'a, str>>,
#[setters(strip_option, into)]
save_label: Option<Cow<'a, str>>,
#[setters(strip_option, into)]
cancel_label: Option<Cow<'a, str>>,
}
impl<'a, Message> ColorPickerBuilder<'a, Message>
where
Message: Clone + 'static,
{
#[allow(clippy::too_many_lines)]
pub fn build<T: Into<Cow<'a, str>> + 'a>(
mut self,
recent_colors_label: T,
copy_to_clipboard_label: T,
copied_to_clipboard_label: T,
) -> ColorPicker<'a, Message> {
let on_update = self.on_update;
let spacing = THEME.lock().unwrap().cosmic().spacing;
let mut inner =
column![
segmented_control::horizontal(self.model)
.on_activate(Box::new(move |e| on_update(
ColorPickerUpdate::ActivateSegmented(e)
)))
.minimum_button_width(0)
.width(self.width),
container(vertical_space().height(self.height))
.width(self.width)
.height(self.height),
slider(
0.0..=359.99,
self.active_color.hue.into_positive_degrees(),
move |v| {
let mut new = self.active_color;
new.hue = v.into();
on_update(ColorPickerUpdate::ActiveColor(new))
}
)
.on_release(on_update(ColorPickerUpdate::ActionFinished))
.class(Slider::Custom {
active: Rc::new(move |t| {
let cosmic = t.cosmic();
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
let pivot = hue * 7.0 / 360.;
let low_end = pivot.floor() as usize;
let high_start = pivot.ceil() as usize;
let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
let low_range = HSV_RAINBOW[0..=low_end]
.iter()
.enumerate()
.map(|(i, color)| ColorStop {
color: *color,
offset: i as f32 / pivot.max(0.0001),
})
.chain(iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 1.,
}))
.collect::<Vec<_>>();
let high_range =
iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 0.,
})
.chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
|(i, color)| ColorStop {
color: *color,
offset: (i as f32 + (1. - pivot.fract()))
/ (7. - pivot).max(0.0001),
},
))
.collect::<Vec<_>>();
a.rail.backgrounds = (
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(low_range),
)),
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(high_range),
)),
);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
hovered: Rc::new(move |t| {
let cosmic = t.cosmic();
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
let pivot = hue * 7.0 / 360.;
let low_end = pivot.floor() as usize;
let high_start = pivot.ceil() as usize;
let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
let low_range = HSV_RAINBOW[0..=low_end]
.iter()
.enumerate()
.map(|(i, color)| ColorStop {
color: *color,
offset: i as f32 / pivot.max(0.0001),
})
.chain(iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 1.,
}))
.collect::<Vec<_>>();
let high_range =
iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 0.,
})
.chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
|(i, color)| ColorStop {
color: *color,
offset: (i as f32 + (1. - pivot.fract()))
/ (7. - pivot).max(0.0001),
},
))
.collect::<Vec<_>>();
a.rail.backgrounds = (
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(low_range),
)),
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(high_range),
)),
);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
dragging: Rc::new(move |t| {
let cosmic = t.cosmic();
let mut a =
slider::Catalog::style(t, &Slider::default(), slider::Status::Active);
let hue = self.active_color.hue.into_positive_degrees();
let pivot = hue * 7.0 / 360.;
let low_end = pivot.floor() as usize;
let high_start = pivot.ceil() as usize;
let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0);
let low_range = HSV_RAINBOW[0..=low_end]
.iter()
.enumerate()
.map(|(i, color)| ColorStop {
color: *color,
offset: i as f32 / pivot.max(0.0001),
})
.chain(iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 1.,
}))
.collect::<Vec<_>>();
let high_range =
iter::once(ColorStop {
color: iced::Color::from(palette::Srgba::from_color(pivot_color)),
offset: 0.,
})
.chain(HSV_RAINBOW[high_start..].iter().enumerate().map(
|(i, color)| ColorStop {
color: *color,
offset: (i as f32 + (1. - pivot.fract()))
/ (7. - pivot).max(0.0001),
},
))
.collect::<Vec<_>>();
a.rail.backgrounds = (
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(low_range),
)),
Background::Gradient(iced::Gradient::Linear(
Linear::new(Radians(90.0)).add_stops(high_range),
)),
);
a.rail.width = 8.0;
a.handle.background = Color::TRANSPARENT.into();
a.handle.shape = HandleShape::Circle { radius: 8.0 };
a.handle.border_color = cosmic.palette.neutral_10.into();
a.handle.border_width = 4.0;
a
}),
})
.width(self.width),
text_input("", self.input_color)
.on_input(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_paste(move |s| on_update(ColorPickerUpdate::Input(s)))
.on_submit(on_update(ColorPickerUpdate::AppliedColor))
.leading_icon(
color_button(
None,
Some(Color::from(palette::Srgb::from_color(self.active_color))),
Length::FillPortion(12)
)
.into()
)
.trailing_icon({
let button = button::custom(crate::widget::icon(
from_name("edit-copy-symbolic").size(spacing.space_s).into(),
))
.on_press(on_update(ColorPickerUpdate::Copied(Instant::now())))
.class(Button::Text);
match self.copied_at.take() {
Some(t)
if Instant::now().duration_since(t) > Duration::from_secs(2) =>
{
button.into()
}
Some(_) => tooltip(
button,
text(copied_to_clipboard_label),
iced_widget::tooltip::Position::Bottom,
)
.into(),
None => tooltip(
button,
text(copy_to_clipboard_label),
iced_widget::tooltip::Position::Bottom,
)
.into(),
}
})
.width(self.width),
]
.padding([
spacing.space_none,
spacing.space_s,
spacing.space_s,
spacing.space_s,
])
.spacing(spacing.space_s);
if !self.recent_colors.is_empty() {
inner = inner.push(horizontal::light().width(self.width));
inner = inner.push(
column![text(recent_colors_label), {
crate::widget::scrollable(
Row::with_children(
self.recent_colors
.iter()
.map(|c| {
let initial_srgb = palette::Srgb::from(*c);
let hsv = palette::Hsv::from_color(initial_srgb);
color_button(
Some(on_update(ColorPickerUpdate::ActiveColor(hsv))),
Some(*c),
Length::FillPortion(12),
)
.into()
})
.collect::<Vec<_>>(),
)
.padding([0.0, 0.0, f32::from(spacing.space_m), 0.0])
.spacing(spacing.space_xxs),
)
.width(self.width)
.direction(iced_widget::scrollable::Direction::Horizontal(
scrollable::Scrollbar::new().anchor(scrollable::Anchor::End),
))
}]
.spacing(spacing.space_xxs),
);
}
if let Some(reset_to_default) = self.reset_label.take() {
inner = inner.push(
column![
horizontal::light().width(self.width),
button::custom(
text(reset_to_default)
.width(self.width)
.align_x(iced_core::Alignment::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::Reset))
]
.spacing(spacing.space_xs)
.width(self.width),
);
}
if let (Some(save), Some(cancel)) = (self.save_label.take(), self.cancel_label.take()) {
inner = inner.push(
column![
horizontal::light().width(self.width),
button::custom(
text(cancel)
.width(self.width)
.align_x(iced_core::Alignment::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::Cancel)),
button::custom(
text(save)
.width(self.width)
.align_x(iced_core::Alignment::Center)
)
.width(self.width)
.on_press(on_update(ColorPickerUpdate::AppliedColor))
.class(Button::Suggested)
]
.spacing(spacing.space_xs)
.width(self.width),
);
}
ColorPicker {
on_update,
inner: inner.into(),
width: self.width,
active_color: self.active_color,
must_clear_cache: self.must_clear_cache,
}
}
}
#[must_use]
pub struct ColorPicker<'a, Message> {
pub(crate) on_update: fn(ColorPickerUpdate) -> Message,
width: Length,
active_color: palette::Hsv,
inner: Element<'a, Message>,
must_clear_cache: Rc<AtomicBool>,
}
impl<'a, Message> Widget<Message, crate::Theme, crate::Renderer> for ColorPicker<'a, Message>
where
Message: Clone + 'static,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<State>()
}
fn state(&self) -> tree::State {
tree::State::new(State::new())
}
fn diff(&mut self, tree: &mut Tree) {
tree.diff_children(std::slice::from_mut(&mut self.inner));
}
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.inner)]
}
fn layout(
&self,
tree: &mut Tree,
renderer: &crate::Renderer,
limits: &layout::Limits,
) -> layout::Node {
self.inner
.as_widget()
.layout(&mut tree.children[0], renderer, limits)
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
fn draw(
&self,
tree: &Tree,
renderer: &mut crate::Renderer,
theme: &crate::Theme,
style: &renderer::Style,
layout: Layout<'_>,
cursor: mouse::Cursor,
viewport: &Rectangle,
) {
let column_layout = layout;
self.inner.as_widget().draw(
&tree.children[0],
renderer,
theme,
style,
layout,
cursor,
viewport,
);
let state: &State = tree.state.downcast_ref();
let active_color = self.active_color;
let canvas_layout = column_layout.children().nth(1).unwrap();
if self
.must_clear_cache
.compare_exchange(true, false, Ordering::SeqCst, Ordering::SeqCst)
.unwrap_or_default()
{
state.canvas_cache.clear();
}
let geo = state
.canvas_cache
.draw(renderer, canvas_layout.bounds().size(), move |frame| {
let column_count = frame.width() as u16;
let row_count = frame.height() as u16;
for column in 0..column_count {
for row in 0..row_count {
let saturation = f32::from(column) / frame.width();
let value = 1.0 - f32::from(row) / frame.height();
let mut c = active_color;
c.saturation = saturation;
c.value = value;
frame.fill_rectangle(
iced::Point::new(f32::from(column), f32::from(row)),
iced::Size::new(1.0, 1.0),
Color::from(palette::Srgb::from_color(c)),
);
}
}
});
let translation = Vector::new(canvas_layout.bounds().x, canvas_layout.bounds().y);
iced_core::Renderer::with_translation(renderer, translation, |renderer| {
iced_renderer::geometry::Renderer::draw_geometry(renderer, geo);
});
let bounds = canvas_layout.bounds();
let t = THEME.lock().unwrap().clone();
let t = t.cosmic();
let handle_radius = f32::from(t.space_xs()) / 2.0;
let (x, y) = (
self.active_color
.saturation
.mul_add(bounds.width, bounds.position().x)
- handle_radius,
(1.0 - self.active_color.value).mul_add(bounds.height, bounds.position().y)
- handle_radius,
);
renderer.with_layer(
Rectangle {
x,
y,
width: handle_radius.mul_add(2.0, 1.0),
height: handle_radius.mul_add(2.0, 1.0),
},
|renderer| {
renderer.fill_quad(
Quad {
bounds: Rectangle {
x,
y,
width: handle_radius.mul_add(2.0, 1.0),
height: handle_radius.mul_add(2.0, 1.0),
},
border: Border {
width: 1.0,
color: t.palette.neutral_5.into(),
radius: (1.0 + handle_radius).into(),
},
shadow: Shadow::default(),
},
Color::TRANSPARENT,
);
renderer.fill_quad(
Quad {
bounds: Rectangle {
x,
y,
width: handle_radius * 2.0,
height: handle_radius * 2.0,
},
border: Border {
width: 1.0,
color: t.palette.neutral_10.into(),
radius: handle_radius.into(),
},
shadow: Shadow::default(),
},
Color::TRANSPARENT,
);
},
);
}
fn overlay<'b>(
&'b mut self,
state: &'b mut Tree,
layout: Layout<'_>,
renderer: &crate::Renderer,
translation: Vector,
) -> Option<iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>> {
self.inner
.as_widget_mut()
.overlay(&mut state.children[0], layout, renderer, translation)
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor: mouse::Cursor,
renderer: &crate::Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
let state: &mut State = tree.state.downcast_mut();
let column_layout = layout;
if state.dragging {
let bounds = column_layout.children().nth(1).unwrap().bounds();
match event {
Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) => {
if let Some(mut clamped) = cursor.position() {
clamped.x = clamped.x.clamp(bounds.x, bounds.x + bounds.width);
clamped.y = clamped.y.clamp(bounds.y, bounds.y + bounds.height);
let relative_pos = clamped - bounds.position();
let (s, v) = (
relative_pos.x / bounds.width,
1.0 - relative_pos.y / bounds.height,
);
let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
}
}
Event::Mouse(
mouse::Event::ButtonReleased(mouse::Button::Left) | mouse::Event::CursorLeft,
) => {
shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished));
state.dragging = false;
}
_ => return event::Status::Ignored,
};
return event::Status::Captured;
}
let column_tree = &mut tree.children[0];
if self.inner.as_widget_mut().on_event(
column_tree,
event.clone(),
column_layout,
cursor,
renderer,
clipboard,
shell,
viewport,
) == event::Status::Captured
{
return event::Status::Captured;
}
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
let bounds = column_layout.children().nth(1).unwrap().bounds();
if let Some(point) = cursor.position_over(bounds) {
let relative_pos = point - bounds.position();
let (s, v) = (
relative_pos.x / bounds.width,
1.0 - relative_pos.y / bounds.height,
);
state.dragging = true;
let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v);
shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv)));
event::Status::Captured
} else {
event::Status::Ignored
}
}
_ => event::Status::Ignored,
}
}
fn size(&self) -> Size<Length> {
Size::new(self.width, Length::Shrink)
}
}
#[derive(Debug, Default)]
pub struct State {
canvas_cache: canvas::Cache,
dragging: bool,
}
impl State {
fn new() -> Self {
Self::default()
}
}
impl<'a, Message> ColorPicker<'a, Message> where Message: Clone + 'static {}
fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
let srgb = palette::Srgb::from_color(c);
let hex = srgb.into_format::<u8>();
if is_hex {
format!("#{:02X}{:02X}{:02X}", hex.red, hex.green, hex.blue)
} else {
format!("rgb({}, {}, {})", hex.red, hex.green, hex.blue)
}
}
#[allow(clippy::too_many_lines)]
pub fn color_button<'a, Message: 'static>(
on_press: Option<Message>,
color: Option<Color>,
icon_portion: Length,
) -> crate::widget::Button<'a, Message> {
let spacing = THEME.lock().unwrap().cosmic().spacing;
button::custom(if color.is_some() {
Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s))))
} else {
Element::from(column![
vertical_space().height(Length::FillPortion(6)),
row![
horizontal_space().width(Length::FillPortion(6)),
Icon::from(
icon::from_name("list-add-symbolic")
.prefer_svg(true)
.symbolic(true)
.size(64)
)
.width(icon_portion)
.height(Length::Fill)
.content_fit(iced_core::ContentFit::Contain),
horizontal_space().width(Length::FillPortion(6)),
]
.height(icon_portion)
.width(Length::Fill),
vertical_space().height(Length::FillPortion(6)),
])
})
.width(Length::Fixed(f32::from(spacing.space_s)))
.height(Length::Fixed(f32::from(spacing.space_s)))
.on_press_maybe(on_press)
.class(crate::theme::Button::Custom {
active: Box::new(move |focused, theme| {
let cosmic = theme.cosmic();
let (outline_width, outline_color) = if focused {
(1.0, cosmic.accent_color().into())
} else {
(0.0, Color::TRANSPARENT)
};
let standard = theme.active(focused, false, &Button::Standard);
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
border_width: 1.0,
border_color: cosmic.on_bg_color().into(),
outline_width,
outline_color,
icon_color: None,
text_color: None,
overlay: None,
}
}),
disabled: Box::new(move |theme| {
let cosmic = theme.cosmic();
let standard = theme.disabled(&Button::Standard);
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
border_width: 1.0,
border_color: cosmic.on_bg_color().into(),
outline_width: 0.0,
outline_color: Color::TRANSPARENT,
icon_color: None,
text_color: None,
overlay: None,
}
}),
hovered: Box::new(move |focused, theme| {
let cosmic = theme.cosmic();
let (outline_width, outline_color) = if focused {
(1.0, cosmic.accent_color().into())
} else {
(0.0, Color::TRANSPARENT)
};
let standard = theme.hovered(focused, false, &Button::Standard);
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
border_width: 1.0,
border_color: cosmic.on_bg_color().into(),
outline_width,
outline_color,
icon_color: None,
text_color: None,
overlay: None,
}
}),
pressed: Box::new(move |focused, theme| {
let cosmic = theme.cosmic();
let (outline_width, outline_color) = if focused {
(1.0, cosmic.accent_color().into())
} else {
(0.0, Color::TRANSPARENT)
};
let standard = theme.pressed(focused, false, &Button::Standard);
button::Style {
shadow_offset: Vector::default(),
background: color.map(Background::from).or(standard.background),
border_radius: cosmic.radius_xs().into(),
border_width: 1.0,
border_color: cosmic.on_bg_color().into(),
outline_width,
outline_color,
icon_color: None,
text_color: None,
overlay: None,
}
}),
})
}
impl<'a, Message> From<ColorPicker<'a, Message>>
for iced::Element<'a, Message, crate::Theme, crate::Renderer>
where
Message: 'static + Clone,
{
fn from(
picker: ColorPicker<'a, Message>,
) -> iced::Element<'a, Message, crate::Theme, crate::Renderer> {
Element::new(picker)
}
}