1use crate::prelude::*;
2use crate::v_flex;
3use gpui::{
4 AnyElement, App, Element, InteractiveElement, IntoElement, ParentElement, Pixels, RenderOnce,
5 Role, SharedString, Styled, Window, div,
6};
7use smallvec::SmallVec;
8
9/// Y height added beyond the size of the contents.
10pub const POPOVER_Y_PADDING: Pixels = px(8.);
11
12/// A popover is used to display a menu or show some options.
13///
14/// Clicking the element that launches the popover should not change the current view,
15/// and the popover should be statically positioned relative to that element (not the
16/// user's mouse.)
17///
18/// Example: A "new" menu with options like "new file", "new folder", etc,
19/// Linear's "Display" menu, a profile menu that appears when you click your avatar.
20///
21/// Related elements:
22///
23/// [`ContextMenu`](crate::ContextMenu):
24///
25/// Used to display a popover menu that only contains a list of items. Context menus are always
26/// launched by secondary clicking on an element. The menu is positioned relative to the user's cursor.
27///
28/// Example: Right clicking a file in the file tree to get a list of actions, right clicking
29/// a tab to in the tab bar to get a list of actions.
30///
31/// `Dropdown`:
32///
33/// Used to display a list of options when the user clicks an element. The menu is
34/// positioned relative the element that was clicked, and clicking an item in the
35/// dropdown should change the value of the element that was clicked.
36///
37/// Example: A theme select control. Displays "One Dark", clicking it opens a list of themes.
38/// When one is selected, the theme select control displays the selected theme.
39#[derive(IntoElement)]
40pub struct Popover {
41 id: Option<ElementId>,
42 a11y_label: Option<SharedString>,
43 children: SmallVec<[AnyElement; 2]>,
44 aside: Option<AnyElement>,
45}
46
47impl RenderOnce for Popover {
48 fn render(self, _: &mut Window, cx: &mut App) -> impl IntoElement {
49 let main_content = v_flex()
50 .elevation_2(cx)
51 .py(POPOVER_Y_PADDING / 2.)
52 .child(div().children(self.children));
53
54 let aside_content = self.aside.map(|aside| {
55 v_flex()
56 .elevation_2(cx)
57 .bg(cx.theme().colors().surface_background)
58 .px_1()
59 .child(aside)
60 });
61
62 if let Some(id) = self.id {
63 div()
64 .flex()
65 .gap_1()
66 .id(id)
67 .role(Role::Dialog)
68 .when_some(self.a11y_label, |this, label| this.aria_label(label))
69 .child(main_content)
70 .when_some(aside_content, |this, aside| this.child(aside))
71 .into_any_element()
72 } else {
73 div()
74 .flex()
75 .gap_1()
76 .child(main_content)
77 .when_some(aside_content, |this, aside| this.child(aside))
78 .into_any_element()
79 }
80 }
81}
82
83impl Default for Popover {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl Popover {
90 pub fn new() -> Self {
91 Self {
92 id: None,
93 a11y_label: None,
94 children: SmallVec::new(),
95 aside: None,
96 }
97 }
98
99 pub fn id(mut self, id: impl Into<ElementId>) -> Self {
100 self.id = Some(id.into());
101 self
102 }
103
104 pub fn aria_label(mut self, label: impl Into<SharedString>) -> Self {
105 self.a11y_label = Some(label.into());
106 self
107 }
108
109 pub fn aside(mut self, aside: impl IntoElement) -> Self
110 where
111 Self: Sized,
112 {
113 self.aside = Some(aside.into_element().into_any());
114 self
115 }
116}
117
118impl ParentElement for Popover {
119 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
120 self.children.extend(elements)
121 }
122}