Skip to main content

liora_components/
dropdown.rs

1use crate::Popover;
2use crate::gpui_compat::element_id;
3use gpui::{
4    AnyElement, App, Component, IntoElement, RenderOnce, SharedString, Window, div, prelude::*, px,
5};
6use liora_core::{Config, Placement, clear_popover, stable_unique_id};
7use std::sync::Arc;
8
9pub struct DropdownItem {
10    pub label: SharedString,
11    pub on_click: Arc<dyn Fn(&mut Window, &mut App) + 'static>,
12}
13
14pub struct Dropdown {
15    trigger: AnyElement,
16    items: Vec<DropdownItem>,
17    placement: Placement,
18    close_on_click_outside: bool,
19    close_on_escape: bool,
20    id: Option<SharedString>,
21}
22
23impl Dropdown {
24    pub fn new(trigger: impl IntoElement) -> Self {
25        Self {
26            trigger: trigger.into_any_element(),
27            items: vec![],
28            placement: Placement::BottomStart,
29            close_on_click_outside: true,
30            close_on_escape: true,
31            id: None,
32        }
33    }
34
35    pub fn item(
36        mut self,
37        label: impl Into<SharedString>,
38        on_click: impl Fn(&mut Window, &mut App) + 'static,
39    ) -> Self {
40        self.items.push(DropdownItem {
41            label: label.into(),
42            on_click: Arc::new(on_click),
43        });
44        self
45    }
46
47    pub fn placement(mut self, p: Placement) -> Self {
48        self.placement = p;
49        self
50    }
51
52    pub fn id(mut self, id: impl Into<SharedString>) -> Self {
53        self.id = Some(id.into());
54        self
55    }
56
57    pub fn close_on_escape(mut self, close: bool) -> Self {
58        self.close_on_escape = close;
59        self
60    }
61
62    pub fn close_on_click_outside(mut self, close: bool) -> Self {
63        self.close_on_click_outside = close;
64        self
65    }
66}
67
68impl RenderOnce for Dropdown {
69    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
70        let theme = cx.global::<Config>().theme.clone();
71        let items = self.items;
72        let close_on_escape = self.close_on_escape;
73        let dropdown_id = self.id.clone().unwrap_or_else(|| {
74            stable_unique_id(format!("dropdown:{}", items.len()), "dropdown", _window, cx)
75        });
76
77        let close_on_click_outside = self.close_on_click_outside;
78        Popover::new(self.trigger)
79            .id(dropdown_id.clone())
80            .placement(self.placement)
81            .offset(px(4.0))
82            .close_on_click_outside(close_on_click_outside)
83            .close_on_escape(close_on_escape)
84            .content(move |_window, _cx| {
85                let theme = theme.clone();
86                div()
87                    .id(element_id(format!("{}-menu", dropdown_id)))
88                    .cursor_default()
89                    .occlude()
90                    .flex()
91                    .flex_col()
92                    .py_1()
93                    .min_w(px(168.0))
94                    .max_h(px(200.0))
95                    .children(items.iter().enumerate().map(|(i, item)| {
96                        let on_click = item.on_click.clone();
97                        let label = item.label.clone();
98                        let dropdown_id = dropdown_id.clone();
99                        let item_id = format!("{}-item-{}", dropdown_id, i);
100
101                        div()
102                            .id(element_id(item_id))
103                            .cursor_pointer()
104                            .flex()
105                            .items_center()
106                            .min_h(px(34.0))
107                            .px_3()
108                            .py_2()
109                            .text_size(px(theme.font_size.md))
110                            .text_color(theme.neutral.text_1)
111                            .hover(|s| {
112                                s.cursor_pointer()
113                                    .bg(theme.neutral.hover)
114                                    .text_color(theme.primary.base)
115                            })
116                            .on_click(move |_, window, cx| {
117                                on_click(window, cx);
118                                clear_popover(&dropdown_id, cx);
119                            })
120                            .child(div().text_sm().child(label))
121                    }))
122            })
123    }
124}
125
126impl IntoElement for Dropdown {
127    type Element = Component<Self>;
128    fn into_element(self) -> Self::Element {
129        Component::new(self)
130    }
131}
132
133#[cfg(test)]
134mod tests {
135    #[test]
136    fn dropdown_inherits_popover_motion_shell() {
137        let source = include_str!("dropdown.rs")
138            .split("#[cfg(test)]")
139            .next()
140            .unwrap();
141
142        assert!(source.contains("Popover::new(self.trigger)"));
143        assert!(source.contains(".content(move |_window, _cx|"));
144    }
145}