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