Detailed changes
@@ -1484,6 +1484,7 @@ dependencies = [
"picker",
"postage",
"project",
+ "recent_projects",
"serde",
"serde_derive",
"settings",
@@ -2691,6 +2692,7 @@ dependencies = [
"smol",
"sum_tree",
"tempfile",
+ "time 0.3.21",
"util",
]
@@ -5703,6 +5705,7 @@ version = "0.1.0"
dependencies = [
"db",
"editor",
+ "futures 0.3.28",
"fuzzy",
"gpui",
"language",
@@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
menu = { path = "../menu" }
picker = { path = "../picker" }
project = { path = "../project" }
+recent_projects = {path = "../recent_projects"}
settings = { path = "../settings" }
theme = { path = "../theme" }
theme_selector = { path = "../theme_selector" }
@@ -42,6 +43,7 @@ util = { path = "../util" }
workspace = { path = "../workspace" }
zed-actions = {path = "../zed-actions"}
+
anyhow.workspace = true
futures.workspace = true
log.workspace = true
@@ -0,0 +1,238 @@
+use anyhow::{anyhow, bail};
+use fuzzy::{StringMatch, StringMatchCandidate};
+use gpui::{elements::*, AppContext, MouseState, Task, ViewContext, ViewHandle};
+use picker::{Picker, PickerDelegate, PickerEvent};
+use std::{ops::Not, sync::Arc};
+use util::ResultExt;
+use workspace::{Toast, Workspace};
+
+pub fn init(cx: &mut AppContext) {
+ Picker::<BranchListDelegate>::init(cx);
+}
+
+pub type BranchList = Picker<BranchListDelegate>;
+
+pub fn build_branch_list(
+ workspace: ViewHandle<Workspace>,
+ cx: &mut ViewContext<BranchList>,
+) -> BranchList {
+ Picker::new(
+ BranchListDelegate {
+ matches: vec![],
+ workspace,
+ selected_index: 0,
+ last_query: String::default(),
+ },
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+}
+
+pub struct BranchListDelegate {
+ matches: Vec<StringMatch>,
+ workspace: ViewHandle<Workspace>,
+ selected_index: usize,
+ last_query: String,
+}
+
+impl PickerDelegate for BranchListDelegate {
+ fn placeholder_text(&self) -> Arc<str> {
+ "Select branch...".into()
+ }
+
+ fn match_count(&self) -> usize {
+ self.matches.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
+ self.selected_index = ix;
+ }
+
+ fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
+ cx.spawn(move |picker, mut cx| async move {
+ let Some(candidates) = picker
+ .read_with(&mut cx, |view, cx| {
+ let delegate = view.delegate();
+ let project = delegate.workspace.read(cx).project().read(&cx);
+ let mut cwd =
+ project
+ .visible_worktrees(cx)
+ .next()
+ .unwrap()
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let Some(repo) = project.fs().open_repo(&cwd) else {bail!("Project does not have associated git repository.")};
+ let mut branches = repo
+ .lock()
+ .branches()?;
+ const RECENT_BRANCHES_COUNT: usize = 10;
+ if query.is_empty() && branches.len() > RECENT_BRANCHES_COUNT {
+ // Truncate list of recent branches
+ // Do a partial sort to show recent-ish branches first.
+ branches.select_nth_unstable_by(RECENT_BRANCHES_COUNT - 1, |lhs, rhs| {
+ rhs.unix_timestamp.cmp(&lhs.unix_timestamp)
+ });
+ branches.truncate(RECENT_BRANCHES_COUNT);
+ branches.sort_unstable_by(|lhs, rhs| lhs.name.cmp(&rhs.name));
+ }
+ Ok(branches
+ .iter()
+ .cloned()
+ .enumerate()
+ .map(|(ix, command)| StringMatchCandidate {
+ id: ix,
+ char_bag: command.name.chars().collect(),
+ string: command.name.into(),
+ })
+ .collect::<Vec<_>>())
+ })
+ .log_err() else { return; };
+ let Some(candidates) = candidates.log_err() else {return;};
+ let matches = if query.is_empty() {
+ candidates
+ .into_iter()
+ .enumerate()
+ .map(|(index, candidate)| StringMatch {
+ candidate_id: index,
+ string: candidate.string,
+ positions: Vec::new(),
+ score: 0.0,
+ })
+ .collect()
+ } else {
+ fuzzy::match_strings(
+ &candidates,
+ &query,
+ true,
+ 10000,
+ &Default::default(),
+ cx.background(),
+ )
+ .await
+ };
+ picker
+ .update(&mut cx, |picker, _| {
+ let delegate = picker.delegate_mut();
+ delegate.matches = matches;
+ if delegate.matches.is_empty() {
+ delegate.selected_index = 0;
+ } else {
+ delegate.selected_index =
+ core::cmp::min(delegate.selected_index, delegate.matches.len() - 1);
+ }
+ delegate.last_query = query;
+ })
+ .log_err();
+ })
+ }
+
+ fn confirm(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ let current_pick = self.selected_index();
+ let current_pick = self.matches[current_pick].string.clone();
+ cx.spawn(|picker, mut cx| async move {
+ picker.update(&mut cx, |this, cx| {
+ let project = this.delegate().workspace.read(cx).project().read(cx);
+ let mut cwd = project
+ .visible_worktrees(cx)
+ .next()
+ .ok_or_else(|| anyhow!("There are no visisible worktrees."))?
+ .read(cx)
+ .abs_path()
+ .to_path_buf();
+ cwd.push(".git");
+ let status = project
+ .fs()
+ .open_repo(&cwd)
+ .ok_or_else(|| anyhow!("Could not open repository at path `{}`", cwd.as_os_str().to_string_lossy()))?
+ .lock()
+ .change_branch(¤t_pick);
+ if status.is_err() {
+ const GIT_CHECKOUT_FAILURE_ID: usize = 2048;
+ this.delegate().workspace.update(cx, |model, ctx| {
+ model.show_toast(
+ Toast::new(
+ GIT_CHECKOUT_FAILURE_ID,
+ format!("Failed to checkout branch '{current_pick}', check for conflicts or unstashed files"),
+ ),
+ ctx,
+ )
+ });
+ status?;
+ }
+ cx.emit(PickerEvent::Dismiss);
+
+ Ok::<(), anyhow::Error>(())
+ }).log_err();
+ }).detach();
+ }
+
+ fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
+ cx.emit(PickerEvent::Dismiss);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ mouse_state: &mut MouseState,
+ selected: bool,
+ cx: &gpui::AppContext,
+ ) -> AnyElement<Picker<Self>> {
+ const DISPLAYED_MATCH_LEN: usize = 29;
+ let theme = &theme::current(cx);
+ let hit = &self.matches[ix];
+ let shortened_branch_name = util::truncate_and_trailoff(&hit.string, DISPLAYED_MATCH_LEN);
+ let highlights = hit
+ .positions
+ .iter()
+ .copied()
+ .filter(|index| index < &DISPLAYED_MATCH_LEN)
+ .collect();
+ let style = theme.picker.item.in_state(selected).style_for(mouse_state);
+ Flex::row()
+ .with_child(
+ Label::new(shortened_branch_name.clone(), style.label.clone())
+ .with_highlights(highlights)
+ .contained()
+ .aligned()
+ .left(),
+ )
+ .contained()
+ .with_style(style.container)
+ .constrained()
+ .with_height(theme.contact_finder.row_height)
+ .into_any()
+ }
+ fn render_header(
+ &self,
+ cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
+ let theme = &theme::current(cx);
+ let style = theme.picker.header.clone();
+ let label = if self.last_query.is_empty() {
+ Flex::row()
+ .with_child(Label::new("Recent branches", style.label.clone()))
+ .contained()
+ .with_style(style.container)
+ } else {
+ Flex::row()
+ .with_child(Label::new("Branches", style.label.clone()))
+ .with_children(self.matches.is_empty().not().then(|| {
+ let suffix = if self.matches.len() == 1 { "" } else { "es" };
+ Label::new(
+ format!("{} match{}", self.matches.len(), suffix),
+ style.label,
+ )
+ .flex_float()
+ }))
+ .contained()
+ .with_style(style.container)
+ };
+ Some(label.into_any())
+ }
+}
@@ -1,5 +1,8 @@
use crate::{
- contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
+ branch_list::{build_branch_list, BranchList},
+ contact_notification::ContactNotification,
+ contacts_popover,
+ face_pile::FacePile,
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
ToggleScreenSharing,
};
@@ -18,19 +21,25 @@ use gpui::{
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
ViewContext, ViewHandle, WeakViewHandle,
};
+use picker::PickerEvent;
use project::{Project, RepositoryEntry};
+use recent_projects::{build_recent_projects, RecentProjects};
use std::{ops::Range, sync::Arc};
use theme::{AvatarStyle, Theme};
use util::ResultExt;
-use workspace::{FollowNextCollaborator, Workspace};
+use workspace::{FollowNextCollaborator, Workspace, WORKSPACE_DB};
-// const MAX_TITLE_LENGTH: usize = 75;
+const MAX_PROJECT_NAME_LENGTH: usize = 40;
+const MAX_BRANCH_NAME_LENGTH: usize = 40;
actions!(
collab,
[
ToggleContactsMenu,
ToggleUserMenu,
+ ToggleVcsMenu,
+ ToggleProjectMenu,
+ SwitchBranch,
ShareProject,
UnshareProject,
]
@@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
cx.add_action(CollabTitlebarItem::share_project);
cx.add_action(CollabTitlebarItem::unshare_project);
cx.add_action(CollabTitlebarItem::toggle_user_menu);
+ cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
+ cx.add_action(CollabTitlebarItem::toggle_project_menu);
}
pub struct CollabTitlebarItem {
@@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
client: Arc<Client>,
workspace: WeakViewHandle<Workspace>,
contacts_popover: Option<ViewHandle<ContactsPopover>>,
+ branch_popover: Option<ViewHandle<BranchList>>,
+ project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
user_menu: ViewHandle<ContextMenu>,
_subscriptions: Vec<Subscription>,
}
@@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
return Empty::new().into_any();
};
- let project = self.project.read(cx);
let theme = theme::current(cx).clone();
let mut left_container = Flex::row();
let mut right_container = Flex::row().align_children_center();
- left_container.add_child(self.collect_title_root_names(&project, theme.clone(), cx));
+ left_container.add_child(self.collect_title_root_names(theme.clone(), cx));
let user = self.user_store.read(cx).current_user();
let peer_id = self.client.peer_id();
@@ -182,52 +194,97 @@ impl CollabTitlebarItem {
menu.set_position_mode(OverlayPositionMode::Local);
menu
}),
+ branch_popover: None,
+ project_popover: None,
_subscriptions: subscriptions,
}
}
fn collect_title_root_names(
&self,
- project: &Project,
theme: Arc<Theme>,
- cx: &ViewContext<Self>,
+ cx: &mut ViewContext<Self>,
) -> AnyElement<Self> {
- let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
- let worktree = worktree.read(cx);
- (worktree.root_name(), worktree.root_git_entry())
- });
+ let project = self.project.read(cx);
- let (name, entry) = names_and_branches.next().unwrap_or(("", None));
+ let (name, entry) = {
+ let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
+ let worktree = worktree.read(cx);
+ (worktree.root_name(), worktree.root_git_entry())
+ });
+
+ names_and_branches.next().unwrap_or(("", None))
+ };
+
+ let name = util::truncate_and_trailoff(name, MAX_PROJECT_NAME_LENGTH);
let branch_prepended = entry
.as_ref()
.and_then(RepositoryEntry::branch)
- .map(|branch| format!("/{branch}"));
- let text_style = theme.titlebar.title.clone();
+ .map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
+ let project_style = theme.titlebar.project_menu_button.clone();
+ let git_style = theme.titlebar.git_menu_button.clone();
+ let divider_style = theme.titlebar.project_name_divider.clone();
let item_spacing = theme.titlebar.item_spacing;
- let mut highlight = text_style.clone();
- highlight.color = theme.titlebar.highlight_color;
-
- let style = LabelStyle {
- text: text_style,
- highlight_text: Some(highlight),
- };
let mut ret = Flex::row().with_child(
- Label::new(name.to_owned(), style.clone())
- .with_highlights((0..name.len()).into_iter().collect())
- .contained()
- .aligned()
- .left()
- .into_any_named("title-project-name"),
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
+ let style = project_style
+ .in_state(self.project_popover.is_some())
+ .style_for(mouse_state);
+ Label::new(name, style.text.clone())
+ .contained()
+ .with_style(style.container)
+ .aligned()
+ .left()
+ .into_any_named("title-project-name")
+ })
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.toggle_project_menu(&Default::default(), cx)
+ })
+ .on_click(MouseButton::Left, move |_, _, _| {}),
+ )
+ .with_children(self.render_project_popover_host(&theme.titlebar, cx)),
);
if let Some(git_branch) = branch_prepended {
ret = ret.with_child(
- Label::new(git_branch, style)
- .contained()
- .with_margin_right(item_spacing)
- .aligned()
- .left()
- .into_any_named("title-project-branch"),
+ Flex::row()
+ .with_child(
+ Label::new("/", divider_style.text)
+ .contained()
+ .with_style(divider_style.container)
+ .aligned()
+ .left(),
+ )
+ .with_child(
+ Stack::new()
+ .with_child(
+ MouseEventHandler::<ToggleVcsMenu, Self>::new(
+ 0,
+ cx,
+ |mouse_state, _| {
+ let style = git_style
+ .in_state(self.branch_popover.is_some())
+ .style_for(mouse_state);
+ Label::new(git_branch, style.text.clone())
+ .contained()
+ .with_style(style.container.clone())
+ .with_margin_right(item_spacing)
+ .aligned()
+ .left()
+ .into_any_named("title-project-branch")
+ },
+ )
+ .with_cursor_style(CursorStyle::PointingHand)
+ .on_down(MouseButton::Left, move |_, this, cx| {
+ this.toggle_vcs_menu(&Default::default(), cx)
+ })
+ .on_click(MouseButton::Left, move |_, _, _| {}),
+ )
+ .with_children(self.render_branches_popover_host(&theme.titlebar, cx)),
+ ),
)
}
ret.into_any()
@@ -320,7 +377,135 @@ impl CollabTitlebarItem {
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
});
}
+ fn render_branches_popover_host<'a>(
+ &'a self,
+ _theme: &'a theme::Titlebar,
+ cx: &'a mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ self.branch_popover.as_ref().map(|child| {
+ let theme = theme::current(cx).clone();
+ let child = ChildView::new(child, cx);
+ let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
+ child
+ .flex(1., true)
+ .contained()
+ .constrained()
+ .with_width(theme.contacts_popover.width)
+ .with_height(theme.contacts_popover.height)
+ })
+ .on_click(MouseButton::Left, |_, _, _| {})
+ .on_down_out(MouseButton::Left, move |_, this, cx| {
+ this.branch_popover.take();
+ cx.emit(());
+ cx.notify();
+ })
+ .contained()
+ .into_any();
+ Overlay::new(child)
+ .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ .with_anchor_corner(AnchorCorner::TopLeft)
+ .with_z_index(999)
+ .aligned()
+ .bottom()
+ .left()
+ .into_any()
+ })
+ }
+ fn render_project_popover_host<'a>(
+ &'a self,
+ _theme: &'a theme::Titlebar,
+ cx: &'a mut ViewContext<Self>,
+ ) -> Option<AnyElement<Self>> {
+ self.project_popover.as_ref().map(|child| {
+ let theme = theme::current(cx).clone();
+ let child = ChildView::new(child, cx);
+ let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
+ child
+ .flex(1., true)
+ .contained()
+ .constrained()
+ .with_width(theme.contacts_popover.width)
+ .with_height(theme.contacts_popover.height)
+ })
+ .on_click(MouseButton::Left, |_, _, _| {})
+ .on_down_out(MouseButton::Left, move |_, this, cx| {
+ this.project_popover.take();
+ cx.emit(());
+ cx.notify();
+ })
+ .into_any();
+
+ Overlay::new(child)
+ .with_fit_mode(OverlayFitMode::SwitchAnchor)
+ .with_anchor_corner(AnchorCorner::TopLeft)
+ .with_z_index(999)
+ .aligned()
+ .bottom()
+ .left()
+ .into_any()
+ })
+ }
+ pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
+ if self.branch_popover.take().is_none() {
+ if let Some(workspace) = self.workspace.upgrade(cx) {
+ let view = cx.add_view(|cx| build_branch_list(workspace, cx));
+ cx.subscribe(&view, |this, _, event, cx| {
+ match event {
+ PickerEvent::Dismiss => {
+ this.branch_popover = None;
+ }
+ }
+
+ cx.notify();
+ })
+ .detach();
+ self.project_popover.take();
+ cx.focus(&view);
+ self.branch_popover = Some(view);
+ }
+ }
+
+ cx.notify();
+ }
+
+ pub fn toggle_project_menu(&mut self, _: &ToggleProjectMenu, cx: &mut ViewContext<Self>) {
+ let workspace = self.workspace.clone();
+ if self.project_popover.take().is_none() {
+ cx.spawn(|this, mut cx| async move {
+ let workspaces = WORKSPACE_DB
+ .recent_workspaces_on_disk()
+ .await
+ .unwrap_or_default()
+ .into_iter()
+ .map(|(_, location)| location)
+ .collect();
+
+ let workspace = workspace.clone();
+ this.update(&mut cx, move |this, cx| {
+ let view = cx.add_view(|cx| build_recent_projects(workspace, workspaces, cx));
+
+ cx.subscribe(&view, |this, _, event, cx| {
+ match event {
+ PickerEvent::Dismiss => {
+ this.project_popover = None;
+ }
+ }
+
+ cx.notify();
+ })
+ .detach();
+ cx.focus(&view);
+ this.branch_popover.take();
+ this.project_popover = Some(view);
+ cx.notify();
+ })
+ .log_err();
+ })
+ .detach();
+ }
+ cx.notify();
+ }
fn render_toggle_contacts_button(
&self,
theme: &Theme,
@@ -733,7 +918,7 @@ impl CollabTitlebarItem {
self.contacts_popover.as_ref().map(|popover| {
Overlay::new(ChildView::new(popover, cx))
.with_fit_mode(OverlayFitMode::SwitchAnchor)
- .with_anchor_corner(AnchorCorner::TopRight)
+ .with_anchor_corner(AnchorCorner::TopLeft)
.with_z_index(999)
.aligned()
.bottom()
@@ -1,3 +1,4 @@
+mod branch_list;
mod collab_titlebar_item;
mod contact_finder;
mod contact_list;
@@ -28,6 +29,7 @@ actions!(
);
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
+ branch_list::init(cx);
collab_titlebar_item::init(cx);
contact_list::init(cx);
contact_finder::init(cx);
@@ -31,6 +31,7 @@ serde_derive.workspace = true
serde_json.workspace = true
log.workspace = true
libc = "0.2"
+time.workspace = true
[dev-dependencies]
gpui = { path = "../gpui", features = ["test-support"] }
@@ -1,6 +1,6 @@
use anyhow::Result;
use collections::HashMap;
-use git2::ErrorCode;
+use git2::{BranchType, ErrorCode};
use parking_lot::Mutex;
use rpc::proto;
use serde_derive::{Deserialize, Serialize};
@@ -16,6 +16,12 @@ use util::ResultExt;
pub use git2::Repository as LibGitRepository;
+#[derive(Clone, Debug, Hash, PartialEq)]
+pub struct Branch {
+ pub name: Box<str>,
+ /// Timestamp of most recent commit, normalized to Unix Epoch format.
+ pub unix_timestamp: Option<i64>,
+}
#[async_trait::async_trait]
pub trait GitRepository: Send {
fn reload_index(&self);
@@ -27,6 +33,12 @@ pub trait GitRepository: Send {
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
fn status(&self, path: &RepoPath) -> Result<Option<GitFileStatus>>;
+ fn branches(&self) -> Result<Vec<Branch>> {
+ Ok(vec![])
+ }
+ fn change_branch(&self, _: &str) -> Result<()> {
+ Ok(())
+ }
}
impl std::fmt::Debug for dyn GitRepository {
@@ -106,6 +118,40 @@ impl GitRepository for LibGitRepository {
}
}
}
+ fn branches(&self) -> Result<Vec<Branch>> {
+ let local_branches = self.branches(Some(BranchType::Local))?;
+ let valid_branches = local_branches
+ .filter_map(|branch| {
+ branch.ok().and_then(|(branch, _)| {
+ let name = branch.name().ok().flatten().map(Box::from)?;
+ let timestamp = branch.get().peel_to_commit().ok()?.time();
+ let unix_timestamp = timestamp.seconds();
+ let timezone_offset = timestamp.offset_minutes();
+ let utc_offset =
+ time::UtcOffset::from_whole_seconds(timezone_offset * 60).ok()?;
+ let unix_timestamp =
+ time::OffsetDateTime::from_unix_timestamp(unix_timestamp).ok()?;
+ Some(Branch {
+ name,
+ unix_timestamp: Some(unix_timestamp.to_offset(utc_offset).unix_timestamp()),
+ })
+ })
+ })
+ .collect();
+ Ok(valid_branches)
+ }
+ fn change_branch(&self, name: &str) -> Result<()> {
+ let revision = self.find_branch(name, BranchType::Local)?;
+ let revision = revision.get();
+ let as_tree = revision.peel_to_tree()?;
+ self.checkout_tree(as_tree.as_object(), None)?;
+ self.set_head(
+ revision
+ .name()
+ .ok_or_else(|| anyhow::anyhow!("Branch name could not be retrieved"))?,
+ )?;
+ Ok(())
+ }
}
fn read_status(status: git2::Status) -> Option<GitFileStatus> {
@@ -3301,11 +3301,15 @@ impl<'a, 'b, V: View> ViewContext<'a, 'b, V> {
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
MouseState {
hovered: self.window.hovered_region_ids.contains(®ion_id),
- clicked: self
- .window
- .clicked_region_ids
- .get(®ion_id)
- .and_then(|_| self.window.clicked_button),
+ clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
+ if region_id == clicked_region_id {
+ Some(button)
+ } else {
+ None
+ }
+ } else {
+ None
+ },
accessed_hovered: false,
accessed_clicked: false,
}
@@ -53,7 +53,7 @@ pub struct Window {
last_mouse_moved_event: Option<Event>,
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
pub(crate) clicked_region_ids: HashSet<MouseRegionId>,
- pub(crate) clicked_button: Option<MouseButton>,
+ pub(crate) clicked_region: Option<(MouseRegionId, MouseButton)>,
mouse_position: Vector2F,
text_layout_cache: TextLayoutCache,
}
@@ -86,7 +86,7 @@ impl Window {
last_mouse_moved_event: None,
hovered_region_ids: Default::default(),
clicked_region_ids: Default::default(),
- clicked_button: None,
+ clicked_region: None,
mouse_position: vec2f(0., 0.),
titlebar_height,
appearance,
@@ -484,8 +484,8 @@ impl<'a> WindowContext<'a> {
// specific ancestor element that contained both [positions]'
// So we need to store the overlapping regions on mouse down.
- // If there is already clicked_button stored, don't replace it.
- if self.window.clicked_button.is_none() {
+ // If there is already region being clicked, don't replace it.
+ if self.window.clicked_region.is_none() {
self.window.clicked_region_ids = self
.window
.mouse_regions
@@ -499,7 +499,17 @@ impl<'a> WindowContext<'a> {
})
.collect();
- self.window.clicked_button = Some(e.button);
+ let mut highest_z_index = 0;
+ let mut clicked_region_id = None;
+ for (region, z_index) in self.window.mouse_regions.iter() {
+ if region.bounds.contains_point(e.position) && *z_index >= highest_z_index {
+ highest_z_index = *z_index;
+ clicked_region_id = Some(region.id());
+ }
+ }
+
+ self.window.clicked_region =
+ clicked_region_id.map(|region_id| (region_id, e.button));
}
mouse_events.push(MouseEvent::Down(MouseDown {
@@ -564,7 +574,7 @@ impl<'a> WindowContext<'a> {
prev_mouse_position: self.window.mouse_position,
platform_event: e.clone(),
}));
- } else if let Some(clicked_button) = self.window.clicked_button {
+ } else if let Some((_, clicked_button)) = self.window.clicked_region {
// Mouse up event happened outside the current window. Simulate mouse up button event
let button_event = e.to_button_event(clicked_button);
mouse_events.push(MouseEvent::Up(MouseUp {
@@ -687,8 +697,8 @@ impl<'a> WindowContext<'a> {
// Only raise click events if the released button is the same as the one stored
if self
.window
- .clicked_button
- .map(|clicked_button| clicked_button == e.button)
+ .clicked_region
+ .map(|(_, clicked_button)| clicked_button == e.button)
.unwrap_or(false)
{
// Clear clicked regions and clicked button
@@ -696,7 +706,7 @@ impl<'a> WindowContext<'a> {
&mut self.window.clicked_region_ids,
Default::default(),
);
- self.window.clicked_button = None;
+ self.window.clicked_region = None;
// Find regions which still overlap with the mouse since the last MouseDown happened
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
@@ -871,18 +881,10 @@ impl<'a> WindowContext<'a> {
}
for view_id in &invalidation.updated {
let titlebar_height = self.window.titlebar_height;
- let hovered_region_ids = self.window.hovered_region_ids.clone();
- let clicked_region_ids = self
- .window
- .clicked_button
- .map(|button| (self.window.clicked_region_ids.clone(), button));
-
let element = self
.render_view(RenderParams {
view_id: *view_id,
titlebar_height,
- hovered_region_ids,
- clicked_region_ids,
refreshing: false,
appearance,
})
@@ -1191,8 +1193,6 @@ impl<'a> WindowContext<'a> {
pub struct RenderParams {
pub view_id: usize,
pub titlebar_height: f32,
- pub hovered_region_ids: HashSet<MouseRegionId>,
- pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
pub refreshing: bool,
pub appearance: Appearance,
}
@@ -46,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
fn center_selection_after_match_updates(&self) -> bool {
false
}
- fn render_header(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+ fn render_header(
+ &self,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
None
}
- fn render_footer(&self, _cx: &AppContext) -> Option<AnyElement<Picker<Self>>> {
+ fn render_footer(
+ &self,
+ _cx: &mut ViewContext<Picker<Self>>,
+ ) -> Option<AnyElement<Picker<Self>>> {
None
}
}
@@ -21,6 +21,7 @@ util = { path = "../util"}
theme = { path = "../theme" }
workspace = { path = "../workspace" }
+futures.workspace = true
ordered-float.workspace = true
postage.workspace = true
smol.workspace = true
@@ -48,7 +48,7 @@ fn toggle(
let workspace = cx.weak_handle();
cx.add_view(|cx| {
RecentProjects::new(
- RecentProjectsDelegate::new(workspace, workspace_locations),
+ RecentProjectsDelegate::new(workspace, workspace_locations, true),
cx,
)
.with_max_size(800., 1200.)
@@ -64,25 +64,40 @@ fn toggle(
}))
}
-type RecentProjects = Picker<RecentProjectsDelegate>;
+pub fn build_recent_projects(
+ workspace: WeakViewHandle<Workspace>,
+ workspaces: Vec<WorkspaceLocation>,
+ cx: &mut ViewContext<RecentProjects>,
+) -> RecentProjects {
+ Picker::new(
+ RecentProjectsDelegate::new(workspace, workspaces, false),
+ cx,
+ )
+ .with_theme(|theme| theme.picker.clone())
+}
+
+pub type RecentProjects = Picker<RecentProjectsDelegate>;
-struct RecentProjectsDelegate {
+pub struct RecentProjectsDelegate {
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
selected_match_index: usize,
matches: Vec<StringMatch>,
+ render_paths: bool,
}
impl RecentProjectsDelegate {
fn new(
workspace: WeakViewHandle<Workspace>,
workspace_locations: Vec<WorkspaceLocation>,
+ render_paths: bool,
) -> Self {
Self {
workspace,
workspace_locations,
selected_match_index: 0,
matches: Default::default(),
+ render_paths,
}
}
}
@@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
highlighted_location
.paths
.into_iter()
+ .filter(|_| self.render_paths)
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
)
.flex(1., false)
@@ -117,8 +117,9 @@ pub struct Titlebar {
#[serde(flatten)]
pub container: ContainerStyle,
pub height: f32,
- pub title: TextStyle,
- pub highlight_color: Color,
+ pub project_menu_button: Toggleable<Interactive<ContainedText>>,
+ pub project_name_divider: ContainedText,
+ pub git_menu_button: Toggleable<Interactive<ContainedText>>,
pub item_spacing: f32,
pub face_pile_spacing: f32,
pub avatar_ribbon: AvatarRibbon,
@@ -584,6 +585,8 @@ pub struct Picker {
pub empty_input_editor: FieldEditor,
pub no_matches: ContainedLabel,
pub item: Toggleable<Interactive<ContainedLabel>>,
+ pub header: ContainedLabel,
+ pub footer: ContainedLabel,
}
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
@@ -46,6 +46,8 @@ export default function contact_finder(): any {
no_matches: picker_style.no_matches,
input_editor: picker_input,
empty_input_editor: picker_input,
+ header: picker_style.header,
+ footer: picker_style.footer,
},
row_height: 28,
contact_avatar: {
@@ -110,5 +110,23 @@ export default function picker(): any {
top: 8,
},
},
+ header: {
+ text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+
+ margin: {
+ top: 1,
+ left: 8,
+ right: 8,
+ },
+ },
+ footer: {
+ text: text(theme.lowest, "sans", "variant", { size: "xs" }),
+ margin: {
+ top: 1,
+ left: 8,
+ right: 8,
+ },
+
+ }
}
}
@@ -179,8 +179,14 @@ export function titlebar(): any {
},
// Project
- title: text(theme.lowest, "sans", "variant"),
- highlight_color: text(theme.lowest, "sans", "active").color,
+ project_name_divider: text(theme.lowest, "sans", "variant"),
+
+ project_menu_button: toggleable_text_button(theme, {
+ color: 'base',
+ }),
+ git_menu_button: toggleable_text_button(theme, {
+ color: 'variant',
+ }),
// Collaborators
leader_avatar: {