Fix popin for project panel by pre-resolving keybindings in the project panel (#4141)

Also, factors out the fluent building APIs from IntoElement into their
own traits.

Also makes the project panel context menu initialization fluent:

<img width="1328" alt="Screenshot 2024-01-18 at 3 33 45 PM"
src="https://github.com/zed-industries/zed/assets/2280405/3468b6f2-07f0-48cf-bec1-ac0379333209">

Release Notes:

- Fixed pop in when right clicking on the project panel.
This commit is contained in:
Mikayla Maki 2024-01-18 16:16:50 -08:00 committed by GitHub
commit bac2e59eac
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 133 additions and 104 deletions

View file

@ -1,5 +1,6 @@
use gpui::{div, AnyElement, Div, IntoElement, ParentElement, Styled}; use gpui::{div, AnyElement, Div, ParentElement, Styled};
use smallvec::SmallVec; use smallvec::SmallVec;
use ui::FluentBuilder;
#[derive(Default)] #[derive(Default)]
pub struct FacePile { pub struct FacePile {

View file

@ -1,6 +1,6 @@
use crate::{ use crate::{
ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId, Pixels, Point, Size, util::FluentBuilder, ArenaBox, AvailableSpace, BorrowWindow, Bounds, ElementId, LayoutId,
ViewContext, WindowContext, ELEMENT_ARENA, Pixels, Point, Size, ViewContext, WindowContext, ELEMENT_ARENA,
}; };
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
pub(crate) use smallvec::SmallVec; pub(crate) use smallvec::SmallVec;
@ -77,40 +77,10 @@ pub trait IntoElement: Sized {
}) })
} }
} }
/// Convert self to another type by calling the given closure. Useful in rendering code.
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where
Self: Sized,
U: IntoElement,
{
f(self)
}
/// Conditionally chain onto self with the given closure. Useful in rendering code.
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { this })
}
/// Conditionally chain onto self with the given closure if the given option is Some.
/// The contents of the option are provided to the closure.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| {
if let Some(value) = option {
then(this, value)
} else {
this
}
})
}
} }
impl<T: IntoElement> FluentBuilder for T {}
pub trait Render: 'static + Sized { pub trait Render: 'static + Sized {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement; fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement;
} }

View file

@ -1,5 +1,5 @@
pub use crate::{ pub use crate::{
BorrowAppContext, BorrowWindow, Context, Element, FocusableElement, InteractiveElement, util::FluentBuilder, BorrowAppContext, BorrowWindow, Context, Element, FocusableElement,
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, InteractiveElement, IntoElement, ParentElement, Refineable, Render, RenderOnce,
VisualContext, StatefulInteractiveElement, Styled, VisualContext,
}; };

View file

@ -9,6 +9,58 @@ use smol::future::FutureExt;
pub use util::*; pub use util::*;
/// A helper trait for building complex objects with imperative conditionals in a fluent style.
pub trait FluentBuilder {
/// Imperatively modify self with the given closure.
fn map<U>(self, f: impl FnOnce(Self) -> U) -> U
where
Self: Sized,
{
f(self)
}
/// Conditionally modify self with the given closure.
fn when(self, condition: bool, then: impl FnOnce(Self) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| if condition { then(this) } else { this })
}
/// Conditionally unwrap and modify self with the given closure, if the given option is Some.
fn when_some<T>(self, option: Option<T>, then: impl FnOnce(Self, T) -> Self) -> Self
where
Self: Sized,
{
self.map(|this| {
if let Some(value) = option {
then(this, value)
} else {
this
}
})
}
/// Conditionally modify self with one closure or another
fn when_else(
self,
condition: bool,
then: impl FnOnce(Self) -> Self,
otherwise: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| {
if condition {
then(this)
} else {
otherwise(this)
}
})
}
}
#[cfg(any(test, feature = "test-support"))] #[cfg(any(test, feature = "test-support"))]
pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()> pub async fn timeout<F, T>(timeout: Duration, f: F) -> Result<T, ()>
where where

View file

@ -1,7 +1,7 @@
use editor::Editor; use editor::Editor;
use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView}; use gpui::{div, IntoElement, ParentElement, Render, Subscription, View, ViewContext, WeakView};
use std::sync::Arc; use std::sync::Arc;
use ui::{Button, ButtonCommon, Clickable, LabelSize, Tooltip}; use ui::{Button, ButtonCommon, Clickable, FluentBuilder, LabelSize, Tooltip};
use workspace::{item::ItemHandle, StatusItemView, Workspace}; use workspace::{item::ItemHandle, StatusItemView, Workspace};
use crate::LanguageSelector; use crate::LanguageSelector;

View file

@ -381,67 +381,57 @@ impl ProjectPanel {
let is_local = project.is_local(); let is_local = project.is_local();
let is_read_only = project.is_read_only(); let is_read_only = project.is_read_only();
let context_menu = ContextMenu::build(cx, |mut menu, cx| { let context_menu = ContextMenu::build(cx, |menu, cx| {
if is_read_only { menu.context(self.focus_handle.clone()).when_else(
menu = menu.action("Copy Relative Path", Box::new(CopyRelativePath)); is_read_only,
if is_dir { |menu| {
menu = menu.action("Search Inside", Box::new(NewSearchInDirectory)) menu.action("Copy Relative Path", Box::new(CopyRelativePath))
} .when(is_dir, |menu| {
menu.action("Search Inside", Box::new(NewSearchInDirectory))
return menu; })
} },
|menu| {
if is_local { menu.when(is_local, |menu| {
menu = menu.action( menu.action(
"Add Folder to Project", "Add Folder to Project",
Box::new(workspace::AddFolderToProject), Box::new(workspace::AddFolderToProject),
); )
if is_root { .when(is_root, |menu| {
menu = menu.entry( menu.entry(
"Remove from Project", "Remove from Project",
None, None,
cx.handler_for(&this, move |this, cx| { cx.handler_for(&this, move |this, cx| {
this.project.update(cx, |project, cx| { this.project.update(cx, |project, cx| {
project.remove_worktree(worktree_id, cx) project.remove_worktree(worktree_id, cx)
}); });
}), }),
); )
} })
} })
.action("New File", Box::new(NewFile))
menu = menu .action("New Folder", Box::new(NewDirectory))
.action("New File", Box::new(NewFile)) .separator()
.action("New Folder", Box::new(NewDirectory)) .action("Cut", Box::new(Cut))
.separator() .action("Copy", Box::new(Copy))
.action("Cut", Box::new(Cut)) .when_some(self.clipboard_entry, |menu, entry| {
.action("Copy", Box::new(Copy)); menu.when(entry.worktree_id() == worktree_id, |menu| {
menu.action("Paste", Box::new(Paste))
if let Some(clipboard_entry) = self.clipboard_entry { })
if clipboard_entry.worktree_id() == worktree_id { })
menu = menu.action("Paste", Box::new(Paste)); .separator()
} .action("Copy Path", Box::new(CopyPath))
} .action("Copy Relative Path", Box::new(CopyRelativePath))
.separator()
menu = menu .action("Reveal in Finder", Box::new(RevealInFinder))
.separator() .when(is_dir, |menu| {
.action("Copy Path", Box::new(CopyPath)) menu.action("Open in Terminal", Box::new(OpenInTerminal))
.action("Copy Relative Path", Box::new(CopyRelativePath)) .action("Search Inside", Box::new(NewSearchInDirectory))
.separator() })
.action("Reveal in Finder", Box::new(RevealInFinder)); .separator()
.action("Rename", Box::new(Rename))
if is_dir { .when(!is_root, |menu| menu.action("Delete", Box::new(Delete)))
menu = menu },
.action("Open in Terminal", Box::new(OpenInTerminal)) )
.action("Search Inside", Box::new(NewSearchInDirectory))
}
menu = menu.separator().action("Rename", Box::new(Rename));
if !is_root {
menu = menu.action("Delete", Box::new(Delete));
}
menu
}); });
cx.focus_view(&context_menu); cx.focus_view(&context_menu);

View file

@ -27,6 +27,7 @@ enum ContextMenuItem {
pub struct ContextMenu { pub struct ContextMenu {
items: Vec<ContextMenuItem>, items: Vec<ContextMenuItem>,
focus_handle: FocusHandle, focus_handle: FocusHandle,
action_context: Option<FocusHandle>,
selected_index: Option<usize>, selected_index: Option<usize>,
delayed: bool, delayed: bool,
clicked: bool, clicked: bool,
@ -41,6 +42,8 @@ impl FocusableView for ContextMenu {
impl EventEmitter<DismissEvent> for ContextMenu {} impl EventEmitter<DismissEvent> for ContextMenu {}
impl FluentBuilder for ContextMenu {}
impl ContextMenu { impl ContextMenu {
pub fn build( pub fn build(
cx: &mut WindowContext, cx: &mut WindowContext,
@ -56,6 +59,7 @@ impl ContextMenu {
Self { Self {
items: Default::default(), items: Default::default(),
focus_handle, focus_handle,
action_context: None,
selected_index: None, selected_index: None,
delayed: false, delayed: false,
clicked: false, clicked: false,
@ -66,6 +70,11 @@ impl ContextMenu {
}) })
} }
pub fn context(mut self, focus: FocusHandle) -> Self {
self.action_context = Some(focus);
self
}
pub fn header(mut self, title: impl Into<SharedString>) -> Self { pub fn header(mut self, title: impl Into<SharedString>) -> Self {
self.items.push(ContextMenuItem::Header(title.into())); self.items.push(ContextMenuItem::Header(title.into()));
self self
@ -305,7 +314,14 @@ impl Render for ContextMenu {
.child(label_element) .child(label_element)
.debug_selector(|| format!("MENU_ITEM-{}", label)) .debug_selector(|| format!("MENU_ITEM-{}", label))
.children(action.as_ref().and_then(|action| { .children(action.as_ref().and_then(|action| {
KeyBinding::for_action(&**action, cx) self.action_context
.as_ref()
.map(|focus| {
KeyBinding::for_action_in(&**action, focus, cx)
})
.unwrap_or_else(|| {
KeyBinding::for_action(&**action, cx)
})
.map(|binding| div().ml_1().child(binding)) .map(|binding| div().ml_1().child(binding))
})), })),
) )

View file

@ -1,9 +1,9 @@
use std::{cell::RefCell, rc::Rc}; use std::{cell::RefCell, rc::Rc};
use gpui::{ use gpui::{
overlay, point, px, rems, AnchorCorner, AnyElement, Bounds, DismissEvent, DispatchPhase, overlay, point, prelude::FluentBuilder, px, rems, AnchorCorner, AnyElement, Bounds,
Element, ElementId, InteractiveBounds, IntoElement, LayoutId, ManagedView, MouseDownEvent, DismissEvent, DispatchPhase, Element, ElementId, InteractiveBounds, IntoElement, LayoutId,
ParentElement, Pixels, Point, View, VisualContext, WindowContext, ManagedView, MouseDownEvent, ParentElement, Pixels, Point, View, VisualContext, WindowContext,
}; };
use crate::{Clickable, Selectable}; use crate::{Clickable, Selectable};