disconnected_overlay.rs

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