diff --git a/crates/assets/assets/icons/chevron-down.svg b/crates/assets/assets/icons/chevron-down.svg
index 7a0a041..36be93e 100644
--- a/crates/assets/assets/icons/chevron-down.svg
+++ b/crates/assets/assets/icons/chevron-down.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
diff --git a/crates/assets/assets/icons/git-commit.svg b/crates/assets/assets/icons/git-commit.svg
new file mode 100644
index 0000000..931e995
--- /dev/null
+++ b/crates/assets/assets/icons/git-commit.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/assets/assets/icons/git-file.svg b/crates/assets/assets/icons/git-file.svg
new file mode 100644
index 0000000..c98599d
--- /dev/null
+++ b/crates/assets/assets/icons/git-file.svg
@@ -0,0 +1,3 @@
+
diff --git a/crates/assets/assets/icons/plus.svg b/crates/assets/assets/icons/plus.svg
index 7e2e5b8..c4f4bd7 100644
--- a/crates/assets/assets/icons/plus.svg
+++ b/crates/assets/assets/icons/plus.svg
@@ -1 +1,3 @@
-
\ No newline at end of file
+
diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs
index 3a16192..658af48 100644
--- a/crates/assets/src/lib.rs
+++ b/crates/assets/src/lib.rs
@@ -76,6 +76,8 @@ pub enum CustomIconName {
Filter,
GlobalOn,
GlobalOff,
+ GitFile,
+ GitCommit,
GitIssueDone,
GitIssueOpen,
GitIssueClosed,
@@ -98,6 +100,8 @@ impl IconNamed for CustomIconName {
CustomIconName::Filter => "icons/filter.svg",
CustomIconName::GlobalOn => "icons/global-on.svg",
CustomIconName::GlobalOff => "icons/global-off.svg",
+ CustomIconName::GitCommit => "icons/git-commit.svg",
+ CustomIconName::GitFile => "icons/git-file.svg",
CustomIconName::GitIssueDone => "icons/git-issue-done.svg",
CustomIconName::GitIssueOpen => "icons/git-issue-open.svg",
CustomIconName::GitIssueClosed => "icons/git-issue-close.svg",
diff --git a/crates/workspace/src/views/repo_detail/issues.rs b/crates/workspace/src/views/repo_detail/issues.rs
index 7a4e9a1..fda3ec6 100644
--- a/crates/workspace/src/views/repo_detail/issues.rs
+++ b/crates/workspace/src/views/repo_detail/issues.rs
@@ -1,7 +1,3 @@
-//! Issues panel: a bottom panel listing every issue of the repository with
-//! its title, event id, author, age and status, filterable by status via
-//! the header's All/Open/Closed filter.
-
use std::rc::Rc;
use assets::CustomIconName;
@@ -43,8 +39,8 @@ enum IssueFilter {
All,
/// Issues whose resolved status is [`RepoStatus::Open`].
Open,
- /// Issues whose resolved status is [`RepoStatus::Closed`] or
- /// [`RepoStatus::Applied`] (both are "done" states).
+ /// Issues whose resolved status is
+ /// [`RepoStatus::Closed`] or [`RepoStatus::Applied`] (both are "done" states).
Closed,
}
@@ -74,8 +70,7 @@ pub struct IssuesView {
filter: IssueFilter,
/// Per-row heights of the virtual list.
item_sizes: Rc>>,
- /// Number of rows [`Self::item_sizes`] was built for (the filtered
- /// issue count); rebuilt on change.
+ /// Number of rows [`Self::item_sizes`] was built for (the filtered issue count).
issue_len: usize,
/// Indices into the store's `issues` matching [`Self::filter`], rebuilt
/// every render; the virtual list renders this slice.
@@ -350,7 +345,7 @@ impl IssuesView {
/// Open the "new issue" dialog: a title and a content input that submit
/// through [`RepoStore::open_issue`] when confirmed.
-fn open_new_issue_dialog(store: Entity, window: &mut Window, cx: &mut App) {
+pub(super) fn open_new_issue_dialog(store: Entity, window: &mut Window, cx: &mut App) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Issue title"));
let content = cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the issue…"));
diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs
index dbad492..f9ff6cf 100644
--- a/crates/workspace/src/views/repo_detail/mod.rs
+++ b/crates/workspace/src/views/repo_detail/mod.rs
@@ -8,12 +8,13 @@ use dock::{BasePanel, DockArea, DockPlacement, Panel, PanelEvent, panel_handle};
use gix::Repository;
use gpui::prelude::*;
use gpui::{
- AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, PathPromptOptions,
- Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window, div, px, size,
+ Action, AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable,
+ PathPromptOptions, Pixels, Render, SharedString, Size, Subscription, Task, WeakEntity, Window,
+ div, px, relative, size,
};
-use gpui_base::Disableable;
-use gpui_component::avatar::{Avatar, AvatarGroup};
-use gpui_component::button::{Button, ButtonVariants};
+use gpui_base::{Button as BaseButton, Disableable};
+use gpui_component::avatar::Avatar;
+use gpui_component::button::{Button, ButtonVariants, DropdownButton};
use gpui_component::combobox::{
Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext,
};
@@ -21,8 +22,7 @@ use gpui_component::searchable_list::SearchableVec;
use gpui_component::tag::Tag;
use gpui_component::tree::TreeState;
use gpui_component::{
- ActiveTheme, Icon, IconName, Selectable, Sizable, StyledExt, VirtualListScrollHandle, h_flex,
- v_flex,
+ ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex,
};
use signed_core::Announcement;
use signed_git::{CommitList, FileCommit};
@@ -46,8 +46,8 @@ use browser::{
use commits::COMMIT_ROW_HEIGHT;
use diff::CommitDiffView;
use helpers::{TreeItemSeed, build_tree_items, is_markdown_path, tree_items};
-use issues::IssuesView;
-use pull_requests::PullRequestsView;
+use issues::{IssuesView, open_new_issue_dialog};
+use pull_requests::{PullRequestsView, open_new_pull_request_dialog};
/// What kind of ref the header selectors switch to.
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -58,6 +58,16 @@ enum RefKind {
Tag,
}
+/// Header actions dispatched by the dropdown menus of the header buttons.
+#[derive(Clone, Action, PartialEq, Eq)]
+#[action(namespace = repo_detail, no_json)]
+enum RepoAction {
+ /// Open the "new issue" dialog.
+ NewIssue,
+ /// Open the "new pull request" dialog.
+ NewPR,
+}
+
/// Everything loaded from the local clone for the explorer: the tree seeds,
/// README, refs and HEAD commit. Computed on a background thread (see
/// [`load_repo_data`]) and applied on the main thread.
@@ -1029,8 +1039,8 @@ impl RepoDetailView {
fn render_header(&self, cx: &mut Context) -> AnyElement {
let store = self.store.read(cx);
let announcement = store.announcement.as_ref().unwrap_or(&self.initial);
- let issue_count = store.issue_count();
- let pull_request_count = store.pull_request_count();
+ let issue_count = SharedString::from(store.issue_count().to_string());
+ let pr_count = SharedString::from(store.pull_request_count().to_string());
let name = self.display_name(cx);
let description = announcement.description();
@@ -1039,6 +1049,16 @@ impl RepoDetailView {
let worktree_empty = self.switching_ref || self.worktree.is_none();
v_flex()
+ .on_action(
+ cx.listener(|this, action: &RepoAction, window, cx| match action {
+ RepoAction::NewIssue => {
+ open_new_issue_dialog(this.store.clone(), window, cx);
+ }
+ RepoAction::NewPR => {
+ open_new_pull_request_dialog(this.store.clone(), window, cx);
+ }
+ }),
+ )
.px_4()
.pb_4()
.w_full()
@@ -1048,13 +1068,14 @@ impl RepoDetailView {
.child(
h_flex()
.w_full()
- .gap_2()
+ .gap_4()
.items_start()
.justify_between()
.child(
v_flex()
.flex_1()
.min_w_0()
+ .gap_1()
.child(div().font_semibold().child(name))
.child(
div()
@@ -1062,6 +1083,7 @@ impl RepoDetailView {
.text_sm()
.text_color(cx.theme().muted_foreground)
.line_clamp(2)
+ .line_height(relative(1.2))
.text_ellipsis()
.child(description),
)
@@ -1086,36 +1108,59 @@ impl RepoDetailView {
.gap_2()
.justify_end()
.child(
- Button::new("issues")
- .child(
- h_flex()
- .gap_2()
- .text_sm()
- .child(SharedString::from("Issues"))
- .child(Tag::new().xsmall().child(SharedString::from(
- issue_count.to_string(),
- ))),
- )
+ DropdownButton::new("issues")
.outline()
- .on_click(cx.listener(|this, _event, window, cx| {
- this.open_issue_detail(window, cx);
- })),
+ .button(
+ Button::new("issues-open")
+ .child(
+ h_flex().gap_2().text_sm().child("Issues").child(
+ Tag::secondary()
+ .xsmall()
+ .border_0()
+ .child(issue_count),
+ ),
+ )
+ .secondary()
+ .on_click(cx.listener(|this, _event, window, cx| {
+ this.open_issue_detail(window, cx);
+ })),
+ )
+ .dropdown_menu(|menu, _, _| {
+ menu.menu_element_with_icon(
+ IconName::Plus,
+ Box::new(RepoAction::NewIssue),
+ |_, _| div().text_xs().child("New issue"),
+ )
+ }),
)
.child(
- Button::new("prs")
- .child(
- h_flex()
- .gap_2()
- .text_sm()
- .child(SharedString::from("Pull Requests"))
- .child(Tag::new().xsmall().child(SharedString::from(
- pull_request_count.to_string(),
- ))),
- )
+ DropdownButton::new("prs")
.outline()
- .on_click(cx.listener(|this, _event, window, cx| {
- this.open_pull_request_detail(window, cx);
- })),
+ .button(
+ Button::new("prs-open")
+ .child(
+ h_flex()
+ .gap_2()
+ .text_sm()
+ .child("Pull Requests")
+ .child(
+ Tag::secondary()
+ .xsmall()
+ .border_0()
+ .child(pr_count),
+ ),
+ )
+ .on_click(cx.listener(|this, _event, window, cx| {
+ this.open_pull_request_detail(window, cx);
+ })),
+ )
+ .dropdown_menu(|menu, _, _| {
+ menu.menu_element_with_icon(
+ IconName::Plus,
+ Box::new(RepoAction::NewPR),
+ |_, _| div().text_xs().child("New PR"),
+ )
+ }),
)
.child(
Button::new("link")
@@ -1141,27 +1186,69 @@ impl RepoDetailView {
.items_center()
.gap_2()
.child(
- Button::new("files-tab")
- .label("Files")
+ BaseButton::new("files-tab")
+ .flex()
+ .items_center()
+ .h_8()
+ .px_2()
+ .gap_2()
+ .child(
+ h_flex()
+ .gap_1()
+ .text_sm()
+ .child(Icon::new(CustomIconName::GitFile).small())
+ .child("Files"),
+ )
+ .text_color(cx.theme().button_foreground)
+ .rounded(cx.theme().radius)
+ .hover(|this| this.bg(cx.theme().button_hover))
+ .active(|this| this.bg(cx.theme().button_active))
.selected(self.active_tab == 0)
- .toggled(self.active_tab == 0)
+ .when(self.active_tab == 0, |this| {
+ this.bg(cx.theme().button_active)
+ })
.on_click(cx.listener(|this, _event, _window, cx| {
this.active_tab = 0;
cx.notify();
})),
)
.child(
- Button::new("commits-tab")
- .label("Commits")
- .selected(self.active_tab == 1)
- .toggled(self.active_tab == 1)
+ BaseButton::new("commits-tab")
+ .flex()
+ .items_center()
+ .h_8()
+ .px_2()
+ .gap_2()
+ .child(
+ h_flex()
+ .gap_1()
+ .text_sm()
+ .child(Icon::new(CustomIconName::GitCommit).small())
+ .child("Commits"),
+ )
.when_some(commits_count, |this, count| {
this.child(
- Tag::secondary()
- .xsmall()
+ h_flex()
+ .justify_center()
+ .px_1()
+ .py_0p5()
+ .min_w_4()
+ .text_size(px(8.))
+ .bg(cx.theme().muted)
+ .text_color(cx.theme().muted_foreground)
+ .rounded(cx.theme().radius)
+ .line_height(relative(1.))
.child(SharedString::from(count.to_string())),
)
})
+ .text_color(cx.theme().button_foreground)
+ .rounded(cx.theme().radius)
+ .hover(|this| this.bg(cx.theme().button_hover))
+ .active(|this| this.bg(cx.theme().button_active))
+ .selected(self.active_tab == 1)
+ .when(self.active_tab == 1, |this| {
+ this.bg(cx.theme().button_active)
+ })
.on_click(cx.listener(|this, _event, _window, cx| {
this.active_tab = 1;
cx.notify();
@@ -1254,7 +1341,7 @@ impl RepoDetailView {
.maintainers
.iter()
.copied()
- .filter(|key| *key != announcement.owner && seen.insert(*key))
+ .filter(|key| key != &announcement.owner && seen.insert(*key))
.collect();
let owner = profile_store.read(cx).get(&announcement.owner);
@@ -1264,31 +1351,32 @@ impl RepoDetailView {
h_flex()
.w_full()
.gap_3()
- .items_center()
.child(
- h_flex()
- .gap_1()
- .items_center()
- .child(
- Avatar::new()
- .name(owner_name.clone())
- .when_some(owner_picture, |this, url| this.src(url))
- .rounded(cx.theme().radius)
- .small(),
- )
- .child(div().text_xs().whitespace_nowrap().child(owner_name)),
+ Button::new("maintainers").ghost().child(
+ h_flex()
+ .gap_2()
+ .child(
+ h_flex()
+ .gap_1()
+ .child(
+ Avatar::new()
+ .name(owner_name.clone())
+ .when_some(owner_picture, |this, url| this.src(url))
+ .rounded(cx.theme().radius)
+ .small(),
+ )
+ .child(div().text_xs().whitespace_nowrap().child(owner_name)),
+ )
+ .when(!rest.is_empty(), |this| {
+ this.child(
+ div()
+ .text_xs()
+ .text_color(cx.theme().muted_foreground)
+ .child(SharedString::from(format!("+{}", rest.len()))),
+ )
+ }),
+ ),
)
- .when(!rest.is_empty(), |this| {
- this.child(AvatarGroup::new().small().limit(5).ellipsis().children(
- rest.into_iter().map(|key| {
- let profile = profile_store.read(cx).get(&key);
- Avatar::new()
- .name(profile.name())
- .when_some(profile.picture(), |this, url| this.src(url))
- .rounded(cx.theme().radius)
- }),
- ))
- })
.into_any_element()
}
}
diff --git a/crates/workspace/src/views/repo_detail/pull_requests.rs b/crates/workspace/src/views/repo_detail/pull_requests.rs
index fc72b91..02c29d4 100644
--- a/crates/workspace/src/views/repo_detail/pull_requests.rs
+++ b/crates/workspace/src/views/repo_detail/pull_requests.rs
@@ -435,7 +435,11 @@ impl PullRequestsView {
/// Open the "new pull request" dialog: a title, an optional description and
/// a patch input that submit through [`RepoStore::open_pull_request`] when
/// confirmed.
-fn open_new_pull_request_dialog(store: Entity, window: &mut Window, cx: &mut App) {
+pub(super) fn open_new_pull_request_dialog(
+ store: Entity,
+ window: &mut Window,
+ cx: &mut App,
+) {
let subject = cx.new(|cx| InputState::new(window, cx).placeholder("Pull request title"));
let description =
cx.new(|cx| TextareaState::new(window, cx).placeholder("Describe the change…"));