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 const MIN_S: f32 = 0.07;
184 const MIN_V: f32 = 0.07;
185
186 let mut update_sv = {
187 let on_change = self.on_change.clone();
188 move |coords: CursorPoint| {
189 let sv_area = area.read().to_f64();
190 let rel_x = (((coords.x - sv_area.min_x()) / sv_area.width()).clamp(0., 1.)) as f32;
191 let rel_y = (((coords.y - sv_area.min_y()) / sv_area.height())
192 .clamp(MIN_V as f64, 1. - MIN_V as f64)) as f32;
193 let sat = rel_x.max(MIN_S);
194 let v = (1.0 - rel_y).clamp(MIN_V, 1.0 - MIN_V);
195 let hsv = color.read().to_hsv();
196 color.set(Color::from_hsv(hsv.h, sat, v));
197 on_change.call(color());
198 }
199 };
200
201 let mut update_hue = {
202 let on_change = self.on_change.clone();
203 move |coords: CursorPoint| {
204 let bar_area = hue_area.read().to_f64();
205 let rel_x =
206 ((coords.x - bar_area.min_x()) / bar_area.width()).clamp(0.01, 1.) as f32;
207 let hsv = color.read().to_hsv();
208 color.set(Color::from_hsv(rel_x * 360.0, hsv.s, hsv.v));
209 on_change.call(color());
210 }
211 };
212
213 let on_sv_pointer_down = {
214 let mut update_sv = update_sv.clone();
215 move |e: Event<PointerEventData>| {
216 dragging.set(DragTarget::Sv);
217 update_sv(e.global_location());
218 e.stop_propagation();
219 e.prevent_default();
220 }
221 };
222
223 let on_hue_pointer_down = {
224 let mut update_hue = update_hue.clone();
225 move |e: Event<PointerEventData>| {
226 dragging.set(DragTarget::Hue);
227 update_hue(e.global_location());
228 e.stop_propagation();
229 e.prevent_default();
230 }
231 };
232
233 let on_global_pointer_move = move |e: Event<PointerEventData>| match *dragging.read() {
234 DragTarget::Sv => {
235 update_sv(e.global_location());
236 }
237 DragTarget::Hue => {
238 update_hue(e.global_location());
239 }
240 DragTarget::None => {}
241 };
242
243 let on_global_pointer_press = move |_| {
244 if is_open && dragging() == DragTarget::None {
246 open.set(false);
247 }
248 dragging.set_if_modified(DragTarget::None);
249 };
250
251 let animation = use_animation(move |conf| {
252 conf.on_change(OnChange::Rerun);
253 conf.on_creation(OnCreation::Finish);
254
255 let scale = AnimNum::new(0.8, 1.)
256 .time(200)
257 .ease(Ease::Out)
258 .function(Function::Expo);
259 let opacity = AnimNum::new(0., 1.)
260 .time(200)
261 .ease(Ease::Out)
262 .function(Function::Expo);
263
264 if open() {
265 (scale, opacity)
266 } else {
267 (scale, opacity).into_reversed()
268 }
269 });
270
271 let (scale, opacity) = animation.read().value();
272
273 let popup = rect()
274 .on_global_pointer_move(on_global_pointer_move)
275 .on_global_pointer_press(on_global_pointer_press)
276 .width(self.width.clone())
277 .padding(8.)
278 .corner_radius(6.)
279 .background(theme.background)
280 .border(
281 Border::new()
282 .fill(theme.border_fill)
283 .width(1.)
284 .alignment(BorderAlignment::Inner),
285 )
286 .color(theme.color)
287 .spacing(8.)
288 .shadow(Shadow::new().x(0.).y(2.).blur(8.).color((0, 0, 0, 0.1)))
289 .child(
290 rect()
291 .on_sized(move |e: Event<SizedEventData>| area.set(e.area))
292 .on_pointer_down(on_sv_pointer_down)
293 .child(sv_area),
294 )
295 .child(
296 rect()
297 .height(Size::px(18.))
298 .on_pointer_down(on_hue_pointer_down)
299 .child(hue_bar),
300 )
301 .child({
302 let hex = format!(
303 "#{:02X}{:02X}{:02X}",
304 color.read().r(),
305 color.read().g(),
306 color.read().b()
307 );
308
309 rect()
310 .horizontal()
311 .width(Size::fill())
312 .main_align(Alignment::center())
313 .spacing(8.)
314 .child(
315 Button::new()
316 .on_press(move |e: Event<PressEventData>| {
317 e.stop_propagation();
318 e.prevent_default();
319 if ContextMenu::is_open() {
320 ContextMenu::close();
321 } else {
322 ContextMenu::open_from_event(
323 &e,
324 Menu::new()
325 .child(
326 MenuButton::new()
327 .on_press(move |e: Event<PressEventData>| {
328 e.stop_propagation();
329 e.prevent_default();
330 ContextMenu::close();
331 let _ =
332 Clipboard::set(color().to_rgb_string());
333 })
334 .child("Copy as RGB"),
335 )
336 .child(
337 MenuButton::new()
338 .on_press(move |e: Event<PressEventData>| {
339 e.stop_propagation();
340 e.prevent_default();
341 ContextMenu::close();
342 let _ =
343 Clipboard::set(color().to_hex_string());
344 })
345 .child("Copy as HEX"),
346 ),
347 )
348 }
349 })
350 .compact()
351 .child(hex),
352 )
353 });
354
355 rect()
356 .horizontal()
357 .spacing(8.)
358 .child(preview)
359 .maybe_child((opacity > 0.).then(|| {
360 rect()
361 .layer(Layer::Overlay)
362 .width(Size::px(0.))
363 .height(Size::px(0.))
364 .opacity(opacity)
365 .child(rect().scale(scale).child(popup))
366 }))
367 }
368
369 fn render_key(&self) -> DiffKey {
370 self.key.clone().or(self.default_key())
371 }
372}