disconnected_overlay.rs

  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}