Improve disconnected modal for SSH (#19567)

Closes #ISSUE

Release Notes:

- SSH Remoting: made reconnect modal more robust
This commit is contained in:
Conrad Irwin 2024-10-22 13:22:27 -06:00 committed by GitHub
parent 5dbf68ddc4
commit e93d62680d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 156 additions and 122 deletions

View file

@ -2,6 +2,7 @@ use std::path::PathBuf;
use dev_server_projects::DevServer; use dev_server_projects::DevServer;
use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView}; use gpui::{ClickEvent, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render, WeakView};
use project::project_settings::ProjectSettings;
use remote::SshConnectionOptions; use remote::SshConnectionOptions;
use settings::Settings; use settings::Settings;
use ui::{ use ui::{
@ -26,6 +27,7 @@ pub struct DisconnectedOverlay {
workspace: WeakView<Workspace>, workspace: WeakView<Workspace>,
host: Host, host: Host,
focus_handle: FocusHandle, focus_handle: FocusHandle,
finished: bool,
} }
impl EventEmitter<DismissEvent> for DisconnectedOverlay {} impl EventEmitter<DismissEvent> for DisconnectedOverlay {}
@ -35,6 +37,9 @@ impl FocusableView for DisconnectedOverlay {
} }
} }
impl ModalView for DisconnectedOverlay { impl ModalView for DisconnectedOverlay {
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> workspace::DismissDecision {
return workspace::DismissDecision::Dismiss(self.finished);
}
fn fade_out_background(&self) -> bool { fn fade_out_background(&self) -> bool {
true true
} }
@ -70,6 +75,7 @@ impl DisconnectedOverlay {
}; };
workspace.toggle_modal(cx, |cx| DisconnectedOverlay { workspace.toggle_modal(cx, |cx| DisconnectedOverlay {
finished: false,
workspace: handle, workspace: handle,
host, host,
focus_handle: cx.focus_handle(), focus_handle: cx.focus_handle(),
@ -79,6 +85,7 @@ impl DisconnectedOverlay {
} }
fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) { fn handle_reconnect(&mut self, _: &ClickEvent, cx: &mut ViewContext<Self>) {
self.finished = true;
cx.emit(DismissEvent); cx.emit(DismissEvent);
match &self.host { match &self.host {
@ -186,6 +193,7 @@ impl DisconnectedOverlay {
} }
fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
self.finished = true;
cx.emit(DismissEvent) cx.emit(DismissEvent)
} }
} }
@ -202,9 +210,17 @@ impl Render for DisconnectedOverlay {
"Your connection to the remote project has been lost.".to_string() "Your connection to the remote project has been lost.".to_string()
} }
Host::SshRemoteProject(options) => { Host::SshRemoteProject(options) => {
let autosave = if ProjectSettings::get_global(cx)
.session
.restore_unsaved_buffers
{
"\nUnsaved changes are stored locally."
} else {
""
};
format!( format!(
"Your connection to {} has been lost", "Your connection to {} has been lost.{}",
options.connection_string() options.host, autosave
) )
} }
}; };

View file

@ -31,7 +31,7 @@ use futures::{
}; };
use gpui::{ use gpui::{
action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size, action_as, actions, canvas, impl_action_as, impl_actions, point, relative, size,
transparent_black, Action, AnyElement, AnyView, AnyWeakView, AppContext, AsyncAppContext, transparent_black, Action, AnyView, AnyWeakView, AppContext, AsyncAppContext,
AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId, AsyncWindowContext, Bounds, CursorStyle, Decorations, DragMoveEvent, Entity as _, EntityId,
EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke, EventEmitter, Flatten, FocusHandle, FocusableView, Global, Hsla, KeyContext, Keystroke,
ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render, ManagedView, Model, ModelContext, MouseButton, PathPromptOptions, Point, PromptLevel, Render,
@ -762,8 +762,6 @@ pub struct Workspace {
bounds_save_task_queued: Option<Task<()>>, bounds_save_task_queued: Option<Task<()>>,
on_prompt_for_new_path: Option<PromptForNewPath>, on_prompt_for_new_path: Option<PromptForNewPath>,
on_prompt_for_open_path: Option<PromptForOpenPath>, on_prompt_for_open_path: Option<PromptForOpenPath>,
render_disconnected_overlay:
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>, serializable_items_tx: UnboundedSender<Box<dyn SerializableItemHandle>>,
serialized_ssh_project: Option<SerializedSshProject>, serialized_ssh_project: Option<SerializedSshProject>,
_items_serializer: Task<Result<()>>, _items_serializer: Task<Result<()>>,
@ -1067,7 +1065,6 @@ impl Workspace {
bounds_save_task_queued: None, bounds_save_task_queued: None,
on_prompt_for_new_path: None, on_prompt_for_new_path: None,
on_prompt_for_open_path: None, on_prompt_for_open_path: None,
render_disconnected_overlay: None,
serializable_items_tx, serializable_items_tx,
_items_serializer, _items_serializer,
session_id: Some(session_id), session_id: Some(session_id),
@ -1472,13 +1469,6 @@ impl Workspace {
self.serialized_ssh_project = Some(serialized_ssh_project); self.serialized_ssh_project = Some(serialized_ssh_project);
} }
pub fn set_render_disconnected_overlay(
&mut self,
render: impl Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement + 'static,
) {
self.render_disconnected_overlay = Some(Box::new(render))
}
pub fn prompt_for_open_path( pub fn prompt_for_open_path(
&mut self, &mut self,
path_prompt_options: PathPromptOptions, path_prompt_options: PathPromptOptions,
@ -4746,130 +4736,158 @@ impl Render for Workspace {
.children(self.titlebar_item.clone()) .children(self.titlebar_item.clone())
.child( .child(
div() div()
.id("workspace") .size_full()
.bg(colors.background)
.relative() .relative()
.flex_1() .flex_1()
.w_full()
.flex() .flex()
.flex_col() .flex_col()
.overflow_hidden()
.border_t_1()
.border_b_1()
.border_color(colors.border)
.child({
let this = cx.view().clone();
canvas(
move |bounds, cx| this.update(cx, |this, _cx| this.bounds = bounds),
|_, _, _| {},
)
.absolute()
.size_full()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| match e.drag(cx).0 {
DockPosition::Left => {
let size = e.event.position.x - workspace.bounds.left();
workspace.left_dock.update(cx, |left_dock, cx| {
left_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Right => {
let size = workspace.bounds.right() - e.event.position.x;
workspace.right_dock.update(cx, |right_dock, cx| {
right_dock.resize_active_panel(Some(size), cx);
});
}
DockPosition::Bottom => {
let size = workspace.bounds.bottom() - e.event.position.y;
workspace.bottom_dock.update(cx, |bottom_dock, cx| {
bottom_dock.resize_active_panel(Some(size), cx);
});
}
},
))
})
.child( .child(
div() div()
.id("workspace")
.bg(colors.background)
.relative()
.flex_1()
.w_full()
.flex() .flex()
.flex_row() .flex_col()
.h_full() .overflow_hidden()
// Left Dock .border_t_1()
.children(self.render_dock(DockPosition::Left, &self.left_dock, cx)) .border_b_1()
// Panes .border_color(colors.border)
.child({
let this = cx.view().clone();
canvas(
move |bounds, cx| {
this.update(cx, |this, _cx| this.bounds = bounds)
},
|_, _, _| {},
)
.absolute()
.size_full()
})
.when(self.zoomed.is_none(), |this| {
this.on_drag_move(cx.listener(
|workspace, e: &DragMoveEvent<DraggedDock>, cx| {
match e.drag(cx).0 {
DockPosition::Left => {
let size = e.event.position.x
- workspace.bounds.left();
workspace.left_dock.update(
cx,
|left_dock, cx| {
left_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
DockPosition::Right => {
let size = workspace.bounds.right()
- e.event.position.x;
workspace.right_dock.update(
cx,
|right_dock, cx| {
right_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
DockPosition::Bottom => {
let size = workspace.bounds.bottom()
- e.event.position.y;
workspace.bottom_dock.update(
cx,
|bottom_dock, cx| {
bottom_dock.resize_active_panel(
Some(size),
cx,
);
},
);
}
}
},
))
})
.child( .child(
div() div()
.flex() .flex()
.flex_col() .flex_row()
.flex_1() .h_full()
.overflow_hidden() // Left Dock
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
self.zoomed.as_ref(),
&self.app_state,
cx,
))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(self.render_dock( .children(self.render_dock(
DockPosition::Bottom, DockPosition::Left,
&self.bottom_dock, &self.left_dock,
cx,
))
// Panes
.child(
div()
.flex()
.flex_col()
.flex_1()
.overflow_hidden()
.child(
h_flex()
.flex_1()
.when_some(paddings.0, |this, p| {
this.child(p.border_r_1())
})
.child(self.center.render(
&self.project,
&self.follower_states,
self.active_call(),
&self.active_pane,
self.zoomed.as_ref(),
&self.app_state,
cx,
))
.when_some(paddings.1, |this, p| {
this.child(p.border_l_1())
}),
)
.children(self.render_dock(
DockPosition::Bottom,
&self.bottom_dock,
cx,
)),
)
// Right Dock
.children(self.render_dock(
DockPosition::Right,
&self.right_dock,
cx, cx,
)), )),
) )
// Right Dock .children(self.zoomed.as_ref().and_then(|view| {
.children(self.render_dock( let zoomed_view = view.upgrade()?;
DockPosition::Right, let div = div()
&self.right_dock, .occlude()
cx, .absolute()
)), .overflow_hidden()
) .border_color(colors.border)
.children(self.zoomed.as_ref().and_then(|view| { .bg(colors.background)
let zoomed_view = view.upgrade()?; .child(zoomed_view)
let div = div() .inset_0()
.occlude() .shadow_lg();
.absolute()
.overflow_hidden()
.border_color(colors.border)
.bg(colors.background)
.child(zoomed_view)
.inset_0()
.shadow_lg();
Some(match self.zoomed_position { Some(match self.zoomed_position {
Some(DockPosition::Left) => div.right_2().border_r_1(), Some(DockPosition::Left) => div.right_2().border_r_1(),
Some(DockPosition::Right) => div.left_2().border_l_1(), Some(DockPosition::Right) => div.left_2().border_l_1(),
Some(DockPosition::Bottom) => div.top_2().border_t_1(), Some(DockPosition::Bottom) => div.top_2().border_t_1(),
None => div.top_2().bottom_2().left_2().right_2().border_1(), None => {
}) div.top_2().bottom_2().left_2().right_2().border_1()
})) }
.child(self.modal_layer.clone()) })
.children(self.render_notifications(cx)), }))
) .children(self.render_notifications(cx)),
.child(self.status_bar.clone()) )
.children(if self.project.read(cx).is_disconnected(cx) { .child(self.status_bar.clone())
if let Some(render) = self.render_disconnected_overlay.take() { .child(self.modal_layer.clone()),
let result = render(self, cx); ),
self.render_disconnected_overlay = Some(render);
Some(result)
} else {
None
}
} else {
None
}),
cx, cx,
) )
} }