1use gpui::{
2 AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView,
3 MouseButton, Pixels, Point, Subscription,
4};
5use ui::prelude::*;
6
7#[derive(Debug, Clone, Copy, Default)]
8pub enum ModalPlacement {
9 #[default]
10 Centered,
11 Anchored {
12 position: Point<Pixels>,
13 },
14}
15
16#[derive(Debug)]
17pub enum DismissDecision {
18 Dismiss(bool),
19 Pending,
20}
21
22pub trait ModalView: ManagedView {
23 fn on_before_dismiss(
24 &mut self,
25 _window: &mut Window,
26 _: &mut Context<Self>,
27 ) -> DismissDecision {
28 DismissDecision::Dismiss(true)
29 }
30
31 fn fade_out_background(&self) -> bool {
32 false
33 }
34
35 fn render_bare(&self) -> bool {
36 false
37 }
38}
39
40trait ModalViewHandle {
41 fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision;
42 fn view(&self) -> AnyView;
43 fn fade_out_background(&self, cx: &mut App) -> bool;
44 fn render_bare(&self, cx: &mut App) -> bool;
45}
46
47impl<V: ModalView> ModalViewHandle for Entity<V> {
48 fn on_before_dismiss(&mut self, window: &mut Window, cx: &mut App) -> DismissDecision {
49 self.update(cx, |this, cx| this.on_before_dismiss(window, cx))
50 }
51
52 fn view(&self) -> AnyView {
53 self.clone().into()
54 }
55
56 fn fade_out_background(&self, cx: &mut App) -> bool {
57 self.read(cx).fade_out_background()
58 }
59
60 fn render_bare(&self, cx: &mut App) -> bool {
61 self.read(cx).render_bare()
62 }
63}
64
65pub struct ActiveModal {
66 modal: Box<dyn ModalViewHandle>,
67 _subscriptions: [Subscription; 2],
68 previous_focus_handle: Option<FocusHandle>,
69 focus_handle: FocusHandle,
70 placement: ModalPlacement,
71}
72
73pub struct ModalLayer {
74 active_modal: Option<ActiveModal>,
75 dismiss_on_focus_lost: bool,
76}
77
78pub(crate) struct ModalOpenedEvent;
79
80impl EventEmitter<ModalOpenedEvent> for ModalLayer {}
81
82impl Default for ModalLayer {
83 fn default() -> Self {
84 Self::new()
85 }
86}
87
88impl ModalLayer {
89 pub fn new() -> Self {
90 Self {
91 active_modal: None,
92 dismiss_on_focus_lost: false,
93 }
94 }
95
96 pub fn toggle_modal<V, B>(&mut self, window: &mut Window, cx: &mut Context<Self>, build_view: B)
97 where
98 V: ModalView,
99 B: FnOnce(&mut Window, &mut Context<V>) -> V,
100 {
101 self.toggle_modal_with_placement(window, cx, ModalPlacement::Centered, build_view);
102 }
103
104 pub fn toggle_modal_with_placement<V, B>(
105 &mut self,
106 window: &mut Window,
107 cx: &mut Context<Self>,
108 placement: ModalPlacement,
109 build_view: B,
110 ) where
111 V: ModalView,
112 B: FnOnce(&mut Window, &mut Context<V>) -> V,
113 {
114 if let Some(active_modal) = &self.active_modal {
115 let is_close = active_modal.modal.view().downcast::<V>().is_ok();
116 let did_close = self.hide_modal(window, cx);
117 if is_close || !did_close {
118 return;
119 }
120 }
121 let new_modal = cx.new(|cx| build_view(window, cx));
122 self.show_modal(new_modal, placement, window, cx);
123 cx.emit(ModalOpenedEvent);
124 }
125
126 fn show_modal<V>(
127 &mut self,
128 new_modal: Entity<V>,
129 placement: ModalPlacement,
130 window: &mut Window,
131 cx: &mut Context<Self>,
132 ) where
133 V: ModalView,
134 {
135 let focus_handle = cx.focus_handle();
136 self.active_modal = Some(ActiveModal {
137 modal: Box::new(new_modal.clone()),
138 _subscriptions: [
139 cx.subscribe_in(
140 &new_modal,
141 window,
142 |this, _, _: &DismissEvent, window, cx| {
143 this.hide_modal(window, cx);
144 },
145 ),
146 cx.on_focus_out(&focus_handle, window, |this, _event, window, cx| {
147 if this.dismiss_on_focus_lost {
148 this.hide_modal(window, cx);
149 }
150 }),
151 ],
152 previous_focus_handle: window.focused(cx),
153 focus_handle,
154 placement,
155 });
156 cx.defer_in(window, move |_, window, cx| {
157 window.focus(&new_modal.focus_handle(cx), cx);
158 });
159 cx.notify();
160 }
161
162 pub fn hide_modal(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
163 let Some(active_modal) = self.active_modal.as_mut() else {
164 self.dismiss_on_focus_lost = false;
165 return false;
166 };
167
168 match active_modal.modal.on_before_dismiss(window, cx) {
169 DismissDecision::Dismiss(dismiss) => {
170 self.dismiss_on_focus_lost = !dismiss;
171 if !dismiss {
172 return false;
173 }
174 }
175 DismissDecision::Pending => {
176 self.dismiss_on_focus_lost = false;
177 return false;
178 }
179 }
180
181 if let Some(active_modal) = self.active_modal.take() {
182 if let Some(previous_focus) = active_modal.previous_focus_handle
183 && active_modal.focus_handle.contains_focused(window, cx)
184 {
185 previous_focus.focus(window, cx);
186 }
187 cx.notify();
188 }
189 true
190 }
191
192 pub fn active_modal<V>(&self) -> Option<Entity<V>>
193 where
194 V: 'static,
195 {
196 let active_modal = self.active_modal.as_ref()?;
197 active_modal.modal.view().downcast::<V>().ok()
198 }
199
200 pub fn has_active_modal(&self) -> bool {
201 self.active_modal.is_some()
202 }
203}
204
205impl Render for ModalLayer {
206 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
207 let Some(active_modal) = &self.active_modal else {
208 return div().into_any_element();
209 };
210
211 if active_modal.modal.render_bare(cx) {
212 return active_modal.modal.view().into_any_element();
213 }
214
215 let content = h_flex()
216 .occlude()
217 .child(active_modal.modal.view())
218 .on_mouse_down(MouseButton::Left, |_, _, cx| {
219 cx.stop_propagation();
220 });
221
222 let positioned = match active_modal.placement {
223 ModalPlacement::Centered => v_flex()
224 .h(px(0.0))
225 .top_20()
226 .items_center()
227 .track_focus(&active_modal.focus_handle)
228 .child(content)
229 .into_any_element(),
230 ModalPlacement::Anchored { position } => div()
231 .absolute()
232 .left(position.x)
233 .top(position.y - px(20.))
234 .track_focus(&active_modal.focus_handle)
235 .child(content)
236 .into_any_element(),
237 };
238
239 div()
240 .absolute()
241 .size_full()
242 .inset_0()
243 .occlude()
244 .when(active_modal.modal.fade_out_background(cx), |this| {
245 let mut background = cx.theme().colors().elevated_surface_background;
246 background.fade_out(0.2);
247 this.bg(background)
248 })
249 .on_mouse_down(
250 MouseButton::Left,
251 cx.listener(|this, _, window, cx| {
252 this.hide_modal(window, cx);
253 }),
254 )
255 .child(positioned)
256 .into_any_element()
257 }
258}