1use gpui::{
2 anchored, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds, Context,
3 Corner, DismissEvent, DispatchPhase, Element, ElementId, Entity, EventEmitter, FocusHandle,
4 Focusable, GlobalElementId, Hitbox, InteractiveElement as _, IntoElement, KeyBinding, LayoutId,
5 ManagedView, MouseButton, MouseDownEvent, ParentElement, Pixels, Point, Render, Style,
6 StyleRefinement, Styled, Window,
7};
8use std::{cell::RefCell, rc::Rc};
9
10use crate::{actions::Cancel, Selectable, StyledExt as _};
11
12const CONTEXT: &str = "Popover";
13
14pub(crate) fn init(cx: &mut App) {
15 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
16}
17
18pub struct PopoverContent {
20 style: StyleRefinement,
21 focus_handle: FocusHandle,
22 content: Rc<dyn Fn(&mut Window, &mut Context<Self>) -> AnyElement>,
23}
24
25impl PopoverContent {
26 pub fn new<B>(_: &mut Window, cx: &mut App, content: B) -> Self
27 where
28 B: Fn(&mut Window, &mut Context<Self>) -> AnyElement + 'static,
29 {
30 let focus_handle = cx.focus_handle();
31
32 Self {
33 style: StyleRefinement::default(),
34 focus_handle,
35 content: Rc::new(content),
36 }
37 }
38}
39impl EventEmitter<DismissEvent> for PopoverContent {}
40
41impl Focusable for PopoverContent {
42 fn focus_handle(&self, _cx: &App) -> FocusHandle {
43 self.focus_handle.clone()
44 }
45}
46
47impl Styled for PopoverContent {
48 fn style(&mut self) -> &mut StyleRefinement {
49 &mut self.style
50 }
51}
52
53impl Render for PopoverContent {
54 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
55 div()
56 .p_2()
57 .refine_style(&self.style)
58 .track_focus(&self.focus_handle)
59 .key_context(CONTEXT)
60 .on_action(cx.listener(|_, _: &Cancel, _, cx| {
61 cx.propagate();
62 cx.emit(DismissEvent);
63 }))
64 .child(self.content.clone()(window, cx))
65 }
66}
67
68pub struct Popover<M: ManagedView> {
70 id: ElementId,
71 anchor: Corner,
72 trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
73 content: Option<Rc<dyn Fn(&mut Window, &mut App) -> Entity<M> + 'static>>,
74 trigger_style: Option<StyleRefinement>,
77 mouse_button: MouseButton,
78 appearance: bool,
79}
80
81impl<M> Popover<M>
82where
83 M: ManagedView,
84{
85 pub fn new(id: impl Into<ElementId>) -> Self {
87 Self {
88 id: id.into(),
89 anchor: Corner::TopLeft,
90 trigger: None,
91 trigger_style: None,
92 content: None,
93 mouse_button: MouseButton::Left,
94 appearance: true,
95 }
96 }
97
98 pub fn anchor(mut self, anchor: Corner) -> Self {
100 self.anchor = anchor;
101 self
102 }
103
104 pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
106 self.mouse_button = mouse_button;
107 self
108 }
109
110 pub fn trigger<T>(mut self, trigger: T) -> Self
112 where
113 T: Selectable + IntoElement + 'static,
114 {
115 self.trigger = Some(Box::new(|is_open, _, _| {
116 let selected = trigger.is_selected();
117 trigger.selected(selected || is_open).into_any_element()
118 }));
119 self
120 }
121
122 pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
124 self.trigger_style = Some(style);
125 self
126 }
127
128 pub fn content<C>(mut self, content: C) -> Self
132 where
133 C: Fn(&mut Window, &mut App) -> Entity<M> + 'static,
134 {
135 self.content = Some(Rc::new(content));
136 self
137 }
138
139 pub fn appearance(mut self, appearance: bool) -> Self {
146 self.appearance = appearance;
147 self
148 }
149
150 fn render_trigger(&mut self, open: bool, window: &mut Window, cx: &mut App) -> AnyElement {
151 let Some(trigger) = self.trigger.take() else {
152 return div().into_any_element();
153 };
154
155 (trigger)(open, window, cx)
156 }
157
158 fn resolved_corner(&self, bounds: Bounds<Pixels>) -> Point<Pixels> {
159 bounds.corner(match self.anchor {
160 Corner::TopLeft => Corner::BottomLeft,
161 Corner::TopRight => Corner::BottomRight,
162 Corner::BottomLeft => Corner::TopLeft,
163 Corner::BottomRight => Corner::TopRight,
164 })
165 }
166
167 fn with_element_state<R>(
168 &mut self,
169 id: &GlobalElementId,
170 window: &mut Window,
171 cx: &mut App,
172 f: impl FnOnce(&mut Self, &mut PopoverElementState<M>, &mut Window, &mut App) -> R,
173 ) -> R {
174 window.with_optional_element_state::<PopoverElementState<M>, _>(
175 Some(id),
176 |element_state, window| {
177 let mut element_state = element_state.unwrap().unwrap_or_default();
178 let result = f(self, &mut element_state, window, cx);
179 (result, Some(element_state))
180 },
181 )
182 }
183}
184
185impl<M> IntoElement for Popover<M>
186where
187 M: ManagedView,
188{
189 type Element = Self;
190
191 fn into_element(self) -> Self::Element {
192 self
193 }
194}
195
196pub struct PopoverElementState<M> {
197 trigger_layout_id: Option<LayoutId>,
198 popover_layout_id: Option<LayoutId>,
199 popover_element: Option<AnyElement>,
200 trigger_element: Option<AnyElement>,
201 content_view: Rc<RefCell<Option<Entity<M>>>>,
202 trigger_bounds: Option<Bounds<Pixels>>,
204}
205
206impl<M> Default for PopoverElementState<M> {
207 fn default() -> Self {
208 Self {
209 trigger_layout_id: None,
210 popover_layout_id: None,
211 popover_element: None,
212 trigger_element: None,
213 content_view: Rc::new(RefCell::new(None)),
214 trigger_bounds: None,
215 }
216 }
217}
218
219pub struct PrepaintState {
220 hitbox: Hitbox,
221 trigger_bounds: Option<Bounds<Pixels>>,
223}
224
225impl<M: ManagedView> Element for Popover<M> {
226 type RequestLayoutState = PopoverElementState<M>;
227 type PrepaintState = PrepaintState;
228
229 fn id(&self) -> Option<ElementId> {
230 Some(self.id.clone())
231 }
232
233 fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
234 None
235 }
236
237 fn request_layout(
238 &mut self,
239 id: Option<&gpui::GlobalElementId>,
240 _: Option<&gpui::InspectorElementId>,
241 window: &mut Window,
242 cx: &mut App,
243 ) -> (gpui::LayoutId, Self::RequestLayoutState) {
244 let mut style = Style::default();
245
246 if let Some(trigger_style) = self.trigger_style.clone() {
251 if let Some(width) = trigger_style.size.width {
252 style.size.width = width;
253 }
254 if let Some(display) = trigger_style.display {
255 style.display = display;
256 }
257 }
258
259 self.with_element_state(
260 id.unwrap(),
261 window,
262 cx,
263 |view, element_state, window, cx| {
264 let mut popover_layout_id = None;
265 let mut popover_element = None;
266 let mut is_open = false;
267
268 if let Some(content_view) = element_state.content_view.borrow_mut().as_mut() {
269 is_open = true;
270
271 let mut anchored = anchored()
272 .snap_to_window_with_margin(px(8.))
273 .anchor(view.anchor);
274 if let Some(trigger_bounds) = element_state.trigger_bounds {
275 anchored = anchored.position(view.resolved_corner(trigger_bounds));
276 }
277
278 let mut element = {
279 let content_view_mut = element_state.content_view.clone();
280 let anchor = view.anchor;
281 let appearance = view.appearance;
282 deferred(
283 anchored.child(
284 div()
285 .size_full()
286 .occlude()
287 .tab_group()
288 .when(appearance, |this| this.popover_style(cx))
289 .map(|this| match anchor {
290 Corner::TopLeft | Corner::TopRight => this.top_1(),
291 Corner::BottomLeft | Corner::BottomRight => this.bottom_1(),
292 })
293 .child(content_view.clone())
294 .when(appearance, |this| {
295 this.on_mouse_down_out(move |_, window, _| {
296 *content_view_mut.borrow_mut() = None;
299 window.refresh();
300 })
301 }),
302 ),
303 )
304 .with_priority(1)
305 .into_any()
306 };
307
308 popover_layout_id = Some(element.request_layout(window, cx));
309 popover_element = Some(element);
310 }
311
312 let mut trigger_element = view.render_trigger(is_open, window, cx);
313 let trigger_layout_id = trigger_element.request_layout(window, cx);
314
315 let layout_id = window.request_layout(
316 style,
317 Some(trigger_layout_id).into_iter().chain(popover_layout_id),
318 cx,
319 );
320
321 (
322 layout_id,
323 PopoverElementState {
324 trigger_layout_id: Some(trigger_layout_id),
325 popover_layout_id,
326 popover_element,
327 trigger_element: Some(trigger_element),
328 ..Default::default()
329 },
330 )
331 },
332 )
333 }
334
335 fn prepaint(
336 &mut self,
337 _id: Option<&gpui::GlobalElementId>,
338 _: Option<&gpui::InspectorElementId>,
339 _bounds: gpui::Bounds<gpui::Pixels>,
340 request_layout: &mut Self::RequestLayoutState,
341 window: &mut Window,
342 cx: &mut App,
343 ) -> Self::PrepaintState {
344 if let Some(element) = &mut request_layout.trigger_element {
345 element.prepaint(window, cx);
346 }
347 if let Some(element) = &mut request_layout.popover_element {
348 element.prepaint(window, cx);
349 }
350
351 let trigger_bounds = request_layout
352 .trigger_layout_id
353 .map(|id| window.layout_bounds(id));
354
355 let _ = request_layout
357 .popover_layout_id
358 .map(|id| window.layout_bounds(id));
359
360 let hitbox = window.insert_hitbox(
361 trigger_bounds.unwrap_or_default(),
362 gpui::HitboxBehavior::Normal,
363 );
364
365 PrepaintState {
366 trigger_bounds,
367 hitbox,
368 }
369 }
370
371 fn paint(
372 &mut self,
373 id: Option<&GlobalElementId>,
374 _: Option<&gpui::InspectorElementId>,
375 _bounds: Bounds<Pixels>,
376 request_layout: &mut Self::RequestLayoutState,
377 prepaint: &mut Self::PrepaintState,
378 window: &mut Window,
379 cx: &mut App,
380 ) {
381 self.with_element_state(
382 id.unwrap(),
383 window,
384 cx,
385 |this, element_state, window, cx| {
386 element_state.trigger_bounds = prepaint.trigger_bounds;
387
388 if let Some(mut element) = request_layout.trigger_element.take() {
389 element.paint(window, cx);
390 }
391
392 if let Some(mut element) = request_layout.popover_element.take() {
393 element.paint(window, cx);
394 return;
395 }
396
397 let Some(content_build) = this.content.take() else {
399 return;
400 };
401 let old_content_view = element_state.content_view.clone();
402 let hitbox_id = prepaint.hitbox.id;
403 let mouse_button = this.mouse_button;
404 window.on_mouse_event(move |event: &MouseDownEvent, phase, window, cx| {
405 if phase == DispatchPhase::Bubble
406 && event.button == mouse_button
407 && hitbox_id.is_hovered(window)
408 {
409 cx.stop_propagation();
410 window.prevent_default();
411
412 let new_content_view = (content_build)(window, cx);
413 let old_content_view1 = old_content_view.clone();
414
415 let previous_focus_handle = window.focused(cx);
416
417 window
418 .subscribe(
419 &new_content_view,
420 cx,
421 move |modal, _: &DismissEvent, window, cx| {
422 if modal.focus_handle(cx).contains_focused(window, cx) {
423 if let Some(previous_focus_handle) =
424 previous_focus_handle.as_ref()
425 {
426 window.focus(previous_focus_handle);
427 }
428 }
429 *old_content_view1.borrow_mut() = None;
430
431 window.refresh();
432 },
433 )
434 .detach();
435
436 window.focus(&new_content_view.focus_handle(cx));
437 *old_content_view.borrow_mut() = Some(new_content_view);
438 window.refresh();
439 }
440 });
441 },
442 );
443 }
444}