Skip to main content

gpui_component/
color_picker.rs

1use gpui::{
2    Anchor, AnyElement, App, ElementId, Entity, FocusHandle, Focusable, Hsla,
3    InteractiveElement as _, IntoElement, ParentElement, RenderOnce, SharedString,
4    StatefulInteractiveElement as _, StyleRefinement, Styled, TextAlign, Window, div, hsla,
5    linear_color_stop, linear_gradient, prelude::FluentBuilder as _,
6};
7use rust_i18n::t;
8
9use gpui_base::{ColorPicker as BaseColorPicker, ColorSwatch};
10pub use gpui_base::{ColorPickerEvent, ColorPickerState};
11
12use crate::{
13    ActiveTheme as _, Colorize as _, Icon, Selectable, Sizable, Size, StyleSized, StyledExt as _,
14    ThemeStyled as _, h_flex,
15    input::{Input, input_style},
16    popover::Popover,
17    select::Caret,
18    separator::Separator,
19    slider::Slider,
20    tab::{Tab, TabBar},
21    tooltip::{ManagedTooltipExt as _, Tooltip},
22    v_flex,
23};
24
25fn color_palettes() -> Vec<Vec<Hsla>> {
26    use crate::theme::DEFAULT_COLORS;
27    use itertools::Itertools as _;
28
29    macro_rules! c {
30        ($color:tt) => {
31            DEFAULT_COLORS
32                .$color
33                .keys()
34                .sorted()
35                .map(|k| DEFAULT_COLORS.$color.get(k).map(|c| c.hsla).unwrap())
36                .collect::<Vec<_>>()
37        };
38    }
39
40    vec![
41        c!(stone),
42        c!(red),
43        c!(orange),
44        c!(yellow),
45        c!(green),
46        c!(cyan),
47        c!(blue),
48        c!(purple),
49        c!(pink),
50    ]
51}
52
53/// A color picker element.
54#[derive(IntoElement)]
55pub struct ColorPicker {
56    id: ElementId,
57    style: StyleRefinement,
58    state: Entity<ColorPickerState>,
59    featured_colors: Option<Vec<Hsla>>,
60    label: Option<SharedString>,
61    /// The announced name, when the visible label is not it.
62    accessibility_label: Option<SharedString>,
63    icon: Option<Icon>,
64    size: Size,
65    anchor: Anchor,
66    /// Draws the trigger as a framed field, see [`ColorSelect`].
67    field: bool,
68    placeholder: Option<SharedString>,
69}
70
71impl ColorPicker {
72    /// Create a new color picker element with the given [`ColorPickerState`].
73    pub fn new(state: &Entity<ColorPickerState>) -> Self {
74        Self {
75            id: ("color-picker", state.entity_id()).into(),
76            style: StyleRefinement::default(),
77            state: state.clone(),
78            featured_colors: None,
79            size: Size::Medium,
80            label: None,
81            accessibility_label: None,
82            icon: None,
83            anchor: Anchor::TopLeft,
84            field: false,
85            placeholder: None,
86        }
87    }
88
89    /// Set the featured colors to be displayed in the color picker.
90    ///
91    /// This is used to display a set of colors that the user can quickly select from,
92    /// for example provided user's last used colors.
93    pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
94        self.featured_colors = Some(colors);
95        self
96    }
97
98    /// Set the icon to the color picker button.
99    ///
100    /// If this is set the color picker button will display the icon.
101    /// Else it will display the square color of the current value.
102    pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
103        self.icon = Some(icon.into());
104        self
105    }
106
107    /// Set the label to be displayed above the color picker.
108    ///
109    /// Default is `None`.
110    pub fn label(mut self, label: impl Into<SharedString>) -> Self {
111        self.label = Some(label.into());
112        self
113    }
114
115    /// Set the name a screen reader announces, when the visible label is not
116    /// it.
117    ///
118    /// A color picker's name comes from its [`label`](Self::label) by default.
119    /// Setting this replaces the announced name without changing the visible
120    /// label.
121    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
122        self.accessibility_label = Some(label.into());
123        self
124    }
125
126    /// Set the anchor corner of the color picker.
127    ///
128    /// Default is `Anchor::TopLeft`.
129    pub fn anchor(mut self, anchor: Anchor) -> Self {
130        self.anchor = anchor;
131        self
132    }
133
134    fn render_item(&self, id: impl Into<ElementId>, color: Hsla, cx: &mut App) -> ColorSwatch {
135        let selected = self.state.read(cx).value() == Some(color);
136        let hover_state = self.state.clone();
137        let click_state = self.state.clone();
138
139        ColorSwatch::new(id, color)
140            .selected(selected)
141            .h_5()
142            .w_5()
143            .bg(color)
144            .border_1()
145            .border_color(color.darken(0.1))
146            .hover(|this| this.border_color(color.darken(0.3)).bg(color.lighten(0.1)))
147            .active(|this| this.border_color(color.darken(0.5)).bg(color.darken(0.2)))
148            .on_hover(move |color, entered, window, cx| {
149                if entered {
150                    hover_state.update(cx, |state, cx| state.preview_color(color, window, cx));
151                }
152            })
153            .on_click(move |color, _, window, cx| {
154                click_state.update(cx, |state, cx| state.select_color(color, window, cx));
155            })
156    }
157
158    fn render_colors(&self, window: &mut Window, cx: &mut App) -> impl IntoElement {
159        self.state
160            .update(cx, |state, cx| state.sync_pending_value(window, cx));
161
162        let active_tab = self.state.read(cx).active_tab();
163        let (slider_color, hovered_color) = {
164            let state = self.state.read(cx);
165            let slider_color = state
166                .displayed_color()
167                .unwrap_or_else(|| hsla(0., 0., 0., 1.));
168            (slider_color, state.preview())
169        };
170        let tab_state = self.state.clone();
171
172        v_flex()
173            .p_0p5()
174            .gap_3()
175            .child(
176                TabBar::new("mode")
177                    .segmented()
178                    .selected_index(active_tab)
179                    .on_click(move |ix: &usize, _, cx| {
180                        tab_state.update(cx, |state, cx| state.set_active_tab(*ix, cx));
181                    })
182                    .child(Tab::new().flex_1().label(t!("ColorPicker.Palette")))
183                    .child(Tab::new().flex_1().label(t!("ColorPicker.HSLA"))),
184            )
185            .child(match active_tab {
186                0 => self.render_palette_panel(cx).into_any_element(),
187                _ => self
188                    .render_slider_tab_panel(slider_color, cx)
189                    .into_any_element(),
190            })
191            .when_some(hovered_color, |this, hovered_color| {
192                this.child(Separator::horizontal()).child(
193                    h_flex()
194                        .gap_2()
195                        .items_center()
196                        .child(
197                            div()
198                                .bg(hovered_color)
199                                .flex_shrink_0()
200                                .border_1()
201                                .border_color(hovered_color.darken(0.2))
202                                .size_5()
203                                .rounded(cx.theme().radius),
204                        )
205                        .child(Input::new(self.state.read(cx).hex_input()).small().px_2p5()),
206                )
207            })
208    }
209
210    fn render_palette_panel(&self, cx: &mut App) -> impl IntoElement {
211        let featured_colors = self.featured_colors.clone().unwrap_or(vec![
212            cx.theme().red,
213            cx.theme().red_light,
214            cx.theme().blue,
215            cx.theme().blue_light,
216            cx.theme().green,
217            cx.theme().green_light,
218            cx.theme().yellow,
219            cx.theme().yellow_light,
220            cx.theme().cyan,
221            cx.theme().cyan_light,
222            cx.theme().magenta,
223            cx.theme().magenta_light,
224        ]);
225
226        v_flex()
227            .gap_3()
228            .child(
229                h_flex().gap_1().children(
230                    featured_colors
231                        .iter()
232                        // Featured slots may contain the same color more than once.
233                        .enumerate()
234                        .map(|(ix, color)| self.render_item(("featured-color", ix), *color, cx)),
235                ),
236            )
237            .child(Separator::horizontal())
238            .child(
239                v_flex()
240                    .gap_1()
241                    .children(color_palettes().iter().enumerate().map(|(ix, sub_colors)| {
242                        h_flex().id(("palette-row", ix)).gap_1().children(
243                            sub_colors.iter().rev().map(|color| {
244                                self.render_item(
245                                    SharedString::from(format!("color-{}", color.to_hex())),
246                                    *color,
247                                    cx,
248                                )
249                            }),
250                        )
251                    })),
252            )
253    }
254
255    fn render_slider_tab_panel(&self, slider_color: Hsla, cx: &mut App) -> impl IntoElement {
256        let sliders = self.state.read(cx).sliders().clone();
257        let steps = 96usize;
258        let hue_colors = (0..steps)
259            .map(|ix| {
260                let h = ix as f32 / (steps.saturating_sub(1)) as f32;
261                hsla(h, 1.0, 0.5, 1.0)
262            })
263            .collect::<Vec<_>>();
264        let saturation_start = hsla(slider_color.h, 0.0, slider_color.l, 1.0);
265        let saturation_end = hsla(slider_color.h, 1.0, slider_color.l, 1.0);
266        let lightness_colors = (0..steps)
267            .map(|ix| {
268                let l = ix as f32 / (steps.saturating_sub(1)) as f32;
269                hsla(slider_color.h, 1.0, l, 1.0)
270            })
271            .collect::<Vec<_>>();
272        let alpha_start = hsla(slider_color.h, slider_color.s, slider_color.l, 0.0);
273        let alpha_end = hsla(slider_color.h, slider_color.s, slider_color.l, 1.0);
274
275        let label_color = cx.theme().foreground.opacity(0.7);
276
277        v_flex()
278            .gap_2()
279            .child(
280                h_flex()
281                    .gap_2()
282                    .items_center()
283                    .child(
284                        div()
285                            .min_w_16()
286                            .text_xs()
287                            .text_color(label_color)
288                            .child(t!("ColorPicker.Hue")),
289                    )
290                    .child(
291                        div()
292                            .relative()
293                            .flex()
294                            .items_center()
295                            .flex_1()
296                            .h_8()
297                            .child(self.render_slider_track(hue_colors, cx))
298                            .child(
299                                Slider::new(sliders.hue())
300                                    .flex_1()
301                                    .bg(cx.theme().transparent),
302                            ),
303                    )
304                    .child(
305                        div()
306                            .w_10()
307                            .text_xs()
308                            .text_color(label_color)
309                            .text_align(TextAlign::Right)
310                            .child(format!("{:.0}", slider_color.h * 360.)),
311                    ),
312            )
313            .child(
314                h_flex()
315                    .gap_2()
316                    .items_center()
317                    .child(
318                        div()
319                            .min_w_16()
320                            .text_xs()
321                            .text_color(label_color)
322                            .child(t!("ColorPicker.Saturation")),
323                    )
324                    .child(
325                        div()
326                            .relative()
327                            .flex()
328                            .items_center()
329                            .flex_1()
330                            .h_8()
331                            .child(self.render_slider_track_gradient(
332                                saturation_start,
333                                saturation_end,
334                                cx,
335                            ))
336                            .child(
337                                Slider::new(sliders.saturation())
338                                    .flex_1()
339                                    .bg(cx.theme().transparent),
340                            ),
341                    )
342                    .child(
343                        div()
344                            .w_10()
345                            .text_xs()
346                            .text_color(label_color)
347                            .text_align(TextAlign::Right)
348                            .child(format!("{:.0}", slider_color.s * 100.)),
349                    ),
350            )
351            .child(
352                h_flex()
353                    .gap_2()
354                    .items_center()
355                    .child(
356                        div()
357                            .min_w_16()
358                            .text_xs()
359                            .text_color(label_color)
360                            .child(t!("ColorPicker.Lightness")),
361                    )
362                    .child(
363                        div()
364                            .relative()
365                            .flex()
366                            .items_center()
367                            .flex_1()
368                            .h_8()
369                            .child(self.render_slider_track(lightness_colors, cx))
370                            .child(
371                                Slider::new(sliders.lightness())
372                                    .flex_1()
373                                    .bg(cx.theme().transparent),
374                            ),
375                    )
376                    .child(
377                        div()
378                            .w_10()
379                            .text_xs()
380                            .text_color(label_color)
381                            .text_align(TextAlign::Right)
382                            .child(format!("{:.0}", slider_color.l * 100.)),
383                    ),
384            )
385            .child(
386                h_flex()
387                    .gap_2()
388                    .items_center()
389                    .child(
390                        div()
391                            .min_w_16()
392                            .text_xs()
393                            .text_color(label_color)
394                            .child(t!("ColorPicker.Alpha")),
395                    )
396                    .child(
397                        div()
398                            .relative()
399                            .flex()
400                            .items_center()
401                            .flex_1()
402                            .h_8()
403                            .child(self.render_slider_track_gradient(alpha_start, alpha_end, cx))
404                            .child(
405                                Slider::new(sliders.alpha())
406                                    .flex_1()
407                                    .bg(cx.theme().transparent),
408                            ),
409                    )
410                    .child(
411                        div()
412                            .w_10()
413                            .text_xs()
414                            .text_color(label_color)
415                            .text_align(TextAlign::Right)
416                            .child(format!("{:.0}", slider_color.a * 100.)),
417                    ),
418            )
419    }
420
421    fn render_slider_track(&self, colors: Vec<Hsla>, _: &App) -> impl IntoElement {
422        h_flex()
423            .absolute()
424            .left_0()
425            .right_0()
426            .h_2_5()
427            .overflow_hidden()
428            .children(
429                colors
430                    .into_iter()
431                    .map(|color| div().flex_1().h_full().bg(color)),
432            )
433    }
434
435    fn render_slider_track_gradient(&self, start: Hsla, end: Hsla, _: &App) -> impl IntoElement {
436        div()
437            .absolute()
438            .left_0()
439            .right_0()
440            .h_2_5()
441            .overflow_hidden()
442            .bg(linear_gradient(
443                90.,
444                linear_color_stop(start, 0.),
445                linear_color_stop(end, 1.),
446            ))
447    }
448}
449
450impl Sizable for ColorPicker {
451    fn with_size(mut self, size: impl Into<Size>) -> Self {
452        self.size = size.into();
453        self
454    }
455}
456
457impl Focusable for ColorPicker {
458    fn focus_handle(&self, cx: &App) -> FocusHandle {
459        self.state.focus_handle(cx)
460    }
461}
462
463impl Styled for ColorPicker {
464    fn style(&mut self) -> &mut StyleRefinement {
465        &mut self.style
466    }
467}
468
469impl RenderOnce for ColorPicker {
470    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
471        let state = self.state.read(cx);
472        let display_title: SharedString = if let Some(value) = state.value() {
473            value.to_hex()
474        } else {
475            "".to_string()
476        }
477        .into();
478
479        let open = state.is_open();
480        let value = state.value();
481        let focus_handle = self.state.focus_handle(cx);
482        let focused = focus_handle.is_focused(window);
483        let open_state = self.state.clone();
484        let popover_state = self.state.clone();
485
486        BaseColorPicker::new(self.id.clone())
487            .when(self.field, |this| this.w_full())
488            .refine_style(&self.style)
489            .open(open)
490            .track_focus(&focus_handle)
491            .when_some(
492                self.accessibility_label
493                    .clone()
494                    .or_else(|| self.label.clone()),
495                |this, label| this.accessibility_label(label),
496            )
497            .on_open_change(move |open, _, cx| {
498                open_state.update(cx, |state, cx| state.set_open(open, cx));
499            })
500            .child(
501                Popover::new("popover")
502                    .open(open)
503                    .w_72()
504                    .on_open_change(move |open: &bool, _, cx| {
505                        popover_state.update(cx, |state, cx| state.set_open(*open, cx));
506                    })
507                    .when(self.field, |this| {
508                        this.trigger_style(StyleRefinement::default().w_full())
509                    })
510                    .trigger(ColorPickerButton {
511                        id: "trigger".into(),
512                        size: self.size,
513                        label: self.label.clone(),
514                        value,
515                        tooltip: if display_title.is_empty() || self.field {
516                            None
517                        } else {
518                            Some(display_title.clone())
519                        },
520                        icon: self.icon.clone(),
521                        selected: false,
522                        field: self.field,
523                        focused,
524                        placeholder: self.placeholder.clone(),
525                    })
526                    // The popover drops its content while closed, so building the
527                    // palette swatches or slider tracks then is wasted work.
528                    .when(open, |this| this.child(self.render_colors(window, cx))),
529            )
530    }
531}
532
533/// A color picker drawn as a framed field, like a [`Select`](crate::select::Select).
534///
535/// The field shows a swatch of the current color and its hex value; clicking
536/// anywhere on it opens the same popover as [`ColorPicker`]. Use it in forms,
537/// where a control is expected to share the height and frame of the inputs
538/// around it; use [`ColorPicker`] for a compact swatch in a toolbar.
539#[derive(IntoElement)]
540pub struct ColorSelect {
541    picker: ColorPicker,
542}
543
544impl ColorSelect {
545    /// Create a new color select with the given [`ColorPickerState`].
546    pub fn new(state: &Entity<ColorPickerState>) -> Self {
547        let mut picker = ColorPicker::new(state);
548        picker.id = ("color-select", state.entity_id()).into();
549        picker.field = true;
550        Self { picker }
551    }
552
553    /// Set the featured colors shown at the top of the palette.
554    pub fn featured_colors(mut self, colors: Vec<Hsla>) -> Self {
555        self.picker = self.picker.featured_colors(colors);
556        self
557    }
558
559    /// Set the text shown while no color is selected.
560    ///
561    /// Default is the same placeholder as [`Select`](crate::select::Select).
562    pub fn placeholder(mut self, placeholder: impl Into<SharedString>) -> Self {
563        self.picker.placeholder = Some(placeholder.into());
564        self
565    }
566
567    /// Set the name a screen reader announces.
568    pub fn accessibility_label(mut self, label: impl Into<SharedString>) -> Self {
569        self.picker = self.picker.accessibility_label(label);
570        self
571    }
572}
573
574impl Sizable for ColorSelect {
575    fn with_size(mut self, size: impl Into<Size>) -> Self {
576        self.picker = self.picker.with_size(size);
577        self
578    }
579}
580
581impl Focusable for ColorSelect {
582    fn focus_handle(&self, cx: &App) -> FocusHandle {
583        self.picker.focus_handle(cx)
584    }
585}
586
587impl Styled for ColorSelect {
588    fn style(&mut self) -> &mut StyleRefinement {
589        self.picker.style()
590    }
591}
592
593impl RenderOnce for ColorSelect {
594    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
595        self.picker.render(window, cx)
596    }
597}
598
599#[cfg(test)]
600mod tests {
601    use gpui::{AppContext as _, Context, Render, TestAppContext};
602
603    use super::*;
604
605    struct PaletteHarness {
606        state: Entity<ColorPickerState>,
607    }
608
609    impl Render for PaletteHarness {
610        fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
611            let color = color_palettes()[0][0];
612            ColorPicker::new(&self.state)
613                .featured_colors(vec![color, color])
614                .render_palette_panel(cx)
615                .into_any_element()
616        }
617    }
618
619    #[gpui::test]
620    fn repeated_palette_colors_have_independent_focus_stops(cx: &mut TestAppContext) {
621        cx.update(crate::init);
622        let (_, cx) = cx.add_window_view(|window, cx| PaletteHarness {
623            state: cx.new(|cx| ColorPickerState::new(window, cx)),
624        });
625        cx.update(|window, cx| {
626            window.draw(cx).clear(cx);
627            let swatch_count = 2 + color_palettes().iter().map(Vec::len).sum::<usize>();
628            let mut focused = Vec::new();
629            for _ in 0..swatch_count {
630                window.focus_next(cx);
631                let handle = window.focused(cx).expect("each swatch is focusable");
632                assert!(
633                    !focused.contains(&handle),
634                    "equal colors must not share element identity or a focus stop"
635                );
636                focused.push(handle);
637            }
638            window.focus_next(cx);
639            assert_eq!(window.focused(cx), focused.first().cloned());
640        });
641    }
642
643    #[gpui::test]
644    fn an_explicit_accessibility_label_replaces_the_visible_one(cx: &mut TestAppContext) {
645        cx.update(crate::init);
646        let cx = cx.add_empty_window();
647        cx.update(|window, cx| {
648            let state = cx.new(|cx| ColorPickerState::new(window, cx));
649
650            let plain = ColorPicker::new(&state).label("Color");
651            assert_eq!(plain.accessibility_label, None);
652            assert_eq!(plain.label.as_deref(), Some("Color"));
653
654            let named = ColorPicker::new(&state)
655                .label("Color")
656                .accessibility_label("Text color");
657            assert_eq!(
658                named.accessibility_label.as_deref(),
659                Some("Text color"),
660                "an explicit name must win over the visible label"
661            );
662            assert_eq!(
663                named.label.as_deref(),
664                Some("Color"),
665                "and must not change what is drawn"
666            );
667        });
668    }
669
670    #[gpui::test]
671    fn test_color_select_builder(cx: &mut TestAppContext) {
672        cx.update(crate::init);
673        let cx = cx.add_empty_window();
674        cx.update(|window, cx| {
675            let state = cx.new(|cx| ColorPickerState::new(window, cx));
676            let select = ColorSelect::new(&state)
677                .large()
678                .placeholder("Pick a color")
679                .accessibility_label("Theme color")
680                .featured_colors(vec![hsla(0., 1., 0.5, 1.)]);
681
682            assert!(select.picker.field);
683            assert_eq!(select.picker.size, Size::Large);
684            assert_eq!(select.picker.placeholder.as_deref(), Some("Pick a color"));
685            assert_eq!(
686                select.picker.accessibility_label.as_deref(),
687                Some("Theme color")
688            );
689            assert_eq!(
690                select.picker.featured_colors.as_ref().map(Vec::len),
691                Some(1)
692            );
693        });
694    }
695
696    struct SelectHarness {
697        state: Entity<ColorPickerState>,
698    }
699
700    impl Render for SelectHarness {
701        fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
702            div().w(gpui::px(400.)).child(ColorSelect::new(&self.state))
703        }
704    }
705
706    #[gpui::test]
707    fn a_click_anywhere_on_the_color_select_opens_the_picker(cx: &mut TestAppContext) {
708        cx.update(crate::init);
709        let (view, cx) = cx.add_window_view(|window, cx| SelectHarness {
710            state: cx.new(|cx| ColorPickerState::new(window, cx)),
711        });
712        cx.update(|window, cx| window.draw(cx).clear(cx));
713        let open = cx.update(|_, cx| view.read(cx).state.read(cx).is_open());
714        assert!(!open, "the picker starts closed");
715
716        // Far from the swatch, where only the field frame can take the click.
717        cx.simulate_click(
718            gpui::point(gpui::px(300.), gpui::px(16.)),
719            Default::default(),
720        );
721        cx.update(|window, cx| window.draw(cx).clear(cx));
722        let open = cx.update(|_, cx| view.read(cx).state.read(cx).is_open());
723        assert!(open, "the whole field is the trigger, like a Select");
724    }
725}
726
727#[derive(IntoElement)]
728struct ColorPickerButton {
729    id: ElementId,
730    selected: bool,
731    icon: Option<Icon>,
732    value: Option<Hsla>,
733    size: Size,
734    label: Option<SharedString>,
735    tooltip: Option<SharedString>,
736    field: bool,
737    focused: bool,
738    placeholder: Option<SharedString>,
739}
740
741impl Selectable for ColorPickerButton {
742    fn selected(mut self, selected: bool) -> Self {
743        self.selected = selected;
744        self
745    }
746
747    fn is_selected(&self) -> bool {
748        self.selected
749    }
750}
751
752impl Sizable for ColorPickerButton {
753    fn with_size(mut self, size: impl Into<Size>) -> Self {
754        self.size = size.into();
755        self
756    }
757}
758
759impl ColorPickerButton {
760    /// The framed trigger of a [`ColorSelect`]: the swatch, the hex value and
761    /// a caret, laid out like a [`Select`](crate::select::Select) trigger.
762    fn render_field(self, window: &mut Window, cx: &mut App) -> AnyElement {
763        let (bg, fg) = input_style(false, cx);
764        let outline_visible = self.selected || self.focused;
765
766        h_flex()
767            .id(self.id)
768            .w_full()
769            .gap_2()
770            .bg(bg)
771            .text_color(fg)
772            .border_1()
773            .border_color(cx.theme().input)
774            .rounded(cx.theme().radius)
775            .input_size(self.size)
776            .input_text_size(self.size)
777            .when(outline_visible, |this| {
778                this.border_color(cx.theme().ring)
779                    .focus_ring_style(window, cx)
780            })
781            .child(
782                div()
783                    .id("swatch")
784                    .flex_shrink_0()
785                    .border_1()
786                    .border_color(cx.theme().input)
787                    .rounded(cx.theme().radius / 2.)
788                    .map(|this| match self.size {
789                        Size::XSmall => this.size_3(),
790                        Size::Small => this.size_3p5(),
791                        Size::Large => this.size_5(),
792                        _ => this.size_4(),
793                    })
794                    .when_some(self.value, |this, value| {
795                        this.bg(value).border_color(value.darken(0.3))
796                    }),
797            )
798            .child(
799                div()
800                    .flex_1()
801                    .min_w_0()
802                    .overflow_hidden()
803                    .whitespace_nowrap()
804                    .map(|this| match self.value {
805                        Some(value) => this.child(value.to_hex()),
806                        None => this.text_color(cx.theme().muted_foreground).child(
807                            self.placeholder
808                                .unwrap_or_else(|| t!("Select.placeholder").into()),
809                        ),
810                    }),
811            )
812            .child(Caret::new(self.size).text_color(cx.theme().muted_foreground))
813            .into_any_element()
814    }
815}
816
817impl RenderOnce for ColorPickerButton {
818    fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
819        if self.field {
820            return self.render_field(window, cx);
821        }
822
823        let has_icon = self.icon.is_some();
824        h_flex()
825            .id(self.id)
826            .gap_2()
827            .children(self.icon)
828            .when(!has_icon, |this| {
829                this.child(
830                    div()
831                        .id("square")
832                        .bg(cx.theme().tokens.background)
833                        .border_1()
834                        .border_color(cx.theme().input)
835                        .rounded(cx.theme().radius)
836                        .overflow_hidden()
837                        .size_with(self.size)
838                        .when_some(self.value, |this, value| {
839                            this.bg(value)
840                                .border_color(value.darken(0.3))
841                                .when(self.selected, |this| this.border_2())
842                        })
843                        .when_some(self.tooltip, |this, tooltip| {
844                            this.managed_tooltip(move |window, cx| {
845                                Tooltip::new(tooltip.clone()).build(window, cx)
846                            })
847                        }),
848                )
849            })
850            .when_some(self.label, |this, label| this.child(label))
851            .into_any_element()
852    }
853}