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