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 disabled: bool,
172 selected: bool,
173 // TODO: Reintroduce this
174 // disclosure_control_style: DisclosureControlVisibility,
175 indent_level: usize,
176 indent_step_size: Pixels,
177 left_slot: Option<GraphicSlot>,
178 overflow: OverflowStyle,
179 toggle: Toggle,
180 inset: bool,
181 on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
182 on_secondary_mouse_down: Option<Rc<dyn Fn(&MouseDownEvent, &mut WindowContext) + 'static>>,
183 children: SmallVec<[AnyElement; 2]>,
184}
185
186impl ListItem {
187 pub fn new(id: impl Into<ElementId>) -> Self {
188 Self {
189 id: id.into(),
190 disabled: false,
191 selected: false,
192 indent_level: 0,
193 indent_step_size: px(12.),
194 left_slot: None,
195 overflow: OverflowStyle::Hidden,
196 toggle: Toggle::NotToggleable,
197 inset: false,
198 on_click: None,
199 on_secondary_mouse_down: None,
200 children: SmallVec::new(),
201 }
202 }
203
204 pub fn on_click(mut self, handler: impl Fn(&ClickEvent, &mut WindowContext) + 'static) -> Self {
205 self.on_click = Some(Rc::new(handler));
206 self
207 }
208
209 pub fn on_secondary_mouse_down(
210 mut self,
211 handler: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
212 ) -> Self {
213 self.on_secondary_mouse_down = Some(Rc::new(handler));
214 self
215 }
216
217 pub fn inset(mut self, inset: bool) -> Self {
218 self.inset = inset;
219 self
220 }
221
222 pub fn indent_level(mut self, indent_level: usize) -> Self {
223 self.indent_level = indent_level;
224 self
225 }
226
227 pub fn indent_step_size(mut self, indent_step_size: Pixels) -> Self {
228 self.indent_step_size = indent_step_size;
229 self
230 }
231
232 pub fn toggle(mut self, toggle: Toggle) -> Self {
233 self.toggle = toggle;
234 self
235 }
236
237 pub fn selected(mut self, selected: bool) -> Self {
238 self.selected = selected;
239 self
240 }
241
242 pub fn left_content(mut self, left_content: GraphicSlot) -> Self {
243 self.left_slot = Some(left_content);
244 self
245 }
246
247 pub fn left_icon(mut self, left_icon: Icon) -> Self {
248 self.left_slot = Some(GraphicSlot::Icon(left_icon));
249 self
250 }
251
252 pub fn left_avatar(mut self, left_avatar: impl Into<ImageSource>) -> Self {
253 self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into()));
254 self
255 }
256}
257
258impl RenderOnce for ListItem {
259 type Rendered = Stateful<Div>;
260
261 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
262 let left_content = match self.left_slot.clone() {
263 Some(GraphicSlot::Icon(i)) => Some(
264 h_stack().child(
265 IconElement::new(i)
266 .size(IconSize::Small)
267 .color(Color::Muted),
268 ),
269 ),
270 Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::source(src))),
271 Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))),
272 None => None,
273 };
274
275 div()
276 .id(self.id)
277 .relative()
278 // TODO: Add focus state
279 // .when(self.state == InteractionState::Focused, |this| {
280 // this.border()
281 // .border_color(cx.theme().colors().border_focused)
282 // })
283 .when(self.inset, |this| this.rounded_md())
284 .hover(|style| style.bg(cx.theme().colors().ghost_element_hover))
285 .active(|style| style.bg(cx.theme().colors().ghost_element_active))
286 .when(self.selected, |this| {
287 this.bg(cx.theme().colors().ghost_element_selected)
288 })
289 .when_some(self.on_click.clone(), |this, on_click| {
290 this.on_click(move |event, cx| {
291 // HACK: GPUI currently fires `on_click` with any mouse button,
292 // but we only care about the left button.
293 if event.down.button == MouseButton::Left {
294 (on_click)(event, cx)
295 }
296 })
297 })
298 .when_some(self.on_secondary_mouse_down, |this, on_mouse_down| {
299 this.on_mouse_down(MouseButton::Right, move |event, cx| {
300 (on_mouse_down)(event, cx)
301 })
302 })
303 .child(
304 div()
305 .when(self.inset, |this| this.px_2())
306 .ml(self.indent_level as f32 * self.indent_step_size)
307 .flex()
308 .gap_1()
309 .items_center()
310 .relative()
311 .child(disclosure_control(self.toggle))
312 .children(left_content)
313 .children(self.children)
314 // HACK: We need to attach the `on_click` handler to the child element in order to have the click
315 // event actually fire.
316 // Once this is fixed in GPUI we can remove this and rely on the `on_click` handler set above on the
317 // outer `div`.
318 .id("on_click_hack")
319 .when_some(self.on_click, |this, on_click| {
320 this.on_click(move |event, cx| {
321 // HACK: GPUI currently fires `on_click` with any mouse button,
322 // but we only care about the left button.
323 if event.down.button == MouseButton::Left {
324 (on_click)(event, cx)
325 }
326 })
327 }),
328 )
329 }
330}
331
332impl ParentElement for ListItem {
333 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
334 &mut self.children
335 }
336}
337
338#[derive(IntoElement, Clone)]
339pub struct ListSeparator;
340
341impl ListSeparator {
342 pub fn new() -> Self {
343 Self
344 }
345}
346
347impl RenderOnce for ListSeparator {
348 type Rendered = Div;
349
350 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
351 div().h_px().w_full().bg(cx.theme().colors().border_variant)
352 }
353}
354
355#[derive(IntoElement)]
356pub struct List {
357 /// Message to display when the list is empty
358 /// Defaults to "No items"
359 empty_message: SharedString,
360 header: Option<ListHeader>,
361 toggle: Toggle,
362 children: SmallVec<[AnyElement; 2]>,
363}
364
365impl RenderOnce for List {
366 type Rendered = Div;
367
368 fn render(self, cx: &mut WindowContext) -> Self::Rendered {
369 let list_content = match (self.children.is_empty(), self.toggle) {
370 (false, _) => div().children(self.children),
371 (true, Toggle::Toggled(false)) => div(),
372 (true, _) => div().child(Label::new(self.empty_message.clone()).color(Color::Muted)),
373 };
374
375 v_stack()
376 .w_full()
377 .py_1()
378 .children(self.header.map(|header| header))
379 .child(list_content)
380 }
381}
382
383impl List {
384 pub fn new() -> Self {
385 Self {
386 empty_message: "No items".into(),
387 header: None,
388 toggle: Toggle::NotToggleable,
389 children: SmallVec::new(),
390 }
391 }
392
393 pub fn empty_message(mut self, empty_message: impl Into<SharedString>) -> Self {
394 self.empty_message = empty_message.into();
395 self
396 }
397
398 pub fn header(mut self, header: ListHeader) -> Self {
399 self.header = Some(header);
400 self
401 }
402
403 pub fn toggle(mut self, toggle: Toggle) -> Self {
404 self.toggle = toggle;
405 self
406 }
407}
408
409impl ParentElement for List {
410 fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> {
411 &mut self.children
412 }
413}