1use crate::input::InputState;
2use std::rc::Rc;
3
4use gpui::{
5 AnyElement, App, AppContext as _, ClickEvent, Context, Div, ElementId, Entity, EventEmitter,
6 FocusHandle, Focusable, Hsla, InteractiveElement, Interactivity, IntoElement, KeyBinding,
7 ParentElement, Render, RenderOnce, Rgba, Role, SharedString, Stateful,
8 StatefulInteractiveElement, StyleRefinement, Styled, Subscription, Toggled, Window, div, hsla,
9 prelude::FluentBuilder as _,
10};
11use smallvec::SmallVec;
12
13use crate::{
14 RoleOverride, StyledExt as _,
15 actions::{Cancel, Confirm},
16 input::InputEvent,
17 slider::{SliderEvent, SliderState},
18};
19
20const CONTEXT: &str = "ColorPicker";
21
22const HEX_PATTERN: &str = r"^#[0-9a-fA-F]{0,8}$";
24
25pub(crate) fn init(cx: &mut App) {
26 cx.bind_keys([
27 KeyBinding::new("enter", Confirm { secondary: false }, Some(CONTEXT)),
28 KeyBinding::new("escape", Cancel, Some(CONTEXT)),
29 ]);
30}
31
32fn parse_hex(value: &str) -> Option<Hsla> {
34 let value = value.strip_prefix('#').unwrap_or(value);
35 if !value.bytes().all(|byte| byte.is_ascii_hexdigit()) {
38 return None;
39 }
40
41 let (width, has_alpha) = match value.len() {
42 3 => (1, false),
43 4 => (1, true),
44 6 => (2, false),
45 8 => (2, true),
46 _ => return None,
47 };
48
49 let component = |index: usize| {
50 let start = index * width;
51 let raw = u8::from_str_radix(&value[start..start + width], 16).ok()?;
52 let raw = if width == 1 { raw * 0x11 } else { raw };
54 Some(raw as f32 / 255.0)
55 };
56
57 Some(
58 Rgba {
59 r: component(0)?,
60 g: component(1)?,
61 b: component(2)?,
62 a: if has_alpha { component(3)? } else { 1.0 },
63 }
64 .into(),
65 )
66}
67
68fn hex_string(color: Hsla) -> String {
70 let rgba = Rgba::from(color);
71 let channel = |value: f32| (value.clamp(0., 1.) * 255.).round() as u8;
72 if rgba.a < 1. {
73 format!(
74 "#{:02X}{:02X}{:02X}{:02X}",
75 channel(rgba.r),
76 channel(rgba.g),
77 channel(rgba.b),
78 channel(rgba.a)
79 )
80 } else {
81 format!(
82 "#{:02X}{:02X}{:02X}",
83 channel(rgba.r),
84 channel(rgba.g),
85 channel(rgba.b)
86 )
87 }
88}
89
90#[derive(Clone)]
92pub enum ColorPickerEvent {
93 Change(Option<Hsla>),
95}
96
97#[derive(Clone)]
102pub struct HslaSliders {
103 hue: Entity<SliderState>,
104 saturation: Entity<SliderState>,
105 lightness: Entity<SliderState>,
106 alpha: Entity<SliderState>,
107}
108
109impl HslaSliders {
110 fn new(cx: &mut App) -> Self {
111 let component = |cx: &mut App| {
112 cx.new(|_| {
113 SliderState::new()
114 .min(0.)
115 .max(1.)
116 .step(0.01)
117 .default_value(0.)
118 })
119 };
120
121 Self {
122 hue: component(cx),
123 saturation: component(cx),
124 lightness: component(cx),
125 alpha: component(cx),
126 }
127 }
128
129 pub fn hue(&self) -> &Entity<SliderState> {
131 &self.hue
132 }
133
134 pub fn saturation(&self) -> &Entity<SliderState> {
136 &self.saturation
137 }
138
139 pub fn lightness(&self) -> &Entity<SliderState> {
141 &self.lightness
142 }
143
144 pub fn alpha(&self) -> &Entity<SliderState> {
146 &self.alpha
147 }
148
149 fn read(&self, cx: &App) -> Hsla {
150 hsla(
151 self.hue.read(cx).value().start(),
152 self.saturation.read(cx).value().start(),
153 self.lightness.read(cx).value().start(),
154 self.alpha.read(cx).value().start(),
155 )
156 }
157
158 fn write(&self, color: Hsla, window: &mut Window, cx: &mut App) {
159 let components = [
160 (&self.hue, color.h),
161 (&self.saturation, color.s),
162 (&self.lightness, color.l),
163 (&self.alpha, color.a),
164 ];
165 for (slider, value) in components {
166 slider.update(cx, |slider, cx| slider.set_value(value, window, cx));
167 }
168 }
169}
170
171pub struct ColorPickerState {
178 focus_handle: FocusHandle,
179 value: Option<Hsla>,
180 preview: Option<Hsla>,
181 open: bool,
182 active_tab: usize,
183 hex_input: Entity<InputState>,
184 sliders: HslaSliders,
185 needs_slider_sync: bool,
188 _subscriptions: Vec<Subscription>,
189}
190
191impl ColorPickerState {
192 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
194 let hex_input = cx
195 .new(|cx| InputState::new(window, cx).pattern(regex::Regex::new(HEX_PATTERN).unwrap()));
196 let sliders = HslaSliders::new(cx);
197
198 let mut subscriptions = vec![cx.subscribe_in(
199 &hex_input,
200 window,
201 |this, input, event: &InputEvent, window, cx| match event {
202 InputEvent::Change => {
203 let value = input.read(cx).value();
204 if this.preview_hex(value.as_str(), window, cx) {
205 let color = this.preview.expect("valid hex leaves a preview");
206 this.sliders.write(color, window, cx);
207 }
208 }
209 InputEvent::PressEnter { .. } => {
210 let value = input.read(cx).value();
211 this.commit_hex(value.as_str(), window, cx);
212 }
213 _ => {}
214 },
215 )];
216
217 subscriptions.extend(
218 [
219 sliders.hue.clone(),
220 sliders.saturation.clone(),
221 sliders.lightness.clone(),
222 sliders.alpha.clone(),
223 ]
224 .iter()
225 .map(|slider| {
226 cx.subscribe_in(slider, window, |this, _, _: &SliderEvent, window, cx| {
227 let color = this.sliders.read(cx);
228 this.update_value_from_slider(color, window, cx);
229 })
230 }),
231 );
232
233 Self {
234 focus_handle: cx.focus_handle(),
235 value: None,
236 preview: None,
237 open: false,
238 active_tab: 0,
239 hex_input,
240 sliders,
241 needs_slider_sync: false,
242 _subscriptions: subscriptions,
243 }
244 }
245
246 pub fn default_value(mut self, value: impl Into<Hsla>) -> Self {
248 let value = value.into();
249 self.value = Some(value);
250 self.preview = Some(value);
251 self.needs_slider_sync = true;
252 self
253 }
254
255 pub fn value(&self) -> Option<Hsla> {
257 self.value
258 }
259
260 pub fn preview(&self) -> Option<Hsla> {
262 self.preview
263 }
264
265 pub fn displayed_color(&self) -> Option<Hsla> {
267 self.preview.or(self.value)
268 }
269
270 pub fn hex_input(&self) -> &Entity<InputState> {
272 &self.hex_input
273 }
274
275 pub fn sliders(&self) -> &HslaSliders {
277 &self.sliders
278 }
279
280 pub fn set_value(
282 &mut self,
283 value: impl Into<Hsla>,
284 window: &mut Window,
285 cx: &mut Context<Self>,
286 ) {
287 self.update_value(Some(value.into()), false, window, cx);
288 }
289
290 pub fn clear_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
292 self.update_value(None, false, window, cx);
293 }
294
295 pub fn sync_pending_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
298 if self.needs_slider_sync {
299 self.update_value(self.value, false, window, cx);
300 }
301 }
302
303 pub fn preview_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
305 self.preview = Some(value);
306 self.write_hex_input(Some(value), window, cx);
307 cx.notify();
308 }
309
310 pub fn clear_preview(&mut self, window: &mut Window, cx: &mut Context<Self>) {
312 if self.preview == self.value {
313 return;
314 }
315 self.preview = self.value;
316 self.write_hex_input(self.value, window, cx);
317 cx.notify();
318 }
319
320 pub fn preview_hex(&mut self, value: &str, _: &mut Window, cx: &mut Context<Self>) -> bool {
325 let Some(value) = parse_hex(value) else {
326 return false;
327 };
328 self.preview = Some(value);
329 cx.notify();
330 true
331 }
332
333 pub fn commit_hex(
335 &mut self,
336 value: &str,
337 window: &mut Window,
338 cx: &mut Context<Self>,
339 ) -> Option<Hsla> {
340 let value = parse_hex(value)?;
341 self.select_color(value, window, cx);
342 Some(value)
343 }
344
345 pub fn select_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
347 self.open = false;
348 self.update_value(Some(value), true, window, cx);
349 }
350
351 pub fn update_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
353 self.update_value(Some(value), true, window, cx);
354 }
355
356 pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
358 if self.open == open {
359 return;
360 }
361 self.open = open;
362 cx.notify();
363 }
364
365 pub fn toggle_open(&mut self, cx: &mut Context<Self>) {
367 self.open = !self.open;
368 cx.notify();
369 }
370
371 pub fn is_open(&self) -> bool {
373 self.open
374 }
375
376 pub fn set_active_tab(&mut self, tab: usize, cx: &mut Context<Self>) {
378 if self.active_tab == tab {
379 return;
380 }
381 self.active_tab = tab;
382 cx.notify();
383 }
384
385 pub fn active_tab(&self) -> usize {
387 self.active_tab
388 }
389
390 fn update_value(
391 &mut self,
392 value: Option<Hsla>,
393 emit: bool,
394 window: &mut Window,
395 cx: &mut Context<Self>,
396 ) {
397 self.needs_slider_sync = false;
398 self.value = value;
399 self.preview = value;
400 self.write_hex_input(value, window, cx);
401 if let Some(value) = value {
404 self.sliders.write(value, window, cx);
405 }
406 if emit {
407 cx.emit(ColorPickerEvent::Change(value));
408 }
409 cx.notify();
410 }
411
412 fn update_value_from_slider(
413 &mut self,
414 value: Hsla,
415 window: &mut Window,
416 cx: &mut Context<Self>,
417 ) {
418 self.needs_slider_sync = false;
419 self.value = Some(value);
420 self.preview = Some(value);
421 self.write_hex_input(Some(value), window, cx);
424 cx.emit(ColorPickerEvent::Change(Some(value)));
425 cx.notify();
426 }
427
428 fn write_hex_input(&mut self, value: Option<Hsla>, window: &mut Window, cx: &mut App) {
431 let text = value.map(hex_string).unwrap_or_default();
432 self.hex_input
433 .update(cx, |input, cx| input.set_value(text, window, cx));
434 }
435}
436
437impl EventEmitter<ColorPickerEvent> for ColorPickerState {}
438
439impl Focusable for ColorPickerState {
440 fn focus_handle(&self, _: &App) -> FocusHandle {
441 self.focus_handle.clone()
442 }
443}
444
445impl Render for ColorPickerState {
446 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
447 self.hex_input.clone()
448 }
449}
450
451type OpenChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
452
453#[derive(IntoElement)]
459pub struct ColorPicker {
460 base: Stateful<Div>,
461 open: bool,
462 disabled: bool,
463 focus_handle: Option<FocusHandle>,
464 accessibility_label: Option<SharedString>,
465 style: StyleRefinement,
466 children: SmallVec<[AnyElement; 2]>,
467 on_open_change: Option<OpenChangeHandler>,
468 key_context: &'static str,
469 role: RoleOverride,
470}
471
472impl ColorPicker {
473 pub fn new(id: impl Into<ElementId>) -> Self {
474 Self {
475 base: div().id(id),
476 open: false,
477 disabled: false,
478 focus_handle: None,
479 accessibility_label: None,
480 style: StyleRefinement::default(),
481 children: SmallVec::new(),
482 on_open_change: None,
483 key_context: CONTEXT,
484 role: RoleOverride::Implicit,
485 }
486 }
487
488 pub fn open(mut self, open: bool) -> Self {
490 self.open = open;
491 self
492 }
493
494 pub fn disabled(mut self, disabled: bool) -> Self {
496 self.disabled = disabled;
497 self
498 }
499
500 pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
502 self.focus_handle = Some(focus_handle.clone());
503 self
504 }
505
506 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
508 self.accessibility_label = Some(label.into());
509 self
510 }
511
512 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
514 self.role = role.into();
515 self
516 }
517
518 pub fn on_open_change(
522 mut self,
523 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
524 ) -> Self {
525 self.on_open_change = Some(Rc::new(handler));
526 self
527 }
528
529 #[doc(hidden)]
530 pub fn key_context(mut self, key_context: &'static str) -> Self {
531 self.key_context = key_context;
532 self
533 }
534}
535
536impl Styled for ColorPicker {
537 fn style(&mut self) -> &mut StyleRefinement {
538 &mut self.style
539 }
540}
541
542impl ParentElement for ColorPicker {
543 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
544 self.children.extend(elements);
545 }
546}
547
548impl InteractiveElement for ColorPicker {
549 fn interactivity(&mut self) -> &mut Interactivity {
550 self.base.interactivity()
551 }
552}
553
554impl StatefulInteractiveElement for ColorPicker {}
555
556impl RenderOnce for ColorPicker {
557 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
558 let open = self.open;
559 let disabled = self.disabled;
560 let handler = self.on_open_change;
561
562 self.base
563 .when_some(self.role.resolve(|| Role::Button), |this, role| {
564 this.role(role)
565 })
566 .aria_expanded(open)
567 .when_some(self.accessibility_label, |this, label| {
568 this.aria_label(label)
569 })
570 .key_context(self.key_context)
571 .when_some(
572 self.focus_handle.filter(|_| !disabled),
573 |this, focus_handle| this.track_focus(&focus_handle.tab_stop(true)),
574 )
575 .on_action({
576 let handler = handler.clone();
577 move |_: &Confirm, window, cx| {
578 if disabled {
579 cx.propagate();
580 return;
581 }
582 if let Some(handler) = handler.as_ref() {
583 handler(!open, window, cx);
584 }
585 }
586 })
587 .on_action(move |_: &Cancel, window, cx| {
588 if !open {
589 cx.propagate();
590 return;
591 }
592 cx.stop_propagation();
593 if let Some(handler) = handler.as_ref() {
594 handler(false, window, cx);
595 }
596 })
597 .children(self.children)
598 .refine_style(&self.style)
599 }
600}
601
602type SwatchClickHandler = Rc<dyn Fn(Hsla, &ClickEvent, &mut Window, &mut App)>;
603type SwatchHoverHandler = Rc<dyn Fn(Hsla, bool, &mut Window, &mut App)>;
604
605#[derive(IntoElement)]
611pub struct ColorSwatch {
612 id: ElementId,
613 base: Stateful<Div>,
614 color: Hsla,
615 selected: bool,
616 disabled: bool,
617 style: StyleRefinement,
618 children: SmallVec<[AnyElement; 1]>,
619 accessibility_label: Option<SharedString>,
620 on_click: Option<SwatchClickHandler>,
621 on_hover: Option<SwatchHoverHandler>,
622 tab_index: isize,
623 tab_stop: bool,
624 role: RoleOverride,
625}
626
627impl ColorSwatch {
628 pub fn new(id: impl Into<ElementId>, color: Hsla) -> Self {
629 let id = id.into();
630 Self {
631 base: div().id(id.clone()),
632 id,
633 color,
634 selected: false,
635 disabled: false,
636 style: StyleRefinement::default(),
637 children: SmallVec::new(),
638 accessibility_label: None,
639 on_click: None,
640 on_hover: None,
641 tab_index: 0,
642 tab_stop: true,
643 role: RoleOverride::Implicit,
644 }
645 }
646
647 pub fn color(&self) -> Hsla {
649 self.color
650 }
651
652 pub fn selected(mut self, selected: bool) -> Self {
654 self.selected = selected;
655 self
656 }
657
658 pub fn disabled(mut self, disabled: bool) -> Self {
660 self.disabled = disabled;
661 self
662 }
663
664 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
666 self.accessibility_label = Some(label.into());
667 self
668 }
669
670 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
672 self.role = role.into();
673 self
674 }
675
676 pub fn on_click(
678 mut self,
679 handler: impl Fn(Hsla, &ClickEvent, &mut Window, &mut App) + 'static,
680 ) -> Self {
681 self.on_click = Some(Rc::new(handler));
682 self
683 }
684
685 pub fn on_hover(
687 mut self,
688 handler: impl Fn(Hsla, bool, &mut Window, &mut App) + 'static,
689 ) -> Self {
690 self.on_hover = Some(Rc::new(handler));
691 self
692 }
693
694 pub fn tab_index(mut self, tab_index: isize) -> Self {
696 self.tab_index = tab_index;
697 self
698 }
699
700 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
702 self.tab_stop = tab_stop;
703 self
704 }
705}
706
707impl Styled for ColorSwatch {
708 fn style(&mut self) -> &mut StyleRefinement {
709 &mut self.style
710 }
711}
712
713impl ParentElement for ColorSwatch {
714 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
715 self.children.extend(elements);
716 }
717}
718
719impl InteractiveElement for ColorSwatch {
720 fn interactivity(&mut self) -> &mut Interactivity {
721 self.base.interactivity()
722 }
723}
724
725impl StatefulInteractiveElement for ColorSwatch {}
726
727impl RenderOnce for ColorSwatch {
728 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
729 let color = self.color;
730 let disabled = self.disabled;
731 let selected = self.selected;
732 let label = self
733 .accessibility_label
734 .unwrap_or_else(|| hex_string(color).into());
735 let focus_handle = window
736 .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
737 .read(cx)
738 .clone();
739
740 self.base
741 .when_some(self.role.resolve(|| Role::RadioButton), |this, role| {
742 this.role(role)
743 })
744 .aria_label(label)
745 .aria_toggled(if selected {
748 Toggled::True
749 } else {
750 Toggled::False
751 })
752 .aria_selected(selected)
753 .when(!disabled, |this| {
754 this.track_focus(
755 &focus_handle
756 .tab_index(self.tab_index)
757 .tab_stop(self.tab_stop),
758 )
759 })
760 .when_some(
761 (!disabled).then_some(self.on_hover).flatten(),
762 |this, on_hover| {
763 this.on_hover(move |entered, window, cx| on_hover(color, *entered, window, cx))
764 },
765 )
766 .when_some(
767 (!disabled).then_some(self.on_click).flatten(),
768 |this, on_click| {
769 this.on_click(move |event, window, cx| on_click(color, event, window, cx))
770 },
771 )
772 .children(self.children)
773 .refine_style(&self.style)
774 }
775}
776
777#[cfg(test)]
778mod tests {
779 use super::*;
780 use gpui::{TestAppContext, hsla, px};
781 use std::{cell::RefCell, rc::Rc};
782
783 #[test]
784 fn parses_every_supported_hex_width() {
785 let white = parse_hex("#fff").unwrap();
786 assert_eq!(hex_string(white), "#FFFFFF");
787 assert_eq!(parse_hex("ffffff"), Some(white));
788
789 let half = parse_hex("#ff000080").unwrap();
790 assert!((half.a - 0.5).abs() < 0.01);
791 assert_eq!(parse_hex("#f008").map(|c| c.h), Some(half.h));
792 }
793
794 #[test]
795 fn rejects_malformed_hex() {
796 for value in ["#nope", "#12", "#1234567", "", "#+f0000", "#-fffff"] {
797 assert_eq!(parse_hex(value), None, "{value} should not parse");
798 }
799 }
800
801 #[test]
802 fn formats_alpha_only_when_translucent() {
803 assert_eq!(hex_string(hsla(0., 1., 0.5, 1.)), "#FF0000");
804 assert_eq!(hex_string(hsla(0., 1., 0.5, 0.5)), "#FF000080");
805 }
806
807 fn state(cx: &mut TestAppContext) -> (Entity<ColorPickerState>, &mut gpui::VisualTestContext) {
808 let (state, cx) = cx.add_window_view(ColorPickerState::new);
809 cx.update(|window, cx| window.draw(cx).clear(cx));
810 (state, cx)
811 }
812
813 #[gpui::test]
814 fn default_value_reaches_the_hex_field_and_sliders(cx: &mut TestAppContext) {
815 let (state, cx) = cx.add_window_view(|window, cx| {
816 ColorPickerState::new(window, cx).default_value(hsla(0., 1., 0.5, 1.))
817 });
818 cx.update(|window, cx| {
819 state.update(cx, |state, cx| state.sync_pending_value(window, cx));
820 });
821
822 state.read_with(cx, |state, cx| {
823 assert_eq!(state.hex_input().read(cx).value().as_str(), "#FF0000");
824 assert_eq!(state.sliders().lightness().read(cx).value().start(), 0.5);
825 });
826 }
827
828 #[gpui::test]
829 fn preview_does_not_change_the_committed_value(cx: &mut TestAppContext) {
830 let (state, cx) = state(cx);
831 let committed = hsla(0.1, 0.2, 0.3, 1.);
832 let previewed = hsla(0.6, 0.7, 0.8, 1.);
833
834 cx.update(|window, cx| {
835 state.update(cx, |state, cx| {
836 state.set_value(committed, window, cx);
837 state.preview_color(previewed, window, cx);
838 });
839 });
840
841 state.read_with(cx, |state, _| {
842 assert_eq!(state.value(), Some(committed));
843 assert_eq!(state.displayed_color(), Some(previewed));
844 });
845
846 cx.update(|window, cx| {
847 state.update(cx, |state, cx| state.clear_preview(window, cx));
848 });
849 state.read_with(cx, |state, _| {
850 assert_eq!(state.displayed_color(), Some(committed));
851 });
852 }
853
854 #[gpui::test]
855 fn first_hex_edit_after_set_value_previews(cx: &mut TestAppContext) {
856 let (state, cx) = state(cx);
857 cx.update(|window, cx| {
858 state.update(cx, |state, cx| {
859 state.set_value(hsla(0., 1., 0.5, 1.), window, cx)
860 });
861 let input = state.read(cx).hex_input().clone();
863 input.update(cx, |input, cx| {
864 input.set_value("#00FF00", window, cx);
865 cx.emit(InputEvent::Change);
866 });
867 });
868
869 state.read_with(cx, |state, _| {
870 assert_eq!(state.preview().map(hex_string).as_deref(), Some("#00FF00"));
871 });
872 }
873
874 #[gpui::test]
875 fn invalid_hex_leaves_preview_and_value_alone(cx: &mut TestAppContext) {
876 let (state, cx) = state(cx);
877 let color = hsla(0.1, 0.2, 0.3, 1.);
878
879 cx.update(|window, cx| {
880 state.update(cx, |state, cx| {
881 state.set_value(color, window, cx);
882 assert!(!state.preview_hex("#nope", window, cx));
883 assert_eq!(state.commit_hex("#12", window, cx), None);
884 });
885 });
886
887 state.read_with(cx, |state, _| {
888 assert_eq!(state.preview(), Some(color));
889 assert_eq!(state.value(), Some(color));
890 });
891 }
892
893 #[gpui::test]
894 fn palette_selection_closes_but_a_slider_update_stays_open(cx: &mut TestAppContext) {
895 let (state, cx) = state(cx);
896 let changes = Rc::new(RefCell::new(Vec::new()));
897 let _subscription = cx.update(|_, cx| {
898 let changes = changes.clone();
899 cx.subscribe(&state, move |_, event: &ColorPickerEvent, _| {
900 let ColorPickerEvent::Change(color) = event;
901 changes.borrow_mut().push(*color);
902 })
903 });
904
905 cx.update(|window, cx| {
906 state.update(cx, |state, cx| {
907 state.set_open(true, cx);
908 state.update_color(hsla(0.2, 0.3, 0.4, 1.), window, cx);
909 });
910 });
911 assert!(state.read_with(cx, |state, _| state.is_open()));
912
913 cx.update(|window, cx| {
914 state.update(cx, |state, cx| {
915 state.select_color(hsla(0.5, 0.6, 0.7, 1.), window, cx);
916 });
917 });
918 assert!(!state.read_with(cx, |state, _| state.is_open()));
919 assert_eq!(changes.borrow().len(), 2);
920 }
921
922 #[gpui::test]
923 fn committing_hex_updates_the_value_and_closes(cx: &mut TestAppContext) {
924 let (state, cx) = state(cx);
925
926 let committed = cx.update(|window, cx| {
927 state.update(cx, |state, cx| {
928 state.set_open(true, cx);
929 state.commit_hex("#ff0000", window, cx)
930 })
931 });
932
933 state.read_with(cx, |state, _| {
934 assert_eq!(state.value(), committed);
935 assert_eq!(state.preview(), committed);
936 assert!(!state.is_open());
937 });
938 }
939
940 #[gpui::test]
941 fn open_and_active_panel_are_controlled(cx: &mut TestAppContext) {
942 let (state, cx) = state(cx);
943
944 cx.update(|_, cx| {
945 state.update(cx, |state, cx| {
946 state.toggle_open(cx);
947 state.set_active_tab(1, cx);
948 });
949 });
950
951 state.read_with(cx, |state, _| {
952 assert!(state.is_open());
953 assert_eq!(state.active_tab(), 1);
954 });
955 }
956
957 struct PickerHarness {
958 open: bool,
959 focus_handle: FocusHandle,
960 changes: Rc<RefCell<Vec<bool>>>,
961 }
962
963 impl Render for PickerHarness {
964 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
965 let entity = cx.entity();
966 let changes = self.changes.clone();
967 ColorPicker::new("picker")
968 .open(self.open)
969 .track_focus(&self.focus_handle)
970 .size(px(20.))
971 .on_open_change(move |open, _, cx| {
972 changes.borrow_mut().push(open);
973 entity.update(cx, |this, cx| {
974 this.open = open;
975 cx.notify();
976 });
977 })
978 }
979 }
980
981 #[gpui::test]
982 fn confirm_toggles_and_cancel_dismisses(cx: &mut TestAppContext) {
983 cx.update(crate::init);
984 let changes = Rc::new(RefCell::new(Vec::new()));
985 let (state, cx) = cx.add_window_view({
986 let changes = changes.clone();
987 move |_, cx| PickerHarness {
988 open: false,
989 focus_handle: cx.focus_handle(),
990 changes,
991 }
992 });
993 cx.update(|window, cx| {
994 state.read(cx).focus_handle.clone().focus(window, cx);
995 window.draw(cx).clear(cx);
996 });
997
998 cx.simulate_keystrokes("enter");
999 assert!(state.read_with(cx, |state, _| state.open));
1000
1001 cx.simulate_keystrokes("enter");
1002 assert!(!state.read_with(cx, |state, _| state.open));
1003
1004 cx.simulate_keystrokes("enter escape");
1005 assert!(!state.read_with(cx, |state, _| state.open));
1006 assert_eq!(&*changes.borrow(), &[true, false, true, false]);
1007 }
1008}