Reconnect button for remote projects (#12669)

Release Notes:

- N/A

---------

Co-authored-by: Max <max@zed.dev>
This commit is contained in:
Conrad Irwin 2024-06-10 18:09:47 -06:00 committed by GitHub
parent 1914a42b1c
commit 4e98c23463
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
15 changed files with 437 additions and 136 deletions

View file

@ -2,6 +2,7 @@ use gpui::{
div, prelude::*, px, AnyView, DismissEvent, FocusHandle, ManagedView, Render, Subscription,
View, ViewContext, WindowContext,
};
use theme::ActiveTheme as _;
use ui::{h_flex, v_flex};
pub enum DismissDecision {
@ -13,11 +14,16 @@ pub trait ModalView: ManagedView {
fn on_before_dismiss(&mut self, _: &mut ViewContext<Self>) -> DismissDecision {
DismissDecision::Dismiss(true)
}
fn fade_out_background(&self) -> bool {
false
}
}
trait ModalViewHandle {
fn on_before_dismiss(&mut self, cx: &mut WindowContext) -> DismissDecision;
fn view(&self) -> AnyView;
fn fade_out_background(&self, cx: &WindowContext) -> bool;
}
impl<V: ModalView> ModalViewHandle for View<V> {
@ -28,6 +34,10 @@ impl<V: ModalView> ModalViewHandle for View<V> {
fn view(&self) -> AnyView {
self.clone().into()
}
fn fade_out_background(&self, cx: &WindowContext) -> bool {
self.read(cx).fade_out_background()
}
}
pub struct ActiveModal {
@ -134,20 +144,34 @@ impl ModalLayer {
}
impl Render for ModalLayer {
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let Some(active_modal) = &self.active_modal else {
return div();
};
div().absolute().size_full().top_0().left_0().child(
v_flex()
.h(px(0.0))
.top_20()
.flex()
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
)
div()
.absolute()
.size_full()
.top_0()
.left_0()
.when(active_modal.modal.fade_out_background(cx), |el| {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
el.bg(background)
.occlude()
.on_mouse_down_out(cx.listener(|this, _, cx| {
this.hide_modal(cx);
}))
})
.child(
v_flex()
.h(px(0.0))
.top_20()
.flex()
.flex_col()
.items_center()
.track_focus(&active_modal.focus_handle)
.child(h_flex().occlude().child(active_modal.modal.view())),
)
}
}

View file

@ -468,6 +468,99 @@ impl WorkspaceDb {
})
}
pub(crate) fn workspace_for_dev_server_project(
&self,
dev_server_project_id: DevServerProjectId,
) -> Option<SerializedWorkspace> {
// Note that we re-assign the workspace_id here in case it's empty
// and we've grabbed the most recent workspace
let (
workspace_id,
local_paths,
local_paths_order,
dev_server_project_id,
window_bounds,
display,
centered_layout,
docks,
): (
WorkspaceId,
Option<LocalPaths>,
Option<LocalPathsOrder>,
Option<u64>,
Option<SerializedWindowBounds>,
Option<Uuid>,
Option<bool>,
DockStructure,
) = self
.select_row_bound(sql! {
SELECT
workspace_id,
local_paths,
local_paths_order,
dev_server_project_id,
window_state,
window_x,
window_y,
window_width,
window_height,
display,
centered_layout,
left_dock_visible,
left_dock_active_panel,
left_dock_zoom,
right_dock_visible,
right_dock_active_panel,
right_dock_zoom,
bottom_dock_visible,
bottom_dock_active_panel,
bottom_dock_zoom
FROM workspaces
WHERE dev_server_project_id = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id.0))
.context("No workspaces found")
.warn_on_err()
.flatten()?;
let location = if let Some(dev_server_project_id) = dev_server_project_id {
let dev_server_project: SerializedDevServerProject = self
.select_row_bound(sql! {
SELECT id, path, dev_server_name
FROM dev_server_projects
WHERE id = ?
})
.and_then(|mut prepared_statement| (prepared_statement)(dev_server_project_id))
.context("No remote project found")
.warn_on_err()
.flatten()?;
SerializedWorkspaceLocation::DevServer(dev_server_project)
} else if let Some(local_paths) = local_paths {
match local_paths_order {
Some(order) => SerializedWorkspaceLocation::Local(local_paths, order),
None => {
let order = LocalPathsOrder::default_for_paths(&local_paths);
SerializedWorkspaceLocation::Local(local_paths, order)
}
}
} else {
return None;
};
Some(SerializedWorkspace {
id: workspace_id,
location,
center_group: self
.get_center_pane_group(workspace_id)
.context("Getting center group")
.log_err()?,
window_bounds,
centered_layout: centered_layout.unwrap_or(false),
display,
docks,
})
}
/// Saves a workspace using the worktree roots. Will garbage collect any workspaces
/// that used this workspace previously
pub(crate) async fn save_workspace(&self, workspace: SerializedWorkspace) {

View file

@ -16,7 +16,7 @@ use anyhow::{anyhow, Context as _, Result};
use call::{call_settings::CallSettings, ActiveCall};
use client::{
proto::{self, ErrorCode, PeerId},
ChannelId, Client, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
ChannelId, Client, DevServerProjectId, ErrorExt, ProjectId, Status, TypedEnvelope, UserStore,
};
use collections::{hash_map, HashMap, HashSet};
use derive_more::{Deref, DerefMut};
@ -29,10 +29,9 @@ use futures::{
use gpui::{
actions, canvas, impl_actions, point, relative, size, Action, AnyElement, AnyView, AnyWeakView,
AppContext, AsyncAppContext, AsyncWindowContext, Bounds, DevicePixels, DragMoveEvent,
ElementId, Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global,
GlobalElementId, KeyContext, Keystroke, LayoutId, ManagedView, Model, ModelContext,
PathPromptOptions, Point, PromptLevel, Render, Size, Subscription, Task, View, WeakView,
WindowBounds, WindowHandle, WindowOptions,
Entity as _, EntityId, EventEmitter, FocusHandle, FocusableView, Global, KeyContext, Keystroke,
ManagedView, Model, ModelContext, PathPromptOptions, Point, PromptLevel, Render, Size,
Subscription, Task, View, WeakView, WindowBounds, WindowHandle, WindowOptions,
};
use item::{
FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, PreviewTabsSettings,
@ -80,8 +79,8 @@ use theme::{ActiveTheme, SystemAppearance, ThemeSettings};
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
pub use ui;
use ui::{
div, h_flex, Context as _, Div, Element, FluentBuilder, InteractiveElement as _, IntoElement,
Label, ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
div, h_flex, Context as _, Div, FluentBuilder, InteractiveElement as _, IntoElement,
ParentElement as _, Pixels, SharedString, Styled as _, ViewContext, VisualContext as _,
WindowContext,
};
use util::{maybe, ResultExt};
@ -600,6 +599,8 @@ pub struct Workspace {
centered_layout: bool,
bounds_save_task_queued: Option<Task<()>>,
on_prompt_for_new_path: Option<PromptForNewPath>,
render_disconnected_overlay:
Option<Box<dyn Fn(&mut Self, &mut ViewContext<Self>) -> AnyElement>>,
}
impl EventEmitter<Event> for Workspace {}
@ -650,7 +651,6 @@ impl Workspace {
for pane in panes_to_unfollow {
this.unfollow(&pane, cx);
}
cx.disable_focus();
}
project::Event::Closed => {
@ -879,10 +879,11 @@ impl Workspace {
centered_layout: false,
bounds_save_task_queued: None,
on_prompt_for_new_path: None,
render_disconnected_overlay: None,
}
}
fn new_local(
pub fn new_local(
abs_paths: Vec<PathBuf>,
app_state: Arc<AppState>,
requesting_window: Option<WindowHandle<Workspace>>,
@ -1255,6 +1256,13 @@ impl Workspace {
self.on_prompt_for_new_path = Some(prompt)
}
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_new_path(
&mut self,
cx: &mut ViewContext<Self>,
@ -4285,7 +4293,13 @@ impl Render for Workspace {
)
.child(self.status_bar.clone())
.children(if self.project.read(cx).is_disconnected() {
Some(DisconnectedOverlay)
if let Some(render) = self.render_disconnected_overlay.take() {
let result = render(self, cx);
self.render_disconnected_overlay = Some(render);
Some(result)
} else {
None
}
} else {
None
})
@ -4935,6 +4949,7 @@ pub fn join_hosted_project(
}
pub fn join_dev_server_project(
dev_server_project_id: DevServerProjectId,
project_id: ProjectId,
app_state: Arc<AppState>,
window_to_replace: Option<WindowHandle<Workspace>>,
@ -4969,10 +4984,19 @@ pub fn join_dev_server_project(
)
.await?;
let serialized_workspace: Option<SerializedWorkspace> =
persistence::DB.workspace_for_dev_server_project(dev_server_project_id);
let workspace_id = if let Some(serialized_workspace) = serialized_workspace {
serialized_workspace.id
} else {
persistence::DB.next_id().await?
};
if let Some(window_to_replace) = window_to_replace {
cx.update_window(window_to_replace.into(), |_, cx| {
cx.replace_root_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
});
})?;
window_to_replace
@ -4984,7 +5008,7 @@ pub fn join_dev_server_project(
window_bounds_override.map(|bounds| WindowBounds::Windowed(bounds));
cx.open_window(options, |cx| {
cx.new_view(|cx| {
Workspace::new(Default::default(), project, app_state.clone(), cx)
Workspace::new(Some(workspace_id), project, app_state.clone(), cx)
})
})
})?
@ -5150,72 +5174,6 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<DevicePixels>> {
Some(size((width as i32).into(), (height as i32).into()))
}
struct DisconnectedOverlay;
impl Element for DisconnectedOverlay {
type RequestLayoutState = AnyElement;
type PrepaintState = ();
fn id(&self) -> Option<ElementId> {
None
}
fn request_layout(
&mut self,
_id: Option<&GlobalElementId>,
cx: &mut WindowContext,
) -> (LayoutId, Self::RequestLayoutState) {
let mut background = cx.theme().colors().elevated_surface_background;
background.fade_out(0.2);
let mut overlay = div()
.bg(background)
.absolute()
.left_0()
.top(ui::TitleBar::height(cx))
.size_full()
.flex()
.items_center()
.justify_center()
.capture_any_mouse_down(|_, cx| cx.stop_propagation())
.capture_any_mouse_up(|_, cx| cx.stop_propagation())
.child(Label::new(
"Your connection to the remote project has been lost.",
))
.into_any();
(overlay.request_layout(cx), overlay)
}
fn prepaint(
&mut self,
_id: Option<&GlobalElementId>,
bounds: Bounds<Pixels>,
overlay: &mut Self::RequestLayoutState,
cx: &mut WindowContext,
) {
cx.insert_hitbox(bounds, true);
overlay.prepaint(cx);
}
fn paint(
&mut self,
_id: Option<&GlobalElementId>,
_: Bounds<Pixels>,
overlay: &mut Self::RequestLayoutState,
_: &mut Self::PrepaintState,
cx: &mut WindowContext,
) {
overlay.paint(cx)
}
}
impl IntoElement for DisconnectedOverlay {
type Element = Self;
fn into_element(self) -> Self::Element {
self
}
}
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc};