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