@@ -166,11 +166,11 @@ impl Render for TitleBar {
.when(title_bar_settings.show_project_items, |title_bar| {
title_bar
.children(self.render_restricted_mode(cx))
- .children(self.render_project_host(cx))
- .child(self.render_project_name(cx))
+ .children(self.render_project_host(window, cx))
+ .child(self.render_project_name(window, cx))
})
.when(title_bar_settings.show_branch_name, |title_bar| {
- title_bar.children(self.render_project_repo(cx))
+ title_bar.children(self.render_project_repo(window, cx))
})
})
})
@@ -350,7 +350,14 @@ impl TitleBar {
.next()
}
- fn render_remote_project_connection(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
+ fn render_remote_project_connection(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<AnyElement> {
+ let workspace = self.workspace.clone();
+ let is_picker_open = self.is_picker_open(window, cx);
+
let options = self.project.read(cx).remote_connection_options(cx)?;
let host: SharedString = options.display_name().into();
@@ -395,7 +402,7 @@ impl TitleBar {
let meta = SharedString::from(meta);
Some(
- ButtonLike::new("ssh-server-icon")
+ ButtonLike::new("remote_project")
.child(
h_flex()
.gap_2()
@@ -410,26 +417,35 @@ impl TitleBar {
)
.child(Label::new(nickname).size(LabelSize::Small).truncate()),
)
- .tooltip(move |_window, cx| {
- Tooltip::with_meta(
- tooltip_title,
- Some(&OpenRemote {
- from_existing_connection: false,
- create_new_window: false,
- }),
- meta.clone(),
- cx,
- )
+ .when(!is_picker_open, |this| {
+ this.tooltip(move |_window, cx| {
+ Tooltip::with_meta(
+ tooltip_title,
+ Some(&OpenRemote {
+ from_existing_connection: false,
+ create_new_window: false,
+ }),
+ meta.clone(),
+ cx,
+ )
+ })
})
- .on_click(|_, window, cx| {
- window.dispatch_action(
- OpenRemote {
- from_existing_connection: false,
- create_new_window: false,
- }
- .boxed_clone(),
- cx,
- );
+ .on_click(move |event, window, cx| {
+ let position = event.position();
+ let _ = workspace.update(cx, |this, cx| {
+ this.set_next_modal_placement(workspace::ModalPlacement::Anchored {
+ position,
+ });
+
+ window.dispatch_action(
+ OpenRemote {
+ from_existing_connection: false,
+ create_new_window: false,
+ }
+ .boxed_clone(),
+ cx,
+ );
+ });
})
.into_any_element(),
)
@@ -481,9 +497,13 @@ impl TitleBar {
}
}
- pub fn render_project_host(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
+ pub fn render_project_host(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<AnyElement> {
if self.project.read(cx).is_via_remote_server() {
- return self.render_remote_project_connection(cx);
+ return self.render_remote_project_connection(window, cx);
}
if self.project.read(cx).is_disconnected(cx) {
@@ -491,7 +511,6 @@ impl TitleBar {
Button::new("disconnected", "Disconnected")
.disabled(true)
.color(Color::Disabled)
- .style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
.into_any_element(),
);
@@ -504,15 +523,19 @@ impl TitleBar {
.read(cx)
.participant_indices()
.get(&host_user.id)?;
+
Some(
Button::new("project_owner_trigger", host_user.github_login.clone())
.color(Color::Player(participant_index.0))
- .style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
- .tooltip(Tooltip::text(format!(
- "{} is sharing this project. Click to follow.",
- host_user.github_login
- )))
+ .tooltip(move |_, cx| {
+ let tooltip_title = format!(
+ "{} is sharing this project. Click to follow.",
+ host_user.github_login
+ );
+
+ Tooltip::with_meta(tooltip_title, None, "Click to Follow", cx)
+ })
.on_click({
let host_peer_id = host.peer_id;
cx.listener(move |this, _, window, cx| {
@@ -527,7 +550,14 @@ impl TitleBar {
)
}
- pub fn render_project_name(&self, cx: &mut Context<Self>) -> impl IntoElement {
+ pub fn render_project_name(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> impl IntoElement {
+ let workspace = self.workspace.clone();
+ let is_picker_open = self.is_picker_open(window, cx);
+
let name = self.project_name(cx);
let is_project_selected = name.is_some();
let name = if let Some(name) = name {
@@ -537,19 +567,25 @@ impl TitleBar {
};
Button::new("project_name_trigger", name)
- .when(!is_project_selected, |b| b.color(Color::Muted))
- .style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
- .tooltip(move |_window, cx| {
- Tooltip::for_action(
- "Recent Projects",
- &zed_actions::OpenRecent {
- create_new_window: false,
- },
- cx,
- )
+ .when(!is_project_selected, |s| s.color(Color::Muted))
+ .when(!is_picker_open, |this| {
+ this.tooltip(move |_window, cx| {
+ Tooltip::for_action(
+ "Recent Projects",
+ &zed_actions::OpenRecent {
+ create_new_window: false,
+ },
+ cx,
+ )
+ })
})
- .on_click(cx.listener(move |_, _, window, cx| {
+ .on_click(move |event, window, cx| {
+ let position = event.position();
+ let _ = workspace.update(cx, |this, _cx| {
+ this.set_next_modal_placement(workspace::ModalPlacement::Anchored { position })
+ });
+
window.dispatch_action(
OpenRecent {
create_new_window: false,
@@ -557,84 +593,102 @@ impl TitleBar {
.boxed_clone(),
cx,
);
- }))
+ })
}
- pub fn render_project_repo(&self, cx: &mut Context<Self>) -> Option<impl IntoElement> {
- let settings = TitleBarSettings::get_global(cx);
+ pub fn render_project_repo(
+ &self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Option<impl IntoElement> {
let repository = self.project.read(cx).active_repository(cx)?;
let repository_count = self.project.read(cx).repositories(cx).len();
let workspace = self.workspace.upgrade()?;
- let repo = repository.read(cx);
- let branch_name = repo
- .branch
- .as_ref()
- .map(|branch| branch.name())
- .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
- .or_else(|| {
- repo.head_commit.as_ref().map(|commit| {
- commit
- .sha
- .chars()
- .take(MAX_SHORT_SHA_LENGTH)
- .collect::<String>()
- })
- })?;
- let project_name = self.project_name(cx);
- let repo_name = repo
- .work_directory_abs_path
- .file_name()
- .and_then(|name| name.to_str())
- .map(SharedString::new);
- let show_repo_name =
- repository_count > 1 && repo.branch.is_some() && repo_name != project_name;
- let branch_name = if let Some(repo_name) = repo_name.filter(|_| show_repo_name) {
- format!("{repo_name}/{branch_name}")
- } else {
- branch_name
+
+ let (branch_name, icon_info) = {
+ let repo = repository.read(cx);
+ let branch_name = repo
+ .branch
+ .as_ref()
+ .map(|branch| branch.name())
+ .map(|name| util::truncate_and_trailoff(name, MAX_BRANCH_NAME_LENGTH))
+ .or_else(|| {
+ repo.head_commit.as_ref().map(|commit| {
+ commit
+ .sha
+ .chars()
+ .take(MAX_SHORT_SHA_LENGTH)
+ .collect::<String>()
+ })
+ });
+
+ let branch_name = branch_name?;
+
+ let project_name = self.project_name(cx);
+ let repo_name = repo
+ .work_directory_abs_path
+ .file_name()
+ .and_then(|name| name.to_str())
+ .map(SharedString::new);
+ let show_repo_name =
+ repository_count > 1 && repo.branch.is_some() && repo_name != project_name;
+ let branch_name = if let Some(repo_name) = repo_name.filter(|_| show_repo_name) {
+ format!("{repo_name}/{branch_name}")
+ } else {
+ branch_name
+ };
+
+ let status = repo.status_summary();
+ let tracked = status.index + status.worktree;
+ let icon_info = if status.conflict > 0 {
+ (IconName::Warning, Color::VersionControlConflict)
+ } else if tracked.modified > 0 {
+ (IconName::SquareDot, Color::VersionControlModified)
+ } else if tracked.added > 0 || status.untracked > 0 {
+ (IconName::SquarePlus, Color::VersionControlAdded)
+ } else if tracked.deleted > 0 {
+ (IconName::SquareMinus, Color::VersionControlDeleted)
+ } else {
+ (IconName::GitBranch, Color::Muted)
+ };
+
+ (branch_name, icon_info)
};
+ let is_picker_open = self.is_picker_open(window, cx);
+ let settings = TitleBarSettings::get_global(cx);
+
Some(
Button::new("project_branch_trigger", branch_name)
- .color(Color::Muted)
- .style(ButtonStyle::Subtle)
.label_size(LabelSize::Small)
- .tooltip(move |_window, cx| {
- Tooltip::with_meta(
- "Recent Branches",
- Some(&zed_actions::git::Branch),
- "Local branches only",
- cx,
- )
- })
- .on_click(move |_, window, cx| {
- let _ = workspace.update(cx, |this, cx| {
- window.focus(&this.active_pane().focus_handle(cx), cx);
- window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
- });
+ .color(Color::Muted)
+ .when(!is_picker_open, |this| {
+ this.tooltip(move |_window, cx| {
+ Tooltip::with_meta(
+ "Recent Branches",
+ Some(&zed_actions::git::Branch),
+ "Local branches only",
+ cx,
+ )
+ })
})
.when(settings.show_branch_icon, |branch_button| {
- let (icon, icon_color) = {
- let status = repo.status_summary();
- let tracked = status.index + status.worktree;
- if status.conflict > 0 {
- (IconName::Warning, Color::VersionControlConflict)
- } else if tracked.modified > 0 {
- (IconName::SquareDot, Color::VersionControlModified)
- } else if tracked.added > 0 || status.untracked > 0 {
- (IconName::SquarePlus, Color::VersionControlAdded)
- } else if tracked.deleted > 0 {
- (IconName::SquareMinus, Color::VersionControlDeleted)
- } else {
- (IconName::GitBranch, Color::Muted)
- }
- };
-
+ let (icon, icon_color) = icon_info;
branch_button
.icon(icon)
.icon_position(IconPosition::Start)
.icon_color(icon_color)
.icon_size(IconSize::Indicator)
+ })
+ .on_click(move |event, window, cx| {
+ let position = event.position();
+ let _ = workspace.update(cx, |this, cx| {
+ this.set_next_modal_placement(workspace::ModalPlacement::Anchored {
+ position,
+ });
+ window.focus(&this.active_pane().focus_handle(cx), cx);
+ window.dispatch_action(zed_actions::git::Branch.boxed_clone(), cx);
+ });
}),
)
}
@@ -726,7 +780,7 @@ impl TitleBar {
pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
let client = self.client.clone();
- Button::new("sign_in", "Sign in")
+ Button::new("sign_in", "Sign In")
.label_size(LabelSize::Small)
.on_click(move |_, window, cx| {
let client = client.clone();
@@ -848,4 +902,10 @@ impl TitleBar {
})
.anchor(gpui::Corner::TopRight)
}
+
+ fn is_picker_open(&self, window: &mut Window, cx: &mut Context<Self>) -> bool {
+ self.workspace
+ .update(cx, |workspace, cx| workspace.has_active_modal(window, cx))
+ .unwrap_or(false)
+ }
}
@@ -1,9 +1,18 @@
use gpui::{
AnyView, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable as _, ManagedView,
- MouseButton, Subscription,
+ MouseButton, Pixels, Point, Subscription,
};
use ui::prelude::*;
+#[derive(Debug, Clone, Copy, Default)]
+pub enum ModalPlacement {
+ #[default]
+ Centered,
+ Anchored {
+ position: Point<Pixels>,
+ },
+}
+
#[derive(Debug)]
pub enum DismissDecision {
Dismiss(bool),
@@ -58,6 +67,7 @@ pub struct ActiveModal {
_subscriptions: [Subscription; 2],
previous_focus_handle: Option<FocusHandle>,
focus_handle: FocusHandle,
+ placement: ModalPlacement,
}
pub struct ModalLayer {
@@ -87,6 +97,19 @@ impl ModalLayer {
where
V: ModalView,
B: FnOnce(&mut Window, &mut Context<V>) -> V,
+ {
+ self.toggle_modal_with_placement(window, cx, ModalPlacement::Centered, build_view);
+ }
+
+ pub fn toggle_modal_with_placement<V, B>(
+ &mut self,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ placement: ModalPlacement,
+ build_view: B,
+ ) where
+ V: ModalView,
+ B: FnOnce(&mut Window, &mut Context<V>) -> V,
{
if let Some(active_modal) = &self.active_modal {
let is_close = active_modal.modal.view().downcast::<V>().is_ok();
@@ -96,12 +119,17 @@ impl ModalLayer {
}
}
let new_modal = cx.new(|cx| build_view(window, cx));
- self.show_modal(new_modal, window, cx);
+ self.show_modal(new_modal, placement, window, cx);
cx.emit(ModalOpenedEvent);
}
- fn show_modal<V>(&mut self, new_modal: Entity<V>, window: &mut Window, cx: &mut Context<Self>)
- where
+ fn show_modal<V>(
+ &mut self,
+ new_modal: Entity<V>,
+ placement: ModalPlacement,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) where
V: ModalView,
{
let focus_handle = cx.focus_handle();
@@ -123,6 +151,7 @@ impl ModalLayer {
],
previous_focus_handle: window.focused(cx),
focus_handle,
+ placement,
});
cx.defer_in(window, move |_, window, cx| {
window.focus(&new_modal.focus_handle(cx), cx);
@@ -183,6 +212,30 @@ impl Render for ModalLayer {
return active_modal.modal.view().into_any_element();
}
+ let content = h_flex()
+ .occlude()
+ .child(active_modal.modal.view())
+ .on_mouse_down(MouseButton::Left, |_, _, cx| {
+ cx.stop_propagation();
+ });
+
+ let positioned = match active_modal.placement {
+ ModalPlacement::Centered => v_flex()
+ .h(px(0.0))
+ .top_20()
+ .items_center()
+ .track_focus(&active_modal.focus_handle)
+ .child(content)
+ .into_any_element(),
+ ModalPlacement::Anchored { position } => div()
+ .absolute()
+ .left(position.x)
+ .top(position.y - px(20.))
+ .track_focus(&active_modal.focus_handle)
+ .child(content)
+ .into_any_element(),
+ };
+
div()
.absolute()
.size_full()
@@ -199,21 +252,7 @@ impl Render for ModalLayer {
this.hide_modal(window, cx);
}),
)
- .child(
- v_flex()
- .h(px(0.0))
- .top_20()
- .items_center()
- .track_focus(&active_modal.focus_handle)
- .child(
- h_flex()
- .occlude()
- .child(active_modal.modal.view())
- .on_mouse_down(MouseButton::Left, |_, _, cx| {
- cx.stop_propagation();
- }),
- ),
- )
+ .child(positioned)
.into_any_element()
}
}
@@ -1204,6 +1204,7 @@ pub struct Workspace {
last_open_dock_positions: Vec<DockPosition>,
removing: bool,
utility_panes: UtilityPaneState,
+ next_modal_placement: Option<ModalPlacement>,
}
impl EventEmitter<Event> for Workspace {}
@@ -1620,6 +1621,7 @@ impl Workspace {
last_open_dock_positions: Vec::new(),
removing: false,
utility_panes: UtilityPaneState::default(),
+ next_modal_placement: None,
}
}
@@ -6326,12 +6328,25 @@ impl Workspace {
self.modal_layer.read(cx).active_modal()
}
+ pub fn is_modal_open<V: 'static>(&self, cx: &App) -> bool {
+ self.modal_layer.read(cx).active_modal::<V>().is_some()
+ }
+
+ pub fn set_next_modal_placement(&mut self, placement: ModalPlacement) {
+ self.next_modal_placement = Some(placement);
+ }
+
+ fn take_next_modal_placement(&mut self) -> ModalPlacement {
+ self.next_modal_placement.take().unwrap_or_default()
+ }
+
pub fn toggle_modal<V: ModalView, B>(&mut self, window: &mut Window, cx: &mut App, build: B)
where
B: FnOnce(&mut Window, &mut Context<V>) -> V,
{
+ let placement = self.take_next_modal_placement();
self.modal_layer.update(cx, |modal_layer, cx| {
- modal_layer.toggle_modal(window, cx, build)
+ modal_layer.toggle_modal_with_placement(window, cx, placement, build)
})
}