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