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