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 set_exclusive_active_tooltip(data: TooltipData, cx: &mut App) -> bool {
203 let active = &mut cx.global_mut::<ActiveTooltip>().0;
204 let changed = active.len() != 1
205 || active.first().is_none_or(|current| {
206 current.id != data.id
207 || current.content != data.content
208 || current.anchor_bounds != data.anchor_bounds
209 || current.placement != data.placement
210 || current.offset != data.offset
211 });
212 if changed {
213 *active = vec![data];
214 }
215 changed
216}
217
218pub fn clear_tooltip(id: &SharedString, cx: &mut App) {
220 cx.global_mut::<ActiveTooltip>()
221 .0
222 .retain(|tooltip| &tooltip.id != id);
223}
224
225pub fn clear_active_tooltip(cx: &mut App) {
227 cx.global_mut::<ActiveTooltip>().0.clear();
228}
229
230#[derive(Clone)]
231pub struct ActiveOverlayEntry {
233 pub id: SharedString,
235 pub view: gpui::AnyView,
237}
238
239pub struct ActivePopover(pub Vec<ActiveOverlayEntry>);
241impl Global for ActivePopover {}
242
243pub fn is_popover_active(id: &SharedString, cx: &App) -> bool {
245 cx.global::<ActivePopover>()
246 .0
247 .iter()
248 .any(|entry| &entry.id == id)
249}
250
251pub fn set_active_popover(id: SharedString, view: gpui::AnyView, cx: &mut App) {
253 let popovers = &mut cx.global_mut::<ActivePopover>().0;
254 if let Some(existing) = popovers.iter_mut().find(|entry| entry.id == id) {
255 existing.view = view;
256 } else {
257 popovers.push(ActiveOverlayEntry { id, view });
258 }
259}
260
261pub fn clear_popover(id: &SharedString, cx: &mut App) {
263 cx.global_mut::<ActivePopover>()
264 .0
265 .retain(|entry| &entry.id != id);
266}
267
268pub fn clear_active_popover(cx: &mut App) {
270 cx.global_mut::<ActivePopover>().0.clear();
271}
272
273pub struct ActiveModal(pub Vec<ActiveOverlayEntry>);
275impl Global for ActiveModal {}
276
277pub fn set_active_modal(id: SharedString, view: gpui::AnyView, cx: &mut App) {
279 let modals = &mut cx.global_mut::<ActiveModal>().0;
280 if let Some(existing) = modals.iter_mut().find(|entry| entry.id == id) {
281 existing.view = view;
282 } else {
283 modals.push(ActiveOverlayEntry { id, view });
284 }
285}
286
287pub fn clear_modal(id: &SharedString, cx: &mut App) {
289 cx.global_mut::<ActiveModal>()
290 .0
291 .retain(|entry| &entry.id != id);
292}
293
294pub fn clear_active_modal(cx: &mut App) {
296 cx.global_mut::<ActiveModal>().0.clear();
297}
298
299pub struct ActiveDrawer(pub Vec<ActiveOverlayEntry>);
301impl Global for ActiveDrawer {}
302
303pub fn set_active_drawer(id: SharedString, view: gpui::AnyView, cx: &mut App) {
305 let drawers = &mut cx.global_mut::<ActiveDrawer>().0;
306 if let Some(existing) = drawers.iter_mut().find(|entry| entry.id == id) {
307 existing.view = view;
308 } else {
309 drawers.push(ActiveOverlayEntry { id, view });
310 }
311}
312
313pub fn clear_drawer(id: &SharedString, cx: &mut App) {
315 cx.global_mut::<ActiveDrawer>()
316 .0
317 .retain(|entry| &entry.id != id);
318}
319
320pub fn clear_active_drawer(cx: &mut App) {
322 cx.global_mut::<ActiveDrawer>().0.clear();
323}
324
325pub struct Popper {
327 pub anchor_bounds: Bounds<Pixels>,
329 pub placement: Placement,
331 pub offset: Pixels,
333}
334
335impl Popper {
336 pub fn calculate_position(&self, content_size: gpui::Size<Pixels>) -> Point<Pixels> {
338 self.calculate_position_with_placement(self.placement, content_size)
339 }
340
341 fn calculate_position_with_placement(
342 &self,
343 placement: Placement,
344 content_size: gpui::Size<Pixels>,
345 ) -> Point<Pixels> {
346 let anchor = self.anchor_bounds;
347 let (x, y) = match placement {
348 Placement::Top => (
349 anchor.left() + (anchor.size.width - content_size.width) / 2.0,
350 anchor.top() - content_size.height - self.offset,
351 ),
352 Placement::TopStart => (
353 anchor.left(),
354 anchor.top() - content_size.height - self.offset,
355 ),
356 Placement::TopEnd => (
357 anchor.right() - content_size.width,
358 anchor.top() - content_size.height - self.offset,
359 ),
360 Placement::Bottom => (
361 anchor.left() + (anchor.size.width - content_size.width) / 2.0,
362 anchor.bottom() + self.offset,
363 ),
364 Placement::BottomStart => (anchor.left(), anchor.bottom() + self.offset),
365 Placement::BottomEnd => (
366 anchor.right() - content_size.width,
367 anchor.bottom() + self.offset,
368 ),
369 Placement::Left => (
370 anchor.left() - content_size.width - self.offset,
371 anchor.top() + (anchor.size.height - content_size.height) / 2.0,
372 ),
373 Placement::LeftStart => (
374 anchor.left() - content_size.width - self.offset,
375 anchor.top(),
376 ),
377 Placement::LeftEnd => (
378 anchor.left() - content_size.width - self.offset,
379 anchor.bottom() - content_size.height,
380 ),
381 Placement::Right => (
382 anchor.right() + self.offset,
383 anchor.top() + (anchor.size.height - content_size.height) / 2.0,
384 ),
385 Placement::RightStart => (anchor.right() + self.offset, anchor.top()),
386 Placement::RightEnd => (
387 anchor.right() + self.offset,
388 anchor.bottom() - content_size.height,
389 ),
390 };
391
392 Point { x, y }
393 }
394
395 pub fn calculate_position_with_flip(
397 &self,
398 content_size: gpui::Size<Pixels>,
399 viewport: Bounds<Pixels>,
400 ) -> (Point<Pixels>, Placement) {
401 let pos = self.calculate_position_with_placement(self.placement, content_size);
402 let mut final_pos = pos;
403 let mut final_placement = self.placement;
404
405 let out_of_bounds = pos.x < viewport.left()
406 || pos.x + content_size.width > viewport.right()
407 || pos.y < viewport.top()
408 || pos.y + content_size.height > viewport.bottom();
409
410 if out_of_bounds {
411 let flipped_placement = self.placement.flip();
412 let flipped_pos =
413 self.calculate_position_with_placement(flipped_placement, content_size);
414
415 let flipped_out_of_bounds = flipped_pos.x < viewport.left()
416 || flipped_pos.x + content_size.width > viewport.right()
417 || flipped_pos.y < viewport.top()
418 || flipped_pos.y + content_size.height > viewport.bottom();
419
420 if !flipped_out_of_bounds {
421 final_pos = flipped_pos;
422 final_placement = flipped_placement;
423 }
424 }
425
426 final_pos.x = final_pos
427 .x
428 .clamp(viewport.left(), viewport.right() - content_size.width);
429 final_pos.y = final_pos
430 .y
431 .clamp(viewport.top(), viewport.bottom() - content_size.height);
432
433 (final_pos, final_placement)
434 }
435}
436
437#[cfg(test)]
438mod tests {
439 use super::*;
440 use gpui::{point, px, size};
441
442 fn viewport() -> Bounds<Pixels> {
443 Bounds {
444 origin: point(px(0.0), px(0.0)),
445 size: size(px(800.0), px(600.0)),
446 }
447 }
448
449 fn anchor(x: f32, width: f32) -> Bounds<Pixels> {
450 Bounds {
451 origin: point(px(x), px(200.0)),
452 size: size(px(width), px(40.0)),
453 }
454 }
455
456 #[test]
457 fn centered_vertical_placements_align_content_center_with_anchor_center() {
458 let content_size = size(px(180.0), px(80.0));
459 let anchor_bounds = anchor(300.0, 80.0);
460 let popper = Popper {
461 anchor_bounds,
462 placement: Placement::Bottom,
463 offset: px(8.0),
464 };
465
466 let (pos, placement) = popper.calculate_position_with_flip(content_size, viewport());
467
468 assert_eq!(placement, Placement::Bottom);
469 assert_eq!(
470 pos.x + content_size.width / 2.0,
471 anchor_bounds.left() + anchor_bounds.size.width / 2.0
472 );
473 assert_eq!(pos.y, anchor_bounds.bottom() + px(8.0));
474 }
475
476 #[test]
477 fn centered_vertical_placements_clamp_horizontally_to_viewport() {
478 let content_size = size(px(220.0), px(80.0));
479 let near_left = Popper {
480 anchor_bounds: anchor(8.0, 40.0),
481 placement: Placement::Bottom,
482 offset: px(8.0),
483 };
484 let near_right = Popper {
485 anchor_bounds: anchor(760.0, 32.0),
486 placement: Placement::Bottom,
487 offset: px(8.0),
488 };
489
490 let (left_pos, _) = near_left.calculate_position_with_flip(content_size, viewport());
491 let (right_pos, _) = near_right.calculate_position_with_flip(content_size, viewport());
492
493 assert_eq!(left_pos.x, px(0.0));
494 assert_eq!(right_pos.x + content_size.width, viewport().right());
495 }
496}