1use super::*;
4
5#[derive(Clone, Copy, Debug, Default, PartialEq)]
10#[non_exhaustive]
11pub struct ColorSliderThumbState {
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_SLIDER_TRACK_INSET_PX: f32 = 10.0;
27const COLOR_SLIDER_THUMB_TRANSITION_MS: u64 = 250;
28
29#[derive(Clone)]
30struct ColorSliderThumbMotion {
31 target: f32,
32 generation: usize,
33 from: f32,
34 position: Rc<Cell<f32>>,
35}
36
37struct ColorSliderThumbMotionFrame {
38 base: ElementId,
39 generation: usize,
40 from: f32,
41 to: f32,
42 position: Rc<Cell<f32>>,
43 animate: bool,
44}
45
46impl ColorSliderThumbMotionFrame {
47 fn render(self, thumb: gpui::Div, travel: Pixels, vertical: bool) -> gpui::AnyElement {
48 if !self.animate {
49 self.position.set(self.to);
50 return thumb.into_any_element();
51 }
52
53 let base = self.base;
54 let generation = self.generation;
55 let from = self.from;
56 let to = self.to;
57 let position = self.position;
58 thumb
59 .relative()
60 .with_animation(
61 element_id::indexed(&base, "thumb-position", generation),
62 Animation::new(Duration::from_millis(COLOR_SLIDER_THUMB_TRANSITION_MS))
63 .with_easing(|t| crate::anim::Curve::Out.at(t)),
64 move |thumb, delta| {
65 let next = from + (to - from) * delta;
66 position.set(next);
67 let offset = color_slider_thumb_transition_offset(next, to, travel, vertical);
72 if vertical {
73 thumb.top(offset)
74 } else {
75 thumb.left(offset)
76 }
77 },
78 )
79 .into_any_element()
80 }
81}
82
83fn color_slider_thumb_motion(
84 id: &ElementId,
85 target: f32,
86 window: &mut Window,
87 cx: &mut App,
88) -> ColorSliderThumbMotionFrame {
89 let state = window.use_keyed_state(element_id::scoped(id, "thumb-position"), cx, |_, _| {
90 ColorSliderThumbMotion {
91 target,
92 generation: 0,
93 from: target,
94 position: Rc::new(Cell::new(target)),
95 }
96 });
97 let mut current = state.read(cx).clone();
98 if (current.target - target).abs() > f32::EPSILON {
99 current.target = target;
100 current.generation = current.generation.wrapping_add(1);
101 current.from = current.position.get();
102 state.update(cx, |stored, _| *stored = current.clone());
103 }
104 if ActiveTheme::reduce_motion(cx) && (current.position.get() - target).abs() > f32::EPSILON {
105 current.from = target;
106 current.position.set(target);
107 state.update(cx, |stored, _| *stored = current.clone());
108 }
109 ColorSliderThumbMotionFrame {
110 base: id.clone(),
111 generation: current.generation,
112 from: current.from,
113 to: target,
114 position: current.position,
115 animate: current.generation != 0
116 && !ActiveTheme::reduce_motion(cx)
117 && (current.from - target).abs() > f32::EPSILON,
118 }
119}
120
121pub(super) fn color_slider_thumb_transition_offset(
122 from: f32,
123 to: f32,
124 travel: Pixels,
125 vertical: bool,
126) -> Pixels {
127 let offset = if vertical {
128 (to - from) * f32::from(travel)
129 } else {
130 (from - to) * f32::from(travel)
131 };
132 px(offset)
133}
134
135#[must_use = "a component does nothing until it is rendered: add it as a child or return it from `render`"]
137#[derive(IntoElement)]
138pub struct ColorSlider {
139 name: Option<SharedString>,
142 default_value: Option<PickerColor>,
144 id: ElementId,
145 value: PickerColor,
146 channel: ColorChannel,
147 color_space: ColorSpace,
150 orientation: herogpui_core::Orientation,
151 length: Pixels,
152 show_label: bool,
153 output: Option<Arc<dyn Fn(PickerColor, &str) -> gpui::AnyElement + 'static>>,
156 thumb: Option<Arc<dyn Fn(ColorSliderThumbState) -> gpui::AnyElement + 'static>>,
157 is_disabled: bool,
158 on_change: Option<OnColorChange>,
159 on_change_end: Option<OnColorChange>,
160 form_state: Rc<RefCell<crate::form::LiveFormFieldState>>,
161}
162
163impl ColorSlider {
164 pub fn new(id: impl Into<ElementId>, value: PickerColor, channel: ColorChannel) -> Self {
166 Self {
167 name: None,
168 default_value: None,
169 id: id.into(),
170 value,
171 channel,
172 color_space: ColorSpace::default(),
173 orientation: herogpui_core::Orientation::Horizontal,
174 length: px(240.),
175 show_label: true,
176 output: None,
177 thumb: None,
178 is_disabled: false,
179 on_change: None,
180 on_change_end: None,
181 form_state: live_color_form_state(crate::form::FormValue::Number(0.0)),
182 }
183 }
184
185 pub fn name(mut self, name: impl Into<SharedString>) -> Self {
187 self.name = Some(name.into());
188 self
189 }
190
191 pub fn form_field(&self) -> Option<crate::form::FormField> {
215 let name = self.name.clone()?;
216 let value = self.default_value.unwrap_or(self.value);
217 sync_color_form_state(
220 &self.form_state,
221 color_slider_form_value(value, self.channel, self.color_space),
222 !self.is_disabled,
223 false,
224 );
225 Some(crate::form::FormField::live(name, self.form_state.clone()).is_required(false))
226 }
227
228 pub fn default_value(mut self, value: PickerColor) -> Self {
233 self.default_value = Some(value);
234 self
235 }
236
237 pub fn color_space(mut self, space: ColorSpace) -> Self {
241 self.color_space = space;
242 self
243 }
244
245 pub fn orientation(mut self, orientation: herogpui_core::Orientation) -> Self {
247 self.orientation = orientation;
248 self
249 }
250
251 pub fn on_change_end(
253 mut self,
254 handler: impl Fn(&PickerColor, &mut Window, &mut App) + 'static,
255 ) -> Self {
256 self.on_change_end = Some(Arc::new(handler));
257 self
258 }
259
260 pub fn length(mut self, length: impl Into<Pixels>) -> Self {
262 self.length = length.into();
263 self
264 }
265
266 pub fn output(
269 mut self,
270 render: impl Fn(PickerColor, &str) -> gpui::AnyElement + 'static,
271 ) -> Self {
272 self.output = Some(Arc::new(render));
273 self
274 }
275
276 pub fn thumb(
280 mut self,
281 render: impl Fn(ColorSliderThumbState) -> gpui::AnyElement + 'static,
282 ) -> Self {
283 self.thumb = Some(Arc::new(render));
284 self
285 }
286
287 pub fn show_label(mut self, v: bool) -> Self {
289 self.show_label = v;
290 self
291 }
292
293 pub fn is_disabled(mut self, v: bool) -> Self {
295 self.is_disabled = v;
296 self
297 }
298
299 pub fn on_change(
301 mut self,
302 handler: impl Fn(&PickerColor, &mut Window, &mut App) + 'static,
303 ) -> Self {
304 self.on_change = Some(Arc::new(handler));
305 self
306 }
307
308 fn display_color(&self) -> PickerColor {
311 match self.channel {
312 ColorChannel::Hue => PickerColor::hsb(self.value.hue, 1.0, 1.0),
313 ColorChannel::Alpha => self.value,
314 _ => self.value.with_alpha(1.0),
315 }
316 }
317
318 fn gradient_ends(&self) -> (Hsla, Hsla) {
320 let (min, max) = self.channel.range();
321 let color = self.display_color();
322 (
323 color
324 .with_channel_in(self.channel, self.color_space, min)
325 .to_hsla(),
326 color
327 .with_channel_in(self.channel, self.color_space, max)
328 .to_hsla(),
329 )
330 }
331}
332
333pub(super) fn three_stop_gradient(
351 vertical: bool,
352 start: Hsla,
353 middle: Hsla,
354 end: Hsla,
355 radius: Pixels,
356 inset: f32,
357) -> gpui::Div {
358 let inset = inset.clamp(0.0, 0.45);
359 let half = (1.0 - inset * 2.0) / 2.0;
360 let extent = gpui::relative(half + inset);
361 let total = half + inset;
362 let (near_from, far_to) = if total > 0.0 {
363 (inset / total, half / total)
364 } else {
365 (0.0, 1.0)
366 };
367 if vertical {
368 div()
370 .absolute()
371 .inset_0()
372 .child(
373 div()
374 .absolute()
375 .top_0()
376 .left_0()
377 .right_0()
378 .rounded_tl(radius)
379 .rounded_tr(radius)
380 .h(extent)
381 .bg(gpui::linear_gradient(
382 0.0,
383 gpui::linear_color_stop(middle, 0.0),
384 gpui::linear_color_stop(end, far_to),
385 )),
386 )
387 .child(
388 div()
389 .absolute()
390 .bottom_0()
391 .left_0()
392 .right_0()
393 .rounded_bl(radius)
394 .rounded_br(radius)
395 .h(extent)
396 .bg(gpui::linear_gradient(
397 0.0,
398 gpui::linear_color_stop(start, near_from),
399 gpui::linear_color_stop(middle, 1.0),
400 )),
401 )
402 } else {
403 div()
404 .absolute()
405 .inset_0()
406 .child(
407 div()
408 .absolute()
409 .top_0()
410 .bottom_0()
411 .left_0()
412 .rounded_tl(radius)
413 .rounded_bl(radius)
414 .w(extent)
415 .bg(gpui::linear_gradient(
416 90.0,
417 gpui::linear_color_stop(start, near_from),
418 gpui::linear_color_stop(middle, 1.0),
419 )),
420 )
421 .child(
422 div()
423 .absolute()
424 .top_0()
425 .bottom_0()
426 .right_0()
427 .rounded_tr(radius)
428 .rounded_br(radius)
429 .w(extent)
430 .bg(gpui::linear_gradient(
431 90.0,
432 gpui::linear_color_stop(middle, 0.0),
433 gpui::linear_color_stop(end, far_to),
434 )),
435 )
436 }
437}
438
439impl RenderOnce for ColorSlider {
440 fn render(mut self, window: &mut Window, cx: &mut App) -> impl IntoElement {
441 let (resolved, own) = util::controlled(
444 window,
445 cx,
446 element_id::scoped(&self.id, "slider-value"),
447 match self.default_value {
448 Some(_) => None,
449 None => Some(self.value),
450 },
451 self.default_value.unwrap_or(self.value),
452 );
453 self.value = resolved;
454 let form_default = window.use_keyed_state(
455 element_id::scoped(&self.id, "slider-form-default"),
456 cx,
457 |_, _| None::<PickerColor>,
458 );
459 if form_default.read(cx).is_none() {
460 let initial = self.value;
461 form_default.update(cx, |slot, cx| {
462 *slot = Some(initial);
463 cx.notify();
464 });
465 }
466 let restore_default = form_default.read(cx).unwrap_or(self.value);
467 sync_color_form_state(
471 &self.form_state,
472 color_slider_form_value(self.value, self.channel, self.color_space),
473 !self.is_disabled,
474 false,
475 );
476 let restore_own = own.clone();
477 let restore_on_change = self.on_change.clone();
478 let restore_form_state = Rc::downgrade(&self.form_state);
479 let restore_channel = self.channel;
480 let restore_space = self.color_space;
481 let restore_is_disabled = self.is_disabled;
482 let restore: Arc<dyn Fn(&mut Window, &mut App)> = util::shared(move |window, cx| {
483 if let Some(own) = &restore_own {
484 own.update(cx, |current, cx| {
485 *current = restore_default;
486 cx.notify();
487 });
488 }
489 if let Some(callback) = &restore_on_change {
490 callback(&restore_default, window, cx);
491 }
492 if let Some(state) = restore_form_state.upgrade() {
493 sync_color_form_state(
494 &state,
495 color_slider_form_value(restore_default, restore_channel, restore_space),
496 !restore_is_disabled,
497 false,
498 );
499 }
500 });
501 self.form_state.borrow_mut().restore = Some(restore);
502 let focus_handle =
505 window.use_keyed_state(element_id::scoped(&self.id, "slider-focus"), cx, |_, cx| {
506 cx.focus_handle().tab_stop(true)
507 });
508 let focus_handle = focus_handle.read(cx).clone();
509 self.form_state.borrow_mut().focus = Some(focus_handle.clone());
510 let bounds_slot =
511 window.use_keyed_state(element_id::scoped(&self.id, "slider-bounds"), cx, |_, _| {
512 Bounds::<f32> {
513 origin: gpui::point(0., 0.),
514 size: gpui::size(0., 0.),
515 }
516 });
517 let dragging = window.use_keyed_state(
518 element_id::scoped(&self.id, "slider-dragging"),
519 cx,
520 |_, _| false,
521 );
522 let thumb_hovered = window.use_keyed_state(
523 element_id::scoped(&self.id, "slider-thumb-hovered"),
524 cx,
525 |_, _| false,
526 );
527 let (min, max) = self.channel.range();
528 let raw = self.value.channel_in(self.channel, self.color_space);
531 let norm = ((raw - min) / (max - min)).clamp(0.0, 1.0);
532 let thumb_motion = color_slider_thumb_motion(&self.id, norm, window, cx);
533 let default_thumb_color = cx.colors().default.color;
534 let track_h = px(20.);
537
538 let vertical = !self.orientation.is_horizontal();
539 let mut track = div()
540 .id(self.id.clone())
541 .relative()
542 .rounded(px(COLOR_SLIDER_TRACK_INSET_PX));
543 track = if vertical {
544 track.w(track_h).h(self.length)
545 } else {
546 track.w(self.length).h(track_h)
547 };
548
549 let recorder_bounds = bounds_slot.clone();
550 track = track.child(
551 gpui::canvas(
552 move |bounds: Bounds<Pixels>, _, cx| {
553 recorder_bounds.update(cx, |slot, _| {
554 *slot = Bounds {
555 origin: gpui::point(
556 f32::from(bounds.origin.x),
557 f32::from(bounds.origin.y),
558 ),
559 size: gpui::size(
560 f32::from(bounds.size.width),
561 f32::from(bounds.size.height),
562 ),
563 };
564 });
565 bounds
566 },
567 |_, _, _, _| {},
568 )
569 .absolute()
570 .inset_0(),
571 );
572
573 let track_r = px(COLOR_SLIDER_TRACK_INSET_PX);
576 let mut layers = div()
577 .absolute()
578 .inset_0()
579 .rounded(track_r)
580 .overflow_hidden();
581
582 if self.channel == ColorChannel::Alpha {
583 let (checker_w, checker_h) = if vertical {
587 (track_h, self.length)
588 } else {
589 (self.length, track_h)
590 };
591 layers = layers
592 .bg(gpui::rgb(CHECKER_LIGHT))
593 .child(transparency_checker_cells(
594 checker_w,
595 checker_h,
596 track_r,
597 if self.is_disabled {
598 cx.layout().disabled_opacity
599 } else {
600 1.0
601 },
602 ));
603 }
604
605 let (start_color, end_color) = self.gradient_ends();
625 let inset =
626 (COLOR_SLIDER_TRACK_INSET_PX / f32::from(self.length).max(1.0)).clamp(0.0, 0.45);
627 let ramp = div().absolute().inset_0().rounded(track_r);
628 let ramp = if self.channel == ColorChannel::Lightness {
629 let (start, middle, end) =
630 lightness_gradient_colors(self.display_color(), self.color_space, min, max);
631 ramp.child(three_stop_gradient(
632 vertical, start, middle, end, track_r, inset,
633 ))
634 } else if self.channel == ColorChannel::Hue {
635 ramp.child(hue_gradient(
636 self.display_color(),
637 self.color_space,
638 vertical,
639 track_r,
640 inset,
641 ))
642 } else {
643 ramp.bg(gpui::linear_gradient(
644 if vertical { 0.0 } else { 90.0 },
645 gpui::linear_color_stop(start_color, inset),
646 gpui::linear_color_stop(end_color, 1.0 - inset),
647 ))
648 };
649 layers = layers.child(ramp);
650
651 track = track.child(layers);
652
653 track = track.child(
657 div()
658 .absolute()
659 .inset_0()
660 .rounded(px(COLOR_SLIDER_TRACK_INSET_PX))
661 .shadow(color_track_shadows()),
662 );
663
664 let travel = (f32::from(self.length) - COLOR_SLIDER_TRACK_INSET_PX * 2.0).max(0.0);
667 let thumb_offset = px(COLOR_SLIDER_TRACK_INSET_PX
668 + travel * if vertical { 1.0 - norm } else { norm }
669 - 8.0);
670 let is_dragging = !self.is_disabled && *dragging.read(cx);
671 let is_focused = !self.is_disabled && focus_handle.is_focused(window);
672 let is_focus_visible = is_focused && util::focus_visible(cx);
673 let thumb_state = ColorSliderThumbState {
674 color: self.value.with_alpha(1.0),
675 is_dragging,
676 is_hovered: !self.is_disabled && *thumb_hovered.read(cx),
677 is_focused,
678 is_focus_visible,
679 is_disabled: self.is_disabled,
680 };
681 let thumb_content = self.thumb.as_ref().map(|render| render(thumb_state));
682 let focus_motion = color_focus_ring_motion(
683 &element_id::scoped(&self.id, "slider-thumb"),
684 is_focus_visible,
685 window,
686 cx,
687 );
688 let thumb_surface = focus_motion.render(
689 div()
690 .size(px(16.))
692 .rounded(px(16.))
693 .border(px(3.))
694 .border_color(gpui::white())
695 .bg(if self.is_disabled {
696 default_thumb_color
697 } else {
698 self.value.with_alpha(1.0).to_hsla()
699 })
700 .when_some(thumb_content, |thumb, content| thumb.child(content)),
701 color_thumb_shadows(),
702 true,
703 cx,
704 );
705 let thumb_visual = div()
706 .size(px(16.))
707 .flex()
708 .items_center()
709 .justify_center()
710 .child(thumb_surface);
711 let mut thumb = div()
712 .id(element_id::scoped(&self.id, "slider-thumb"))
713 .absolute()
714 .when(vertical, |t| t.left(px(2.)).top(thumb_offset))
715 .when(!vertical, |t| t.top(px(2.)).left(thumb_offset))
716 .size(px(16.))
717 .when(!self.is_disabled, |t| {
718 t.cursor(if is_dragging {
719 gpui::CursorStyle::ClosedHand
720 } else {
721 gpui::CursorStyle::OpenHand
722 })
723 })
724 .child(thumb_motion.render(thumb_visual, px(travel), vertical));
725 if !self.is_disabled {
726 let hovered = thumb_hovered;
727 thumb = thumb.on_hover(move |is_hovered, _, cx| {
728 hovered.update(cx, |value, cx| {
729 if *value != *is_hovered {
730 *value = *is_hovered;
731 cx.notify();
732 }
733 });
734 });
735 }
736 track = track.child(thumb);
737
738 if self.is_disabled {
739 track = track.opacity(cx.layout().disabled_opacity);
740 } else {
741 let value = self.value;
742 let channel = self.channel;
743 let space = self.color_space;
744 track = util::cursor_interactive(track, cx);
745 let keys_value = self.value;
749 let on_change_keys = self.on_change.clone();
750 let end_keys = self.on_change_end.clone();
751 let own_keys = own.clone();
752 let step = if max - min > 2.0 { 1.0 } else { 0.01 };
755 let page = ((max - min) / 10.0).max(step);
756 track = track
757 .track_focus(&focus_handle)
758 .key_context("ColorSlider")
759 .on_key_down(move |event, window, cx| {
760 let current = keys_value.channel_in(channel, space);
761 let next = match event.keystroke.key.as_str() {
762 "right" | "up" => current + step,
763 "left" | "down" => current - step,
764 "pageup" => current + page,
765 "pagedown" => current - page,
766 "home" => min,
767 "end" => max,
768 _ => return,
769 };
770 let next = keys_value.with_channel_in(channel, space, next.clamp(min, max));
771 if next == keys_value {
772 return;
773 }
774 if let Some(held) = &own_keys {
775 held.update(cx, |v, cx| {
776 *v = next;
777 cx.notify();
778 });
779 }
780 if let Some(cb) = &on_change_keys {
781 cb(&next, window, cx);
782 }
783 if let Some(cb) = &end_keys {
786 cb(&next, window, cx);
787 }
788 });
789 if self.on_change.is_some() || self.on_change_end.is_some() || own.is_some() {
790 let down_bounds = bounds_slot.clone();
791 let down_dragging = dragging.clone();
792 let down_change = self.on_change.clone();
793 let down_own = own.clone();
794 let focus_for_press = focus_handle.clone();
795 track = track.on_mouse_down(
796 gpui::MouseButton::Left,
797 move |event: &MouseDownEvent, window, cx| {
798 if event.modifiers.alt
799 || event.modifiers.control
800 || event.modifiers.platform
801 {
802 return;
803 }
804 down_dragging.update(cx, |value, cx| {
805 if !*value {
806 *value = true;
807 cx.notify();
808 }
809 });
810 window.focus(&focus_for_press, cx);
811 if let Some(next) = slider_color_from_pointer(
812 &down_bounds,
813 event.position,
814 vertical,
815 value,
816 channel,
817 space,
818 cx,
819 ) {
820 if next.changed {
821 report_color_change(
822 next.value,
823 &down_own,
824 &down_change,
825 window,
826 cx,
827 );
828 }
829 }
830 },
831 );
832
833 let global_bounds = bounds_slot;
834 let global_dragging = dragging;
835 let global_change = self.on_change;
836 let global_end = self.on_change_end;
837 let global_own = own;
838 track = track.child(
839 gpui::canvas(
840 |bounds, _, _| bounds,
841 move |_, _, window, _| {
842 let move_bounds = global_bounds.clone();
843 let move_dragging = global_dragging.clone();
844 let move_change = global_change.clone();
845 let move_own = global_own.clone();
846 window.on_mouse_event(
847 move |event: &MouseMoveEvent, phase, window, cx| {
848 if phase == gpui::DispatchPhase::Capture
849 && event.pressed_button == Some(gpui::MouseButton::Left)
850 && *move_dragging.read(cx)
851 {
852 if let Some(next) = slider_color_from_pointer(
853 &move_bounds,
854 event.position,
855 vertical,
856 value,
857 channel,
858 space,
859 cx,
860 ) {
861 if next.changed {
862 report_color_change(
863 next.value,
864 &move_own,
865 &move_change,
866 window,
867 cx,
868 );
869 }
870 }
871 }
872 },
873 );
874
875 let up_bounds = global_bounds.clone();
876 let up_dragging = global_dragging.clone();
877 let up_end = global_end.clone();
878 window.on_mouse_event(
879 move |event: &MouseUpEvent, phase, window, cx| {
880 if phase == gpui::DispatchPhase::Capture
881 && event.button == gpui::MouseButton::Left
882 {
883 finish_slider_drag(
884 &up_dragging,
885 &up_bounds,
886 event.position,
887 vertical,
888 value,
889 channel,
890 space,
891 &up_end,
892 window,
893 cx,
894 );
895 }
896 },
897 );
898 },
899 )
900 .absolute()
901 .inset_0(),
902 );
903 }
904 }
905
906 track = track
909 .a11y_named(a11y::Role::Slider, &a11y::Name::maybe(self.name.clone()))
910 .a11y_orientation(self.orientation);
911 if !self.is_disabled {
912 track = util::record_focus_bounds(track, &focus_handle, window, cx);
913 }
914
915 if !self.show_label {
916 return div().child(track);
917 }
918
919 let display = match self.channel {
920 ColorChannel::Hue => format!("{}\u{00B0}", raw.round()),
921 ColorChannel::Red | ColorChannel::Green | ColorChannel::Blue => {
922 format!("{}", raw.round())
923 }
924 _ => format!("{}%", (raw * 100.0).round()),
925 };
926 let output = match &self.output {
927 Some(render) => render(self.value, &display),
931 None => div()
932 .text_color(cx.colors().muted)
933 .child(display)
934 .into_any_element(),
935 };
936
937 div()
938 .flex()
939 .flex_col()
940 .gap(px(4.))
942 .child(
943 div()
944 .flex()
945 .flex_row()
946 .items_center()
947 .justify_between()
948 .w(self.length)
949 .text_size(px(14.))
950 .line_height(px(20.))
951 .font_weight(gpui::FontWeight::MEDIUM)
952 .child(
953 div()
954 .text_color(cx.colors().foreground)
955 .child(self.channel.localized_label(cx)),
956 )
957 .child(
960 div()
961 .when(self.is_disabled, |output| {
962 output.opacity(cx.layout().disabled_opacity)
963 })
964 .font_features(util::tabular_font_features())
965 .child(output),
966 ),
967 )
968 .child(track)
969 }
970}
971
972pub(super) fn lightness_gradient_colors(
973 value: PickerColor,
974 color_space: ColorSpace,
975 min: f32,
976 max: f32,
977) -> (Hsla, Hsla, Hsla) {
978 (
979 value
980 .with_channel_in(ColorChannel::Lightness, color_space, min)
981 .to_hsla(),
982 value
983 .with_channel_in(ColorChannel::Lightness, color_space, (max - min) / 2.0)
984 .to_hsla(),
985 value
986 .with_channel_in(ColorChannel::Lightness, color_space, max)
987 .to_hsla(),
988 )
989}
990
991#[allow(clippy::float_cmp)] pub(super) fn slider_color_from_pointer(
993 bounds: &Entity<Bounds<f32>>,
994 position: gpui::Point<Pixels>,
995 vertical: bool,
996 value: PickerColor,
997 channel: ColorChannel,
998 color_space: ColorSpace,
999 cx: &App,
1000) -> Option<PointerColor> {
1001 let bounds = *bounds.read(cx);
1002 let (reach, extent) = if vertical {
1003 (
1004 bounds.origin.y + bounds.size.height
1005 - COLOR_SLIDER_TRACK_INSET_PX
1006 - f32::from(position.y),
1007 bounds.size.height - COLOR_SLIDER_TRACK_INSET_PX * 2.0,
1008 )
1009 } else {
1010 (
1011 f32::from(position.x) - bounds.origin.x - COLOR_SLIDER_TRACK_INSET_PX,
1012 bounds.size.width - COLOR_SLIDER_TRACK_INSET_PX * 2.0,
1013 )
1014 };
1015 if extent <= 0.0 {
1016 return None;
1017 }
1018 let fraction = (reach / extent).clamp(0.0, 1.0);
1019 let (min, max) = channel.range();
1020 let next = snap_color_channel(channel, min + fraction * (max - min));
1021 Some(PointerColor {
1022 changed: next != value.channel_in(channel, color_space),
1023 value: value.with_channel_in(channel, color_space, next),
1024 })
1025}
1026
1027#[allow(clippy::too_many_arguments)] pub(super) fn finish_slider_drag(
1029 dragging: &Entity<bool>,
1030 bounds: &Entity<Bounds<f32>>,
1031 position: gpui::Point<Pixels>,
1032 vertical: bool,
1033 value: PickerColor,
1034 channel: ColorChannel,
1035 color_space: ColorSpace,
1036 on_change_end: &Option<OnColorChange>,
1037 window: &mut Window,
1038 cx: &mut App,
1039) {
1040 if !*dragging.read(cx) {
1041 return;
1042 }
1043 dragging.update(cx, |value, cx| {
1044 *value = false;
1045 cx.notify();
1046 });
1047 if let (Some(callback), Some(next)) = (
1048 on_change_end,
1049 slider_color_from_pointer(bounds, position, vertical, value, channel, color_space, cx),
1050 ) {
1051 callback(&next.value, window, cx);
1052 }
1053}
1054
1055