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 open: bool,
197 on_open_change: Option<Rc<dyn Fn(&bool, &mut Window, &mut App)>>,
198
199 _dismiss_subscription: Option<Subscription>,
200}
201
202impl PopoverState {
203 pub fn new(default_open: bool, cx: &mut App) -> Self {
204 Self {
205 focus_handle: cx.focus_handle(),
206 tracked_focus_handle: None,
207 trigger_bounds: None,
208 open: default_open,
209 on_open_change: None,
210 _dismiss_subscription: None,
211 }
212 }
213
214 pub fn is_open(&self) -> bool {
216 self.open
217 }
218
219 pub fn dismiss(&mut self, window: &mut Window, cx: &mut Context<Self>) {
221 if self.open {
222 self.toggle_open(window, cx);
223 }
224 }
225
226 pub fn show(&mut self, window: &mut Window, cx: &mut Context<Self>) {
228 if !self.open {
229 self.toggle_open(window, cx);
230 }
231 }
232
233 fn toggle_open(&mut self, window: &mut Window, cx: &mut Context<Self>) {
234 self.open = !self.open;
235 if self.open {
236 let state = cx.entity();
237 let focus_handle = if let Some(tracked_focus_handle) = self.tracked_focus_handle.clone()
238 {
239 tracked_focus_handle
240 } else {
241 self.focus_handle.clone()
242 };
243 focus_handle.focus(window);
244
245 self._dismiss_subscription =
246 Some(
247 window.subscribe(&cx.entity(), cx, move |_, _: &DismissEvent, window, cx| {
248 state.update(cx, |state, cx| {
249 state.dismiss(window, cx);
250 });
251 window.refresh();
252 }),
253 );
254 } else {
255 self._dismiss_subscription = None;
256 }
257
258 if let Some(callback) = self.on_open_change.as_ref() {
259 callback(&self.open, window, cx);
260 }
261 cx.notify();
262 }
263
264 fn on_action_cancel(&mut self, _: &Cancel, window: &mut Window, cx: &mut Context<Self>) {
265 self.dismiss(window, cx);
266 }
267}
268
269impl Focusable for PopoverState {
270 fn focus_handle(&self, _: &App) -> FocusHandle {
271 self.focus_handle.clone()
272 }
273}
274
275impl Render for PopoverState {
276 fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
277 div()
278 }
279}
280
281impl EventEmitter<DismissEvent> for PopoverState {}
282
283impl RenderOnce for Popover {
284 fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
285 let force_open = self.open;
286 let default_open = self.default_open;
287 let tracked_focus_handle = self.tracked_focus_handle.clone();
288 let state = window.use_keyed_state(self.id.clone(), cx, |_, cx| {
289 PopoverState::new(default_open, cx)
290 });
291
292 state.update(cx, |state, _| {
293 if let Some(tracked_focus_handle) = tracked_focus_handle {
294 state.tracked_focus_handle = Some(tracked_focus_handle);
295 }
296 state.on_open_change = self.on_open_change.clone();
297 if let Some(force_open) = force_open {
298 state.open = force_open;
299 }
300 });
301
302 let open = state.read(cx).open;
303 let focus_handle = state.read(cx).focus_handle.clone();
304 let trigger_bounds = state.read(cx).trigger_bounds;
305
306 let Some(trigger) = self.trigger else {
307 return div().id("empty");
308 };
309
310 let parent_view_id = window.current_view();
311
312 let el = div()
313 .id(self.id)
314 .child((trigger)(open, window, cx))
315 .on_mouse_down(self.mouse_button, {
316 let state = state.clone();
317 move |_, window, cx| {
318 state.update(cx, |state, cx| {
319 state.open = open;
322 state.toggle_open(window, cx);
323 });
324 cx.notify(parent_view_id);
325 }
326 })
327 .child(
328 canvas(
329 {
330 let state = state.clone();
331 move |bounds, _, cx| {
332 state.update(cx, |state, _| {
333 state.trigger_bounds = Some(bounds);
334 })
335 }
336 },
337 |_, _, _, _| {},
338 )
339 .absolute()
340 .size_full(),
341 );
342
343 if !open {
344 return el;
345 }
346
347 el.child(
348 deferred(
349 anchored()
350 .snap_to_window_with_margin(px(8.))
351 .anchor(self.anchor)
352 .when_some(trigger_bounds, |this, trigger_bounds| {
353 this.position(Self::resolved_corner(self.anchor, trigger_bounds))
354 })
355 .child(
356 v_flex()
357 .id("content")
358 .track_focus(&focus_handle)
359 .key_context(CONTEXT)
360 .on_action(window.listener_for(&state, PopoverState::on_action_cancel))
361 .size_full()
362 .occlude()
363 .tab_group()
364 .when(self.appearance, |this| this.popover_style(cx).p_3())
365 .map(|this| match self.anchor {
366 Corner::TopLeft | Corner::TopRight => this.top_1(),
367 Corner::BottomLeft | Corner::BottomRight => this.bottom_1(),
368 })
369 .when_some(self.content, |this, content| {
370 this.child(
371 state.update(cx, |state, cx| (content)(state, window, cx)),
372 )
373 })
374 .children(self.children)
375 .when(self.overlay_closable, |this| {
376 this.on_mouse_down_out({
377 let state = state.clone();
378 move |_, window, cx| {
379 state.update(cx, |state, cx| {
380 state.dismiss(window, cx);
381 });
382 cx.notify(parent_view_id);
383 }
384 })
385 })
386 .refine_style(&self.style),
387 ),
388 )
389 .with_priority(1),
390 )
391 }
392}