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