use freya_animation::{
easing::Function,
hook::{
AnimatedValue,
Ease,
OnChange,
OnCreation,
ReadAnimatedValue,
use_animation,
},
prelude::AnimNum,
};
use freya_core::prelude::*;
use freya_edit::Clipboard;
use torin::prelude::{
Alignment,
Area,
CursorPoint,
Point2D,
Position,
Size,
Size2D,
};
use crate::{
button::Button,
context_menu::ContextMenu,
define_theme,
get_theme,
menu::{
Menu,
MenuButton,
},
};
define_theme! {
%[component]
pub ColorPicker {
%[fields]
background: Color,
color: Color,
border_fill: Color,
}
}
#[derive(Clone, PartialEq)]
pub struct ColorPicker {
pub(crate) theme: Option<ColorPickerThemePartial>,
value: Color,
on_change: EventHandler<Color>,
width: Size,
key: DiffKey,
}
impl KeyExt for ColorPicker {
fn write_key(&mut self) -> &mut DiffKey {
&mut self.key
}
}
impl ColorPicker {
pub fn new(on_change: impl Into<EventHandler<Color>>) -> Self {
Self {
theme: None,
value: Color::WHITE,
on_change: on_change.into(),
width: Size::px(220.),
key: DiffKey::None,
}
}
pub fn value(mut self, value: Color) -> Self {
self.value = value;
self
}
pub fn width(mut self, width: impl Into<Size>) -> Self {
self.width = width.into();
self
}
}
#[derive(Clone, Copy, PartialEq, Default)]
enum DragTarget {
#[default]
None,
Sv,
Hue,
}
impl Component for ColorPicker {
fn render(&self) -> impl IntoElement {
let mut open = use_state(|| false);
let mut color = use_state(|| self.value);
let mut dragging = use_state(DragTarget::default);
let mut area = use_state(Area::default);
let mut hue_area = use_state(Area::default);
let mut preview_area = use_state(|| None::<Area>);
let mut popup_size = use_state(|| None::<Size2D>);
let is_open = open();
let preview = rect()
.width(Size::px(40.))
.height(Size::px(24.))
.corner_radius(4.)
.background(self.value)
.on_sized(move |e: Event<SizedEventData>| preview_area.set_if_modified(Some(e.area)))
.on_press(move |_| {
open.toggle();
});
let theme = get_theme!(&self.theme, ColorPickerThemePreference, "color_picker");
let hue_bar = rect()
.height(Size::px(18.))
.width(Size::fill())
.corner_radius(4.)
.on_sized(move |e: Event<SizedEventData>| hue_area.set(e.area))
.background(
LinearGradient::new()
.angle(-90.)
.stop(((255, 0, 0), 0.))
.stop(((255, 255, 0), 16.))
.stop(((0, 255, 0), 33.))
.stop(((0, 255, 255), 50.))
.stop(((0, 0, 255), 66.))
.stop(((255, 0, 255), 83.))
.stop(((255, 0, 0), 100.)),
);
let sv_area = rect()
.height(Size::px(140.))
.width(Size::fill())
.corner_radius(4.)
.overflow(Overflow::Clip)
.child(
rect()
.expanded()
.background(
LinearGradient::new()
.angle(-90.)
.stop(((255, 255, 255), 0.))
.stop((Color::from_hsv(color.read().to_hsv().h, 1.0, 1.0), 100.)),
)
.child(
rect()
.position(Position::new_absolute())
.expanded()
.background(
LinearGradient::new()
.stop(((255, 255, 255, 0.0), 0.))
.stop(((0, 0, 0), 100.)),
),
),
);
let mut update_sv = {
let on_change = self.on_change.clone();
move |coords: CursorPoint| {
let sv_area = area.read().to_f64();
let sat = ((coords.x - sv_area.min_x()) / sv_area.width()).clamp(0., 1.) as f32;
let rel_y = ((coords.y - sv_area.min_y()) / sv_area.height()).clamp(0., 1.) as f32;
let v = 1.0 - rel_y;
let hsv = color.read().to_hsv();
let new_color = Color::from_hsv(hsv.h, sat, v);
color.set_if_modified_and_then(new_color, || on_change.call(new_color));
}
};
let mut update_hue = {
let on_change = self.on_change.clone();
move |coords: CursorPoint| {
let bar_area = hue_area.read().to_f64();
let rel_x = ((coords.x - bar_area.min_x()) / bar_area.width()).clamp(0., 1.) as f32;
let hsv = color.read().to_hsv();
let new_color = Color::from_hsv(rel_x * 360.0, hsv.s, hsv.v);
color.set_if_modified_and_then(new_color, || on_change.call(new_color));
}
};
let on_sv_pointer_down = {
let mut update_sv = update_sv.clone();
move |e: Event<PointerEventData>| {
if !e.data().is_primary() {
return;
}
dragging.set(DragTarget::Sv);
update_sv(e.global_location());
e.stop_propagation();
e.prevent_default();
}
};
let on_hue_pointer_down = {
let mut update_hue = update_hue.clone();
move |e: Event<PointerEventData>| {
if !e.data().is_primary() {
return;
}
dragging.set(DragTarget::Hue);
update_hue(e.global_location());
e.stop_propagation();
e.prevent_default();
}
};
let on_global_pointer_move = move |e: Event<PointerEventData>| match *dragging.read() {
DragTarget::Sv => {
update_sv(e.global_location());
}
DragTarget::Hue => {
update_hue(e.global_location());
}
DragTarget::None => {}
};
let on_global_pointer_up = move |_| {
if is_open && dragging() == DragTarget::None {
open.set(false);
}
dragging.set_if_modified(DragTarget::None);
};
let animation = use_animation(move |conf| {
conf.on_change(OnChange::Rerun);
conf.on_creation(OnCreation::Finish);
let scale = AnimNum::new(0.8, 1.)
.time(200)
.ease(Ease::Out)
.function(Function::Expo);
let opacity = AnimNum::new(0., 1.)
.time(200)
.ease(Ease::Out)
.function(Function::Expo);
if open() {
(scale, opacity)
} else {
(scale, opacity).into_reversed()
}
});
let (scale, opacity) = animation.read().value();
let (offset_x, offset_y, opacity) = match (preview_area(), popup_size()) {
(Some(preview), Some(size)) => {
let window = Area::from_size(*Platform::get().root_size.peek());
let popup = Area::new(Point2D::new(preview.max_x(), preview.min_y()), size);
let target = if popup.max_x() > window.max_x() && preview.min_x() >= size.width {
Point2D::new(preview.min_x() - size.width, popup.min_y())
} else {
popup.origin
};
let clamped = target.clamp(window.origin, window.max() - size.to_vector());
let offset = clamped - popup.origin;
(offset.x, offset.y, opacity)
}
_ => (0., 0., 0.),
};
let popup = rect()
.on_global_pointer_move(on_global_pointer_move)
.on_global_pointer_up(on_global_pointer_up)
.width(self.width.clone())
.padding(8.)
.corner_radius(6.)
.background(theme.background)
.border(
Border::new()
.fill(theme.border_fill)
.width(1.)
.alignment(BorderAlignment::Inner),
)
.color(theme.color)
.spacing(8.)
.shadow(Shadow::new().x(0.).y(2.).blur(8.).color((0, 0, 0, 0.1)))
.child(
rect()
.on_sized(move |e: Event<SizedEventData>| area.set(e.area))
.on_pointer_down(on_sv_pointer_down)
.child(sv_area),
)
.child(
rect()
.height(Size::px(18.))
.on_pointer_down(on_hue_pointer_down)
.child(hue_bar),
)
.child({
let hex = format!(
"#{:02X}{:02X}{:02X}",
color.read().r(),
color.read().g(),
color.read().b()
);
rect()
.horizontal()
.width(Size::fill())
.main_align(Alignment::center())
.spacing(8.)
.child(
Button::new()
.on_press(move |e: Event<PressEventData>| {
e.stop_propagation();
e.prevent_default();
if ContextMenu::is_open() {
ContextMenu::close();
} else {
ContextMenu::open(
Menu::new()
.child(
MenuButton::new()
.on_press(move |e: Event<PressEventData>| {
e.stop_propagation();
e.prevent_default();
ContextMenu::close();
let _ =
Clipboard::set(color().to_rgb_string());
})
.child("Copy as RGB"),
)
.child(
MenuButton::new()
.on_press(move |e: Event<PressEventData>| {
e.stop_propagation();
e.prevent_default();
ContextMenu::close();
let _ =
Clipboard::set(color().to_hex_string());
})
.child("Copy as HEX"),
),
)
}
})
.compact()
.child(hex),
)
});
rect()
.horizontal()
.child(preview)
.maybe_child((is_open || opacity > 0.).then(|| {
rect()
.layer(Layer::Overlay)
.width(Size::px(0.))
.height(Size::px(0.))
.offset_x(offset_x)
.offset_y(offset_y)
.opacity(opacity)
.child(
popup
.scale(scale)
.on_sized(move |e: Event<SizedEventData>| {
popup_size.set_if_modified(Some(e.area.size))
}),
)
}))
}
fn render_key(&self) -> DiffKey {
self.key.clone().or(self.default_key())
}
}