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 * 255.) as u32;
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 suppress_input_change: bool,
189 _subscriptions: Vec<Subscription>,
190}
191
192impl ColorPickerState {
193 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
195 let hex_input = cx
196 .new(|cx| InputState::new(window, cx).pattern(regex::Regex::new(HEX_PATTERN).unwrap()));
197 let sliders = HslaSliders::new(cx);
198
199 let mut subscriptions = vec![cx.subscribe_in(
200 &hex_input,
201 window,
202 |this, input, event: &InputEvent, window, cx| match event {
203 InputEvent::Change => {
204 if this.suppress_input_change {
205 this.suppress_input_change = false;
206 return;
207 }
208 let value = input.read(cx).value();
209 if this.preview_hex(value.as_str(), window, cx) {
210 let color = this.preview.expect("valid hex leaves a preview");
211 this.sliders.write(color, window, cx);
212 }
213 }
214 InputEvent::PressEnter { .. } => {
215 let value = input.read(cx).value();
216 this.commit_hex(value.as_str(), window, cx);
217 }
218 _ => {}
219 },
220 )];
221
222 subscriptions.extend(
223 [
224 sliders.hue.clone(),
225 sliders.saturation.clone(),
226 sliders.lightness.clone(),
227 sliders.alpha.clone(),
228 ]
229 .iter()
230 .map(|slider| {
231 cx.subscribe_in(slider, window, |this, _, _: &SliderEvent, window, cx| {
232 let color = this.sliders.read(cx);
233 this.update_value_from_slider(color, window, cx);
234 })
235 }),
236 );
237
238 Self {
239 focus_handle: cx.focus_handle(),
240 value: None,
241 preview: None,
242 open: false,
243 active_tab: 0,
244 hex_input,
245 sliders,
246 needs_slider_sync: false,
247 suppress_input_change: false,
248 _subscriptions: subscriptions,
249 }
250 }
251
252 pub fn default_value(mut self, value: impl Into<Hsla>) -> Self {
254 let value = value.into();
255 self.value = Some(value);
256 self.preview = Some(value);
257 self.needs_slider_sync = true;
258 self
259 }
260
261 pub fn value(&self) -> Option<Hsla> {
263 self.value
264 }
265
266 pub fn preview(&self) -> Option<Hsla> {
268 self.preview
269 }
270
271 pub fn displayed_color(&self) -> Option<Hsla> {
273 self.preview.or(self.value)
274 }
275
276 pub fn hex_input(&self) -> &Entity<InputState> {
278 &self.hex_input
279 }
280
281 pub fn sliders(&self) -> &HslaSliders {
283 &self.sliders
284 }
285
286 pub fn set_value(
288 &mut self,
289 value: impl Into<Hsla>,
290 window: &mut Window,
291 cx: &mut Context<Self>,
292 ) {
293 self.update_value(Some(value.into()), false, window, cx);
294 }
295
296 pub fn clear_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
298 self.update_value(None, false, window, cx);
299 }
300
301 pub fn sync_pending_value(&mut self, window: &mut Window, cx: &mut Context<Self>) {
304 if self.needs_slider_sync {
305 self.update_value(self.value, false, window, cx);
306 }
307 }
308
309 pub fn preview_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
311 self.preview = Some(value);
312 self.write_hex_input(Some(value), window, cx);
313 cx.notify();
314 }
315
316 pub fn clear_preview(&mut self, window: &mut Window, cx: &mut Context<Self>) {
318 if self.preview == self.value {
319 return;
320 }
321 self.preview = self.value;
322 self.write_hex_input(self.value, window, cx);
323 cx.notify();
324 }
325
326 pub fn preview_hex(&mut self, value: &str, _: &mut Window, cx: &mut Context<Self>) -> bool {
331 let Some(value) = parse_hex(value) else {
332 return false;
333 };
334 self.preview = Some(value);
335 cx.notify();
336 true
337 }
338
339 pub fn commit_hex(
341 &mut self,
342 value: &str,
343 window: &mut Window,
344 cx: &mut Context<Self>,
345 ) -> Option<Hsla> {
346 let value = parse_hex(value)?;
347 self.select_color(value, window, cx);
348 Some(value)
349 }
350
351 pub fn select_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
353 self.open = false;
354 self.update_value(Some(value), true, window, cx);
355 }
356
357 pub fn update_color(&mut self, value: Hsla, window: &mut Window, cx: &mut Context<Self>) {
359 self.update_value(Some(value), true, window, cx);
360 }
361
362 pub fn set_open(&mut self, open: bool, cx: &mut Context<Self>) {
364 if self.open == open {
365 return;
366 }
367 self.open = open;
368 cx.notify();
369 }
370
371 pub fn toggle_open(&mut self, cx: &mut Context<Self>) {
373 self.open = !self.open;
374 cx.notify();
375 }
376
377 pub fn is_open(&self) -> bool {
379 self.open
380 }
381
382 pub fn set_active_tab(&mut self, tab: usize, cx: &mut Context<Self>) {
384 if self.active_tab == tab {
385 return;
386 }
387 self.active_tab = tab;
388 cx.notify();
389 }
390
391 pub fn active_tab(&self) -> usize {
393 self.active_tab
394 }
395
396 fn update_value(
397 &mut self,
398 value: Option<Hsla>,
399 emit: bool,
400 window: &mut Window,
401 cx: &mut Context<Self>,
402 ) {
403 self.needs_slider_sync = false;
404 self.value = value;
405 self.preview = value;
406 self.write_hex_input(value, window, cx);
407 if let Some(value) = value {
410 self.sliders.write(value, window, cx);
411 }
412 if emit {
413 cx.emit(ColorPickerEvent::Change(value));
414 }
415 cx.notify();
416 }
417
418 fn update_value_from_slider(
419 &mut self,
420 value: Hsla,
421 window: &mut Window,
422 cx: &mut Context<Self>,
423 ) {
424 self.needs_slider_sync = false;
425 self.value = Some(value);
426 self.preview = Some(value);
427 self.write_hex_input(Some(value), window, cx);
430 cx.emit(ColorPickerEvent::Change(Some(value)));
431 cx.notify();
432 }
433
434 fn write_hex_input(&mut self, value: Option<Hsla>, window: &mut Window, cx: &mut App) {
437 self.suppress_input_change = true;
438 let text = value.map(hex_string).unwrap_or_default();
439 self.hex_input
440 .update(cx, |input, cx| input.set_value(text, window, cx));
441 }
442}
443
444impl EventEmitter<ColorPickerEvent> for ColorPickerState {}
445
446impl Focusable for ColorPickerState {
447 fn focus_handle(&self, _: &App) -> FocusHandle {
448 self.focus_handle.clone()
449 }
450}
451
452impl Render for ColorPickerState {
453 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
454 self.hex_input.clone()
455 }
456}
457
458type OpenChangeHandler = Rc<dyn Fn(bool, &mut Window, &mut App)>;
459
460#[derive(IntoElement)]
466pub struct ColorPicker {
467 base: Stateful<Div>,
468 open: bool,
469 disabled: bool,
470 focus_handle: Option<FocusHandle>,
471 accessibility_label: Option<SharedString>,
472 style: StyleRefinement,
473 children: SmallVec<[AnyElement; 2]>,
474 on_open_change: Option<OpenChangeHandler>,
475 key_context: &'static str,
476 role: RoleOverride,
477}
478
479impl ColorPicker {
480 pub fn new(id: impl Into<ElementId>) -> Self {
481 Self {
482 base: div().id(id),
483 open: false,
484 disabled: false,
485 focus_handle: None,
486 accessibility_label: None,
487 style: StyleRefinement::default(),
488 children: SmallVec::new(),
489 on_open_change: None,
490 key_context: CONTEXT,
491 role: RoleOverride::Implicit,
492 }
493 }
494
495 pub fn open(mut self, open: bool) -> Self {
497 self.open = open;
498 self
499 }
500
501 pub fn disabled(mut self, disabled: bool) -> Self {
503 self.disabled = disabled;
504 self
505 }
506
507 pub fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
509 self.focus_handle = Some(focus_handle.clone());
510 self
511 }
512
513 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
515 self.accessibility_label = Some(label.into());
516 self
517 }
518
519 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
521 self.role = role.into();
522 self
523 }
524
525 pub fn on_open_change(
529 mut self,
530 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
531 ) -> Self {
532 self.on_open_change = Some(Rc::new(handler));
533 self
534 }
535
536 #[doc(hidden)]
537 pub fn key_context(mut self, key_context: &'static str) -> Self {
538 self.key_context = key_context;
539 self
540 }
541}
542
543impl Styled for ColorPicker {
544 fn style(&mut self) -> &mut StyleRefinement {
545 &mut self.style
546 }
547}
548
549impl ParentElement for ColorPicker {
550 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
551 self.children.extend(elements);
552 }
553}
554
555impl InteractiveElement for ColorPicker {
556 fn interactivity(&mut self) -> &mut Interactivity {
557 self.base.interactivity()
558 }
559}
560
561impl StatefulInteractiveElement for ColorPicker {}
562
563impl RenderOnce for ColorPicker {
564 fn render(self, _: &mut Window, _: &mut App) -> impl IntoElement {
565 let open = self.open;
566 let disabled = self.disabled;
567 let handler = self.on_open_change;
568
569 self.base
570 .when_some(self.role.resolve(|| Role::Button), |this, role| {
571 this.role(role)
572 })
573 .aria_expanded(open)
574 .when_some(self.accessibility_label, |this, label| {
575 this.aria_label(label)
576 })
577 .key_context(self.key_context)
578 .when_some(
579 self.focus_handle.filter(|_| !disabled),
580 |this, focus_handle| this.track_focus(&focus_handle.tab_stop(true)),
581 )
582 .on_action({
583 let handler = handler.clone();
584 move |_: &Confirm, window, cx| {
585 if disabled {
586 cx.propagate();
587 return;
588 }
589 if let Some(handler) = handler.as_ref() {
590 handler(!open, window, cx);
591 }
592 }
593 })
594 .on_action(move |_: &Cancel, window, cx| {
595 if !open {
596 cx.propagate();
597 return;
598 }
599 cx.stop_propagation();
600 if let Some(handler) = handler.as_ref() {
601 handler(false, window, cx);
602 }
603 })
604 .children(self.children)
605 .refine_style(&self.style)
606 }
607}
608
609type SwatchClickHandler = Rc<dyn Fn(Hsla, &ClickEvent, &mut Window, &mut App)>;
610type SwatchHoverHandler = Rc<dyn Fn(Hsla, bool, &mut Window, &mut App)>;
611
612#[derive(IntoElement)]
618pub struct ColorSwatch {
619 id: ElementId,
620 base: Stateful<Div>,
621 color: Hsla,
622 selected: bool,
623 disabled: bool,
624 style: StyleRefinement,
625 children: SmallVec<[AnyElement; 1]>,
626 accessibility_label: Option<SharedString>,
627 on_click: Option<SwatchClickHandler>,
628 on_hover: Option<SwatchHoverHandler>,
629 tab_index: isize,
630 tab_stop: bool,
631 role: RoleOverride,
632}
633
634impl ColorSwatch {
635 pub fn new(id: impl Into<ElementId>, color: Hsla) -> Self {
636 let id = id.into();
637 Self {
638 base: div().id(id.clone()),
639 id,
640 color,
641 selected: false,
642 disabled: false,
643 style: StyleRefinement::default(),
644 children: SmallVec::new(),
645 accessibility_label: None,
646 on_click: None,
647 on_hover: None,
648 tab_index: 0,
649 tab_stop: true,
650 role: RoleOverride::Implicit,
651 }
652 }
653
654 pub fn color(&self) -> Hsla {
656 self.color
657 }
658
659 pub fn selected(mut self, selected: bool) -> Self {
661 self.selected = selected;
662 self
663 }
664
665 pub fn disabled(mut self, disabled: bool) -> Self {
667 self.disabled = disabled;
668 self
669 }
670
671 pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
673 self.accessibility_label = Some(label.into());
674 self
675 }
676
677 pub fn role(mut self, role: impl Into<RoleOverride>) -> Self {
679 self.role = role.into();
680 self
681 }
682
683 pub fn on_click(
685 mut self,
686 handler: impl Fn(Hsla, &ClickEvent, &mut Window, &mut App) + 'static,
687 ) -> Self {
688 self.on_click = Some(Rc::new(handler));
689 self
690 }
691
692 pub fn on_hover(
694 mut self,
695 handler: impl Fn(Hsla, bool, &mut Window, &mut App) + 'static,
696 ) -> Self {
697 self.on_hover = Some(Rc::new(handler));
698 self
699 }
700
701 pub fn tab_index(mut self, tab_index: isize) -> Self {
703 self.tab_index = tab_index;
704 self
705 }
706
707 pub fn tab_stop(mut self, tab_stop: bool) -> Self {
709 self.tab_stop = tab_stop;
710 self
711 }
712}
713
714impl Styled for ColorSwatch {
715 fn style(&mut self) -> &mut StyleRefinement {
716 &mut self.style
717 }
718}
719
720impl ParentElement for ColorSwatch {
721 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
722 self.children.extend(elements);
723 }
724}
725
726impl InteractiveElement for ColorSwatch {
727 fn interactivity(&mut self) -> &mut Interactivity {
728 self.base.interactivity()
729 }
730}
731
732impl StatefulInteractiveElement for ColorSwatch {}
733
734impl RenderOnce for ColorSwatch {
735 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
736 let color = self.color;
737 let disabled = self.disabled;
738 let selected = self.selected;
739 let label = self
740 .accessibility_label
741 .unwrap_or_else(|| hex_string(color).into());
742 let focus_handle = window
743 .use_keyed_state(self.id.clone(), cx, |_, cx| cx.focus_handle())
744 .read(cx)
745 .clone();
746
747 self.base
748 .when_some(self.role.resolve(|| Role::RadioButton), |this, role| {
749 this.role(role)
750 })
751 .aria_label(label)
752 .aria_toggled(if selected {
755 Toggled::True
756 } else {
757 Toggled::False
758 })
759 .aria_selected(selected)
760 .when(!disabled, |this| {
761 this.track_focus(
762 &focus_handle
763 .tab_index(self.tab_index)
764 .tab_stop(self.tab_stop),
765 )
766 })
767 .when_some(
768 (!disabled).then_some(self.on_hover).flatten(),
769 |this, on_hover| {
770 this.on_hover(move |entered, window, cx| on_hover(color, *entered, window, cx))
771 },
772 )
773 .when_some(
774 (!disabled).then_some(self.on_click).flatten(),
775 |this, on_click| {
776 this.on_click(move |event, window, cx| on_click(color, event, window, cx))
777 },
778 )
779 .children(self.children)
780 .refine_style(&self.style)
781 }
782}
783
784#[cfg(test)]
785mod tests {
786 use super::*;
787 use gpui::{TestAppContext, hsla, px};
788 use std::{cell::RefCell, rc::Rc};
789
790 #[test]
791 fn parses_every_supported_hex_width() {
792 let white = parse_hex("#fff").unwrap();
793 assert_eq!(hex_string(white), "#FFFFFF");
794 assert_eq!(parse_hex("ffffff"), Some(white));
795
796 let half = parse_hex("#ff000080").unwrap();
797 assert!((half.a - 0.5).abs() < 0.01);
798 assert_eq!(parse_hex("#f008").map(|c| c.h), Some(half.h));
799 }
800
801 #[test]
802 fn rejects_malformed_hex() {
803 for value in ["#nope", "#12", "#1234567", "", "#+f0000", "#-fffff"] {
804 assert_eq!(parse_hex(value), None, "{value} should not parse");
805 }
806 }
807
808 #[test]
809 fn formats_alpha_only_when_translucent() {
810 assert_eq!(hex_string(hsla(0., 1., 0.5, 1.)), "#FF0000");
811 assert_eq!(hex_string(hsla(0., 1., 0.5, 0.5)), "#FF00007F");
812 }
813
814 fn state(cx: &mut TestAppContext) -> (Entity<ColorPickerState>, &mut gpui::VisualTestContext) {
815 let (state, cx) = cx.add_window_view(ColorPickerState::new);
816 cx.update(|window, cx| window.draw(cx).clear(cx));
817 (state, cx)
818 }
819
820 #[gpui::test]
821 fn default_value_reaches_the_hex_field_and_sliders(cx: &mut TestAppContext) {
822 let (state, cx) = cx.add_window_view(|window, cx| {
823 ColorPickerState::new(window, cx).default_value(hsla(0., 1., 0.5, 1.))
824 });
825 cx.update(|window, cx| {
826 state.update(cx, |state, cx| state.sync_pending_value(window, cx));
827 });
828
829 state.read_with(cx, |state, cx| {
830 assert_eq!(state.hex_input().read(cx).value().as_str(), "#FF0000");
831 assert_eq!(state.sliders().lightness().read(cx).value().start(), 0.5);
832 });
833 }
834
835 #[gpui::test]
836 fn preview_does_not_change_the_committed_value(cx: &mut TestAppContext) {
837 let (state, cx) = state(cx);
838 let committed = hsla(0.1, 0.2, 0.3, 1.);
839 let previewed = hsla(0.6, 0.7, 0.8, 1.);
840
841 cx.update(|window, cx| {
842 state.update(cx, |state, cx| {
843 state.set_value(committed, window, cx);
844 state.preview_color(previewed, window, cx);
845 });
846 });
847
848 state.read_with(cx, |state, _| {
849 assert_eq!(state.value(), Some(committed));
850 assert_eq!(state.displayed_color(), Some(previewed));
851 });
852
853 cx.update(|window, cx| {
854 state.update(cx, |state, cx| state.clear_preview(window, cx));
855 });
856 state.read_with(cx, |state, _| {
857 assert_eq!(state.displayed_color(), Some(committed));
858 });
859 }
860
861 #[gpui::test]
862 fn invalid_hex_leaves_preview_and_value_alone(cx: &mut TestAppContext) {
863 let (state, cx) = state(cx);
864 let color = hsla(0.1, 0.2, 0.3, 1.);
865
866 cx.update(|window, cx| {
867 state.update(cx, |state, cx| {
868 state.set_value(color, window, cx);
869 assert!(!state.preview_hex("#nope", window, cx));
870 assert_eq!(state.commit_hex("#12", window, cx), None);
871 });
872 });
873
874 state.read_with(cx, |state, _| {
875 assert_eq!(state.preview(), Some(color));
876 assert_eq!(state.value(), Some(color));
877 });
878 }
879
880 #[gpui::test]
881 fn palette_selection_closes_but_a_slider_update_stays_open(cx: &mut TestAppContext) {
882 let (state, cx) = state(cx);
883 let changes = Rc::new(RefCell::new(Vec::new()));
884 let _subscription = cx.update(|_, cx| {
885 let changes = changes.clone();
886 cx.subscribe(&state, move |_, event: &ColorPickerEvent, _| {
887 let ColorPickerEvent::Change(color) = event;
888 changes.borrow_mut().push(*color);
889 })
890 });
891
892 cx.update(|window, cx| {
893 state.update(cx, |state, cx| {
894 state.set_open(true, cx);
895 state.update_color(hsla(0.2, 0.3, 0.4, 1.), window, cx);
896 });
897 });
898 assert!(state.read_with(cx, |state, _| state.is_open()));
899
900 cx.update(|window, cx| {
901 state.update(cx, |state, cx| {
902 state.select_color(hsla(0.5, 0.6, 0.7, 1.), window, cx);
903 });
904 });
905 assert!(!state.read_with(cx, |state, _| state.is_open()));
906 assert_eq!(changes.borrow().len(), 2);
907 }
908
909 #[gpui::test]
910 fn committing_hex_updates_the_value_and_closes(cx: &mut TestAppContext) {
911 let (state, cx) = state(cx);
912
913 let committed = cx.update(|window, cx| {
914 state.update(cx, |state, cx| {
915 state.set_open(true, cx);
916 state.commit_hex("#ff0000", window, cx)
917 })
918 });
919
920 state.read_with(cx, |state, _| {
921 assert_eq!(state.value(), committed);
922 assert_eq!(state.preview(), committed);
923 assert!(!state.is_open());
924 });
925 }
926
927 #[gpui::test]
928 fn open_and_active_panel_are_controlled(cx: &mut TestAppContext) {
929 let (state, cx) = state(cx);
930
931 cx.update(|_, cx| {
932 state.update(cx, |state, cx| {
933 state.toggle_open(cx);
934 state.set_active_tab(1, cx);
935 });
936 });
937
938 state.read_with(cx, |state, _| {
939 assert!(state.is_open());
940 assert_eq!(state.active_tab(), 1);
941 });
942 }
943
944 struct PickerHarness {
945 open: bool,
946 focus_handle: FocusHandle,
947 changes: Rc<RefCell<Vec<bool>>>,
948 }
949
950 impl Render for PickerHarness {
951 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
952 let entity = cx.entity();
953 let changes = self.changes.clone();
954 ColorPicker::new("picker")
955 .open(self.open)
956 .track_focus(&self.focus_handle)
957 .size(px(20.))
958 .on_open_change(move |open, _, cx| {
959 changes.borrow_mut().push(open);
960 entity.update(cx, |this, cx| {
961 this.open = open;
962 cx.notify();
963 });
964 })
965 }
966 }
967
968 #[gpui::test]
969 fn confirm_toggles_and_cancel_dismisses(cx: &mut TestAppContext) {
970 cx.update(crate::init);
971 let changes = Rc::new(RefCell::new(Vec::new()));
972 let (state, cx) = cx.add_window_view({
973 let changes = changes.clone();
974 move |_, cx| PickerHarness {
975 open: false,
976 focus_handle: cx.focus_handle(),
977 changes,
978 }
979 });
980 cx.update(|window, cx| {
981 state.read(cx).focus_handle.clone().focus(window, cx);
982 window.draw(cx).clear(cx);
983 });
984
985 cx.simulate_keystrokes("enter");
986 assert!(state.read_with(cx, |state, _| state.open));
987
988 cx.simulate_keystrokes("enter");
989 assert!(!state.read_with(cx, |state, _| state.open));
990
991 cx.simulate_keystrokes("enter escape");
992 assert!(!state.read_with(cx, |state, _| state.open));
993 assert_eq!(&*changes.borrow(), &[true, false, true, false]);
994 }
995}