disconnected_overlay.rs

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