1use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, Focusable, Render, WeakEntity};
2use project::project_settings::ProjectSettings;
3use remote::RemoteConnectionOptions;
4use settings::Settings;
5use ui::{ElevationIndex, Modal, ModalFooter, ModalHeader, Section, prelude::*};
6use workspace::{
7 ModalView, MultiWorkspace, OpenOptions, Workspace, notifications::DetachAndPromptErr,
8};
9
10use crate::open_remote_project;
11
12enum Host {
13 CollabGuestProject,
14 RemoteServerProject(RemoteConnectionOptions, bool),
15}
16
17pub struct DisconnectedOverlay {
18 workspace: WeakEntity<Workspace>,
19 host: Host,
20 focus_handle: FocusHandle,
21 finished: bool,
22}
23
24impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
25impl Focusable for DisconnectedOverlay {
26 fn focus_handle(&self, _cx: &gpui::App) -> gpui::FocusHandle {
27 self.focus_handle.clone()
28 }
29}
30impl ModalView for DisconnectedOverlay {
31 fn on_before_dismiss(
32 &mut self,
33 _window: &mut Window,
34 _: &mut Context<Self>,
35 ) -> workspace::DismissDecision {
36 workspace::DismissDecision::Dismiss(self.finished)
37 }
38 fn fade_out_background(&self) -> bool {
39 true
40 }
41}
42
43impl DisconnectedOverlay {
44 pub fn register(
45 workspace: &mut Workspace,
46 window: Option<&mut Window>,
47 cx: &mut Context<Workspace>,
48 ) {
49 let Some(window) = window else {
50 return;
51 };
52 cx.subscribe_in(
53 workspace.project(),
54 window,
55 |workspace, project, event, window, cx| {
56 if !matches!(
57 event,
58 project::Event::DisconnectedFromHost
59 | project::Event::DisconnectedFromRemote { .. }
60 ) {
61 return;
62 }
63 let handle = cx.entity().downgrade();
64
65 let remote_connection_options = project.read(cx).remote_connection_options(cx);
66 let host = if let Some(remote_connection_options) = remote_connection_options {
67 Host::RemoteServerProject(
68 remote_connection_options,
69 matches!(
70 event,
71 project::Event::DisconnectedFromRemote {
72 server_not_running: true
73 }
74 ),
75 )
76 } else {
77 Host::CollabGuestProject
78 };
79
80 workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay {
81 finished: false,
82 workspace: handle,
83 host,
84 focus_handle: cx.focus_handle(),
85 });
86 },
87 )
88 .detach();
89 }
90
91 fn handle_reconnect(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
92 self.finished = true;
93 cx.emit(DismissEvent);
94
95 if let Host::RemoteServerProject(remote_connection_options, _) = &self.host {
96 self.reconnect_to_remote_project(remote_connection_options.clone(), window, cx);
97 }
98 }
99
100 fn reconnect_to_remote_project(
101 &self,
102 connection_options: RemoteConnectionOptions,
103 window: &mut Window,
104 cx: &mut Context<Self>,
105 ) {
106 let Some(workspace) = self.workspace.upgrade() else {
107 return;
108 };
109
110 let Some(window_handle) = window.window_handle().downcast::<MultiWorkspace>() else {
111 return;
112 };
113
114 let app_state = workspace.read(cx).app_state().clone();
115 let paths = workspace
116 .read(cx)
117 .root_paths(cx)
118 .iter()
119 .map(|path| path.to_path_buf())
120 .collect();
121
122 cx.spawn_in(window, async move |_, cx| {
123 open_remote_project(
124 connection_options,
125 paths,
126 app_state,
127 OpenOptions {
128 requesting_window: Some(window_handle),
129 ..Default::default()
130 },
131 cx,
132 )
133 .await?;
134 Ok(())
135 })
136 .detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None);
137 }
138
139 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
140 self.finished = true;
141 cx.emit(DismissEvent)
142 }
143}
144
145impl Render for DisconnectedOverlay {
146 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
147 let can_reconnect = matches!(self.host, Host::RemoteServerProject(..));
148
149 let message = match &self.host {
150 Host::CollabGuestProject => {
151 "Your connection to the remote project has been lost.".to_string()
152 }
153 Host::RemoteServerProject(options, server_not_running) => {
154 let autosave = if ProjectSettings::get_global(cx)
155 .session
156 .restore_unsaved_buffers
157 {
158 "\nUnsaved changes are stored locally."
159 } else {
160 ""
161 };
162 let reason = if *server_not_running {
163 "process exiting unexpectedly"
164 } else {
165 "not responding"
166 };
167 format!(
168 "Your connection to {} has been lost due to the server {reason}.{autosave}",
169 options.display_name(),
170 )
171 }
172 };
173
174 div()
175 .track_focus(&self.focus_handle(cx))
176 .elevation_3(cx)
177 .on_action(cx.listener(Self::cancel))
178 .occlude()
179 .w(rems(24.))
180 .max_h(rems(40.))
181 .child(
182 Modal::new("disconnected", None)
183 .header(
184 ModalHeader::new()
185 .show_dismiss_button(true)
186 .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
187 )
188 .section(Section::new().child(Label::new(message)))
189 .footer(
190 ModalFooter::new().end_slot(
191 h_flex()
192 .gap_2()
193 .child(
194 Button::new("close-window", "Close Window")
195 .style(ButtonStyle::Filled)
196 .layer(ElevationIndex::ModalSurface)
197 .on_click(cx.listener(move |_, _, window, _| {
198 window.remove_window();
199 })),
200 )
201 .when(can_reconnect, |el| {
202 el.child(
203 Button::new("reconnect", "Reconnect")
204 .style(ButtonStyle::Filled)
205 .layer(ElevationIndex::ModalSurface)
206 .start_icon(Icon::new(IconName::ArrowCircle))
207 .on_click(cx.listener(Self::handle_reconnect)),
208 )
209 }),
210 ),
211 ),
212 )
213 }
214}