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 Point2D,
20 Position,
21 Size,
22 Size2D,
23};
24
25use crate::{
26 button::Button,
27 context_menu::ContextMenu,
28 define_theme,
29 get_theme,
30 menu::{
31 Menu,
32 MenuButton,
33 },
34};
35
36define_theme! {
37 %[component]
38 pub ColorPicker {
39 %[fields]
40 background: Color,
41 color: Color,
42 border_fill: Color,
43 }
44}
45
46#[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 key_mut(&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 let mut preview_area = use_state(|| None::<Area>);
115 let mut popup_size = use_state(|| None::<Size2D>);
116
117 let is_open = open();
118
119 let preview = rect()
120 .width(Size::px(40.))
121 .height(Size::px(24.))
122 .corner_radius(4.)
123 .background(self.value)
124 .on_sized(move |e: Event<SizedEventData>| preview_area.set_if_modified(Some(e.area)))
125 .on_press(move |_| {
126 open.toggle();
127 });
128
129 let theme = get_theme!(&self.theme, ColorPickerThemePreference, "color_picker");
130 let hue_bar = rect()
131 .height(Size::px(18.))
132 .width(Size::fill())
133 .corner_radius(4.)
134 .on_sized(move |e: Event<SizedEventData>| hue_area.set(e.area))
135 .background(
136 LinearGradient::new()
137 .angle(-90.)
138 .stop(((255, 0, 0), 0.))
139 .stop(((255, 255, 0), 16.))
140 .stop(((0, 255, 0), 33.))
141 .stop(((0, 255, 255), 50.))
142 .stop(((0, 0, 255), 66.))
143 .stop(((255, 0, 255), 83.))
144 .stop(((255, 0, 0), 100.)),
145 );
146
147 let sv_area = rect()
148 .height(Size::px(140.))
149 .width(Size::fill())
150 .corner_radius(4.)
151 .overflow(Overflow::Clip)
152 .child(
153 rect()
154 .expanded()
155 .background(
156 LinearGradient::new()
158 .angle(-90.)
159 .stop(((255, 255, 255), 0.))
160 .stop((Color::from_hsv(color.read().to_hsv().h, 1.0, 1.0), 100.)),
161 )
162 .child(
163 rect()
164 .position(Position::new_absolute())
165 .expanded()
166 .background(
167 LinearGradient::new()
169 .stop(((255, 255, 255, 0.0), 0.))
170 .stop(((0, 0, 0), 100.)),
171 ),
172 ),
173 );
174
175 let mut update_sv = {
176 let on_change = self.on_change.clone();
177 move |coords: CursorPoint| {
178 let sv_area = area.read().to_f64();
179 let sat = ((coords.x - sv_area.min_x()) / sv_area.width()).clamp(0., 1.) as f32;
180 let rel_y = ((coords.y - sv_area.min_y()) / sv_area.height()).clamp(0., 1.) as f32;
181 let v = 1.0 - rel_y;
182 let hsv = color.read().to_hsv();
183 let new_color = Color::from_hsv(hsv.h, sat, v);
184 color.set_if_modified_and_then(new_color, || on_change.call(new_color));
185 }
186 };
187
188 let mut update_hue = {
189 let on_change = self.on_change.clone();
190 move |coords: CursorPoint| {
191 let bar_area = hue_area.read().to_f64();
192 let rel_x = ((coords.x - bar_area.min_x()) / bar_area.width()).clamp(0., 1.) as f32;
193 let hsv = color.read().to_hsv();
194 let new_color = Color::from_hsv(rel_x * 360.0, hsv.s, hsv.v);
195 color.set_if_modified_and_then(new_color, || on_change.call(new_color));
196 }
197 };
198
199 let on_sv_pointer_down = {
200 let mut update_sv = update_sv.clone();
201 move |e: Event<PointerEventData>| {
202 if !e.data().is_primary() {
203 return;
204 }
205 dragging.set(DragTarget::Sv);
206 update_sv(e.global_location());
207 e.stop_propagation();
208 e.prevent_default();
209 }
210 };
211
212 let on_hue_pointer_down = {
213 let mut update_hue = update_hue.clone();
214 move |e: Event<PointerEventData>| {
215 if !e.data().is_primary() {
216 return;
217 }
218 dragging.set(DragTarget::Hue);
219 update_hue(e.global_location());
220 e.stop_propagation();
221 e.prevent_default();
222 }
223 };
224
225 let on_global_pointer_move = move |e: Event<PointerEventData>| match *dragging.read() {
226 DragTarget::Sv => {
227 update_sv(e.global_location());
228 }
229 DragTarget::Hue => {
230 update_hue(e.global_location());
231 }
232 DragTarget::None => {}
233 };
234
235 let on_global_pointer_up = move |_| {
236 if is_open && dragging() == DragTarget::None {
238 open.set(false);
239 }
240 dragging.set_if_modified(DragTarget::None);
241 };
242
243 let animation = use_animation(move |conf| {
244 conf.on_change(OnChange::Rerun);
245 conf.on_creation(OnCreation::Finish);
246
247 let scale = AnimNum::new(0.8, 1.)
248 .time(200)
249 .ease(Ease::Out)
250 .function(Function::Expo);
251 let opacity = AnimNum::new(0., 1.)
252 .time(200)
253 .ease(Ease::Out)
254 .function(Function::Expo);
255
256 if open() {
257 (scale, opacity)
258 } else {
259 (scale, opacity).into_reversed()
260 }
261 });
262
263 let (scale, opacity) = animation.read().value();
264
265 let (offset_x, offset_y, opacity) = match (preview_area(), popup_size()) {
266 (Some(preview), Some(size)) => {
267 let window = Area::from_size(*Platform::get().current_window().root_size.peek());
268 let popup = Area::new(Point2D::new(preview.max_x(), preview.min_y()), size);
269
270 let target = if popup.max_x() > window.max_x() && preview.min_x() >= size.width {
271 Point2D::new(preview.min_x() - size.width, popup.min_y())
272 } else {
273 popup.origin
274 };
275
276 let clamped = target.clamp(window.origin, window.max() - size.to_vector());
277 let offset = clamped - popup.origin;
278
279 (offset.x, offset.y, opacity)
280 }
281 _ => (0., 0., 0.),
282 };
283
284 let popup = rect()
285 .on_global_pointer_move(on_global_pointer_move)
286 .on_global_pointer_up(on_global_pointer_up)
287 .width(self.width.clone())
288 .padding(8.)
289 .corner_radius(6.)
290 .background(theme.background)
291 .border(
292 Border::new()
293 .fill(theme.border_fill)
294 .width(1.)
295 .alignment(BorderAlignment::Inner),
296 )
297 .color(theme.color)
298 .spacing(8.)
299 .shadow(Shadow::new().x(0.).y(2.).blur(8.).color((0, 0, 0, 0.1)))
300 .child(
301 rect()
302 .on_sized(move |e: Event<SizedEventData>| area.set(e.area))
303 .on_pointer_down(on_sv_pointer_down)
304 .child(sv_area),
305 )
306 .child(
307 rect()
308 .height(Size::px(18.))
309 .on_pointer_down(on_hue_pointer_down)
310 .child(hue_bar),
311 )
312 .child({
313 let hex = format!(
314 "#{:02X}{:02X}{:02X}",
315 color.read().r(),
316 color.read().g(),
317 color.read().b()
318 );
319
320 rect()
321 .horizontal()
322 .width(Size::fill())
323 .main_align(Alignment::center())
324 .spacing(8.)
325 .child(
326 Button::new()
327 .on_press(move |e: Event<PressEventData>| {
328 e.stop_propagation();
329 e.prevent_default();
330 if ContextMenu::is_open() {
331 ContextMenu::close();
332 } else {
333 ContextMenu::open(
334 Menu::new()
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_rgb_string());
343 })
344 .child("Copy as RGB"),
345 )
346 .child(
347 MenuButton::new()
348 .on_press(move |e: Event<PressEventData>| {
349 e.stop_propagation();
350 e.prevent_default();
351 ContextMenu::close();
352 let _ =
353 Clipboard::set(color().to_hex_string());
354 })
355 .child("Copy as HEX"),
356 ),
357 )
358 }
359 })
360 .compact()
361 .child(hex),
362 )
363 });
364
365 rect()
366 .horizontal()
367 .child(preview)
368 .maybe_child((is_open || opacity > 0.).then(|| {
369 rect()
370 .layer(Layer::Overlay)
371 .width(Size::px(0.))
372 .height(Size::px(0.))
373 .offset_x(offset_x)
374 .offset_y(offset_y)
375 .opacity(opacity)
376 .child(
377 popup
378 .scale(scale)
379 .on_sized(move |e: Event<SizedEventData>| {
380 popup_size.set_if_modified(Some(e.area.size))
381 }),
382 )
383 }))
384 }
385
386 fn render_key(&self) -> DiffKey {
387 self.key.clone().or(self.default_key())
388 }
389}