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