1use gpui::prelude::*;
13use gpui::{
14 deferred, div, px, relative, AnyElement, App, Context, FocusHandle, IntoElement, KeyDownEvent,
15 Window,
16};
17
18use crate::theme::theme;
19
20type Builder = Box<dyn Fn(&mut Window, &mut App) -> AnyElement + 'static>;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum Placement {
25 Bottom,
27 BottomEnd,
29 Top,
31 TopEnd,
33}
34
35pub struct Popover {
37 open: bool,
38 focus: FocusHandle,
39 trigger: Builder,
40 content: Builder,
41 placement: Placement,
42 width: Option<f32>,
43}
44
45impl Popover {
46 pub fn new(
47 cx: &mut Context<Self>,
48 trigger: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
49 content: impl Fn(&mut Window, &mut App) -> AnyElement + 'static,
50 ) -> Self {
51 Popover {
52 open: false,
53 focus: cx.focus_handle(),
54 trigger: Box::new(trigger),
55 content: Box::new(content),
56 placement: Placement::Bottom,
57 width: None,
58 }
59 }
60
61 pub fn placement(mut self, placement: Placement) -> Self {
62 self.placement = placement;
63 self
64 }
65
66 pub fn width(mut self, width: f32) -> Self {
68 self.width = Some(width);
69 self
70 }
71
72 pub fn is_open(&self) -> bool {
73 self.open
74 }
75
76 pub fn open(&mut self, cx: &mut Context<Self>) {
77 self.open = true;
78 cx.notify();
79 }
80
81 pub fn close(&mut self, cx: &mut Context<Self>) {
82 self.open = false;
83 cx.notify();
84 }
85
86 pub fn toggle(&mut self, cx: &mut Context<Self>) {
87 self.open = !self.open;
88 cx.notify();
89 }
90}
91
92impl Render for Popover {
93 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
94 let t = theme(cx);
95 let radius = t.radius(t.default_radius);
96 let surface = t.surface().hsla();
97 let border = t.border().hsla();
98
99 let trigger_el = (self.trigger)(window, cx);
100 let mut wrap = div().relative().child(
101 div()
102 .id("guise-popover-trigger")
103 .track_focus(&self.focus)
104 .on_key_down(cx.listener(|this, ev: &KeyDownEvent, _window, cx| {
105 if ev.keystroke.key.as_str() == "escape" {
106 this.open = false;
107 cx.notify();
108 }
109 }))
110 .on_click(cx.listener(|this, _ev, window, cx| {
111 this.open = !this.open;
112 window.focus(&this.focus);
113 cx.notify();
114 }))
115 .child(trigger_el),
116 );
117
118 if self.open {
119 let content_el = (self.content)(window, cx);
120 let base = div()
121 .absolute()
122 .flex()
123 .flex_col()
124 .p(px(4.0))
125 .rounded(px(radius))
126 .border_1()
127 .border_color(border)
128 .bg(surface)
129 .shadow_md()
130 .child(content_el);
131 let placed = match self.placement {
132 Placement::Bottom => base.top(relative(1.0)).mt(px(6.0)).left(px(0.0)),
133 Placement::BottomEnd => base.top(relative(1.0)).mt(px(6.0)).right(px(0.0)),
134 Placement::Top => base.bottom(relative(1.0)).mb(px(6.0)).left(px(0.0)),
135 Placement::TopEnd => base.bottom(relative(1.0)).mb(px(6.0)).right(px(0.0)),
136 };
137 let placed = match self.width {
138 Some(w) => placed.w(px(w)),
139 None => placed.min_w(px(180.0)),
140 };
141 wrap = wrap.child(deferred(placed));
142 }
143
144 wrap
145 }
146}