1use gpui::{
2 App, AppContext, Bounds, ClickEvent, Context, Corner, Div, ElementId, Entity, EventEmitter,
3 FocusHandle, Focusable, Hsla, InteractiveElement as _, IntoElement, KeyBinding, MouseButton,
4 ParentElement, Pixels, Point, Render, RenderOnce, SharedString, Stateful,
5 StatefulInteractiveElement as _, StyleRefinement, Styled, Subscription, Window, anchored,
6 canvas, deferred, div, prelude::FluentBuilder as _, px, relative,
7};
8
9use crate::{
10 ActiveTheme as _, Colorize as _, FocusableExt as _, Icon, Selectable as _, Sizable, Size,
11 StyleSized, StyledExt,
12 actions::{Cancel, Confirm},
13 button::{Button, ButtonVariants},
14 divider::Divider,
15 h_flex,
16 input::{Input, InputEvent, InputState},
17 tooltip::Tooltip,
18 v_flex,
19};
20
21const CONTEXT: &'static str = "ColorPicker";
22pub fn init(cx: &mut App) {
23 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
24}
25
26#[derive(Clone)]
28pub enum ColorPickerEvent {
29 Change(Option<Hsla>),
30}
31
32fn color_palettes() -> Vec<Vec<Hsla>> {
33 use crate::theme::DEFAULT_COLORS;
34 use itertools::Itertools as _;
35
36 macro_rules! c {
37 ($color:tt) => {
38 DEFAULT_COLORS
39 .$color
40 .keys()
41 .sorted()
42 .map(|k| DEFAULT_COLORS.$color.get(k).map(|c| c.hsla).unwrap())
43 .collect::<Vec<_>>()
44 };
45 }
46
47 vec![
48 c!(stone),
49 c!(red),
50 c!(orange),
51 c!(yellow),
52 c!(green),
53 c!(cyan),
54 c!(blue),
55 c!(purple),
56 c!(pink),
57 ]
58}
59
60pub struct ColorPickerState {
62 focus_handle: FocusHandle,
63 value: Option<Hsla>,
64 hovered_color: Option<Hsla>,
65 state: Entity<InputState>,
66 open: bool,
67 bounds: Bounds<Pixels>,
68 _subscriptions: Vec<Subscription>,
69}
70
71impl ColorPickerState {
72 pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
74 let state = cx.new(|cx| InputState::new(window, cx));
75
76 let _subscriptions = vec![cx.subscribe_in(
77 &state,
78 window,
79 |this, state, ev: &InputEvent, window, cx| match ev {
80 InputEvent::Change => {
81 let value = state.read(cx).value();
82 if let Ok(color) = Hsla::parse_hex(value.as_str()) {
83 this.hovered_color = Some(color);
84 }
85 }
86 InputEvent::PressEnter { .. } => {
87 let val = this.state.read(cx).value();
88 if let Ok(color) = Hsla::parse_hex(&val) {
89 this.open = false;
90 this.update_value(Some(color), true, window, cx);
91 }
92 }
93 _ => {}
94 },
95 )];
96
97 Self {
98 focus_handle: cx.focus_handle(),
99 value: None,
100 hovered_color: None,
101 state,
102 open: false,
103 bounds: Bounds::default(),
104 _subscriptions,
105 }
106 }
107
108 pub fn default_value(mut self, value: impl Into<Hsla>) -> Self {
110 self.value = Some(value.into());
111 self
112 }
113
114 pub fn set_value(
116 &mut self,
117 value: impl Into<Hsla>,
118 window: &mut Window,
119 cx: &mut Context<Self>,
120 ) {
121 self.update_value(Some(value.into()), false, window, cx)
122 }
123
124 pub fn value(&self) -> Option<Hsla> {
126 self.value
127 }
128
129 fn on_escape(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
130 if !self.open {
131 cx.propagate();
132 }
133
134 self.open = false;
135 if self.hovered_color != self.value {
136 let color = self.value;
137 self.hovered_color = color;
138 if let Some(color) = color {
139 self.state.update(cx, |input, cx| {
140 input.set_value(color.to_hex(), window, cx);
141 });
142 }
143 }
144 cx.notify();
145 }
146
147 fn on_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
148 self.open = !self.open;
149 cx.notify();
150 }
151
152 fn toggle_picker(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
153 self.open = !self.open;
154 cx.notify();
155 }
156
157 fn update_value(
158 &mut self,
159 value: Option<Hsla>,
160 emit: bool,
161 window: &mut Window,
162 cx: &mut Context<Self>,
163 ) {
164 self.value = value;
165 self.hovered_color = value;
166 self.state.update(cx, |view, cx| {
167 if let Some(value) = value {
168 view.set_value(value.to_hex(), window, cx);
169 } else {
170 view.set_value("", window, cx);
171 }
172 });
173 if emit {
174 cx.emit(ColorPickerEvent::Change(value));
175 }
176 cx.notify();
177 }
178}
179
180impl EventEmitter<ColorPickerEvent> for ColorPickerState {}
181
182impl Render for ColorPickerState {
183 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
184 self.state.clone()
185 }
186}
187
188impl Focusable for ColorPickerState {
189 fn focus_handle(&self, _: &App) -> FocusHandle {
190 self.focus_handle.clone()
191 }
192}
193
194#[derive(IntoElement)]
196pub struct ColorPicker {
197 id: ElementId,
198 style: StyleRefinement,
199 state: Entity<ColorPickerState>,
200 featured_colors: Option<Vec<Hsla>>,
201 label: Option<SharedString>,
202 icon: Option<Icon>,
203 size: Size,
204 anchor: Corner,
205}
206
207impl ColorPicker {
208 pub fn new(state: &Entity<ColorPickerState>) -> Self {
210 Self {
211 id: ("color-picker", state.entity_id()).into(),
212 style: StyleRefinement::default(),
213 state: state.clone(),
214 featured_colors: None,
215 size: Size::Medium,
216 label: None,
217 icon: None,
218 anchor: Corner::TopLeft,
219 }
220 }
221
222 pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
227 self.featured_colors = Some(colors);
228 self
229 }
230
231 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
236 self.icon = Some(icon.into());
237 self
238 }
239
240 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
244 self.label = Some(label.into());
245 self
246 }
247
248 pub fn anchor(mut self, anchor: Corner) -> Self {
252 self.anchor = anchor;
253 self
254 }
255
256 fn render_item(
257 &self,
258 color: Hsla,
259 clickable: bool,
260 window: &mut Window,
261 _: &mut App,
262 ) -> Stateful<Div> {
263 let state = self.state.clone();
264 div()
265 .id(SharedString::from(format!("color-{}", color.to_hex())))
266 .h_5()
267 .w_5()
268 .bg(color)
269 .border_1()
270 .border_color(color.darken(0.1))
271 .when(clickable, |this| {
272 this.hover(|this| {
273 this.border_color(color.darken(0.3))
274 .bg(color.lighten(0.1))
275 .shadow_xs()
276 })
277 .active(|this| this.border_color(color.darken(0.5)).bg(color.darken(0.2)))
278 .on_mouse_move(window.listener_for(&state, move |state, _, window, cx| {
279 state.hovered_color = Some(color);
280 state.state.update(cx, |input, cx| {
281 input.set_value(color.to_hex(), window, cx);
282 });
283 cx.notify();
284 }))
285 .on_click(window.listener_for(
286 &state,
287 move |state, _, window, cx| {
288 state.update_value(Some(color), true, window, cx);
289 state.open = false;
290 cx.notify();
291 },
292 ))
293 })
294 }
295
296 fn render_colors(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
297 let featured_colors = self.featured_colors.clone().unwrap_or(vec![
298 cx.theme().red,
299 cx.theme().red_light,
300 cx.theme().blue,
301 cx.theme().blue_light,
302 cx.theme().green,
303 cx.theme().green_light,
304 cx.theme().yellow,
305 cx.theme().yellow_light,
306 cx.theme().cyan,
307 cx.theme().cyan_light,
308 cx.theme().magenta,
309 cx.theme().magenta_light,
310 ]);
311
312 let state = self.state.clone();
313 let input_value = state.read(cx).state.read(cx).value();
315 if input_value.is_empty()
316 && let Some(value) = state.read(cx).value
317 {
318 state.update(cx, |state, cx| {
319 state.state.update(cx, |input, cx| {
320 input.set_value(value.to_hex(), window, cx);
321 });
322 });
323 }
324
325 v_flex()
326 .gap_3()
327 .child(
328 h_flex().gap_1().children(
329 featured_colors
330 .iter()
331 .map(|color| self.render_item(*color, true, window, cx)),
332 ),
333 )
334 .child(Divider::horizontal())
335 .child(
336 v_flex()
337 .gap_1()
338 .children(color_palettes().iter().map(|sub_colors| {
339 h_flex().gap_1().children(
340 sub_colors
341 .iter()
342 .rev()
343 .map(|color| self.render_item(*color, true, window, cx)),
344 )
345 })),
346 )
347 .when_some(state.read(cx).hovered_color, |this, hovered_color| {
348 this.child(Divider::horizontal()).child(
349 h_flex()
350 .gap_2()
351 .items_center()
352 .child(
353 div()
354 .bg(hovered_color)
355 .flex_shrink_0()
356 .border_1()
357 .border_color(hovered_color.darken(0.2))
358 .size_5()
359 .rounded(cx.theme().radius),
360 )
361 .child(Input::new(&state.read(cx).state).small()),
362 )
363 })
364 }
365
366 fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
367 bounds.corner(match self.anchor {
368 Corner::TopLeft => Corner::BottomLeft,
369 Corner::TopRight => Corner::BottomRight,
370 Corner::BottomLeft => Corner::TopLeft,
371 Corner::BottomRight => Corner::TopRight,
372 })
373 }
374}
375
376impl Sizable for ColorPicker {
377 fn with_size(mut self, size: impl Into<Size>) -> Self {
378 self.size = size.into();
379 self
380 }
381}
382
383impl Focusable for ColorPicker {
384 fn focus_handle(&self, cx: &App) -> FocusHandle {
385 self.state.read(cx).focus_handle.clone()
386 }
387}
388
389impl Styled for ColorPicker {
390 fn style(&mut self) -> &mut StyleRefinement {
391 &mut self.style
392 }
393}
394
395impl RenderOnce for ColorPicker {
396 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
397 let state = self.state.read(cx);
398 let bounds = state.bounds;
399 let display_title: SharedString = if let Some(value) = state.value {
400 value.to_hex()
401 } else {
402 "".to_string()
403 }
404 .into();
405
406 let is_focused = state.focus_handle.is_focused(window);
407 let focus_handle = state.focus_handle.clone().tab_stop(true);
408
409 div()
410 .id(self.id.clone())
411 .key_context(CONTEXT)
412 .track_focus(&focus_handle)
413 .on_action(window.listener_for(&self.state, ColorPickerState::on_escape))
414 .on_action(window.listener_for(&self.state, ColorPickerState::on_confirm))
415 .child(
416 h_flex()
417 .id("color-picker-input")
418 .gap_2()
419 .items_center()
420 .input_text_size(self.size)
421 .line_height(relative(1.))
422 .rounded(cx.theme().radius)
423 .refine_style(&self.style)
424 .when_some(self.icon.clone(), |this, icon| {
425 this.child(
426 Button::new("btn")
427 .track_focus(&focus_handle)
428 .ghost()
429 .selected(state.open)
430 .with_size(self.size)
431 .icon(icon.clone()),
432 )
433 })
434 .when_none(&self.icon, |this| {
435 this.child(
436 div()
437 .id("color-picker-square")
438 .bg(cx.theme().background)
439 .border_1()
440 .m_1()
441 .border_color(cx.theme().input)
442 .shadow_xs()
443 .rounded(cx.theme().radius)
444 .overflow_hidden()
445 .size_with(self.size)
446 .when_some(state.value, |this, value| {
447 this.bg(value)
448 .border_color(value.darken(0.3))
449 .when(state.open, |this| this.border_2())
450 })
451 .when(!display_title.is_empty(), |this| {
452 this.tooltip(move |_, cx| {
453 cx.new(|_| Tooltip::new(display_title.clone())).into()
454 })
455 }),
456 )
457 .focus_ring(is_focused, px(0.), window, cx)
458 })
459 .when_some(self.label.clone(), |this, label| this.child(label))
460 .on_click(window.listener_for(&self.state, ColorPickerState::toggle_picker))
461 .child(
462 canvas(
463 {
464 let state = self.state.clone();
465 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds)
466 },
467 |_, _, _, _| {},
468 )
469 .absolute()
470 .size_full(),
471 ),
472 )
473 .when(state.open, |this| {
474 this.child(
475 deferred(
476 anchored()
477 .anchor(self.anchor)
478 .snap_to_window_with_margin(px(8.))
479 .position(self.resolved_corner(bounds))
480 .child(
481 div()
482 .occlude()
483 .map(|this| match self.anchor {
484 Corner::TopLeft | Corner::TopRight => this.mt_1p5(),
485 Corner::BottomLeft | Corner::BottomRight => this.mb_1p5(),
486 })
487 .w_72()
488 .overflow_hidden()
489 .rounded(cx.theme().radius)
490 .p_3()
491 .border_1()
492 .border_color(cx.theme().border)
493 .shadow_lg()
494 .bg(cx.theme().popover)
495 .text_color(cx.theme().popover_foreground)
496 .child(self.render_colors(window, cx))
497 .on_mouse_up_out(
498 MouseButton::Left,
499 window.listener_for(&self.state, |state, _, window, cx| {
500 state.on_escape(&Cancel, window, cx)
501 }),
502 ),
503 ),
504 )
505 .with_priority(1),
506 )
507 })
508 }
509}