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