disconnected_overlay.rs

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