1use std::rc::Rc;
2
3use gpui::{
4 div, px, AnyElement, ClickEvent, Div, ImageSource, IntoElement, MouseButton, MouseDownEvent,
5 Pixels, Stateful, StatefulInteractiveElement,
6};
7use smallvec::SmallVec;
8
9use crate::{
10 disclosure_control, h_stack, v_stack, Avatar, Icon, IconButton, IconElement, IconSize, Label,
11 Toggle,
12};
13use crate::{prelude::*, GraphicSlot};
14
15pub enum ListHeaderMeta {
16 Tools(Vec<IconButton>),
17 // TODO: This should be a button
18 Button(Label),
19 Text(Label),
20}
21
22#[derive(IntoElement)]
23pub struct ListHeader {
24 label: SharedString,
25 left_icon: Option<Icon>,
26 meta: Option<ListHeaderMeta>,
27 toggle: Toggle,
28 inset: bool,
29}
30
31impl ListHeader {
32 pub fn new(label: impl Into<SharedString>) -> Self {
33 Self {
34 label: label.into(),
35 left_icon: None,
36 meta: None,
37 inset: false,
38 toggle: Toggle::NotToggleable,
39 }
40 }
41
42 pub fn toggle(mut self, toggle: Toggle) -> Self {
43 self.toggle = toggle;
44 self
45 }
46
47 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
48 self.left_icon = left_icon;
49 self
50 }
51
52 pub fn right_button(self, button: IconButton) -> Self {
53 self.meta(Some(ListHeaderMeta::Tools(vec![button])))
54 }
55
56 pub fn meta(mut self, meta: Option<ListHeaderMeta>) -> Self {
57 self.meta = meta;
58 self
59 }
60}
61
62impl RenderOnce for ListHeader {
63 type Rendered = Div;
64
65 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
66 let disclosure_control = disclosure_control(self.toggle);
67
68 let meta = match self.meta {
69 Some(ListHeaderMeta::Tools(icons)) => div().child(
70 h_stack()
71 .gap_2()
72 .items_center()
73 .children(icons.into_iter().map(|i| i.color(Color::Muted))),
74 ),
75 Some(ListHeaderMeta::Button(label)) => div().child(label),
76 Some(ListHeaderMeta::Text(label)) => div().child(label),
77 None => div(),
78 };
79
80 h_stack()
81 .w_full()
82 .bg(cx.theme().colors().surface_background)
83 .relative()
84 .child(
85 div()
86 .h_5()
87 .when(self.inset, |this| this.px_2())
88 .flex()
89 .flex_1()
90 .items_center()
91 .justify_between()
92 .w_full()
93 .gap_1()
94 .child(
95 h_stack()
96 .gap_1()
97 .child(
98 div()
99 .flex()
100 .gap_1()
101 .items_center()
102 .children(self.left_icon.map(|i| {
103 IconElement::new(i)
104 .color(Color::Muted)
105 .size(IconSize::Small)
106 }))
107 .child(Label::new(self.label.clone()).color(Color::Muted)),
108 )
109 .child(disclosure_control),
110 )
111 .child(meta),
112 )
113 }
114}
115
116#[derive(IntoElement, Clone)]
117pub struct ListSubHeader {
118 label: SharedString,
119 left_icon: Option<Icon>,
120 inset: bool,
121}
122
123impl ListSubHeader {
124 pub fn new(label: impl Into<SharedString>) -> Self {
125 Self {
126 label: label.into(),
127 left_icon: None,
128 inset: false,
129 }
130 }
131
132 pub fn left_icon(mut self, left_icon: Option<Icon>) -> Self {
133 self.left_icon = left_icon;
134 self
135 }
136}
137
138impl RenderOnce for ListSubHeader {
139 type Rendered = Div;
140
141 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
142 h_stack().flex_1().w_full().relative().py_1().child(
143 div()
144 .h_6()
145 .when(self.inset, |this| this.px_2())
146 .flex()
147 .flex_1()
148 .w_full()
149 .gap_1()
150 .items_center()
151 .justify_between()
152 .child(
153 div()
154 .flex()
155 .gap_1()
156 .items_center()
157 .children(self.left_icon.map(|i| {
158 IconElement::new(i)
159 .color(Color::Muted)
160 .size(IconSize::Small)
161 }))
162 .child(Label::new(self.label.clone()).color(Color::Muted)),
163 ),
164 )
165 }
166}
167
168#[derive(IntoElement)]
169pub struct ListItem {
170 id: ElementId,
171 selected: bool,
172 // TODO: Reintroduce this
173 // disclosure_control_style: DisclosureControlVisibility,
174 indent_level: usize,
175 indent_step_size: Pixels,
176 left_slot: Option<GraphicSlot>,
177 toggle: Toggle,
178 inset: bool,
179 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
180 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
181 children: SmallVec<[AnyElement; 2]>,
182}
183
184impl ListItem {
185 pub fn new(id: impl Into<ElementId>) -> Self {
186 Self {
187 id: id.into(),
188 selected: false,
189 indent_level: 0,
190 indent_step_size: px(12.),
191 left_slot: None,
192 toggle: Toggle::NotToggleable,
193 inset: false,
194 on_click: None,
195 on_secondary_mouse_down: None,
196 children: SmallVec::new(),
197 }
198 }
199
200 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
201 self.on_click = Some(Rc::new(handler));
202 self
203 }
204
205 pub fn on_secondary_mouse_down(
206 mut self,
207 handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
208 ) -> Self {
209 self.on_secondary_mouse_down = Some(Rc::new(handler));
210 self
211 }
212
213 pub fn inset(mut self, inset: bool) -> Self {
214 self.inset = inset;
215 self
216 }
217
218 pub fn indent_level(mut self, indent_level: usize) -> Self {
219 self.indent_level = indent_level;
220 self
221 }
222
223 pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
224 self.indent_step_size = indent_step_size;
225 self
226 }
227
228 pub fn toggle(mut self, toggle: Toggle) -> Self {
229 self.toggle = toggle;
230 self
231 }
232
233 pub fn selected(mut self, selected: bool) -> Self {
234 self.selected = selected;
235 self
236 }
237
238 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
239 self.left_slot = Some(left_content);
240 self
241 }
242
243 pub fn left_icon(mut self, left_icon: Icon) -> Self {
244 self.left_slot = Some(GraphicSlot::Icon(left_icon));
245 self
246 }
247
248 pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
249 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
250 self
251 }
252}
253
254impl RenderOnce for ListItem {
255 type Rendered = Stateful<Div>;
256
257 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
258 let left_content = match self.left_slot.clone() {
259 Some(GraphicSlot::Icon(i)) => Some(
260 h_stack().child(
261 IconElement::new(i)
262 .size(IconSize::Small)
263 .color(Color::Muted),
264 ),
265 ),
266 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
267 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
268 None => None,
269 };
270
271 div()
272 .id(self.id)
273 .relative()
274 // TODO: Add focus state
275 // .when(self.state == InteractionState::Focused, |this| {
276 // this.border()
277 // .border_color(cx.theme().colors().border_focused)
278 // })
279 .when(self.inset, |this| this.rounded_md())
280 .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
281 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
282 .when(self.selected, |this| {
283 this.bg(cx.theme().colors().ghost_element_selected)
284 })
285 .when_some(self.on_click.clone(), |this, on_click| {
286 this.on_click(move |event, cx| {
287 // HACK: GPUI currently fires `on_click` with any mouse button,
288 // but we only care about the left button.
289 if event.down.button == MouseButton::Left {
290 (on_click)(event, cx)
291 }
292 })
293 })
294 .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
295 this.on_mouse_down(MouseButton::Right, move |event, cx| {
296 (on_mouse_down)(event, cx)
297 })
298 })
299 .child(
300 div()
301 .when(self.inset, |this| this.px_2())
302 .ml(self.indent_level as f32 * self.indent_step_size)
303 .flex()
304 .gap_1()
305 .items_center()
306 .relative()
307 .child(disclosure_control(self.toggle))
308 .children(left_content)
309 .children(self.children)
310 // HACK: We need to attach the `on_click` handler to the child element in order to have the click
311 // event actually fire.
312 // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
313 // outer `div`.
314 .id("on_click_hack")
315 .when_some(self.on_click, |this, on_click| {
316 this.on_click(move |event, cx| {
317 // HACK: GPUI currently fires `on_click` with any mouse button,
318 // but we only care about the left button.
319 if event.down.button == MouseButton::Left {
320 (on_click)(event, cx)
321 }
322 })
323 }),
324 )
325 }
326}
327
328impl ParentElement for ListItem {
329 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
330 &mut self.children
331 }
332}
333
334#[derive(IntoElement, Clone)]
335pub struct ListSeparator;
336
337impl ListSeparator {
338 pub fn new() -> Self {
339 Self
340 }
341}
342
343impl RenderOnce for ListSeparator {
344 type Rendered = Div;
345
346 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
347 div().h_px().w_full().bg(cx.theme().colors().border_variant)
348 }
349}
350
351#[derive(IntoElement)]
352pub struct List {
353 /// Message to display when the list is empty
354 /// Defaults to "No items"
355 empty_message: SharedString,
356 header: Option<ListHeader>,
357 toggle: Toggle,
358 children: SmallVec<[AnyElement; 2]>,
359}
360
361impl RenderOnce for List {
362 type Rendered = Div;
363
364 fn render(self, _cx: &mut WindowContext) -> Self::Rendered {
365 let list_content = match (self.children.is_empty(), self.toggle) {
366 (false, _) => div().children(self.children),
367 (true, Toggle::Toggled(false)) => div(),
368 (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
369 };
370
371 v_stack()
372 .w_full()
373 .py_1()
374 .children(self.header.map(|header| header))
375 .child(list_content)
376 }
377}
378
379impl List {
380 pub fn new() -> Self {
381 Self {
382 empty_message: "No items".into(),
383 header: None,
384 toggle: Toggle::NotToggleable,
385 children: SmallVec::new(),
386 }
387 }
388
389 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
390 self.empty_message = empty_message.into();
391 self
392 }
393
394 pub fn header(mut self, header: ListHeader) -> Self {
395 self.header = Some(header);
396 self
397 }
398
399 pub fn toggle(mut self, toggle: Toggle) -> Self {
400 self.toggle = toggle;
401 self
402 }
403}
404
405impl ParentElement for List {
406 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
407 &mut self.children
408 }
409}