liora_components/
dropdown.rs1use 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}