Detailed changes
@@ -127,6 +127,12 @@ pub enum Status {
ReconnectionError { next_reconnection: Instant },
}
+impl Status {
+ pub fn is_connected(&self) -> bool {
+ matches!(self, Self::Connected { .. })
+ }
+}
+
struct ClientState {
credentials: Option<Credentials>,
status: (watch::Sender<Status>, watch::Receiver<Status>),
@@ -979,14 +979,6 @@ impl MutableAppContext {
.and_then(|window| window.root_view.clone().downcast::<T>())
}
- pub fn root_view_id(&self, window_id: usize) -> Option<usize> {
- self.cx.root_view_id(window_id)
- }
-
- pub fn focused_view_id(&self, window_id: usize) -> Option<usize> {
- self.cx.focused_view_id(window_id)
- }
-
pub fn render_view(
&mut self,
window_id: usize,
@@ -1376,7 +1368,7 @@ impl MutableAppContext {
window_id,
Window {
root_view: root_view.clone().into(),
- focused_view_id: root_view.id(),
+ focused_view_id: Some(root_view.id()),
invalidation: None,
},
);
@@ -1544,7 +1536,7 @@ impl MutableAppContext {
.get_or_insert_with(Default::default)
.removed
.push(view_id);
- if window.focused_view_id == view_id {
+ if window.focused_view_id == Some(view_id) {
Some(window.root_view.id())
} else {
None
@@ -1552,7 +1544,7 @@ impl MutableAppContext {
});
if let Some(view_id) = change_focus_to {
- self.focus(window_id, view_id);
+ self.focus(window_id, Some(view_id));
}
self.pending_effects
@@ -1755,7 +1747,7 @@ impl MutableAppContext {
}
}
- fn focus(&mut self, window_id: usize, focused_id: usize) {
+ fn focus(&mut self, window_id: usize, focused_id: Option<usize>) {
if self
.cx
.windows
@@ -1767,7 +1759,7 @@ impl MutableAppContext {
}
self.update(|this| {
- let blurred_id = this.cx.windows.get_mut(&window_id).map(|window| {
+ let blurred_id = this.cx.windows.get_mut(&window_id).and_then(|window| {
let blurred_id = window.focused_view_id;
window.focused_view_id = focused_id;
blurred_id
@@ -1780,9 +1772,11 @@ impl MutableAppContext {
}
}
- if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) {
- focused_view.on_focus(this, window_id, focused_id);
- this.cx.views.insert((window_id, focused_id), focused_view);
+ if let Some(focused_id) = focused_id {
+ if let Some(mut focused_view) = this.cx.views.remove(&(window_id, focused_id)) {
+ focused_view.on_focus(this, window_id, focused_id);
+ this.cx.views.insert((window_id, focused_id), focused_view);
+ }
}
})
}
@@ -1958,7 +1952,7 @@ impl AppContext {
pub fn focused_view_id(&self, window_id: usize) -> Option<usize> {
self.windows
.get(&window_id)
- .map(|window| window.focused_view_id)
+ .and_then(|window| window.focused_view_id)
}
pub fn background(&self) -> &Arc<executor::Background> {
@@ -2052,7 +2046,7 @@ impl ReadView for AppContext {
struct Window {
root_view: AnyViewHandle,
- focused_view_id: usize,
+ focused_view_id: Option<usize>,
invalidation: Option<WindowInvalidation>,
}
@@ -2080,7 +2074,7 @@ pub enum Effect {
},
Focus {
window_id: usize,
- view_id: usize,
+ view_id: Option<usize>,
},
ResizeWindow {
window_id: usize,
@@ -2514,14 +2508,21 @@ impl<'a, T: View> ViewContext<'a, T> {
let handle = handle.into();
self.app.pending_effects.push_back(Effect::Focus {
window_id: handle.window_id,
- view_id: handle.view_id,
+ view_id: Some(handle.view_id),
});
}
pub fn focus_self(&mut self) {
self.app.pending_effects.push_back(Effect::Focus {
window_id: self.window_id,
- view_id: self.view_id,
+ view_id: Some(self.view_id),
+ });
+ }
+
+ pub fn blur(&mut self) {
+ self.app.pending_effects.push_back(Effect::Focus {
+ window_id: self.window_id,
+ view_id: None,
});
}
@@ -8,6 +8,7 @@ use crate::{
pub struct EventHandler {
child: ElementBox,
+ capture: Option<Box<dyn FnMut(&Event, RectF, &mut EventContext) -> bool>>,
mouse_down: Option<Box<dyn FnMut(&mut EventContext) -> bool>>,
}
@@ -15,6 +16,7 @@ impl EventHandler {
pub fn new(child: ElementBox) -> Self {
Self {
child,
+ capture: None,
mouse_down: None,
}
}
@@ -26,6 +28,14 @@ impl EventHandler {
self.mouse_down = Some(Box::new(callback));
self
}
+
+ pub fn capture<F>(mut self, callback: F) -> Self
+ where
+ F: 'static + FnMut(&Event, RectF, &mut EventContext) -> bool,
+ {
+ self.capture = Some(Box::new(callback));
+ self
+ }
}
impl Element for EventHandler {
@@ -59,6 +69,12 @@ impl Element for EventHandler {
_: &mut Self::PaintState,
cx: &mut EventContext,
) -> bool {
+ if let Some(capture) = self.capture.as_mut() {
+ if capture(event, bounds, cx) {
+ return true;
+ }
+ }
+
if self.child.dispatch_event(event, cx) {
true
} else {
@@ -51,13 +51,15 @@ impl Presenter {
}
pub fn dispatch_path(&self, app: &AppContext) -> Vec<usize> {
- let mut view_id = app.focused_view_id(self.window_id).unwrap();
- let mut path = vec![view_id];
- while let Some(parent_id) = self.parents.get(&view_id).copied() {
- path.push(parent_id);
- view_id = parent_id;
+ let mut path = Vec::new();
+ if let Some(mut view_id) = app.focused_view_id(self.window_id) {
+ path.push(view_id);
+ while let Some(parent_id) = self.parents.get(&view_id).copied() {
+ path.push(parent_id);
+ view_id = parent_id;
+ }
+ path.reverse();
}
- path.reverse();
path
}
@@ -92,6 +92,7 @@ enum ProjectClientState {
sharing_has_stopped: bool,
remote_id: u64,
replica_id: ReplicaId,
+ _detect_unshare_task: Task<Option<()>>,
},
}
@@ -244,7 +245,7 @@ impl Project {
let mut status = rpc.status();
while let Some(status) = status.next().await {
if let Some(this) = this.upgrade(&cx) {
- let remote_id = if let client::Status::Connected { .. } = status {
+ let remote_id = if status.is_connected() {
let response = rpc.request(proto::RegisterProject {}).await?;
Some(response.project_id)
} else {
@@ -333,7 +334,7 @@ impl Project {
}
let (opened_buffer_tx, opened_buffer_rx) = watch::channel();
- let this = cx.add_model(|cx| {
+ let this = cx.add_model(|cx: &mut ModelContext<Self>| {
let mut this = Self {
worktrees: Vec::new(),
loading_buffers: Default::default(),
@@ -346,11 +347,26 @@ impl Project {
user_store: user_store.clone(),
fs,
subscriptions: vec![client.add_model_for_remote_entity(remote_id, cx)],
- client,
+ client: client.clone(),
client_state: ProjectClientState::Remote {
sharing_has_stopped: false,
remote_id,
replica_id,
+ _detect_unshare_task: cx.spawn_weak(move |this, mut cx| {
+ async move {
+ let mut status = client.status();
+ let is_connected =
+ status.next().await.map_or(false, |s| s.is_connected());
+ // Even if we're initially connected, any future change of the status means we momentarily disconnected.
+ if !is_connected || status.next().await.is_some() {
+ if let Some(this) = this.upgrade(&cx) {
+ this.update(&mut cx, |this, cx| this.project_unshared(cx))
+ }
+ }
+ Ok(())
+ }
+ .log_err()
+ }),
},
language_servers_with_diagnostics_running: 0,
language_servers: Default::default(),
@@ -666,6 +682,18 @@ impl Project {
})
}
+ fn project_unshared(&mut self, cx: &mut ModelContext<Self>) {
+ if let ProjectClientState::Remote {
+ sharing_has_stopped,
+ ..
+ } = &mut self.client_state
+ {
+ *sharing_has_stopped = true;
+ self.collaborators.clear();
+ cx.notify();
+ }
+ }
+
pub fn is_read_only(&self) -> bool {
match &self.client_state {
ProjectClientState::Local { .. } => false,
@@ -2672,20 +2700,7 @@ impl Project {
_: Arc<Client>,
mut cx: AsyncAppContext,
) -> Result<()> {
- this.update(&mut cx, |this, cx| {
- if let ProjectClientState::Remote {
- sharing_has_stopped,
- ..
- } = &mut this.client_state
- {
- *sharing_has_stopped = true;
- this.collaborators.clear();
- cx.notify();
- } else {
- unreachable!()
- }
- });
-
+ this.update(&mut cx, |this, cx| this.project_unshared(cx));
Ok(())
}
@@ -39,6 +39,7 @@ pub struct Workspace {
pub right_sidebar: Sidebar,
pub status_bar: StatusBar,
pub toolbar: Toolbar,
+ pub disconnected_overlay: ContainedText,
}
#[derive(Clone, Deserialize, Default)]
@@ -576,7 +576,13 @@ pub struct Workspace {
impl Workspace {
pub fn new(params: &WorkspaceParams, cx: &mut ViewContext<Self>) -> Self {
- cx.observe(¶ms.project, |_, _, cx| cx.notify()).detach();
+ cx.observe(¶ms.project, |_, project, cx| {
+ if project.read(cx).is_read_only() {
+ cx.blur();
+ }
+ cx.notify()
+ })
+ .detach();
let pane = cx.add_view(|_| Pane::new(params.settings.clone()));
let pane_id = pane.id();
@@ -1297,6 +1303,28 @@ impl Workspace {
None
}
}
+
+ fn render_disconnected_overlay(&self, cx: &AppContext) -> Option<ElementBox> {
+ if self.project.read(cx).is_read_only() {
+ let theme = &self.settings.borrow().theme;
+ Some(
+ EventHandler::new(
+ Label::new(
+ "Your connection to the remote project has been lost.".to_string(),
+ theme.workspace.disconnected_overlay.text.clone(),
+ )
+ .aligned()
+ .contained()
+ .with_style(theme.workspace.disconnected_overlay.container)
+ .boxed(),
+ )
+ .capture(|_, _, _| true)
+ .boxed(),
+ )
+ } else {
+ None
+ }
+ }
}
impl Entity for Workspace {
@@ -1311,39 +1339,51 @@ impl View for Workspace {
fn render(&mut self, cx: &mut RenderContext<Self>) -> ElementBox {
let settings = self.settings.borrow();
let theme = &settings.theme;
- Flex::column()
- .with_child(self.render_titlebar(&theme, cx))
+ Stack::new()
.with_child(
- Stack::new()
- .with_child({
- let mut content = Flex::row();
- content.add_child(self.left_sidebar.render(&settings, cx));
- if let Some(element) = self.left_sidebar.render_active_item(&settings, cx) {
- content.add_child(Flexible::new(0.8, false, element).boxed());
- }
- content.add_child(
- Flex::column()
- .with_child(
- Flexible::new(1., true, self.center.render(&settings.theme))
+ Flex::column()
+ .with_child(self.render_titlebar(&theme, cx))
+ .with_child(
+ Stack::new()
+ .with_child({
+ let mut content = Flex::row();
+ content.add_child(self.left_sidebar.render(&settings, cx));
+ if let Some(element) =
+ self.left_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, false, element).boxed());
+ }
+ content.add_child(
+ Flex::column()
+ .with_child(
+ Flexible::new(
+ 1.,
+ true,
+ self.center.render(&settings.theme),
+ )
+ .boxed(),
+ )
+ .with_child(ChildView::new(&self.status_bar).boxed())
+ .flexible(1., true)
.boxed(),
- )
- .with_child(ChildView::new(&self.status_bar).boxed())
- .flexible(1., true)
- .boxed(),
- );
- if let Some(element) = self.right_sidebar.render_active_item(&settings, cx)
- {
- content.add_child(Flexible::new(0.8, false, element).boxed());
- }
- content.add_child(self.right_sidebar.render(&settings, cx));
- content.boxed()
- })
- .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
- .flexible(1.0, true)
+ );
+ if let Some(element) =
+ self.right_sidebar.render_active_item(&settings, cx)
+ {
+ content.add_child(Flexible::new(0.8, false, element).boxed());
+ }
+ content.add_child(self.right_sidebar.render(&settings, cx));
+ content.boxed()
+ })
+ .with_children(self.modal.as_ref().map(|m| ChildView::new(m).boxed()))
+ .flexible(1.0, true)
+ .boxed(),
+ )
+ .contained()
+ .with_background_color(settings.theme.workspace.background)
.boxed(),
)
- .contained()
- .with_background_color(settings.theme.workspace.background)
+ .with_children(self.render_disconnected_overlay(cx))
.named("workspace")
}
@@ -60,3 +60,8 @@ emphasis = "#4ec9b0"
link_uri = { color = "#6a9955", underline = true }
link_text = { color = "#cb8f77", italic = true }
list_marker = "#4e94ce"
+
+[workspace.disconnected_overlay]
+extends = "$text.base"
+color = "#ffffff"
+background = "#000000aa"
@@ -60,3 +60,8 @@ emphasis = "#4ec9b0"
link_uri = { color = "#6a9955", underline = true }
link_text = { color = "#cb8f77", italic = true }
list_marker = "#4e94ce"
+
+[workspace.disconnected_overlay]
+extends = "$text.base"
+color = "#ffffff"
+background = "#000000aa"
@@ -60,3 +60,8 @@ emphasis = "#267f29"
link_uri = { color = "#6a9955", underline = true }
link_text = { color = "#a82121", italic = true }
list_marker = "#4e94ce"
+
+[workspace.disconnected_overlay]
+extends = "$text.base"
+color = "#ffffff"
+background = "#000000cc"