1use freya_animation::{
2 easing::Function,
3 hook::{
4 AnimatedValue,
5 Ease,
6 OnChange,
7 OnCreation,
8 ReadAnimatedValue,
9 use_animation,
10 },
11 prelude::AnimNum,
12};
13use freya_core::prelude::*;
14use freya_edit::Clipboard;
15use torin::prelude::{
16 Alignment,
17 Area,
18 CursorPoint,
19 Position,
20 Size,
21};
22
23use crate::{
24 button::Button,
25 context_menu::ContextMenu,
26 get_theme,
27 menu::{
28 Menu,
29 MenuButton,
30 },
31 theming::component_themes::ColorPickerThemePartial,
32};
33
34#[cfg_attr(feature = "docs",
56 doc = embed_doc_image::embed_image!("gallery_color_picker", "images/gallery_color_picker.png"),
57)]
58#[derive(Clone, PartialEq)]
62pub struct ColorPicker {
63 pub(crate) theme: Option<ColorPickerThemePartial>,
64 value: Color,
65 on_change: EventHandler<Color>,
66 width: Size,
67 key: DiffKey,
68}
69
70impl KeyExt for ColorPicker {
71 fn write_key(&mut self) -> &mut DiffKey {
72 &mut self.key
73 }
74}
75
76impl ColorPicker {
77 pub fn new(on_change: impl Into<EventHandler<Color>>) -> Self {
78 Self {
79 theme: None,
80 value: Color::WHITE,
81 on_change: on_change.into(),
82 width: Size::px(220.),
83 key: DiffKey::None,
84 }
85 }
86
87 pub fn value(mut self, value: Color) -> Self {
88 self.value = value;
89 self
90 }
91
92 pub fn width(mut self, width: impl Into<Size>) -> Self {
93 self.width = width.into();
94 self
95 }
96}
97
98#[derive(Clone, Copy, PartialEq, Default)]
100enum DragTarget {
101 #[default]
102 None,
103 Sv,
104 Hue,
105}
106
107impl Component for ColorPicker {
108 fn render(&self) -> impl IntoElement {
109 let mut open = use_state(|| false);
110 let mut color = use_state(|| self.value);
111 let mut dragging = use_state(DragTarget::default);
112 let mut area = use_state(Area::default);
113 let mut hue_area = use_state(Area::default);
114
115 let is_open = open();
116
117 let preview = rect()
118 .width(Size::px(40.))
119 .height(Size::px(24.))
120 .corner_radius(4.)
121 .background(self.value)
122 .on_press(move |_| {
123 open.toggle();
124 });
125
126 let theme = get_theme!(&self.theme, color_picker);
127 let hue_bar = rect()
128 .height(Size::px(18.))
129 .width(Size::fill())
130 .corner_radius(4.)
131 .on_sized(move |e: Event<SizedEventData>| hue_area.set(e.area))
132 .background_linear_gradient(
133 LinearGradient::new()
134 .angle(-90.)
135 .stop(((255, 0, 0), 0.))
136 .stop(((255, 255, 0), 16.))
137 .stop(((0, 255, 0), 33.))
138 .stop(((0, 255, 255), 50.))
139 .stop(((0, 0, 255), 66.))
140 .stop(((255, 0, 255), 83.))
141 .stop(((255, 0, 0), 100.)),
142 );
143
144 let sv_area = rect()
145 .height(Size::px(140.))
146 .width(Size::fill())
147 .corner_radius(4.)
148 .overflow(Overflow::Clip)
149 .child(
150 rect()
151 .expanded()
152 .background_linear_gradient(
153 LinearGradient::new()
155 .angle(-90.)
156 .stop(((255, 255, 255), 0.))
157 .stop((Color::from_hsv(color.read().to_hsv().h, 1.0, 1.0), 100.)),
158 )
159 .child(
160 rect()
161 .position(Position::new_absolute())
162 .expanded()
163 .background_linear_gradient(
164 LinearGradient::new()
166 .stop(((255, 255, 255, 0.0), 0.))
167 .stop(((0, 0, 0), 100.)),
168 ),
169 ),
170 );
171
172 const MIN_S: f32 = 0.07;
174 const MIN_V: f32 = 0.07;
175
176 let mut update_sv = {
177 let on_change = self.on_change.clone();
178 move |coords: CursorPoint| {
179 let sv_area = area.read().to_f64();
180 let rel_x = (((coords.x - sv_area.min_x()) / sv_area.width()).clamp(0., 1.)) as f32;
181 let rel_y = (((coords.y - sv_area.min_y()) / sv_area.height())
182 .clamp(MIN_V as f64, 1. - MIN_V as f64)) as f32;
183 let sat = rel_x.max(MIN_S);
184 let v = (1.0 - rel_y).clamp(MIN_V, 1.0 - MIN_V);
185 let hsv = color.read().to_hsv();
186 color.set(Color::from_hsv(hsv.h, sat, v));
187 on_change.call(color());
188 }
189 };
190
191 let mut update_hue = {
192 let on_change = self.on_change.clone();
193 move |coords: CursorPoint| {
194 let bar_area = hue_area.read().to_f64();
195 let rel_x =
196 ((coords.x - bar_area.min_x()) / bar_area.width()).clamp(0.01, 1.) as f32;
197 let hsv = color.read().to_hsv();
198 color.set(Color::from_hsv(rel_x * 360.0, hsv.s, hsv.v));
199 on_change.call(color());
200 }
201 };
202
203 let on_sv_pointer_down = {
204 let mut update_sv = update_sv.clone();
205 move |e: Event<PointerEventData>| {
206 dragging.set(DragTarget::Sv);
207 update_sv(e.global_location());
208 }
209 };
210
211 let on_hue_pointer_down = {
212 let mut update_hue = update_hue.clone();
213 move |e: Event<PointerEventData>| {
214 dragging.set(DragTarget::Hue);
215 update_hue(e.global_location());
216 }
217 };
218
219 let on_global_pointer_move = move |e: Event<PointerEventData>| match *dragging.read() {
220 DragTarget::Sv => update_sv(e.global_location()),
221 DragTarget::Hue => update_hue(e.global_location()),
222 DragTarget::None => {}
223 };
224
225 let on_global_pointer_press = move |_| {
226 if is_open && dragging() == DragTarget::None {
228 open.set(false);
229 }
230 dragging.set_if_modified(DragTarget::None);
231 };
232
233 let animation = use_animation(move |conf| {
234 conf.on_change(OnChange::Rerun);
235 conf.on_creation(OnCreation::Finish);
236
237 let scale = AnimNum::new(0.8, 1.)
238 .time(200)
239 .ease(Ease::Out)
240 .function(Function::Expo);
241 let opacity = AnimNum::new(0., 1.)
242 .time(200)
243 .ease(Ease::Out)
244 .function(Function::Expo);
245
246 if open() {
247 (scale, opacity)
248 } else {
249 (scale, opacity).into_reversed()
250 }
251 });
252
253 let (scale, opacity) = animation.read().value();
254
255 let popup = rect()
256 .on_global_pointer_move(on_global_pointer_move)
257 .on_global_pointer_press(on_global_pointer_press)
258 .width(self.width.clone())
259 .padding(8.)
260 .corner_radius(6.)
261 .background(theme.background)
262 .border(
263 Border::new()
264 .fill(theme.border_fill)
265 .width(1.)
266 .alignment(BorderAlignment::Inner),
267 )
268 .color(theme.color)
269 .spacing(8.)
270 .shadow(Shadow::new().x(0.).y(2.).blur(8.).color((0, 0, 0, 0.1)))
271 .child(
272 rect()
273 .on_sized(move |e: Event<SizedEventData>| area.set(e.area))
274 .on_pointer_down(on_sv_pointer_down)
275 .child(sv_area),
276 )
277 .child(
278 rect()
279 .height(Size::px(18.))
280 .on_pointer_down(on_hue_pointer_down)
281 .child(hue_bar),
282 )
283 .child({
284 let hex = format!(
285 "#{:02X}{:02X}{:02X}",
286 color.read().r(),
287 color.read().g(),
288 color.read().b()
289 );
290
291 rect()
292 .horizontal()
293 .width(Size::fill())
294 .main_align(Alignment::center())
295 .spacing(8.)
296 .child(
297 Button::new()
298 .on_press(move |e: Event<PressEventData>| {
299 e.stop_propagation();
300 e.prevent_default();
301 if ContextMenu::is_open() {
302 ContextMenu::close();
303 } else {
304 ContextMenu::open(
305 Menu::new()
306 .child(
307 MenuButton::new()
308 .on_press(move |e: Event<PressEventData>| {
309 e.stop_propagation();
310 e.prevent_default();
311 ContextMenu::close();
312 let _ =
313 Clipboard::set(color().to_rgb_string());
314 })
315 .child("Copy as RGB"),
316 )
317 .child(
318 MenuButton::new()
319 .on_press(move |e: Event<PressEventData>| {
320 e.stop_propagation();
321 e.prevent_default();
322 ContextMenu::close();
323 let _ =
324 Clipboard::set(color().to_hex_string());
325 })
326 .child("Copy as HEX"),
327 ),
328 )
329 }
330 })
331 .compact()
332 .child(hex),
333 )
334 });
335
336 rect().horizontal().spacing(8.).child(preview).child(
337 rect()
338 .width(Size::px(0.))
339 .height(Size::px(0.))
340 .opacity(opacity)
341 .maybe(opacity > 0., |el| {
342 el.child(rect().scale(scale).child(popup))
343 }),
344 )
345 }
346
347 fn render_key(&self) -> DiffKey {
348 self.key.clone().or(self.default_key())
349 }
350}