1use crate::{h_stack, prelude::*, v_stack, KeyBinding, Label};
2use gpui::prelude::*;
3use gpui::Div;
4use gpui::Stateful;
5
6#[derive(RenderOnce)]
7pub struct Palette {
8 id: ElementId,
9 input_placeholder: SharedString,
10 empty_string: SharedString,
11 items: Vec<PaletteItem>,
12 default_order: OrderMethod,
13}
14
15impl<V: 'static> Component<V> for Palette {
16 type Rendered = Stateful<V, Div<V>>;
17
18 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
19 v_stack()
20 .id(self.id)
21 .w_96()
22 .rounded_lg()
23 .bg(cx.theme().colors().elevated_surface_background)
24 .border()
25 .border_color(cx.theme().colors().border)
26 .child(
27 v_stack()
28 .gap_px()
29 .child(v_stack().py_0p5().px_1().child(
30 div().px_2().py_0p5().child(
31 Label::new(self.input_placeholder).color(TextColor::Placeholder),
32 ),
33 ))
34 .child(
35 div()
36 .h_px()
37 .w_full()
38 .bg(cx.theme().colors().element_background),
39 )
40 .child(
41 v_stack()
42 .id("items")
43 .py_0p5()
44 .px_1()
45 .grow()
46 .max_h_96()
47 .overflow_y_scroll()
48 .children(
49 vec![if self.items.is_empty() {
50 Some(h_stack().justify_between().px_2().py_1().child(
51 Label::new(self.empty_string).color(TextColor::Muted),
52 ))
53 } else {
54 None
55 }]
56 .into_iter()
57 .flatten(),
58 )
59 .children(self.items.into_iter().enumerate().map(|(index, item)| {
60 h_stack()
61 .id(index)
62 .justify_between()
63 .px_2()
64 .py_0p5()
65 .rounded_lg()
66 .hover(|style| {
67 style.bg(cx.theme().colors().ghost_element_hover)
68 })
69 .active(|style| {
70 style.bg(cx.theme().colors().ghost_element_active)
71 })
72 .child(item)
73 })),
74 ),
75 )
76 }
77}
78
79impl Palette {
80 pub fn new(id: impl Into<ElementId>) -> Self {
81 Self {
82 id: id.into(),
83 input_placeholder: "Find something...".into(),
84 empty_string: "No items found.".into(),
85 items: vec![],
86 default_order: OrderMethod::default(),
87 }
88 }
89
90 pub fn items(mut self, items: Vec<PaletteItem>) -> Self {
91 self.items = items;
92 self
93 }
94
95 pub fn placeholder(mut self, input_placeholder: impl Into<SharedString>) -> Self {
96 self.input_placeholder = input_placeholder.into();
97 self
98 }
99
100 pub fn empty_string(mut self, empty_string: impl Into<SharedString>) -> Self {
101 self.empty_string = empty_string.into();
102 self
103 }
104
105 // TODO: Hook up sort order
106 pub fn default_order(mut self, default_order: OrderMethod) -> Self {
107 self.default_order = default_order;
108 self
109 }
110}
111
112#[derive(RenderOnce)]
113pub struct PaletteItem {
114 pub label: SharedString,
115 pub sublabel: Option<SharedString>,
116 pub key_binding: Option<KeyBinding>,
117}
118
119impl<V: 'static> Component<V> for PaletteItem {
120 type Rendered = Div<V>;
121
122 fn render(self, view: &mut V, cx: &mut ViewContext<V>) -> Self::Rendered {
123 div()
124 .flex()
125 .flex_row()
126 .grow()
127 .justify_between()
128 .child(
129 v_stack()
130 .child(Label::new(self.label))
131 .children(self.sublabel.map(|sublabel| Label::new(sublabel))),
132 )
133 .children(self.key_binding)
134 }
135}
136
137impl PaletteItem {
138 pub fn new(label: impl Into<SharedString>) -> Self {
139 Self {
140 label: label.into(),
141 sublabel: None,
142 key_binding: None,
143 }
144 }
145
146 pub fn label(mut self, label: impl Into<SharedString>) -> Self {
147 self.label = label.into();
148 self
149 }
150
151 pub fn sublabel(mut self, sublabel: impl Into<Option<SharedString>>) -> Self {
152 self.sublabel = sublabel.into();
153 self
154 }
155
156 pub fn key_binding(mut self, key_binding: impl Into<Option<KeyBinding>>) -> Self {
157 self.key_binding = key_binding.into();
158 self
159 }
160}
161
162use gpui::ElementId;
163#[cfg(feature = "stories")]
164pub use stories::*;
165
166#[cfg(feature = "stories")]
167mod stories {
168 use gpui::{Div, Render};
169
170 use crate::{binding, Story};
171
172 use super::*;
173
174 pub struct PaletteStory;
175
176 impl Render<Self> for PaletteStory {
177 type Element = Div<Self>;
178
179 fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
180 {
181 Story::container(cx)
182 .child(Story::title_for::<_, Palette>(cx))
183 .child(Story::label(cx, "Default"))
184 .child(Palette::new("palette-1"))
185 .child(Story::label(cx, "With Items"))
186 .child(
187 Palette::new("palette-2")
188 .placeholder("Execute a command...")
189 .items(vec![
190 PaletteItem::new("theme selector: toggle")
191 .key_binding(KeyBinding::new(binding("cmd-k cmd-t"))),
192 PaletteItem::new("assistant: inline assist")
193 .key_binding(KeyBinding::new(binding("cmd-enter"))),
194 PaletteItem::new("assistant: quote selection")
195 .key_binding(KeyBinding::new(binding("cmd-<"))),
196 PaletteItem::new("assistant: toggle focus")
197 .key_binding(KeyBinding::new(binding("cmd-?"))),
198 PaletteItem::new("auto update: check"),
199 PaletteItem::new("auto update: view release notes"),
200 PaletteItem::new("branches: open recent")
201 .key_binding(KeyBinding::new(binding("cmd-alt-b"))),
202 PaletteItem::new("chat panel: toggle focus"),
203 PaletteItem::new("cli: install"),
204 PaletteItem::new("client: sign in"),
205 PaletteItem::new("client: sign out"),
206 PaletteItem::new("editor: cancel")
207 .key_binding(KeyBinding::new(binding("escape"))),
208 ]),
209 )
210 }
211 }
212 }
213}