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