1use gpui::{
2 anchored, canvas, deferred, div, prelude::FluentBuilder as _, px, AnyElement, App, Bounds,
3 Context, Corner, DismissEvent, ElementId, EventEmitter, FocusHandle, Focusable,
4 InteractiveElement as _, IntoElement, KeyBinding, MouseButton, ParentElement, Pixels, Point,
5 Render, RenderOnce, StyleRefinement, Styled, Subscription, Window,
6};
7use std::rc::Rc;
8
9use crate::{actions::Cancel, v_flex, Selectable, StyledExt as _};
10
11const CONTEXT: &str = "Popover";
12pub(crate) fn init(cx: &mut App) {
13 cx.bind_keys([KeyBinding::new("escape", Cancel, Some(CONTEXT))])
14}
15
16#[derive(IntoElement)]
18pub struct Popover {
19 id: ElementId,
20 style: StyleRefinement,
21 anchor: Corner,
22 default_open: bool,
23 open: Option<bool>,
24 tracked_focus_handle: Option<FocusHandle>,
25 trigger: Option<Box<dyn FnOnce(bool, &Window, &App) -> AnyElement + 'static>>,
26 content: Option<
27 Rc<
28 dyn Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> AnyElement
29 + 'static,
30 >,
31 >,
32 children: Vec<AnyElement>,
33 trigger_style: Option<StyleRefinement>,
36 mouse_button: MouseButton,
37 appearance: bool,
38 overlay_closable: bool,
39 on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
40}
41
42impl Popover {
43 pub fn new(id: impl Into<ElementId>) -> Self {
45 Self {
46 id: id.into(),
47 style: StyleRefinement::default(),
48 anchor: Corner::TopLeft,
49 trigger: None,
50 trigger_style: None,
51 content: None,
52 tracked_focus_handle: None,
53 children: vec![],
54 mouse_button: MouseButton::Left,
55 appearance: true,
56 overlay_closable: true,
57 default_open: false,
58 open: None,
59 on_open_change: None,
60 }
61 }
62
63 pub fn anchor(mut self, anchor: Corner) -> Self {
65 self.anchor = anchor;
66 self
67 }
68
69 pub fn mouse_button(mut self, mouse_button: MouseButton) -> Self {
71 self.mouse_button = mouse_button;
72 self
73 }
74
75 pub fn trigger<T>(mut self, trigger: T) -> Self
77 where
78 T: Selectable + IntoElement + 'static,
79 {
80 self.trigger = Some(Box::new(|is_open, _, _| {
81 let selected = trigger.is_selected();
82 trigger.selected(selected || is_open).into_any_element()
83 }));
84 self
85 }
86
87 pub fn default_open(mut self, open: bool) -> Self {
93 self.default_open = open;
94 self
95 }
96
97 pub fn open(mut self, open: bool) -> Self {
103 self.open = Some(open);
104 self
105 }
106
107 pub fn on_open_change<F>(mut self, callback: F) -> Self
113 where
114 F: Fn(&bool, &mut Window, &mut App) + 'static,
115 {
116 self.on_open_change = Some(Rc::new(callback));
117 self
118 }
119
120 pub fn trigger_style(mut self, style: StyleRefinement) -> Self {
122 self.trigger_style = Some(style);
123 self
124 }
125
126 pub fn overlay_closable(mut self, closable: bool) -> Self {
128 self.overlay_closable = closable;
129 self
130 }
131
132 pub fn content<F, E>(mut self, content: F) -> Self
137 where
138 E: IntoElement,
139 F: Fn(&mut PopoverState, &mut Window, &mut Context<PopoverState>) -> E + 'static,
140 {
141 self.content = Some(Rc::new(move |state, window, cx| {
142 content(state, window, cx).into_any_element()
143 }));
144 self
145 }
146
147 pub fn appearance(mut self, appearance: bool) -> Self {
154 self.appearance = appearance;
155 self
156 }
157
158 pub fn track_focus(mut self, handle: &FocusHandle) -> Self {
163 self.tracked_focus_handle = Some(handle.clone());
164 self
165 }
166
167 fn resolved_corner(anchor: Corner, bounds: Bounds<Pixels>) -> Point<Pixels> {
168 bounds.corner(match anchor {
169 Corner::TopLeft => Corner::BottomLeft,
170 Corner::TopRight => Corner::BottomRight,
171 Corner::BottomLeft => Corner::TopLeft,
172 Corner::BottomRight => Corner::TopRight,
173 }) + Point {
174 x: px(0.),
175 y: -bounds.size.height,
176 }
177 }
178}
179
180impl ParentElement for Popover {
181 fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
182 self.children.extend(elements);
183 }
184}
185
186impl Styled for Popover {
187 fn style(&mut self) -> &mut StyleRefinement {
188 &mut self.style
189 }
190}
191
192pub struct PopoverState {
193 focus_handle: FocusHandle,
194 pub(crate) tracked_focus_handle: Option<FocusHandle>,
195 trigger_bounds: Option<Bounds<Pixels>>,
196 previous_focus: Option<FocusHandle>,
197 open: bool,
198 on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
199
200 _dismiss_subscription: Option<Subscription>,
201}
202
203impl PopoverState {
204 pub fn new(default_open: bool, cx: &mut App) -> Self {
205 Self {
206 focus_handle: cx.focus_handle(),
207 tracked_focus_handle: None,
208 trigger_bounds: None,
209 previous_focus: None,
210 open: default_open,
211 on_open_change: None,
212 _dismiss_subscription: None,
213 }
214 }
215
216 pub fn is_open(&self) -> bool {
218 self.open
219 }
220
221 pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
223 if self.open {
224 self.toggle_open(window, cx);
225 }
226 }
227
228 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
230 if !self.open {
231 self.toggle_open(window, cx);
232 }
233 }
234
235 fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
236 self.open = !self.open;
237 if self.open {
238 let state = cx.entity();
239 self.previous_focus = window.focused(cx);
240 let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
241 {
242 tracked_focus_handle
243 } else {
244 self.focus_handle.clone()
245 };
246
247 cx.defer_in(window, move |_, window, _| {
248 focus_handle.focus(window);
249 });
250
251 self._dismiss_subscription =
252 Some(
253 window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
254 state.update(cx, |state, cx| {
255 state.dismiss(window, cx);
256 });
257 window.refresh();
258 }),
259 );
260 } else {
261 if let Some(previous_focus) = self.previous_focus.take() {
262 window.focus(&previous_focus);
263 }
264 self._dismiss_subscription = None;
265 }
266
267 if let Some(callback) = self.on_open_change.as_ref() {
268 callback(&self.open, window, cx);
269 }
270 cx.notify();
271 }
272
273 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
274 self.dismiss(window, cx);
275 }
276}
277
278impl Focusable for PopoverState {
279 fn focus_handle(&self, _: &App) -> FocusHandle {
280 self.focus_handle.clone()
281 }
282}
283
284impl Render for PopoverState {
285 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
286 div()
287 }
288}
289
290impl EventEmitter<DismissEvent> for PopoverState {}
291
292impl RenderOnce for Popover {
293 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
294 let force_open = self.open;
295 let default_open = self.default_open;
296 let tracked_focus_handle = self.tracked_focus_handle.clone();
297 let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| {
298 PopoverState::new(default_open, cx)
299 });
300
301 state.update(cx, |state, _| {
302 if let Some(tracked_focus_handle) = tracked_focus_handle {
303 state.tracked_focus_handle = Some(tracked_focus_handle);
304 }
305 state.on_open_change = self.on_open_change.clone();
306 if let Some(force_open) = force_open {
307 state.open = force_open;
308 }
309 });
310
311 let open = state.read(cx).open;
312 let focus_handle = state.focus_handle(cx);
313 let trigger_bounds = state.read(cx).trigger_bounds;
314
315 let Some(trigger) = self.trigger else {
316 return div().id("empty");
317 };
318
319 let parent_view_id = window.current_view();
320
321 let el = div()
322 .id(self.id)
323 .child((trigger)(open, window, cx))
324 .on_mouse_down(self.mouse_button, {
325 let state = state.clone();
326 move |_, window, cx| {
327 state.update(cx, |state, cx| {
328 state.open = open;
331 state.toggle_open(window, cx);
332 });
333 cx.notify(parent_view_id);
334 }
335 })
336 .child(
337 canvas(
338 {
339 let state = state.clone();
340 move |bounds, _, cx| {
341 state.update(cx, |state, _| {
342 state.trigger_bounds = Some(bounds);
343 })
344 }
345 },
346 |_, _, _, _| {},
347 )
348 .absolute()
349 .size_full(),
350 );
351
352 if !open {
353 return el;
354 }
355
356 el.child(
357 deferred(
358 anchored()
359 .snap_to_window_with_margin(px(8.))
360 .anchor(self.anchor)
361 .when_some(trigger_bounds, |this, trigger_bounds| {
362 this.position(Self::resolved_corner(self.anchor, trigger_bounds))
363 })
364 .child(
365 v_flex()
366 .id("content")
367 .key_context(CONTEXT)
368 .track_focus(&focus_handle)
369 .on_action(window.listener_for(&state, PopoverState::on_action_cancel))
370 .size_full()
371 .occlude()
372 .tab_group()
373 .when(self.appearance, |this| this.popover_style(cx).p_3())
374 .map(|this| match self.anchor {
375 Corner::TopLeft | Corner::TopRight => this.top_1(),
376 Corner::BottomLeft | Corner::BottomRight => this.bottom_1(),
377 })
378 .when_some(self.content, |this, content| {
379 this.child(
380 state.update(cx, |state, cx| (content)(state, window, cx)),
381 )
382 })
383 .children(self.children)
384 .when(self.overlay_closable, |this| {
385 this.on_mouse_down_out({
386 let state = state.clone();
387 move |_, window, cx| {
388 state.update(cx, |state, cx| {
389 state.dismiss(window, cx);
390 });
391 cx.notify(parent_view_id);
392 }
393 })
394 })
395 .refine_style(&self.style),
396 ),
397 )
398 .with_priority(1),
399 )
400 }
401}