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::{Input, InputEvent, InputState},
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";
21pub fn init(cx: &mut App) {
22 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
23}
24
25#[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 {
73 let state = cx.new(|cx| InputState::new(window, cx));
74
75 let _subscriptions = vec![cx.subscribe_in(
76 &state,
77 window,
78 |this, state, ev: &InputEvent, window, cx| match ev {
79 InputEvent::Change => {
80 let value = state.read(cx).value();
81 if let Ok(color) = Hsla::parse_hex(value.as_str()) {
82 this.value = Some(color);
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, _: &mut Window, cx: &mut Context<Self>) {
130 if !self.open {
131 cx.propagate();
132 }
133
134 self.open = false;
135 cx.notify();
136 }
137
138 fn on_confirm(&mut self, _: &Confirm, _: &mut Window, cx: &mut Context<Self>) {
139 self.open = !self.open;
140 cx.notify();
141 }
142
143 fn toggle_picker(&mut self, _: &ClickEvent, _: &mut Window, cx: &mut Context<Self>) {
144 self.open = !self.open;
145 cx.notify();
146 }
147
148 fn update_value(
149 &mut self,
150 value: Option<Hsla>,
151 emit: bool,
152 window: &mut Window,
153 cx: &mut Context<Self>,
154 ) {
155 self.value = value;
156 self.hovered_color = value;
157 self.state.update(cx, |view, cx| {
158 if let Some(value) = value {
159 view.set_value(value.to_hex(), window, cx);
160 } else {
161 view.set_value("", window, cx);
162 }
163 });
164 if emit {
165 cx.emit(ColorPickerEvent::Change(value));
166 }
167 cx.notify();
168 }
169}
170
171impl EventEmitter<ColorPickerEvent> for ColorPickerState {}
172
173impl Render for ColorPickerState {
174 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
175 self.state.clone()
176 }
177}
178
179impl Focusable for ColorPickerState {
180 fn focus_handle(&self, _: &App) -> FocusHandle {
181 self.focus_handle.clone()
182 }
183}
184
185#[derive(IntoElement)]
187pub struct ColorPicker {
188 id: ElementId,
189 style: StyleRefinement,
190 state: Entity<ColorPickerState>,
191 featured_colors: Option<Vec<Hsla>>,
192 label: Option<SharedString>,
193 icon: Option<Icon>,
194 size: Size,
195 anchor: Corner,
196}
197
198impl ColorPicker {
199 pub fn new(state: &Entity<ColorPickerState>) -> Self {
201 Self {
202 id: ("color-picker", state.entity_id()).into(),
203 style: StyleRefinement::default(),
204 state: state.clone(),
205 featured_colors: None,
206 size: Size::Medium,
207 label: None,
208 icon: None,
209 anchor: Corner::TopLeft,
210 }
211 }
212
213 pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
218 self.featured_colors = Some(colors);
219 self
220 }
221
222 pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
227 self.icon = Some(icon.into());
228 self
229 }
230
231 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
235 self.label = Some(label.into());
236 self
237 }
238
239 pub fn anchor(mut self, anchor: Corner) -> Self {
243 self.anchor = anchor;
244 self
245 }
246
247 fn render_item(
248 &self,
249 color: Hsla,
250 clickable: bool,
251 window: &mut Window,
252 _: &mut App,
253 ) -> impl IntoElement {
254 let state = self.state.clone();
255 div()
256 .id(SharedString::from(format!("color-{}", color.to_hex())))
257 .h_5()
258 .w_5()
259 .bg(color)
260 .border_1()
261 .border_color(color.darken(0.1))
262 .when(clickable, |this| {
263 this.hover(|this| {
264 this.border_color(color.darken(0.3))
265 .bg(color.lighten(0.1))
266 .shadow_xs()
267 })
268 .active(|this| this.border_color(color.darken(0.5)).bg(color.darken(0.2)))
269 .on_mouse_move(window.listener_for(&state, move |state, _, window, cx| {
270 state.hovered_color = Some(color);
271 state.state.update(cx, |input, cx| {
272 input.set_value(color.to_hex(), window, cx);
273 });
274 cx.notify();
275 }))
276 .on_click(window.listener_for(
277 &state,
278 move |state, _, window, cx| {
279 state.update_value(Some(color), true, window, cx);
280 state.open = false;
281 cx.notify();
282 },
283 ))
284 })
285 }
286
287 fn render_colors(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
288 let featured_colors = self.featured_colors.clone().unwrap_or(vec![
289 cx.theme().red,
290 cx.theme().red_light,
291 cx.theme().blue,
292 cx.theme().blue_light,
293 cx.theme().green,
294 cx.theme().green_light,
295 cx.theme().yellow,
296 cx.theme().yellow_light,
297 cx.theme().cyan,
298 cx.theme().cyan_light,
299 cx.theme().magenta,
300 cx.theme().magenta_light,
301 ]);
302
303 let state = self.state.clone();
304 v_flex()
305 .gap_3()
306 .child(
307 h_flex().gap_1().children(
308 featured_colors
309 .iter()
310 .map(|color| self.render_item(*color, true, window, cx)),
311 ),
312 )
313 .child(Divider::horizontal())
314 .child(
315 v_flex()
316 .gap_1()
317 .children(color_palettes().iter().map(|sub_colors| {
318 h_flex().gap_1().children(
319 sub_colors
320 .iter()
321 .rev()
322 .map(|color| self.render_item(*color, true, window, cx)),
323 )
324 })),
325 )
326 .when_some(state.read(cx).hovered_color, |this, hovered_color| {
327 this.child(Divider::horizontal()).child(
328 h_flex()
329 .gap_2()
330 .items_center()
331 .child(
332 div()
333 .bg(hovered_color)
334 .flex_shrink_0()
335 .border_1()
336 .border_color(hovered_color.darken(0.2))
337 .size_5()
338 .rounded(cx.theme().radius),
339 )
340 .child(Input::new(&state.read(cx).state).small()),
341 )
342 })
343 }
344
345 fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
346 bounds.corner(match self.anchor {
347 Corner::TopLeft => Corner::BottomLeft,
348 Corner::TopRight => Corner::BottomRight,
349 Corner::BottomLeft => Corner::TopLeft,
350 Corner::BottomRight => Corner::TopRight,
351 })
352 }
353}
354
355impl Sizable for ColorPicker {
356 fn with_size(mut self, size: impl Into<Size>) -> Self {
357 self.size = size.into();
358 self
359 }
360}
361
362impl Focusable for ColorPicker {
363 fn focus_handle(&self, cx: &App) -> FocusHandle {
364 self.state.read(cx).focus_handle.clone()
365 }
366}
367
368impl Styled for ColorPicker {
369 fn style(&mut self) -> &mut StyleRefinement {
370 &mut self.style
371 }
372}
373
374impl RenderOnce for ColorPicker {
375 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
376 let state = self.state.read(cx);
377 let bounds = state.bounds;
378 let display_title: SharedString = if let Some(value) = state.value {
379 value.to_hex()
380 } else {
381 "".to_string()
382 }
383 .into();
384
385 let is_focused = state.focus_handle.is_focused(window);
386 let focus_handle = state.focus_handle.clone().tab_stop(true);
387
388 div()
389 .id(self.id.clone())
390 .key_context(CONTEXT)
391 .track_focus(&focus_handle)
392 .on_action(window.listener_for(&self.state, ColorPickerState::on_escape))
393 .on_action(window.listener_for(&self.state, ColorPickerState::on_confirm))
394 .child(
395 h_flex()
396 .id("color-picker-input")
397 .gap_2()
398 .items_center()
399 .input_text_size(self.size)
400 .line_height(relative(1.))
401 .refine_style(&self.style)
402 .when_some(self.icon.clone(), |this, icon| {
403 this.child(
404 Button::new("btn")
405 .track_focus(&focus_handle)
406 .ghost()
407 .selected(state.open)
408 .with_size(self.size)
409 .icon(icon.clone()),
410 )
411 })
412 .when_none(&self.icon, |this| {
413 this.child(
414 div()
415 .id("color-picker-square")
416 .bg(cx.theme().background)
417 .border_1()
418 .m_1()
419 .border_color(cx.theme().input)
420 .rounded(cx.theme().radius)
421 .shadow_xs()
422 .rounded(cx.theme().radius)
423 .overflow_hidden()
424 .size_with(self.size)
425 .when_some(state.value, |this, value| {
426 this.bg(value)
427 .border_color(value.darken(0.3))
428 .when(state.open, |this| this.border_2())
429 })
430 .when(!display_title.is_empty(), |this| {
431 this.tooltip(move |_, cx| {
432 cx.new(|_| Tooltip::new(display_title.clone())).into()
433 })
434 }),
435 )
436 .focus_ring(is_focused, px(0.), window, cx)
437 })
438 .when_some(self.label.clone(), |this, label| this.child(label))
439 .on_click(window.listener_for(&self.state, ColorPickerState::toggle_picker))
440 .child(
441 canvas(
442 {
443 let state = self.state.clone();
444 move |bounds, _, cx| state.update(cx, |r, _| r.bounds = bounds)
445 },
446 |_, _, _, _| {},
447 )
448 .absolute()
449 .size_full(),
450 ),
451 )
452 .when(state.open, |this| {
453 this.child(
454 deferred(
455 anchored()
456 .anchor(self.anchor)
457 .snap_to_window_with_margin(px(8.))
458 .position(self.resolved_corner(bounds))
459 .child(
460 div()
461 .occlude()
462 .map(|this| match self.anchor {
463 Corner::TopLeft | Corner::TopRight => this.mt_1p5(),
464 Corner::BottomLeft | Corner::BottomRight => this.mb_1p5(),
465 })
466 .w_72()
467 .overflow_hidden()
468 .rounded(cx.theme().radius)
469 .p_3()
470 .border_1()
471 .border_color(cx.theme().border)
472 .shadow_lg()
473 .rounded(cx.theme().radius)
474 .bg(cx.theme().background)
475 .child(self.render_colors(window, cx))
476 .on_mouse_up_out(
477 MouseButton::Left,
478 window.listener_for(&self.state, |state, _, window, cx| {
479 state.on_escape(&Cancel, window, cx)
480 }),
481 ),
482 ),
483 )
484 .with_priority(1),
485 )
486 })
487 }
488}