Z-2276/Z-2275: Project & Branch switchers (#2662)
This PR adds project and branch switchers in top left corner. Release Notes: - Added a project & branch switcher under project name.
This commit is contained in:
commit
76873c508a
16 changed files with 601 additions and 68 deletions
3
Cargo.lock
generated
3
Cargo.lock
generated
|
@ -1484,6 +1484,7 @@ dependencies = [
|
||||||
"picker",
|
"picker",
|
||||||
"postage",
|
"postage",
|
||||||
"project",
|
"project",
|
||||||
|
"recent_projects",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_derive",
|
"serde_derive",
|
||||||
"settings",
|
"settings",
|
||||||
|
@ -2691,6 +2692,7 @@ dependencies = [
|
||||||
"smol",
|
"smol",
|
||||||
"sum_tree",
|
"sum_tree",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
|
"time 0.3.21",
|
||||||
"util",
|
"util",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -5703,6 +5705,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"db",
|
"db",
|
||||||
"editor",
|
"editor",
|
||||||
|
"futures 0.3.28",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
"gpui",
|
"gpui",
|
||||||
"language",
|
"language",
|
||||||
|
|
|
@ -35,6 +35,7 @@ gpui = { path = "../gpui" }
|
||||||
menu = { path = "../menu" }
|
menu = { path = "../menu" }
|
||||||
picker = { path = "../picker" }
|
picker = { path = "../picker" }
|
||||||
project = { path = "../project" }
|
project = { path = "../project" }
|
||||||
|
recent_projects = {path = "../recent_projects"}
|
||||||
settings = { path = "../settings" }
|
settings = { path = "../settings" }
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
theme_selector = { path = "../theme_selector" }
|
theme_selector = { path = "../theme_selector" }
|
||||||
|
@ -42,6 +43,7 @@ util = { path = "../util" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
zed-actions = {path = "../zed-actions"}
|
zed-actions = {path = "../zed-actions"}
|
||||||
|
|
||||||
|
|
||||||
anyhow.workspace = true
|
anyhow.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
|
|
238
crates/collab_ui/src/branch_list.rs
Normal file
238
crates/collab_ui/src/branch_list.rs
Normal file
|
@ -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::{
|
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,
|
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||||
ToggleScreenSharing,
|
ToggleScreenSharing,
|
||||||
};
|
};
|
||||||
|
@ -18,19 +21,25 @@ use gpui::{
|
||||||
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
AppContext, Entity, ImageData, LayoutContext, ModelHandle, SceneBuilder, Subscription, View,
|
||||||
ViewContext, ViewHandle, WeakViewHandle,
|
ViewContext, ViewHandle, WeakViewHandle,
|
||||||
};
|
};
|
||||||
|
use picker::PickerEvent;
|
||||||
use project::{Project, RepositoryEntry};
|
use project::{Project, RepositoryEntry};
|
||||||
|
use recent_projects::{build_recent_projects, RecentProjects};
|
||||||
use std::{ops::Range, sync::Arc};
|
use std::{ops::Range, sync::Arc};
|
||||||
use theme::{AvatarStyle, Theme};
|
use theme::{AvatarStyle, Theme};
|
||||||
use util::ResultExt;
|
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!(
|
actions!(
|
||||||
collab,
|
collab,
|
||||||
[
|
[
|
||||||
ToggleContactsMenu,
|
ToggleContactsMenu,
|
||||||
ToggleUserMenu,
|
ToggleUserMenu,
|
||||||
|
ToggleVcsMenu,
|
||||||
|
ToggleProjectMenu,
|
||||||
|
SwitchBranch,
|
||||||
ShareProject,
|
ShareProject,
|
||||||
UnshareProject,
|
UnshareProject,
|
||||||
]
|
]
|
||||||
|
@ -41,6 +50,8 @@ pub fn init(cx: &mut AppContext) {
|
||||||
cx.add_action(CollabTitlebarItem::share_project);
|
cx.add_action(CollabTitlebarItem::share_project);
|
||||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_vcs_menu);
|
||||||
|
cx.add_action(CollabTitlebarItem::toggle_project_menu);
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct CollabTitlebarItem {
|
pub struct CollabTitlebarItem {
|
||||||
|
@ -49,6 +60,8 @@ pub struct CollabTitlebarItem {
|
||||||
client: Arc<Client>,
|
client: Arc<Client>,
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||||
|
branch_popover: Option<ViewHandle<BranchList>>,
|
||||||
|
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||||
user_menu: ViewHandle<ContextMenu>,
|
user_menu: ViewHandle<ContextMenu>,
|
||||||
_subscriptions: Vec<Subscription>,
|
_subscriptions: Vec<Subscription>,
|
||||||
}
|
}
|
||||||
|
@ -69,12 +82,11 @@ impl View for CollabTitlebarItem {
|
||||||
return Empty::new().into_any();
|
return Empty::new().into_any();
|
||||||
};
|
};
|
||||||
|
|
||||||
let project = self.project.read(cx);
|
|
||||||
let theme = theme::current(cx).clone();
|
let theme = theme::current(cx).clone();
|
||||||
let mut left_container = Flex::row();
|
let mut left_container = Flex::row();
|
||||||
let mut right_container = Flex::row().align_children_center();
|
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 user = self.user_store.read(cx).current_user();
|
||||||
let peer_id = self.client.peer_id();
|
let peer_id = self.client.peer_id();
|
||||||
|
@ -182,52 +194,97 @@ impl CollabTitlebarItem {
|
||||||
menu.set_position_mode(OverlayPositionMode::Local);
|
menu.set_position_mode(OverlayPositionMode::Local);
|
||||||
menu
|
menu
|
||||||
}),
|
}),
|
||||||
|
branch_popover: None,
|
||||||
|
project_popover: None,
|
||||||
_subscriptions: subscriptions,
|
_subscriptions: subscriptions,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collect_title_root_names(
|
fn collect_title_root_names(
|
||||||
&self,
|
&self,
|
||||||
project: &Project,
|
|
||||||
theme: Arc<Theme>,
|
theme: Arc<Theme>,
|
||||||
cx: &ViewContext<Self>,
|
cx: &mut ViewContext<Self>,
|
||||||
) -> AnyElement<Self> {
|
) -> AnyElement<Self> {
|
||||||
let mut names_and_branches = project.visible_worktrees(cx).map(|worktree| {
|
let project = self.project.read(cx);
|
||||||
let worktree = worktree.read(cx);
|
|
||||||
(worktree.root_name(), worktree.root_git_entry())
|
|
||||||
});
|
|
||||||
|
|
||||||
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
|
let branch_prepended = entry
|
||||||
.as_ref()
|
.as_ref()
|
||||||
.and_then(RepositoryEntry::branch)
|
.and_then(RepositoryEntry::branch)
|
||||||
.map(|branch| format!("/{branch}"));
|
.map(|branch| util::truncate_and_trailoff(&branch, MAX_BRANCH_NAME_LENGTH));
|
||||||
let text_style = theme.titlebar.title.clone();
|
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 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(
|
let mut ret = Flex::row().with_child(
|
||||||
Label::new(name.to_owned(), style.clone())
|
Stack::new()
|
||||||
.with_highlights((0..name.len()).into_iter().collect())
|
.with_child(
|
||||||
.contained()
|
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, _| {
|
||||||
.aligned()
|
let style = project_style
|
||||||
.left()
|
.in_state(self.project_popover.is_some())
|
||||||
.into_any_named("title-project-name"),
|
.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 {
|
if let Some(git_branch) = branch_prepended {
|
||||||
ret = ret.with_child(
|
ret = ret.with_child(
|
||||||
Label::new(git_branch, style)
|
Flex::row()
|
||||||
.contained()
|
.with_child(
|
||||||
.with_margin_right(item_spacing)
|
Label::new("/", divider_style.text)
|
||||||
.aligned()
|
.contained()
|
||||||
.left()
|
.with_style(divider_style.container)
|
||||||
.into_any_named("title-project-branch"),
|
.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()
|
ret.into_any()
|
||||||
|
@ -320,7 +377,135 @@ impl CollabTitlebarItem {
|
||||||
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
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(
|
fn render_toggle_contacts_button(
|
||||||
&self,
|
&self,
|
||||||
theme: &Theme,
|
theme: &Theme,
|
||||||
|
@ -733,7 +918,7 @@ impl CollabTitlebarItem {
|
||||||
self.contacts_popover.as_ref().map(|popover| {
|
self.contacts_popover.as_ref().map(|popover| {
|
||||||
Overlay::new(ChildView::new(popover, cx))
|
Overlay::new(ChildView::new(popover, cx))
|
||||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||||
.with_anchor_corner(AnchorCorner::TopRight)
|
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||||
.with_z_index(999)
|
.with_z_index(999)
|
||||||
.aligned()
|
.aligned()
|
||||||
.bottom()
|
.bottom()
|
||||||
|
|
|
@ -1,3 +1,4 @@
|
||||||
|
mod branch_list;
|
||||||
mod collab_titlebar_item;
|
mod collab_titlebar_item;
|
||||||
mod contact_finder;
|
mod contact_finder;
|
||||||
mod contact_list;
|
mod contact_list;
|
||||||
|
@ -28,6 +29,7 @@ actions!(
|
||||||
);
|
);
|
||||||
|
|
||||||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||||
|
branch_list::init(cx);
|
||||||
collab_titlebar_item::init(cx);
|
collab_titlebar_item::init(cx);
|
||||||
contact_list::init(cx);
|
contact_list::init(cx);
|
||||||
contact_finder::init(cx);
|
contact_finder::init(cx);
|
||||||
|
|
|
@ -31,6 +31,7 @@ serde_derive.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
libc = "0.2"
|
libc = "0.2"
|
||||||
|
time.workspace = true
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
gpui = { path = "../gpui", features = ["test-support"] }
|
gpui = { path = "../gpui", features = ["test-support"] }
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use git2::ErrorCode;
|
use git2::{BranchType, ErrorCode};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use rpc::proto;
|
use rpc::proto;
|
||||||
use serde_derive::{Deserialize, Serialize};
|
use serde_derive::{Deserialize, Serialize};
|
||||||
|
@ -16,6 +16,12 @@ use util::ResultExt;
|
||||||
|
|
||||||
pub use git2::Repository as LibGitRepository;
|
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]
|
#[async_trait::async_trait]
|
||||||
pub trait GitRepository: Send {
|
pub trait GitRepository: Send {
|
||||||
fn reload_index(&self);
|
fn reload_index(&self);
|
||||||
|
@ -27,6 +33,12 @@ pub trait GitRepository: Send {
|
||||||
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
fn statuses(&self) -> Option<TreeMap<RepoPath, GitFileStatus>>;
|
||||||
|
|
||||||
fn status(&self, path: &RepoPath) -> Result<Option<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 {
|
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> {
|
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);
|
let region_id = MouseRegionId::new::<Tag>(self.view_id, region_id);
|
||||||
MouseState {
|
MouseState {
|
||||||
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
hovered: self.window.hovered_region_ids.contains(®ion_id),
|
||||||
clicked: self
|
clicked: if let Some((clicked_region_id, button)) = self.window.clicked_region {
|
||||||
.window
|
if region_id == clicked_region_id {
|
||||||
.clicked_region_ids
|
Some(button)
|
||||||
.get(®ion_id)
|
} else {
|
||||||
.and_then(|_| self.window.clicked_button),
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
},
|
||||||
accessed_hovered: false,
|
accessed_hovered: false,
|
||||||
accessed_clicked: false,
|
accessed_clicked: false,
|
||||||
}
|
}
|
||||||
|
|
|
@ -53,7 +53,7 @@ pub struct Window {
|
||||||
last_mouse_moved_event: Option<Event>,
|
last_mouse_moved_event: Option<Event>,
|
||||||
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
pub(crate) hovered_region_ids: HashSet<MouseRegionId>,
|
||||||
pub(crate) clicked_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,
|
mouse_position: Vector2F,
|
||||||
text_layout_cache: TextLayoutCache,
|
text_layout_cache: TextLayoutCache,
|
||||||
}
|
}
|
||||||
|
@ -86,7 +86,7 @@ impl Window {
|
||||||
last_mouse_moved_event: None,
|
last_mouse_moved_event: None,
|
||||||
hovered_region_ids: Default::default(),
|
hovered_region_ids: Default::default(),
|
||||||
clicked_region_ids: Default::default(),
|
clicked_region_ids: Default::default(),
|
||||||
clicked_button: None,
|
clicked_region: None,
|
||||||
mouse_position: vec2f(0., 0.),
|
mouse_position: vec2f(0., 0.),
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
appearance,
|
appearance,
|
||||||
|
@ -484,8 +484,8 @@ impl<'a> WindowContext<'a> {
|
||||||
// specific ancestor element that contained both [positions]'
|
// specific ancestor element that contained both [positions]'
|
||||||
// So we need to store the overlapping regions on mouse down.
|
// So we need to store the overlapping regions on mouse down.
|
||||||
|
|
||||||
// If there is already clicked_button stored, don't replace it.
|
// If there is already region being clicked, don't replace it.
|
||||||
if self.window.clicked_button.is_none() {
|
if self.window.clicked_region.is_none() {
|
||||||
self.window.clicked_region_ids = self
|
self.window.clicked_region_ids = self
|
||||||
.window
|
.window
|
||||||
.mouse_regions
|
.mouse_regions
|
||||||
|
@ -499,7 +499,17 @@ impl<'a> WindowContext<'a> {
|
||||||
})
|
})
|
||||||
.collect();
|
.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 {
|
mouse_events.push(MouseEvent::Down(MouseDown {
|
||||||
|
@ -564,7 +574,7 @@ impl<'a> WindowContext<'a> {
|
||||||
prev_mouse_position: self.window.mouse_position,
|
prev_mouse_position: self.window.mouse_position,
|
||||||
platform_event: e.clone(),
|
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
|
// Mouse up event happened outside the current window. Simulate mouse up button event
|
||||||
let button_event = e.to_button_event(clicked_button);
|
let button_event = e.to_button_event(clicked_button);
|
||||||
mouse_events.push(MouseEvent::Up(MouseUp {
|
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
|
// Only raise click events if the released button is the same as the one stored
|
||||||
if self
|
if self
|
||||||
.window
|
.window
|
||||||
.clicked_button
|
.clicked_region
|
||||||
.map(|clicked_button| clicked_button == e.button)
|
.map(|(_, clicked_button)| clicked_button == e.button)
|
||||||
.unwrap_or(false)
|
.unwrap_or(false)
|
||||||
{
|
{
|
||||||
// Clear clicked regions and clicked button
|
// Clear clicked regions and clicked button
|
||||||
|
@ -696,7 +706,7 @@ impl<'a> WindowContext<'a> {
|
||||||
&mut self.window.clicked_region_ids,
|
&mut self.window.clicked_region_ids,
|
||||||
Default::default(),
|
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
|
// Find regions which still overlap with the mouse since the last MouseDown happened
|
||||||
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
|
for (mouse_region, _) in self.window.mouse_regions.iter().rev() {
|
||||||
|
@ -871,18 +881,10 @@ impl<'a> WindowContext<'a> {
|
||||||
}
|
}
|
||||||
for view_id in &invalidation.updated {
|
for view_id in &invalidation.updated {
|
||||||
let titlebar_height = self.window.titlebar_height;
|
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
|
let element = self
|
||||||
.render_view(RenderParams {
|
.render_view(RenderParams {
|
||||||
view_id: *view_id,
|
view_id: *view_id,
|
||||||
titlebar_height,
|
titlebar_height,
|
||||||
hovered_region_ids,
|
|
||||||
clicked_region_ids,
|
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
appearance,
|
appearance,
|
||||||
})
|
})
|
||||||
|
@ -1191,8 +1193,6 @@ impl<'a> WindowContext<'a> {
|
||||||
pub struct RenderParams {
|
pub struct RenderParams {
|
||||||
pub view_id: usize,
|
pub view_id: usize,
|
||||||
pub titlebar_height: f32,
|
pub titlebar_height: f32,
|
||||||
pub hovered_region_ids: HashSet<MouseRegionId>,
|
|
||||||
pub clicked_region_ids: Option<(HashSet<MouseRegionId>, MouseButton)>,
|
|
||||||
pub refreshing: bool,
|
pub refreshing: bool,
|
||||||
pub appearance: Appearance,
|
pub appearance: Appearance,
|
||||||
}
|
}
|
||||||
|
|
|
@ -46,10 +46,16 @@ pub trait PickerDelegate: Sized + 'static {
|
||||||
fn center_selection_after_match_updates(&self) -> bool {
|
fn center_selection_after_match_updates(&self) -> bool {
|
||||||
false
|
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
|
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
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -21,6 +21,7 @@ util = { path = "../util"}
|
||||||
theme = { path = "../theme" }
|
theme = { path = "../theme" }
|
||||||
workspace = { path = "../workspace" }
|
workspace = { path = "../workspace" }
|
||||||
|
|
||||||
|
futures.workspace = true
|
||||||
ordered-float.workspace = true
|
ordered-float.workspace = true
|
||||||
postage.workspace = true
|
postage.workspace = true
|
||||||
smol.workspace = true
|
smol.workspace = true
|
||||||
|
|
|
@ -48,7 +48,7 @@ fn toggle(
|
||||||
let workspace = cx.weak_handle();
|
let workspace = cx.weak_handle();
|
||||||
cx.add_view(|cx| {
|
cx.add_view(|cx| {
|
||||||
RecentProjects::new(
|
RecentProjects::new(
|
||||||
RecentProjectsDelegate::new(workspace, workspace_locations),
|
RecentProjectsDelegate::new(workspace, workspace_locations, true),
|
||||||
cx,
|
cx,
|
||||||
)
|
)
|
||||||
.with_max_size(800., 1200.)
|
.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())
|
||||||
|
}
|
||||||
|
|
||||||
struct RecentProjectsDelegate {
|
pub type RecentProjects = Picker<RecentProjectsDelegate>;
|
||||||
|
|
||||||
|
pub struct RecentProjectsDelegate {
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
selected_match_index: usize,
|
selected_match_index: usize,
|
||||||
matches: Vec<StringMatch>,
|
matches: Vec<StringMatch>,
|
||||||
|
render_paths: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl RecentProjectsDelegate {
|
impl RecentProjectsDelegate {
|
||||||
fn new(
|
fn new(
|
||||||
workspace: WeakViewHandle<Workspace>,
|
workspace: WeakViewHandle<Workspace>,
|
||||||
workspace_locations: Vec<WorkspaceLocation>,
|
workspace_locations: Vec<WorkspaceLocation>,
|
||||||
|
render_paths: bool,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
workspace,
|
workspace,
|
||||||
workspace_locations,
|
workspace_locations,
|
||||||
selected_match_index: 0,
|
selected_match_index: 0,
|
||||||
matches: Default::default(),
|
matches: Default::default(),
|
||||||
|
render_paths,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -188,6 +203,7 @@ impl PickerDelegate for RecentProjectsDelegate {
|
||||||
highlighted_location
|
highlighted_location
|
||||||
.paths
|
.paths
|
||||||
.into_iter()
|
.into_iter()
|
||||||
|
.filter(|_| self.render_paths)
|
||||||
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
.map(|highlighted_path| highlighted_path.render(style.label.clone())),
|
||||||
)
|
)
|
||||||
.flex(1., false)
|
.flex(1., false)
|
||||||
|
|
|
@ -117,8 +117,9 @@ pub struct Titlebar {
|
||||||
#[serde(flatten)]
|
#[serde(flatten)]
|
||||||
pub container: ContainerStyle,
|
pub container: ContainerStyle,
|
||||||
pub height: f32,
|
pub height: f32,
|
||||||
pub title: TextStyle,
|
pub project_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub highlight_color: Color,
|
pub project_name_divider: ContainedText,
|
||||||
|
pub git_menu_button: Toggleable<Interactive<ContainedText>>,
|
||||||
pub item_spacing: f32,
|
pub item_spacing: f32,
|
||||||
pub face_pile_spacing: f32,
|
pub face_pile_spacing: f32,
|
||||||
pub avatar_ribbon: AvatarRibbon,
|
pub avatar_ribbon: AvatarRibbon,
|
||||||
|
@ -584,6 +585,8 @@ pub struct Picker {
|
||||||
pub empty_input_editor: FieldEditor,
|
pub empty_input_editor: FieldEditor,
|
||||||
pub no_matches: ContainedLabel,
|
pub no_matches: ContainedLabel,
|
||||||
pub item: Toggleable<Interactive<ContainedLabel>>,
|
pub item: Toggleable<Interactive<ContainedLabel>>,
|
||||||
|
pub header: ContainedLabel,
|
||||||
|
pub footer: ContainedLabel,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
#[derive(Clone, Debug, Deserialize, Default, JsonSchema)]
|
||||||
|
|
|
@ -46,6 +46,8 @@ export default function contact_finder(): any {
|
||||||
no_matches: picker_style.no_matches,
|
no_matches: picker_style.no_matches,
|
||||||
input_editor: picker_input,
|
input_editor: picker_input,
|
||||||
empty_input_editor: picker_input,
|
empty_input_editor: picker_input,
|
||||||
|
header: picker_style.header,
|
||||||
|
footer: picker_style.footer,
|
||||||
},
|
},
|
||||||
row_height: 28,
|
row_height: 28,
|
||||||
contact_avatar: {
|
contact_avatar: {
|
||||||
|
|
|
@ -110,5 +110,23 @@ export default function picker(): any {
|
||||||
top: 8,
|
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
|
// Project
|
||||||
title: text(theme.lowest, "sans", "variant"),
|
project_name_divider: text(theme.lowest, "sans", "variant"),
|
||||||
highlight_color: text(theme.lowest, "sans", "active").color,
|
|
||||||
|
project_menu_button: toggleable_text_button(theme, {
|
||||||
|
color: 'base',
|
||||||
|
}),
|
||||||
|
git_menu_button: toggleable_text_button(theme, {
|
||||||
|
color: 'variant',
|
||||||
|
}),
|
||||||
|
|
||||||
// Collaborators
|
// Collaborators
|
||||||
leader_avatar: {
|
leader_avatar: {
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue