Skip to main content

kitchen_ui/widgets/
accent_picker.rs

1//! The accent picker: the app's accent, a few suggested colours, or any colour.
2
3use gpui_kit::component::button::Button;
4use gpui_kit::component::color_picker::{ColorPicker, ColorPickerEvent, ColorPickerState};
5use gpui_kit::component::tooltip::Tooltip;
6use gpui_kit::component::{Selectable as _, Sizable as _, h_flex};
7use gpui_kit::prelude::FluentBuilder as _;
8use gpui_kit::{
9    AppContext as _, Context, Entity, EventEmitter, InteractiveElement as _, IntoElement, ParentElement as _, Render, SharedString,
10    StatefulInteractiveElement as _, Styled as _, Subscription, TestSupportExt as _, Window, div, px,
11};
12
13use crate::theme::{self, Rgb, color};
14use crate::widgets::swatch;
15
16/// What the picker reports: the accent picked (`None`: the app's own).
17#[derive(Clone, Copy, Debug, PartialEq, Eq)]
18pub struct AccentChanged(pub Option<Rgb>);
19
20/// A row to choose the accent: "App" (the app's palette), the suggested colours
21/// ([`Rgb::SUGGESTED`]) as swatches, and gpui-kit's colour picker for any other colour (hex,
22/// hue, saturation, lightness). The current choice is ringed, not only coloured (WCAG 1.4.1).
23/// Each swatch is a 24 px target (designparser `touch-target`). It emits [`AccentChanged`]; the
24/// owner stores the value and applies it (`theme::use_look(look.with_accent(…))`).
25pub struct AccentPicker {
26    current: Option<Rgb>,
27    suggested: Vec<Rgb>,
28    custom: Entity<ColorPickerState>,
29    _subs: Vec<Subscription>,
30}
31
32impl EventEmitter<AccentChanged> for AccentPicker {}
33
34impl AccentPicker {
35    /// A picker showing `current` (`None`: the app's accent) and the suggested colours.
36    pub fn new(current: Option<Rgb>, window: &mut Window, cx: &mut Context<Self>) -> Self {
37        let custom = cx.new(|cx| {
38            let state = ColorPickerState::new(window, cx);
39            match current {
40                Some(rgb) => state.default_value(rgb.hsla()),
41                None => state,
42            }
43        });
44        let sub = cx.subscribe(&custom, |this, _, e: &ColorPickerEvent, cx| {
45            if let ColorPickerEvent::Change(Some(c)) = e {
46                this.pick(Some(Rgb::from(*c)), cx);
47            }
48        });
49        Self { current, suggested: Rgb::SUGGESTED.to_vec(), custom, _subs: vec![sub] }
50    }
51
52    /// The colours offered as swatches (by default [`Rgb::SUGGESTED`]).
53    pub fn set_suggested(&mut self, colours: Vec<Rgb>, cx: &mut Context<Self>) {
54        self.suggested = colours;
55        cx.notify();
56    }
57
58    /// The accent shown as chosen (when it changed elsewhere, such as in the preferences).
59    pub fn set_current(&mut self, current: Option<Rgb>, cx: &mut Context<Self>) {
60        self.current = current;
61        cx.notify();
62    }
63
64    /// The accent chosen (`None`: the app's).
65    pub fn current(&self) -> Option<Rgb> {
66        self.current
67    }
68
69    /// Chooses `accent` and tells the owner.
70    pub fn pick(&mut self, accent: Option<Rgb>, cx: &mut Context<Self>) {
71        if self.current != accent {
72            self.current = accent;
73            cx.emit(AccentChanged(accent));
74            cx.notify();
75        }
76    }
77}
78
79impl Render for AccentPicker {
80    fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
81        let current = self.current;
82        let app = Button::new("accent-app").small().outline().label("App").selected(current.is_none()).tooltip("The app's own accent").on_click(cx.listener(|this, _, _, cx| this.pick(None, cx)));
83        let swatches = self.suggested.iter().copied().map(|rgb| {
84            let chosen = current == Some(rgb);
85            let tip = SharedString::from(rgb.to_string());
86            div()
87                .id(SharedString::from(format!("accent-{:06x}", rgb.0)))
88                .flex_none()
89                .size(px(theme::TARGET_MIN))
90                .flex()
91                .items_center()
92                .justify_center()
93                .rounded(px(theme::RADIUS))
94                .border_2()
95                .border_color(if chosen { color::text() } else { gpui_kit::transparent_black() })
96                .when(!chosen, |d| d.hover(|d| d.bg(color::hover())))
97                .cursor_pointer()
98                .child(swatch(rgb.hsla(), 16.))
99                .tooltip(move |window, cx| Tooltip::new(tip.clone()).build(window, cx))
100                .on_click(cx.listener(move |this, _, _, cx| this.pick(Some(rgb), cx)))
101                .test_support()
102        });
103        let custom_chosen = current.is_some_and(|c| !self.suggested.contains(&c));
104        let featured = self.suggested.iter().map(|c| c.hsla()).collect();
105        let custom = div()
106            .flex_none()
107            .rounded(px(theme::RADIUS))
108            .border_2()
109            .border_color(if custom_chosen { color::text() } else { gpui_kit::transparent_black() })
110            .child(ColorPicker::new(&self.custom).small().featured_colors(featured).label("Custom"));
111        h_flex().gap(px(theme::XS)).items_center().flex_wrap().child(app).children(swatches).child(custom)
112    }
113}