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