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