1use std::{cell::Cell, rc::Rc};
4
5use gpui_kit::{
6 AnyElement, App, Bounds, ElementId, FocusHandle, InteractiveElement as _, IntoElement,
7 ParentElement as _, RenderOnce, StyleRefinement, Styled, Window,
8 base::{
9 self, Align, ElementExt as _, Placement, Selectable, StyledExt as _, TestSupportExt as _,
10 },
11 div,
12};
13
14use crate::{
15 ActiveTheme as _,
16 menu::{MenuLook, MenuMotion, corner, placement},
17};
18
19type TriggerBuilder = Box<dyn FnOnce(bool) -> AnyElement>;
20type ContentBuilder = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
21type OpenChange = Box<dyn Fn(bool, &mut Window, &mut App)>;
22
23#[derive(IntoElement)]
49pub struct Popover {
50 id: ElementId,
51 trigger: Option<TriggerBuilder>,
52 content: Option<ContentBuilder>,
53 align: Align,
54 open: Option<bool>,
55 on_open_change: Option<OpenChange>,
56 focus: Option<FocusHandle>,
57 style: StyleRefinement,
58}
59
60impl Popover {
61 pub fn new(id: impl Into<ElementId>) -> Self {
63 Self {
64 id: id.into(),
65 trigger: None,
66 content: None,
67 align: Align::Start,
68 open: None,
69 on_open_change: None,
70 focus: None,
71 style: StyleRefinement::default(),
72 }
73 }
74
75 pub fn trigger(mut self, trigger: impl Selectable + IntoElement + 'static) -> Self {
79 self.trigger = Some(Box::new(move |open| trigger.open(open).into_any_element()));
80 self
81 }
82
83 pub fn content<E: IntoElement>(
85 mut self,
86 content: impl FnOnce(&mut Window, &mut App) -> E + 'static,
87 ) -> Self {
88 self.content = Some(Box::new(move |window, cx| {
89 content(window, cx).into_any_element()
90 }));
91 self
92 }
93
94 pub fn align(mut self, align: Align) -> Self {
97 self.align = align;
98 self
99 }
100
101 pub fn open(mut self, open: bool) -> Self {
105 self.open = Some(open);
106 self
107 }
108
109 pub fn on_open_change(
112 mut self,
113 handler: impl Fn(bool, &mut Window, &mut App) + 'static,
114 ) -> Self {
115 self.on_open_change = Some(Box::new(handler));
116 self
117 }
118
119 pub fn track_focus(mut self, focus: &FocusHandle) -> Self {
122 self.focus = Some(focus.clone());
123 self
124 }
125}
126
127impl Styled for Popover {
129 fn style(&mut self) -> &mut StyleRefinement {
130 &mut self.style
131 }
132}
133
134impl RenderOnce for Popover {
135 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
136 let look = MenuLook::of(cx.theme(), window.rem_size());
137 let trigger_bounds = window
139 .use_keyed_state(
140 ElementId::NamedChild(self.id.clone().into(), "trigger-bounds".into()),
141 cx,
142 |_, _| Rc::new(Cell::new(Bounds::default())),
143 )
144 .read(cx)
145 .clone();
146 let side = placement(
147 trigger_bounds.get(),
148 window.viewport_size().height,
149 look.max_height,
150 look.gap,
151 );
152 let opens_above = side == Placement::Top;
153 let (anchor, _) = corner(side, self.align, trigger_bounds.get(), look.gap);
154 let panel_id = ElementId::NamedChild(self.id.clone().into(), "panel".into());
155 let content = self.content;
156 let style = self.style;
157 let mut popover = base::Popover::new(self.id)
158 .anchor(anchor)
159 .offset(look.gap)
160 .trigger_with(move |open, _, _| {
161 div()
162 .relative()
163 .on_prepaint(move |bounds, _, _| trigger_bounds.set(bounds))
164 .children(self.trigger.map(|trigger| trigger(open)))
165 .into_any_element()
166 })
167 .content(move |_, window, cx| {
168 let Some(motion) = MenuMotion::sample(
169 ElementId::NamedChild(panel_id.clone().into(), "presence".into()),
170 true,
171 true,
172 &look,
173 window,
174 cx,
175 ) else {
176 return div().into_any_element();
177 };
178 div()
179 .id(panel_id)
180 .test_support()
181 .relative()
182 .top(if opens_above {
184 -motion.offset
185 } else {
186 motion.offset
187 })
188 .opacity(motion.opacity)
189 .p(look.padding)
190 .rounded(look.radius)
191 .bg(look.surface)
192 .border_1()
193 .border_color(look.border)
194 .shadow(look.shadow(motion.shadow_strength))
195 .text_size(look.text_size)
196 .line_height(look.line_height)
197 .text_color(look.foreground)
198 .refine_style(&style)
199 .children(content.map(|content| content(window, cx)))
200 .into_any_element()
201 });
202 if let Some(open) = self.open {
203 popover = popover.open(open);
204 }
205 if let Some(handler) = self.on_open_change {
206 popover = popover.on_open_change(move |open, window, cx| handler(*open, window, cx));
207 }
208 if let Some(focus) = &self.focus {
209 popover = popover.track_focus(focus);
210 }
211 popover
212 }
213}