1use gpui::{AnyElement, App, Bounds, Global, Pixels, Point, SharedString, Window};
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq)]
4pub enum Placement {
6 Top,
8 TopStart,
10 TopEnd,
12 Bottom,
14 BottomStart,
16 BottomEnd,
18 Left,
20 LeftStart,
22 LeftEnd,
24 Right,
26 RightStart,
28 RightEnd,
30}
31
32impl Placement {
33 pub fn flip(&self) -> Self {
35 match self {
36 Placement::Top => Placement::Bottom,
37 Placement::TopStart => Placement::BottomStart,
38 Placement::TopEnd => Placement::BottomEnd,
39 Placement::Bottom => Placement::Top,
40 Placement::BottomStart => Placement::TopStart,
41 Placement::BottomEnd => Placement::TopEnd,
42 Placement::Left => Placement::Right,
43 Placement::LeftStart => Placement::RightStart,
44 Placement::LeftEnd => Placement::RightEnd,
45 Placement::Right => Placement::Left,
46 Placement::RightStart => Placement::LeftStart,
47 Placement::RightEnd => Placement::LeftEnd,
48 }
49 }
50}
51
52pub type PortalRender = Box<dyn FnOnce(&mut Window, &mut App) -> AnyElement>;
54
55pub struct PortalEntry {
57 pub id: u64,
59 pub render: PortalRender,
61}
62
63pub struct Portal {
65 pub entries: Vec<PortalEntry>,
67 next_id: u64,
68}
69
70impl Global for Portal {}
71
72pub struct PassivePortal {
74 pub entries: Vec<PortalEntry>,
76 next_id: u64,
77}
78
79impl Global for PassivePortal {}
80
81pub fn push_portal(
83 render: impl FnOnce(&mut Window, &mut App) -> AnyElement + 'static,
84 cx: &mut App,
85) -> u64 {
86 if !cx.has_global::<Portal>() {
87 cx.set_global(Portal {
88 entries: vec![],
89 next_id: 1,
90 });
91 }
92 let portal = cx.global_mut::<Portal>();
93 let id = portal.next_id;
94 portal.next_id += 1;
95 portal.entries.push(PortalEntry {
96 id,
97 render: Box::new(render),
98 });
99 id
100}
101
102pub fn push_passive_portal(
104 render: impl FnOnce(&mut Window, &mut App) -> AnyElement + 'static,
105 cx: &mut App,
106) -> u64 {
107 if !cx.has_global::<PassivePortal>() {
108 cx.set_global(PassivePortal {
109 entries: vec![],
110 next_id: 1,
111 });
112 }
113 let portal = cx.global_mut::<PassivePortal>();
114 let id = portal.next_id;
115 portal.next_id += 1;
116 portal.entries.push(PortalEntry {
117 id,
118 render: Box::new(render),
119 });
120 id
121}
122
123pub fn remove_portal(id: u64, cx: &mut App) {
125 if cx.has_global::<Portal>() {
126 cx.global_mut::<Portal>().entries.retain(|e| e.id != id);
127 }
128}
129
130pub fn clear_portals(cx: &mut App) {
132 if cx.has_global::<Portal>() {
133 cx.global_mut::<Portal>().entries.clear();
134 }
135}
136
137pub struct ZIndexStack {
139 pub base: u32,
141 pub popup: u32,
143 pub modal: u32,
145 pub notification: u32,
147 pub tooltip: u32,
149}
150
151impl Default for ZIndexStack {
152 fn default() -> Self {
153 Self {
154 base: 1000,
155 popup: 1100,
156 modal: 1200,
157 notification: 1300,
158 tooltip: 1400,
159 }
160 }
161}
162
163impl Global for ZIndexStack {}
164
165#[derive(Clone)]
166pub struct TooltipData {
168 pub id: SharedString,
170 pub content: SharedString,
172 pub anchor_bounds: Bounds<Pixels>,
174 pub placement: Placement,
176 pub offset: Pixels,
178}
179
180pub struct ActiveTooltip(pub Vec<TooltipData>);
182impl Global for ActiveTooltip {}
183
184pub fn set_active_tooltip(data: TooltipData, cx: &mut App) {
186 let tooltips = &mut cx.global_mut::<ActiveTooltip>().0;
187 if let Some(existing) = tooltips.iter_mut().find(|tooltip| tooltip.id == data.id) {
188 *existing = data;
189 } else {
190 tooltips.push(data);
191 }
192}
193
194pub fn clear_tooltip(id: &SharedString, cx: &mut App) {
196 cx.global_mut::<ActiveTooltip>()
197 .0
198 .retain(|tooltip| &tooltip.id != id);
199}
200
201pub fn clear_active_tooltip(cx: &mut App) {
203 cx.global_mut::<ActiveTooltip>().0.clear();
204}
205
206#[derive(Clone)]
207pub struct ActiveOverlayEntry {
209 pub id: SharedString,
211 pub view: gpui::AnyView,
213}
214
215pub struct ActivePopover(pub Vec<ActiveOverlayEntry>);
217impl Global for ActivePopover {}
218
219pub fn is_popover_active(id: &SharedString, cx: &App) -> bool {
221 cx.global::<ActivePopover>()
222 .0
223 .iter()
224 .any(|entry| &entry.id == id)
225}
226
227pub fn set_active_popover(id: SharedString, view: gpui::AnyView, cx: &mut App) {
229 let popovers = &mut cx.global_mut::<ActivePopover>().0;
230 if let Some(existing) = popovers.iter_mut().find(|entry| entry.id == id) {
231 existing.view = view;
232 } else {
233 popovers.push(ActiveOverlayEntry { id, view });
234 }
235}
236
237pub fn clear_popover(id: &SharedString, cx: &mut App) {
239 cx.global_mut::<ActivePopover>()
240 .0
241 .retain(|entry| &entry.id != id);
242}
243
244pub fn clear_active_popover(cx: &mut App) {
246 cx.global_mut::<ActivePopover>().0.clear();
247}
248
249pub struct ActiveModal(pub Vec<ActiveOverlayEntry>);
251impl Global for ActiveModal {}
252
253pub fn set_active_modal(id: SharedString, view: gpui::AnyView, cx: &mut App) {
255 let modals = &mut cx.global_mut::<ActiveModal>().0;
256 if let Some(existing) = modals.iter_mut().find(|entry| entry.id == id) {
257 existing.view = view;
258 } else {
259 modals.push(ActiveOverlayEntry { id, view });
260 }
261}
262
263pub fn clear_modal(id: &SharedString, cx: &mut App) {
265 cx.global_mut::<ActiveModal>()
266 .0
267 .retain(|entry| &entry.id != id);
268}
269
270pub fn clear_active_modal(cx: &mut App) {
272 cx.global_mut::<ActiveModal>().0.clear();
273}
274
275pub struct ActiveDrawer(pub Vec<ActiveOverlayEntry>);
277impl Global for ActiveDrawer {}
278
279pub fn set_active_drawer(id: SharedString, view: gpui::AnyView, cx: &mut App) {
281 let drawers = &mut cx.global_mut::<ActiveDrawer>().0;
282 if let Some(existing) = drawers.iter_mut().find(|entry| entry.id == id) {
283 existing.view = view;
284 } else {
285 drawers.push(ActiveOverlayEntry { id, view });
286 }
287}
288
289pub fn clear_drawer(id: &SharedString, cx: &mut App) {
291 cx.global_mut::<ActiveDrawer>()
292 .0
293 .retain(|entry| &entry.id != id);
294}
295
296pub fn clear_active_drawer(cx: &mut App) {
298 cx.global_mut::<ActiveDrawer>().0.clear();
299}
300
301pub struct Popper {
303 pub anchor_bounds: Bounds<Pixels>,
305 pub placement: Placement,
307 pub offset: Pixels,
309}
310
311impl Popper {
312 pub fn calculate_position(&self, content_size: gpui::Size<Pixels>) -> Point<Pixels> {
314 self.calculate_position_with_placement(self.placement, content_size)
315 }
316
317 fn calculate_position_with_placement(
318 &self,
319 placement: Placement,
320 content_size: gpui::Size<Pixels>,
321 ) -> Point<Pixels> {
322 let anchor = self.anchor_bounds;
323 let (x, y) = match placement {
324 Placement::Top => (
325 anchor.left() + (anchor.size.width - content_size.width) / 2.0,
326 anchor.top() - content_size.height - self.offset,
327 ),
328 Placement::TopStart => (
329 anchor.left(),
330 anchor.top() - content_size.height - self.offset,
331 ),
332 Placement::TopEnd => (
333 anchor.right() - content_size.width,
334 anchor.top() - content_size.height - self.offset,
335 ),
336 Placement::Bottom => (
337 anchor.left() + (anchor.size.width - content_size.width) / 2.0,
338 anchor.bottom() + self.offset,
339 ),
340 Placement::BottomStart => (anchor.left(), anchor.bottom() + self.offset),
341 Placement::BottomEnd => (
342 anchor.right() - content_size.width,
343 anchor.bottom() + self.offset,
344 ),
345 Placement::Left => (
346 anchor.left() - content_size.width - self.offset,
347 anchor.top() + (anchor.size.height - content_size.height) / 2.0,
348 ),
349 Placement::LeftStart => (
350 anchor.left() - content_size.width - self.offset,
351 anchor.top(),
352 ),
353 Placement::LeftEnd => (
354 anchor.left() - content_size.width - self.offset,
355 anchor.bottom() - content_size.height,
356 ),
357 Placement::Right => (
358 anchor.right() + self.offset,
359 anchor.top() + (anchor.size.height - content_size.height) / 2.0,
360 ),
361 Placement::RightStart => (anchor.right() + self.offset, anchor.top()),
362 Placement::RightEnd => (
363 anchor.right() + self.offset,
364 anchor.bottom() - content_size.height,
365 ),
366 };
367
368 Point { x, y }
369 }
370
371 pub fn calculate_position_with_flip(
373 &self,
374 content_size: gpui::Size<Pixels>,
375 viewport: Bounds<Pixels>,
376 ) -> (Point<Pixels>, Placement) {
377 let pos = self.calculate_position_with_placement(self.placement, content_size);
378 let mut final_pos = pos;
379 let mut final_placement = self.placement;
380
381 let out_of_bounds = pos.x < viewport.left()
382 || pos.x + content_size.width > viewport.right()
383 || pos.y < viewport.top()
384 || pos.y + content_size.height > viewport.bottom();
385
386 if out_of_bounds {
387 let flipped_placement = self.placement.flip();
388 let flipped_pos =
389 self.calculate_position_with_placement(flipped_placement, content_size);
390
391 let flipped_out_of_bounds = flipped_pos.x < viewport.left()
392 || flipped_pos.x + content_size.width > viewport.right()
393 || flipped_pos.y < viewport.top()
394 || flipped_pos.y + content_size.height > viewport.bottom();
395
396 if !flipped_out_of_bounds {
397 final_pos = flipped_pos;
398 final_placement = flipped_placement;
399 }
400 }
401
402 final_pos.x = final_pos
403 .x
404 .clamp(viewport.left(), viewport.right() - content_size.width);
405 final_pos.y = final_pos
406 .y
407 .clamp(viewport.top(), viewport.bottom() - content_size.height);
408
409 (final_pos, final_placement)
410 }
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416 use gpui::{point, px, size};
417
418 fn viewport() -> Bounds<Pixels> {
419 Bounds {
420 origin: point(px(0.0), px(0.0)),
421 size: size(px(800.0), px(600.0)),
422 }
423 }
424
425 fn anchor(x: f32, width: f32) -> Bounds<Pixels> {
426 Bounds {
427 origin: point(px(x), px(200.0)),
428 size: size(px(width), px(40.0)),
429 }
430 }
431
432 #[test]
433 fn centered_vertical_placements_align_content_center_with_anchor_center() {
434 let content_size = size(px(180.0), px(80.0));
435 let anchor_bounds = anchor(300.0, 80.0);
436 let popper = Popper {
437 anchor_bounds,
438 placement: Placement::Bottom,
439 offset: px(8.0),
440 };
441
442 let (pos, placement) = popper.calculate_position_with_flip(content_size, viewport());
443
444 assert_eq!(placement, Placement::Bottom);
445 assert_eq!(
446 pos.x + content_size.width / 2.0,
447 anchor_bounds.left() + anchor_bounds.size.width / 2.0
448 );
449 assert_eq!(pos.y, anchor_bounds.bottom() + px(8.0));
450 }
451
452 #[test]
453 fn centered_vertical_placements_clamp_horizontally_to_viewport() {
454 let content_size = size(px(220.0), px(80.0));
455 let near_left = Popper {
456 anchor_bounds: anchor(8.0, 40.0),
457 placement: Placement::Bottom,
458 offset: px(8.0),
459 };
460 let near_right = Popper {
461 anchor_bounds: anchor(760.0, 32.0),
462 placement: Placement::Bottom,
463 offset: px(8.0),
464 };
465
466 let (left_pos, _) = near_left.calculate_position_with_flip(content_size, viewport());
467 let (right_pos, _) = near_right.calculate_position_with_flip(content_size, viewport());
468
469 assert_eq!(left_pos.x, px(0.0));
470 assert_eq!(right_pos.x + content_size.width, viewport().right());
471 }
472}