gpui_component/
color_picker.rs

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/// Events emitted by the [`ColorPicker`].
27#[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
60/// State of the [`ColorPicker`].
61pub 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    /// Create a new [`ColorPickerState`].
73    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    /// Set default color value.
109    pub fn default_value(mut self, value: impl Into<Hsla>) -> Self {
110        self.value = Some(value.into());
111        self
112    }
113
114    /// Set current color value.
115    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    /// Get current color value.
125    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/// A color picker element.
195#[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    /// Create a new color picker element with the given [`ColorPickerState`].
209    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    /// Set the featured colors to be displayed in the color picker.
223    ///
224    /// This is used to display a set of colors that the user can quickly select from,
225    /// for example provided user's last used colors.
226    pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
227        self.featured_colors = Some(colors);
228        self
229    }
230
231    /// Set the icon to the color picker button.
232    ///
233    /// If this is set the color picker button will display the icon.
234    /// Else it will display the square color of the current value.
235    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
236        self.icon = Some(icon.into());
237        self
238    }
239
240    /// Set the label to be displayed above the color picker.
241    ///
242    /// Default is `None`.
243    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
244        self.label = Some(label.into());
245        self
246    }
247
248    /// Set the anchor corner of the color picker.
249    ///
250    /// Default is `Corner::TopLeft`.
251    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        // If the input value is empty, fill it with the current value.
314        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}