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),
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::DisconnectedFromSshRemote
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(ssh_connection_options) = remote_connection_options {
69 Host::RemoteServerProject(ssh_connection_options)
70 } else {
71 Host::CollabGuestProject
72 };
73
74 workspace.toggle_modal(window, cx, |_, cx| DisconnectedOverlay {
75 finished: false,
76 workspace: handle,
77 host,
78 focus_handle: cx.focus_handle(),
79 });
80 },
81 )
82 .detach();
83 }
84
85 fn handle_reconnect(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
86 self.finished = true;
87 cx.emit(DismissEvent);
88
89 if let Host::RemoteServerProject(ssh_connection_options) = &self.host {
90 self.reconnect_to_remote_project(ssh_connection_options.clone(), window, cx);
91 }
92 }
93
94 fn reconnect_to_remote_project(
95 &self,
96 connection_options: RemoteConnectionOptions,
97 window: &mut Window,
98 cx: &mut Context<Self>,
99 ) {
100 let Some(workspace) = self.workspace.upgrade() else {
101 return;
102 };
103
104 let Some(window_handle) = window.window_handle().downcast::<Workspace>() else {
105 return;
106 };
107
108 let app_state = workspace.read(cx).app_state().clone();
109 let paths = workspace
110 .read(cx)
111 .root_paths(cx)
112 .iter()
113 .map(|path| path.to_path_buf())
114 .collect();
115
116 cx.spawn_in(window, async move |_, cx| {
117 open_remote_project(
118 connection_options,
119 paths,
120 app_state,
121 OpenOptions {
122 replace_window: Some(window_handle),
123 ..Default::default()
124 },
125 cx,
126 )
127 .await?;
128 Ok(())
129 })
130 .detach_and_prompt_err("Failed to reconnect", window, cx, |_, _, _| None);
131 }
132
133 fn cancel(&mut self, _: &menu::Cancel, _: &mut Window, cx: &mut Context<Self>) {
134 self.finished = true;
135 cx.emit(DismissEvent)
136 }
137}
138
139impl Render for DisconnectedOverlay {
140 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
141 let can_reconnect = matches!(self.host, Host::RemoteServerProject(_));
142
143 let message = match &self.host {
144 Host::CollabGuestProject => {
145 "Your connection to the remote project has been lost.".to_string()
146 }
147 Host::RemoteServerProject(options) => {
148 let autosave = if ProjectSettings::get_global(cx)
149 .session
150 .restore_unsaved_buffers
151 {
152 "\nUnsaved changes are stored locally."
153 } else {
154 ""
155 };
156 format!(
157 "Your connection to {} has been lost.{}",
158 options.display_name(),
159 autosave
160 )
161 }
162 };
163
164 div()
165 .track_focus(&self.focus_handle(cx))
166 .elevation_3(cx)
167 .on_action(cx.listener(Self::cancel))
168 .occlude()
169 .w(rems(24.))
170 .max_h(rems(40.))
171 .child(
172 Modal::new("disconnected", None)
173 .header(
174 ModalHeader::new()
175 .show_dismiss_button(true)
176 .child(Headline::new("Disconnected").size(HeadlineSize::Small)),
177 )
178 .section(Section::new().child(Label::new(message)))
179 .footer(
180 ModalFooter::new().end_slot(
181 h_flex()
182 .gap_2()
183 .child(
184 Button::new("close-window", "Close Window")
185 .style(ButtonStyle::Filled)
186 .layer(ElevationIndex::ModalSurface)
187 .on_click(cx.listener(move |_, _, window, _| {
188 window.remove_window();
189 })),
190 )
191 .when(can_reconnect, |el| {
192 el.child(
193 Button::new("reconnect", "Reconnect")
194 .style(ButtonStyle::Filled)
195 .layer(ElevationIndex::ModalSurface)
196 .icon(IconName::ArrowCircle)
197 .icon_position(IconPosition::Start)
198 .on_click(cx.listener(Self::handle_reconnect)),
199 )
200 }),
201 ),
202 ),
203 )
204 }
205}