1use super::*;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
10#[non_exhaustive]
11pub struct ColorAreaThumbState {
12 pub color: PickerColor,
14 pub is_dragging: bool,
16 pub is_hovered: bool,
18 pub is_focused: bool,
20 pub is_focus_visible: bool,
22 pub is_disabled: bool,
24}
25
26const COLOR_AREA_THUMB_IDLE_PX: f32 = 16.0;
27const COLOR_AREA_THUMB_DRAGGING_PX: f32 = 20.0;
28const COLOR_AREA_THUMB_TRANSITION_MS: u64 = 150;
29
30#[derive(Clone)]
31pub(super) struct ColorAreaThumbMotion {
32 dragging: bool,
33 generation: usize,
34 from: f32,
35 size: Rc<Cell<f32>>,
36}
37
38pub(super) struct ColorAreaThumbMotionFrame {
39 base: ElementId,
40 generation: usize,
41 from: f32,
42 to: f32,
43 size: Rc<Cell<f32>>,
44 animate: bool,
45}
46
47impl ColorAreaThumbMotionFrame {
48 fn render(self, thumb: gpui::Div) -> gpui::AnyElement {
49 if !self.animate {
50 self.size.set(self.to);
51 return place_color_area_thumb(thumb, self.to).into_any_element();
52 }
53
54 let size = self.size;
55 let from = self.from;
56 let to = self.to;
57 thumb
58 .with_animation(
59 element_id::indexed(&self.base, "thumb-size", self.generation),
60 Animation::new(Duration::from_millis(COLOR_AREA_THUMB_TRANSITION_MS))
61 .with_easing(|t| crate::anim::Curve::Out.at(t)),
62 move |thumb, delta| {
63 let next = from + (to - from) * delta;
64 size.set(next);
65 place_color_area_thumb(thumb, next)
66 },
67 )
68 .into_any_element()
69 }
70}
71
72pub(super) fn place_color_area_thumb(thumb: gpui::Div, size: f32) -> gpui::Div {
73 let inset = (COLOR_AREA_THUMB_DRAGGING_PX - size) / 2.0;
74 thumb.size(px(size)).ml(px(inset)).mt(px(inset))
75}
76
77pub(super) fn color_area_thumb_motion(
78 id: &ElementId,
79 dragging: bool,
80 window: &mut Window,
81 cx: &mut App,
82) -> ColorAreaThumbMotionFrame {
83 let state = window.use_keyed_state(element_id::scoped(id, "thumb-motion"), cx, |_, _| {
84 ColorAreaThumbMotion {
85 dragging,
86 generation: 0,
87 from: if dragging {
88 COLOR_AREA_THUMB_DRAGGING_PX
89 } else {
90 COLOR_AREA_THUMB_IDLE_PX
91 },
92 size: Rc::new(Cell::new(if dragging {
93 COLOR_AREA_THUMB_DRAGGING_PX
94 } else {
95 COLOR_AREA_THUMB_IDLE_PX
96 })),
97 }
98 });
99 let mut current = state.read(cx).clone();
100 let to = if dragging {
101 COLOR_AREA_THUMB_DRAGGING_PX
102 } else {
103 COLOR_AREA_THUMB_IDLE_PX
104 };
105 if current.dragging != dragging {
106 current.dragging = dragging;
107 current.generation = current.generation.wrapping_add(1);
108 current.from = current.size.get();
109 state.update(cx, |stored, _| *stored = current.clone());
110 }
111 if ActiveTheme::reduce_motion(cx) && (current.size.get() - to).abs() > f32::EPSILON {
112 current.from = to;
113 current.size.set(to);
114 state.update(cx, |stored, _| *stored = current.clone());
115 }
116 ColorAreaThumbMotionFrame {
117 base: id.clone(),
118 generation: current.generation,
119 from: current.from,
120 to,
121 size: current.size,
122 animate: current.generation != 0
123 && !ActiveTheme::reduce_motion(cx)
124 && (current.from - to).abs() > f32::EPSILON,
125 }
126}
127
128#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
130#[derive(IntoElement)]
131pub struct ColorArea {
132 default_value: Option<PickerColor>,
134 id: ElementId,
135 value: PickerColor,
136 color_space: Option<ColorSpace>,
139 x_channel: ColorChannel,
140 y_channel: ColorChannel,
141 width: Pixels,
142 height: Pixels,
143 is_disabled: bool,
144 show_dots: bool,
145 thumb: Option<Arc<dyn Fn(ColorAreaThumbState) -> gpui::AnyElement + 'static>>,
146 on_change: Option<OnColorChange>,
147 on_change_end: Option<OnColorChange>,
148 sx: Option<Box<gpui::StyleRefinement>>,
150}
151
152impl ColorArea {
153 pub fn new(id: impl Into<ElementId>, value: PickerColor) -> Self {
155 Self {
156 default_value: None,
157 id: id.into(),
158 value,
159 color_space: None,
160 x_channel: ColorChannel::Saturation,
161 y_channel: ColorChannel::Brightness,
162 width: px(224.),
163 height: px(224.),
164 is_disabled: false,
165 show_dots: false,
166 thumb: None,
167 on_change: None,
168 on_change_end: None,
169 sx: None,
170 }
171 }
172
173 pub fn default_value(mut self, value: PickerColor) -> Self {
178 self.default_value = Some(value);
179 self
180 }
181
182 pub fn color_space(mut self, space: ColorSpace) -> Self {
187 let (x, y) = space.area_channels();
188 self.color_space = Some(space);
189 self.x_channel = x;
190 self.y_channel = y;
191 self
192 }
193
194 pub fn x_channel(mut self, channel: ColorChannel) -> Self {
196 self.x_channel = channel;
197 self
198 }
199
200 pub fn y_channel(mut self, channel: ColorChannel) -> Self {
202 self.y_channel = channel;
203 self
204 }
205
206 pub fn size(mut self, width: impl Into<Pixels>, height: impl Into<Pixels>) -> Self {
208 self.width = width.into();
209 self.height = height.into();
210 self
211 }
212
213 pub fn sx(mut self, style: impl FnOnce(gpui::Div) -> gpui::Div) -> Self {
218 util::refine_sx(&mut self.sx, style);
219 self
220 }
221
222 pub fn is_disabled(mut self, v: bool) -> Self {
224 self.is_disabled = v;
225 self
226 }
227
228 pub fn show_dots(mut self, v: bool) -> Self {
230 self.show_dots = v;
231 self
232 }
233
234 pub fn thumb(
238 mut self,
239 render: impl Fn(ColorAreaThumbState) -> gpui::AnyElement + 'static,
240 ) -> Self {
241 self.thumb = Some(Arc::new(render));
242 self
243 }
244
245 pub fn on_change(
247 mut self,
248 handler: impl Fn(&PickerColor, &mut Window, &mut App) + 'static,
249 ) -> Self {
250 self.on_change = Some(Arc::new(handler));
251 self
252 }
253
254 pub fn on_change_end(
256 mut self,
257 handler: impl Fn(&PickerColor, &mut Window, &mut App) + 'static,
258 ) -> Self {
259 self.on_change_end = Some(Arc::new(handler));
260 self
261 }
262}
263
264impl RenderOnce for ColorArea {
265 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
266 let (resolved, own) = util::controlled(
269 window,
270 cx,
271 element_id::scoped(&self.id, "area-value"),
272 match self.default_value {
273 Some(_) => None,
274 None => Some(self.value),
275 },
276 self.default_value.unwrap_or(self.value),
277 );
278 self.value = resolved;
279 let area_focus =
283 util::tab_stop_handle(element_id::scoped(&self.id, "area-focus"), window, cx);
284 let bounds_slot =
285 window.use_keyed_state(element_id::scoped(&self.id, "area-bounds"), cx, |_, _| {
286 Bounds::<f32> {
287 origin: gpui::point(0., 0.),
288 size: gpui::size(0., 0.),
289 }
290 });
291 let dragging =
292 window.use_keyed_state(element_id::scoped(&self.id, "area-dragging"), cx, |_, _| {
293 false
294 });
295 let thumb_hovered = window.use_keyed_state(
296 element_id::scoped(&self.id, "area-thumb-hovered"),
297 cx,
298 |_, _| false,
299 );
300 let colors = cx.colors();
301 let border_color = util::sx_border_color(&self.sx).unwrap_or(colors.border);
302 let radius = util::soft_radius(cx);
304
305 let (x_min, x_max) = self.x_channel.range();
306 let (y_min, y_max) = self.y_channel.range();
307 let color_space = self.color_space.unwrap_or_default();
308 let x_norm = ((self.value.channel_in(self.x_channel, color_space) - x_min)
309 / (x_max - x_min))
310 .clamp(0.0, 1.0);
311 let y_norm = ((self.value.channel_in(self.y_channel, color_space) - y_min)
312 / (y_max - y_min))
313 .clamp(0.0, 1.0);
314
315 let mut area = div()
319 .id(self.id.clone())
320 .when(!self.is_disabled, |el| el.track_focus(&area_focus))
321 .relative()
322 .w(self.width)
323 .h(self.height)
324 .rounded(radius);
325
326 let mut layers = div().absolute().inset_0().rounded(radius).overflow_hidden();
333
334 layers =
341 if self.x_channel == ColorChannel::Hue || self.y_channel == ColorChannel::Hue {
342 layers.child(color_area_hue_layers(
343 self.value,
344 color_space,
345 self.x_channel,
346 self.y_channel,
347 radius,
348 ))
349 } else {
350 match (color_space, self.x_channel, self.y_channel) {
351 (ColorSpace::Hsb, ColorChannel::Saturation, ColorChannel::Brightness) => layers
352 .bg(gpui::linear_gradient(
353 90.0,
354 gpui::linear_color_stop(gpui::white(), 0.0),
355 gpui::linear_color_stop(
356 PickerColor::hsb(self.value.hue, 1.0, 1.0).to_hsla(),
357 1.0,
358 ),
359 ))
360 .child(div().absolute().inset_0().rounded(radius).bg(
361 gpui::linear_gradient(
362 180.0,
363 gpui::linear_color_stop(gpui::transparent_black(), 0.0),
364 gpui::linear_color_stop(gpui::black(), 1.0),
365 ),
366 )),
367 (ColorSpace::Hsl, ColorChannel::Saturation, ColorChannel::Lightness) => {
368 let gray = self.value.with_hsl_channels(0.0, 0.5).to_hsla();
369 let hue = self.value.with_hsl_channels(1.0, 0.5).to_hsla();
370 layers
371 .bg(gpui::linear_gradient(
372 90.0,
373 gpui::linear_color_stop(gray, 0.0),
374 gpui::linear_color_stop(hue, 1.0),
375 ))
376 .child(
377 div()
378 .absolute()
379 .top_0()
380 .left_0()
381 .right_0()
382 .rounded(radius)
383 .h(px(f32::from(self.height) / 2.0))
384 .bg(gpui::linear_gradient(
385 180.0,
386 gpui::linear_color_stop(gpui::white(), 0.0),
387 gpui::linear_color_stop(gpui::transparent_white(), 1.0),
388 )),
389 )
390 .child(
391 div()
392 .absolute()
393 .bottom_0()
394 .left_0()
395 .right_0()
396 .rounded(radius)
397 .h(px(f32::from(self.height) / 2.0))
398 .bg(gpui::linear_gradient(
399 180.0,
400 gpui::linear_color_stop(gpui::transparent_black(), 0.0),
401 gpui::linear_color_stop(gpui::black(), 1.0),
402 )),
403 )
404 }
405 _ => layers.child(color_area_channel_grid(
406 self.value,
407 color_space,
408 self.x_channel,
409 self.y_channel,
410 radius,
411 self.width,
412 self.height,
413 )),
414 }
415 };
416
417 if self.show_dots {
421 const STEP: f32 = 8.0;
422 let cols = (f32::from(self.width) / STEP).floor().max(1.0) as usize;
423 let rows = (f32::from(self.height) / STEP).floor().max(1.0) as usize;
424 let mut grid = div().absolute().inset_0().flex().flex_col();
425 for r in 0..rows {
426 let mut line = div().flex().h(px(STEP));
427 for c in 0..cols {
428 let _ = c;
429 line = line.child(
430 div()
431 .w(px(STEP))
432 .flex()
433 .items_center()
434 .justify_center()
435 .child(
436 div()
437 .size(px(2.))
438 .rounded_full()
439 .bg(gpui::white().alpha(0.25)),
440 ),
441 );
442 }
443 let _ = r;
444 grid = grid.child(line);
445 }
446 layers = layers.child(grid);
447 }
448
449 let recorder_bounds = bounds_slot.clone();
450 area = area.child(
451 gpui::canvas(
452 move |bounds: Bounds<Pixels>, _, cx| {
453 recorder_bounds.update(cx, |slot, _| {
454 *slot = Bounds {
455 origin: gpui::point(
456 f32::from(bounds.origin.x),
457 f32::from(bounds.origin.y),
458 ),
459 size: gpui::size(
460 f32::from(bounds.size.width),
461 f32::from(bounds.size.height),
462 ),
463 };
464 });
465 bounds
466 },
467 |_, _, _, _| {},
468 )
469 .absolute()
470 .inset_0(),
471 );
472
473 area = area.child(layers);
474
475 area = area.child(
481 div()
482 .absolute()
483 .inset_0()
484 .rounded(radius)
485 .border(cx.layout().border_width)
486 .border_color(border_color)
487 .shadow(vec![color_inner_shadow()]),
488 );
489
490 let is_dragging = !self.is_disabled && *dragging.read(cx);
491 let is_focused = !self.is_disabled && area_focus.is_focused(window);
492 let is_focus_visible = is_focused && util::focus_visible(cx);
493 let thumb_state = ColorAreaThumbState {
494 color: self.value,
495 is_dragging,
496 is_hovered: !self.is_disabled && *thumb_hovered.read(cx),
497 is_focused,
498 is_focus_visible,
499 is_disabled: self.is_disabled,
500 };
501 let thumb_content = self.thumb.as_ref().map(|render| render(thumb_state));
502 let thumb_motion = color_area_thumb_motion(&self.id, is_dragging, window, cx);
503 let thumb_visual = util::with_focus_ring_overlay(
508 div()
509 .rounded(px(12.))
512 .border(px(3.))
514 .border_color(gpui::white())
515 .shadow(color_thumb_shadows())
516 .bg(self.value.to_hsla()),
517 is_focus_visible,
518 true,
519 px(12.),
520 Vec::new(),
521 cx,
522 );
523 let mut thumb = div()
527 .id(element_id::scoped(&self.id, "area-thumb"))
528 .absolute()
529 .left(px(
530 f32::from(self.width) * x_norm - COLOR_AREA_THUMB_DRAGGING_PX / 2.0
531 ))
532 .top(px(
533 f32::from(self.height) * (1.0 - y_norm) - COLOR_AREA_THUMB_DRAGGING_PX / 2.0
534 ))
535 .size(px(COLOR_AREA_THUMB_DRAGGING_PX))
536 .child(thumb_motion.render(thumb_visual))
537 .when_some(thumb_content, |thumb, content| thumb.child(content));
538 if !self.is_disabled {
539 let hovered = thumb_hovered;
540 thumb = thumb.on_hover(move |is_hovered, _, cx| {
541 hovered.update(cx, |value, cx| {
542 if *value != *is_hovered {
543 *value = *is_hovered;
544 cx.notify();
545 }
546 });
547 });
548 }
549 area = area.child(thumb);
550
551 if self.is_disabled {
552 return area.opacity(cx.layout().disabled_opacity);
553 }
554
555 let keys_value = self.value;
563 let on_change_keys = self.on_change.clone();
564 let end_keys = self.on_change_end.clone();
565 let own_keys = own.clone();
566 let (x_channel, y_channel) = (self.x_channel, self.y_channel);
567 let key_space = color_space;
568 let (x_min, x_max) = x_channel.range();
569 let (y_min, y_max) = y_channel.range();
570 let x_step = if x_max - x_min > 2.0 { 1.0 } else { 0.01 };
574 let y_step = if y_max - y_min > 2.0 { 1.0 } else { 0.01 };
575 let x_page = ((x_max - x_min) / 10.0).max(x_step);
577 let y_page = ((y_max - y_min) / 10.0).max(y_step);
578 area = area
579 .key_context("ColorArea")
580 .on_key_down(move |event, window, cx| {
581 let x_now = keys_value.channel_in(x_channel, key_space);
582 let y_now = keys_value.channel_in(y_channel, key_space);
583 let (nx, ny) = match event.keystroke.key.as_str() {
584 "left" => (x_now - x_step, y_now),
585 "right" => (x_now + x_step, y_now),
586 "down" => (x_now, y_now - y_step),
587 "up" => (x_now, y_now + y_step),
588 "pageup" => (x_now, y_now + y_page),
589 "pagedown" => (x_now, y_now - y_page),
590 "home" => (x_now - x_page, y_now),
591 "end" => (x_now + x_page, y_now),
592 _ => return,
593 };
594 let next = keys_value
595 .with_channel_in(x_channel, key_space, nx.clamp(x_min, x_max))
596 .with_channel_in(y_channel, key_space, ny.clamp(y_min, y_max));
597 if next == keys_value {
598 return;
599 }
600 if let Some(held) = &own_keys {
602 held.update(cx, |v, cx| {
603 *v = next;
604 cx.notify();
605 });
606 }
607 if let Some(cb) = &on_change_keys {
608 cb(&next, window, cx);
609 }
610 if let Some(cb) = &end_keys {
613 cb(&next, window, cx);
614 }
615 });
616
617 if self.on_change.is_some() || self.on_change_end.is_some() || own.is_some() {
618 let down_bounds = bounds_slot.clone();
619 let down_dragging = dragging.clone();
620 let down_focus = area_focus.clone();
621 let down_change = self.on_change.clone();
622 let down_own = own.clone();
623 let down_value = self.value;
624 let (x_channel, y_channel) = (self.x_channel, self.y_channel);
625 area = area.cursor(util::interactive_cursor(cx)).on_mouse_down(
626 gpui::MouseButton::Left,
627 move |event: &MouseDownEvent, window, cx| {
628 if event.modifiers.alt || event.modifiers.control || event.modifiers.platform {
629 return;
630 }
631 down_dragging.update(cx, |value, cx| {
632 if !*value {
633 *value = true;
634 cx.notify();
635 }
636 });
637 window.focus(&down_focus, cx);
638 if let Some(next) = area_color_from_pointer(
639 &down_bounds,
640 event.position,
641 down_value,
642 color_space,
643 x_channel,
644 y_channel,
645 cx,
646 ) {
647 if next.changed {
648 report_color_change(next.value, &down_own, &down_change, window, cx);
649 }
650 }
651 },
652 );
653
654 let global_bounds = bounds_slot;
655 let global_dragging = dragging;
656 let global_change = self.on_change;
657 let global_end = self.on_change_end;
658 let global_own = own;
659 let global_value = self.value;
660 area = area.child(
661 gpui::canvas(
662 |bounds, _, _| bounds,
663 move |_, _, window, _| {
664 let move_bounds = global_bounds.clone();
665 let move_dragging = global_dragging.clone();
666 let move_change = global_change.clone();
667 let move_own = global_own.clone();
668 window.on_mouse_event(move |event: &MouseMoveEvent, phase, window, cx| {
669 if phase == gpui::DispatchPhase::Capture
670 && event.pressed_button == Some(gpui::MouseButton::Left)
671 && *move_dragging.read(cx)
672 {
673 if let Some(next) = area_color_from_pointer(
674 &move_bounds,
675 event.position,
676 global_value,
677 color_space,
678 x_channel,
679 y_channel,
680 cx,
681 ) {
682 if next.changed {
683 report_color_change(
684 next.value,
685 &move_own,
686 &move_change,
687 window,
688 cx,
689 );
690 }
691 }
692 }
693 });
694
695 let up_bounds = global_bounds.clone();
696 let up_dragging = global_dragging.clone();
697 let up_end = global_end.clone();
698 window.on_mouse_event(move |event: &MouseUpEvent, phase, window, cx| {
699 if phase == gpui::DispatchPhase::Capture
700 && event.button == gpui::MouseButton::Left
701 {
702 finish_area_drag(
703 &up_dragging,
704 &up_bounds,
705 event.position,
706 global_value,
707 color_space,
708 x_channel,
709 y_channel,
710 &up_end,
711 window,
712 cx,
713 );
714 }
715 });
716 },
717 )
718 .absolute()
719 .inset_0(),
720 );
721 }
722
723 area = util::apply_sx(area, &self.sx);
724 if !self.is_disabled {
725 area = util::record_focus_bounds(area, &area_focus, window, cx);
726 }
727 area.a11y(a11y::Role::Group)
730 }
731}
732
733#[allow(clippy::too_many_arguments)] pub(super) fn finish_area_drag(
735 dragging: &Entity<bool>,
736 bounds: &Entity<Bounds<f32>>,
737 position: gpui::Point<Pixels>,
738 value: PickerColor,
739 color_space: ColorSpace,
740 x_channel: ColorChannel,
741 y_channel: ColorChannel,
742 on_change_end: &Option<OnColorChange>,
743 window: &mut Window,
744 cx: &mut App,
745) {
746 if !*dragging.read(cx) {
747 return;
748 }
749 dragging.update(cx, |value, cx| {
750 *value = false;
751 cx.notify();
752 });
753 if let (Some(callback), Some(next)) = (
754 on_change_end,
755 area_color_from_pointer(
756 bounds,
757 position,
758 value,
759 color_space,
760 x_channel,
761 y_channel,
762 cx,
763 ),
764 ) {
765 callback(&next.value, window, cx);
766 }
767}
768
769pub(super) fn color_area_hue_layers(
774 value: PickerColor,
775 color_space: ColorSpace,
776 x_channel: ColorChannel,
777 y_channel: ColorChannel,
778 radius: Pixels,
779) -> gpui::Div {
780 let hue_is_vertical = y_channel == ColorChannel::Hue;
781 let other = if x_channel == ColorChannel::Hue {
782 y_channel
783 } else {
784 x_channel
785 };
786 let other_is_vertical = y_channel == other;
787 let base = match (color_space, other) {
788 (ColorSpace::Hsl, ColorChannel::Saturation) => value
789 .with_hsl_channels(1.0, value.hsl_lightness())
790 .with_alpha(1.0),
791 (ColorSpace::Hsl, ColorChannel::Lightness) => value
792 .with_hsl_channels(value.hsl_saturation(), 0.5)
793 .with_alpha(1.0),
794 (ColorSpace::Hsb, ColorChannel::Saturation) => PickerColor::hsb(0.0, 1.0, value.brightness),
795 (ColorSpace::Hsb, ColorChannel::Brightness) => PickerColor::hsb(0.0, value.saturation, 1.0),
796 _ => PickerColor::hsb(0.0, 1.0, 1.0),
797 };
798
799 let mut layers = div().absolute().inset_0().child(hue_gradient(
800 base,
801 color_space,
802 hue_is_vertical,
803 radius,
804 0.0,
805 ));
806 layers = match other {
807 ColorChannel::Saturation => {
808 let start = base
809 .with_channel_in(other, color_space, other.range().0)
810 .to_hsla();
811 layers.child(
812 div()
813 .absolute()
814 .inset_0()
815 .rounded(radius)
816 .bg(gpui::linear_gradient(
817 if other_is_vertical { 0.0 } else { 90.0 },
818 gpui::linear_color_stop(start, 0.0),
819 gpui::linear_color_stop(start.alpha(0.0), 1.0),
820 )),
821 )
822 }
823 ColorChannel::Brightness => layers.child(div().absolute().inset_0().rounded(radius).bg(
824 gpui::linear_gradient(
825 if other_is_vertical { 0.0 } else { 90.0 },
826 gpui::linear_color_stop(gpui::black(), 0.0),
827 gpui::linear_color_stop(gpui::transparent_black(), 1.0),
828 ),
829 )),
830 ColorChannel::Lightness => layers.child(three_stop_gradient(
831 other_is_vertical,
832 gpui::black(),
833 gpui::transparent_black(),
834 gpui::white(),
835 radius,
836 0.0,
837 )),
838 _ => layers,
839 };
840 layers
841}
842
843pub(super) fn hue_gradient(
862 value: PickerColor,
863 color_space: ColorSpace,
864 vertical: bool,
865 radius: Pixels,
866 inset: f32,
867) -> gpui::Div {
868 let stops = hue_stop_colors(value, color_space);
869 let mut gradient = div().absolute().inset_0();
870 for index in 0..6 {
871 let (first, last) = (index == 0, index == 5);
872 let offset = hue_band_offset(index, vertical, inset);
873 let extent = hue_band_extent(index, inset);
874 let (from, to) = hue_band_stop_percentages(index, inset);
875 gradient = if vertical {
876 gradient.child(
877 div()
878 .absolute()
879 .left_0()
880 .right_0()
881 .top(gpui::relative(offset))
882 .h(gpui::relative(extent))
883 .when(first, |band| band.rounded_bl(radius).rounded_br(radius))
884 .when(last, |band| band.rounded_tl(radius).rounded_tr(radius))
885 .bg(gpui::linear_gradient(
886 0.0,
887 gpui::linear_color_stop(stops[index], from),
888 gpui::linear_color_stop(stops[index + 1], to),
889 )),
890 )
891 } else {
892 gradient.child(
893 div()
894 .absolute()
895 .top_0()
896 .bottom_0()
897 .left(gpui::relative(offset))
898 .w(gpui::relative(extent))
899 .when(first, |band| band.rounded_tl(radius).rounded_bl(radius))
900 .when(last, |band| band.rounded_tr(radius).rounded_br(radius))
901 .bg(gpui::linear_gradient(
902 90.0,
903 gpui::linear_color_stop(stops[index], from),
904 gpui::linear_color_stop(stops[index + 1], to),
905 )),
906 )
907 };
908 }
909 gradient
910}
911
912pub(super) fn hue_band_extent(index: usize, inset: f32) -> f32 {
914 let (start, end) = hue_band_span(index, inset);
915 end - start
916}
917
918fn hue_band_span(index: usize, inset: f32) -> (f32, f32) {
920 let inset = inset.clamp(0.0, 0.45);
921 let width = (1.0 - inset * 2.0) / 6.0;
922 let start = if index == 0 {
923 0.0
924 } else {
925 inset + index as f32 * width
926 };
927 let end = if index == 5 {
928 1.0
929 } else {
930 inset + (index + 1) as f32 * width
931 };
932 (start, end)
933}
934
935fn hue_band_stop_percentages(index: usize, inset: f32) -> (f32, f32) {
938 let inset = inset.clamp(0.0, 0.45);
939 let width = (1.0 - inset * 2.0) / 6.0;
940 let total = width + inset;
941 if total <= 0.0 {
942 return (0.0, 1.0);
943 }
944 match index {
945 0 => (inset / total, 1.0),
946 5 => (0.0, width / total),
947 _ => (0.0, 1.0),
948 }
949}
950
951pub(super) fn hue_band_offset(index: usize, vertical: bool, inset: f32) -> f32 {
954 let (start, end) = hue_band_span(index, inset);
955 if vertical {
956 1.0 - end
957 } else {
958 start
959 }
960}
961
962pub(super) fn hue_stop_colors(value: PickerColor, color_space: ColorSpace) -> [Hsla; 7] {
963 [0.0, 60.0, 120.0, 180.0, 240.0, 300.0, 360.0].map(|hue| {
964 value
965 .with_channel_in(ColorChannel::Hue, color_space, hue)
966 .to_hsla()
967 })
968}
969
970pub(super) fn corner_arc_inset(d: f32, r: f32) -> f32 {
976 if r <= 0.0 || d >= r {
977 return 0.0;
978 }
979 let d = d.max(0.0);
980 let leg = r - d;
981 r - (r * r - leg * leg).max(0.0).sqrt()
982}
983
984pub(super) fn color_area_channel_grid(
998 value: PickerColor,
999 color_space: ColorSpace,
1000 x_channel: ColorChannel,
1001 y_channel: ColorChannel,
1002 radius: Pixels,
1003 width: Pixels,
1004 height: Pixels,
1005) -> gpui::Div {
1006 const STRIPS: usize = 64;
1007 let (x_min, x_max) = x_channel.range();
1008 let (y_min, y_max) = y_channel.range();
1009 let width_px = f32::from(width).max(1.0);
1010 let height_px = f32::from(height).max(1.0);
1011 let radius_px = f32::from(radius)
1012 .max(0.0)
1013 .min(width_px / 2.0)
1014 .min(height_px / 2.0);
1015 let ramp_at = |x: f32, inset_px: f32| {
1021 let at_x = value.with_channel_in(x_channel, color_space, x_min + x * (x_max - x_min));
1022 let bottom = at_x
1023 .with_channel_in(y_channel, color_space, y_min)
1024 .to_hsla();
1025 let top = at_x
1026 .with_channel_in(y_channel, color_space, y_max)
1027 .to_hsla();
1028 let visible = (height_px - 2.0 * inset_px).max(1.0);
1029 let overshoot = inset_px / visible;
1030 gpui::linear_gradient(
1031 0.0,
1032 gpui::linear_color_stop(bottom, -overshoot),
1033 gpui::linear_color_stop(top, 1.0 + overshoot),
1034 )
1035 };
1036
1037 let base_w = px((radius_px * 2.0).min(width_px));
1038 let base_sample = (radius_px / 2.0 / width_px).min(0.5);
1042 let mut grid = div()
1043 .absolute()
1044 .inset_0()
1045 .child(
1046 div()
1047 .absolute()
1048 .top_0()
1049 .bottom_0()
1050 .left_0()
1051 .w(base_w)
1052 .rounded_tl(radius)
1053 .rounded_bl(radius)
1054 .bg(ramp_at(base_sample, 0.0)),
1055 )
1056 .child(
1057 div()
1058 .absolute()
1059 .top_0()
1060 .bottom_0()
1061 .right_0()
1062 .w(base_w)
1063 .rounded_tr(radius)
1064 .rounded_br(radius)
1065 .bg(ramp_at(1.0 - base_sample, 0.0)),
1066 );
1067
1068 let strip_w = width_px / STRIPS as f32;
1069 for index in 0..STRIPS {
1070 let x = index as f32 / (STRIPS - 1) as f32;
1071 let left = index as f32 * strip_w;
1072 let distance = left.min(width_px - (left + strip_w));
1074 let inset_px = corner_arc_inset(distance, radius_px);
1075 let inset = px(inset_px);
1076 grid = grid.child(
1077 div()
1078 .absolute()
1079 .left(px(left))
1080 .w(px(strip_w))
1081 .top(inset)
1082 .bottom(inset)
1083 .bg(ramp_at(x, inset_px)),
1084 );
1085 }
1086 grid
1087}
1088
1089pub(super) struct PointerColor {
1090 pub(super) value: PickerColor,
1091 pub(super) changed: bool,
1092}
1093
1094#[allow(clippy::float_cmp)] pub(super) fn area_color_from_pointer(
1096 bounds: &Entity<Bounds<f32>>,
1097 position: gpui::Point<Pixels>,
1098 value: PickerColor,
1099 color_space: ColorSpace,
1100 x_channel: ColorChannel,
1101 y_channel: ColorChannel,
1102 cx: &App,
1103) -> Option<PointerColor> {
1104 let bounds = *bounds.read(cx);
1105 if bounds.size.width <= 0.0 || bounds.size.height <= 0.0 {
1106 return None;
1107 }
1108 let x = ((f32::from(position.x) - bounds.origin.x) / bounds.size.width).clamp(0.0, 1.0);
1109 let y = ((f32::from(position.y) - bounds.origin.y) / bounds.size.height).clamp(0.0, 1.0);
1110 let (x_min, x_max) = x_channel.range();
1111 let (y_min, y_max) = y_channel.range();
1112 let x_value = snap_color_channel(x_channel, x_min + x * (x_max - x_min));
1113 let y_value = snap_color_channel(y_channel, y_min + (1.0 - y) * (y_max - y_min));
1114 Some(PointerColor {
1115 changed: x_value != value.channel_in(x_channel, color_space)
1116 || y_value != value.channel_in(y_channel, color_space),
1117 value: value
1118 .with_channel_in(x_channel, color_space, x_value)
1119 .with_channel_in(y_channel, color_space, y_value),
1120 })
1121}
1122
1123pub(super) fn report_color_change(
1124 next: PickerColor,
1125 own: &Option<Entity<PickerColor>>,
1126 on_change: &Option<OnColorChange>,
1127 window: &mut Window,
1128 cx: &mut App,
1129) {
1130 if let Some(held) = own {
1131 held.update(cx, |value, cx| {
1132 *value = next;
1133 cx.notify();
1134 });
1135 }
1136 if let Some(callback) = on_change {
1137 callback(&next, window, cx);
1138 }
1139}
1140
1141pub(super) fn live_color_form_state(
1142 value: crate::form::FormValue,
1143) -> Rc<RefCell<crate::form::LiveFormFieldState>> {
1144 Rc::new(RefCell::new(crate::form::LiveFormFieldState {
1145 value,
1146 is_invalid: false,
1147 is_successful: true,
1148 focus: None,
1149 restore: None,
1150 }))
1151}
1152
1153pub(super) fn sync_color_form_state(
1154 state: &Rc<RefCell<crate::form::LiveFormFieldState>>,
1155 value: crate::form::FormValue,
1156 is_successful: bool,
1157 is_invalid: bool,
1158) {
1159 let mut state = state.borrow_mut();
1160 state.value = value;
1161 state.is_successful = is_successful;
1162 state.is_invalid = is_invalid;
1163}
1164
1165pub(super) fn color_slider_form_value(
1167 value: PickerColor,
1168 channel: ColorChannel,
1169 space: ColorSpace,
1170) -> crate::form::FormValue {
1171 let value = value.channel_in(channel, space);
1172 let value = if matches!(
1173 channel,
1174 ColorChannel::Saturation | ColorChannel::Brightness | ColorChannel::Lightness
1175 ) {
1176 value * 100.0
1177 } else {
1178 value
1179 };
1180 crate::form::FormValue::Number(f64::from(value))
1181}
1182
1183pub(super) fn color_field_form_value(
1186 value: Option<PickerColor>,
1187 channel: Option<ColorChannel>,
1188 space: ColorSpace,
1189) -> crate::form::FormValue {
1190 let Some(value) = value else {
1191 return crate::form::FormValue::Text(SharedString::default());
1192 };
1193 match channel {
1194 None => crate::form::FormValue::Text(value.to_hex().into()),
1195 Some(channel) => {
1196 crate::form::FormValue::Number(f64::from(value.channel_in(channel, space)))
1197 }
1198 }
1199}
1200
1201pub(super) fn color_field_display_text(
1202 value: Option<PickerColor>,
1203 channel: Option<ColorChannel>,
1204 space: ColorSpace,
1205) -> String {
1206 let Some(value) = value else {
1207 return String::new();
1208 };
1209 match channel {
1210 None => value.to_hex(),
1211 Some(channel) => format_color_channel_value(value, channel, space),
1212 }
1213}
1214
1215crate::util::impl_component_styled!(ColorArea);