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