Show a "disconnected overlay" when project becomes read-only (#3733)
<img width="1136" alt="image" src="https://github.com/zed-industries/zed/assets/482957/9daaf21a-90d6-4074-9c65-360df5661221"> Release Notes: - N/A
This commit is contained in:
@@ -884,7 +884,11 @@ impl EditorElement {
|
||||
bounds: text_bounds,
|
||||
}),
|
||||
|cx| {
|
||||
if text_bounds.contains(&cx.mouse_position()) {
|
||||
let interactive_text_bounds = InteractiveBounds {
|
||||
bounds: text_bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
};
|
||||
if interactive_text_bounds.visibly_contains(&cx.mouse_position(), cx) {
|
||||
if self
|
||||
.editor
|
||||
.read(cx)
|
||||
@@ -1361,8 +1365,12 @@ impl EditorElement {
|
||||
));
|
||||
}
|
||||
|
||||
let interactive_track_bounds = InteractiveBounds {
|
||||
bounds: track_bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
};
|
||||
let mut mouse_position = cx.mouse_position();
|
||||
if track_bounds.contains(&mouse_position) {
|
||||
if interactive_track_bounds.visibly_contains(&mouse_position, cx) {
|
||||
cx.set_cursor_style(CursorStyle::Arrow);
|
||||
}
|
||||
|
||||
@@ -1392,7 +1400,7 @@ impl EditorElement {
|
||||
cx.stop_propagation();
|
||||
} else {
|
||||
editor.scroll_manager.set_is_dragging_scrollbar(false, cx);
|
||||
if track_bounds.contains(&event.position) {
|
||||
if interactive_track_bounds.visibly_contains(&event.position, cx) {
|
||||
editor.scroll_manager.show_scrollbar(cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -62,6 +62,18 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn capture_any_mouse_down(
|
||||
&mut self,
|
||||
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) {
|
||||
self.mouse_down_listeners
|
||||
.push(Box::new(move |event, bounds, phase, cx| {
|
||||
if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) {
|
||||
(listener)(event, cx)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn on_any_mouse_down(
|
||||
&mut self,
|
||||
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
@@ -90,6 +102,18 @@ impl Interactivity {
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn capture_any_mouse_up(
|
||||
&mut self,
|
||||
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
|
||||
) {
|
||||
self.mouse_up_listeners
|
||||
.push(Box::new(move |event, bounds, phase, cx| {
|
||||
if phase == DispatchPhase::Capture && bounds.visibly_contains(&event.position, cx) {
|
||||
(listener)(event, cx)
|
||||
}
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn on_any_mouse_up(
|
||||
&mut self,
|
||||
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
|
||||
@@ -384,6 +408,14 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
fn capture_any_mouse_down(
|
||||
mut self,
|
||||
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().capture_any_mouse_down(listener);
|
||||
self
|
||||
}
|
||||
|
||||
fn on_any_mouse_down(
|
||||
mut self,
|
||||
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
@@ -401,6 +433,14 @@ pub trait InteractiveElement: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
fn capture_any_mouse_up(
|
||||
mut self,
|
||||
listener: impl Fn(&MouseUpEvent, &mut WindowContext) + 'static,
|
||||
) -> Self {
|
||||
self.interactivity().capture_any_mouse_up(listener);
|
||||
self
|
||||
}
|
||||
|
||||
fn on_mouse_down_out(
|
||||
mut self,
|
||||
listener: impl Fn(&MouseDownEvent, &mut WindowContext) + 'static,
|
||||
|
||||
@@ -358,14 +358,13 @@ impl Element for InteractiveText {
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, state: &mut Self::State, cx: &mut WindowContext) {
|
||||
if let Some(click_listener) = self.click_listener.take() {
|
||||
if let Some(ix) = state
|
||||
.text_state
|
||||
.index_for_position(bounds, cx.mouse_position())
|
||||
{
|
||||
let mouse_position = cx.mouse_position();
|
||||
if let Some(ix) = state.text_state.index_for_position(bounds, mouse_position) {
|
||||
if self
|
||||
.clickable_ranges
|
||||
.iter()
|
||||
.any(|range| range.contains(&ix))
|
||||
&& cx.was_top_layer(&mouse_position, cx.stacking_order())
|
||||
{
|
||||
cx.set_cursor_style(crate::CursorStyle::PointingHand)
|
||||
}
|
||||
|
||||
@@ -273,6 +273,7 @@ pub struct Window {
|
||||
pub(crate) drawing: bool,
|
||||
activation_observers: SubscriberSet<(), AnyObserver>,
|
||||
pub(crate) focus: Option<FocusId>,
|
||||
focus_enabled: bool,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) focus_invalidated: bool,
|
||||
@@ -420,6 +421,7 @@ impl Window {
|
||||
drawing: false,
|
||||
activation_observers: SubscriberSet::new(),
|
||||
focus: None,
|
||||
focus_enabled: true,
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
focus_invalidated: false,
|
||||
@@ -496,7 +498,7 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Move focus to the element associated with the given `FocusHandle`.
|
||||
pub fn focus(&mut self, handle: &FocusHandle) {
|
||||
if self.window.focus == Some(handle.id) {
|
||||
if !self.window.focus_enabled || self.window.focus == Some(handle.id) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -516,10 +518,19 @@ impl<'a> WindowContext<'a> {
|
||||
|
||||
/// Remove focus from all elements within this context's window.
|
||||
pub fn blur(&mut self) {
|
||||
if !self.window.focus_enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
self.window.focus = None;
|
||||
self.notify();
|
||||
}
|
||||
|
||||
pub fn disable_focus(&mut self) {
|
||||
self.blur();
|
||||
self.window.focus_enabled = false;
|
||||
}
|
||||
|
||||
pub fn dispatch_action(&mut self, action: Box<dyn Action>) {
|
||||
let focus_handle = self.focused();
|
||||
|
||||
|
||||
@@ -564,9 +564,9 @@ mod element {
|
||||
use std::{cell::RefCell, iter, rc::Rc, sync::Arc};
|
||||
|
||||
use gpui::{
|
||||
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, IntoElement,
|
||||
MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point, Size, Style,
|
||||
WindowContext,
|
||||
px, relative, Along, AnyElement, Axis, Bounds, CursorStyle, Element, InteractiveBounds,
|
||||
IntoElement, MouseDownEvent, MouseMoveEvent, MouseUpEvent, ParentElement, Pixels, Point,
|
||||
Size, Style, WindowContext,
|
||||
};
|
||||
use parking_lot::Mutex;
|
||||
use smallvec::SmallVec;
|
||||
@@ -717,7 +717,11 @@ mod element {
|
||||
};
|
||||
|
||||
cx.with_z_index(3, |cx| {
|
||||
if handle_bounds.contains(&cx.mouse_position()) {
|
||||
let interactive_handle_bounds = InteractiveBounds {
|
||||
bounds: handle_bounds,
|
||||
stacking_order: cx.stacking_order().clone(),
|
||||
};
|
||||
if interactive_handle_bounds.visibly_contains(&cx.mouse_position(), cx) {
|
||||
cx.set_cursor_style(match axis {
|
||||
Axis::Vertical => CursorStyle::ResizeUpDown,
|
||||
Axis::Horizontal => CursorStyle::ResizeLeftRight,
|
||||
|
||||
@@ -25,12 +25,13 @@ use futures::{
|
||||
Future, FutureExt, StreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, canvas, div, impl_actions, point, size, Action, AnyModel, AnyView, AnyWeakView,
|
||||
AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, Bounds, Context, Div,
|
||||
DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, FocusableView, GlobalPixels,
|
||||
InteractiveElement, KeyContext, ManagedView, Model, ModelContext, ParentElement,
|
||||
PathPromptOptions, Pixels, Point, PromptLevel, Render, Size, Styled, Subscription, Task, View,
|
||||
ViewContext, VisualContext, WeakView, WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
actions, canvas, div, impl_actions, point, size, Action, AnyElement, AnyModel, AnyView,
|
||||
AnyWeakView, AnyWindowHandle, AppContext, AsyncAppContext, AsyncWindowContext, BorrowWindow,
|
||||
Bounds, Context, Div, DragMoveEvent, Element, Entity, EntityId, EventEmitter, FocusHandle,
|
||||
FocusableView, GlobalPixels, InteractiveElement, IntoElement, KeyContext, LayoutId,
|
||||
ManagedView, Model, ModelContext, ParentElement, PathPromptOptions, Pixels, Point, PromptLevel,
|
||||
Render, Size, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
|
||||
WindowBounds, WindowContext, WindowHandle, WindowOptions,
|
||||
};
|
||||
use item::{FollowableItem, FollowableItemHandle, Item, ItemHandle, ItemSettings, ProjectItem};
|
||||
use itertools::Itertools;
|
||||
@@ -64,6 +65,7 @@ use std::{
|
||||
use theme::{ActiveTheme, ThemeSettings};
|
||||
pub use toolbar::{Toolbar, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView};
|
||||
pub use ui;
|
||||
use ui::Label;
|
||||
use util::ResultExt;
|
||||
use uuid::Uuid;
|
||||
pub use workspace_settings::{AutosaveSetting, WorkspaceSettings};
|
||||
@@ -502,7 +504,7 @@ impl Workspace {
|
||||
|
||||
project::Event::DisconnectedFromHost => {
|
||||
this.update_window_edited(cx);
|
||||
cx.blur();
|
||||
cx.disable_focus();
|
||||
}
|
||||
|
||||
project::Event::Closed => {
|
||||
@@ -2519,32 +2521,6 @@ impl Workspace {
|
||||
}
|
||||
}
|
||||
|
||||
// fn render_disconnected_overlay(
|
||||
// &self,
|
||||
// cx: &mut ViewContext<Workspace>,
|
||||
// ) -> Option<AnyElement<Workspace>> {
|
||||
// if self.project.read(cx).is_read_only() {
|
||||
// enum DisconnectedOverlay {}
|
||||
// Some(
|
||||
// MouseEventHandler::new::<DisconnectedOverlay, _>(0, cx, |_, cx| {
|
||||
// let theme = &theme::current(cx);
|
||||
// Label::new(
|
||||
// "Your connection to the remote project has been lost.",
|
||||
// theme.workspace.disconnected_overlay.text.clone(),
|
||||
// )
|
||||
// .aligned()
|
||||
// .contained()
|
||||
// .with_style(theme.workspace.disconnected_overlay.container)
|
||||
// })
|
||||
// .with_cursor_style(CursorStyle::Arrow)
|
||||
// .capture_all()
|
||||
// .into_any_named("disconnected overlay"),
|
||||
// )
|
||||
// } else {
|
||||
// None
|
||||
// }
|
||||
// }
|
||||
|
||||
fn render_notifications(&self, _cx: &ViewContext<Self>) -> Option<Div> {
|
||||
if self.notifications.is_empty() {
|
||||
None
|
||||
@@ -3661,6 +3637,11 @@ impl Render for Workspace {
|
||||
})),
|
||||
)
|
||||
.child(self.status_bar.clone())
|
||||
.children(if self.project.read(cx).is_read_only() {
|
||||
Some(DisconnectedOverlay)
|
||||
} else {
|
||||
None
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4284,6 +4265,56 @@ fn parse_pixel_size_env_var(value: &str) -> Option<Size<GlobalPixels>> {
|
||||
Some(size((width as f64).into(), (height as f64).into()))
|
||||
}
|
||||
|
||||
struct DisconnectedOverlay;
|
||||
|
||||
impl Element for DisconnectedOverlay {
|
||||
type State = AnyElement;
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
_: Option<Self::State>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (LayoutId, Self::State) {
|
||||
let mut background = cx.theme().colors().elevated_surface_background;
|
||||
background.fade_out(0.2);
|
||||
let mut overlay = div()
|
||||
.bg(background)
|
||||
.absolute()
|
||||
.left_0()
|
||||
.top_0()
|
||||
.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.layout(cx), overlay)
|
||||
}
|
||||
|
||||
fn paint(&mut self, bounds: Bounds<Pixels>, overlay: &mut Self::State, cx: &mut WindowContext) {
|
||||
cx.with_z_index(u8::MAX, |cx| {
|
||||
cx.add_opaque_layer(bounds);
|
||||
overlay.paint(cx);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoElement for DisconnectedOverlay {
|
||||
type Element = Self;
|
||||
|
||||
fn element_id(&self) -> Option<ui::prelude::ElementId> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
|
||||
Reference in New Issue
Block a user