From 052c30d12b98e8a953c3cb6279d673ba1ce26f83 Mon Sep 17 00:00:00 2001 From: Ren Amamiya Date: Fri, 21 Aug 2026 09:16:04 +0700 Subject: [PATCH] update issues panel --- Cargo.lock | 1 + crates/assets/assets/icons/circle-plus.svg | 3 + crates/assets/assets/icons/git-issue-done.svg | 3 + crates/assets/src/lib.rs | 4 + crates/dock/src/lib.rs | 2 + crates/dock/src/panel.rs | 10 - crates/dock/src/tab_panel.rs | 8 - crates/workspace/Cargo.toml | 1 + .../workspace/src/views/repo_detail/issues.rs | 212 +++++++++++++----- crates/workspace/src/views/repo_detail/mod.rs | 47 ++-- crates/workspace/src/views/sidebar/mod.rs | 4 - .../src/views/sidebar/passphrase_dialog.rs | 7 +- 12 files changed, 200 insertions(+), 102 deletions(-) create mode 100644 crates/assets/assets/icons/circle-plus.svg create mode 100644 crates/assets/assets/icons/git-issue-done.svg diff --git a/Cargo.lock b/Cargo.lock index 0a47b49..09882ba 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10955,6 +10955,7 @@ dependencies = [ "futures", "gix", "gpui", + "gpui-base", "gpui-component", "log", "nostr", diff --git a/crates/assets/assets/icons/circle-plus.svg b/crates/assets/assets/icons/circle-plus.svg new file mode 100644 index 0000000..96ac654 --- /dev/null +++ b/crates/assets/assets/icons/circle-plus.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assets/assets/icons/git-issue-done.svg b/crates/assets/assets/icons/git-issue-done.svg new file mode 100644 index 0000000..d793147 --- /dev/null +++ b/crates/assets/assets/icons/git-issue-done.svg @@ -0,0 +1,3 @@ + + + diff --git a/crates/assets/src/lib.rs b/crates/assets/src/lib.rs index 72603ed..33c97db 100644 --- a/crates/assets/src/lib.rs +++ b/crates/assets/src/lib.rs @@ -71,10 +71,12 @@ impl Assets { } pub enum CustomIconName { + CirclePlus, Unlock, Filter, GlobalOn, GlobalOff, + GitIssueDone, GitIssueOpen, GitIssueClosed, GitIssueOngoing, @@ -90,10 +92,12 @@ pub enum CustomIconName { impl IconNamed for CustomIconName { fn path(self) -> gpui::SharedString { match self { + CustomIconName::CirclePlus => "icons/circle-plus.svg", CustomIconName::Unlock => "icons/unlock.svg", CustomIconName::Filter => "icons/filter.svg", CustomIconName::GlobalOn => "icons/global-on.svg", CustomIconName::GlobalOff => "icons/global-off.svg", + CustomIconName::GitIssueDone => "icons/git-issue-done.svg", CustomIconName::GitIssueOpen => "icons/git-issue-open.svg", CustomIconName::GitIssueClosed => "icons/git-issue-close.svg", CustomIconName::GitIssueOngoing => "icons/git-issue-ongoing.svg", diff --git a/crates/dock/src/lib.rs b/crates/dock/src/lib.rs index 935fe36..6a57ce4 100644 --- a/crates/dock/src/lib.rs +++ b/crates/dock/src/lib.rs @@ -949,7 +949,9 @@ impl DockArea { .map(|view| view.entity_id()); } } + impl EventEmitter for DockArea {} + impl Render for DockArea { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let view = cx.entity().clone(); diff --git a/crates/dock/src/panel.rs b/crates/dock/src/panel.rs index 141be9f..0e2fa0e 100644 --- a/crates/dock/src/panel.rs +++ b/crates/dock/src/panel.rs @@ -147,11 +147,6 @@ pub trait Panel: EventEmitter + Render + Focusable { fn dump(&self, cx: &App) -> PanelState { PanelState::new(self) } - - /// Whether the panel has inner padding when the panel is in the tabs layout, default is `true`. - fn inner_padding(&self, cx: &App) -> bool { - true - } } /// The PanelView trait used to define the panel view. @@ -174,7 +169,6 @@ pub trait PanelView: 'static + Send + Sync { fn view(&self) -> AnyView; fn focus_handle(&self, cx: &App) -> FocusHandle; fn dump(&self, cx: &App) -> PanelState; - fn inner_padding(&self, cx: &App) -> bool; } impl PanelView for Entity { @@ -252,10 +246,6 @@ impl PanelView for Entity { fn dump(&self, cx: &App) -> PanelState { self.read(cx).dump(cx) } - - fn inner_padding(&self, cx: &App) -> bool { - self.read(cx).inner_padding(cx) - } } impl From<&dyn PanelView> for AnyView { diff --git a/crates/dock/src/tab_panel.rs b/crates/dock/src/tab_panel.rs index 38b8d58..fad7530 100644 --- a/crates/dock/src/tab_panel.rs +++ b/crates/dock/src/tab_panel.rs @@ -230,11 +230,6 @@ impl Panel for TabPanel { } state } - - fn inner_padding(&self, cx: &App) -> bool { - self.active_panel(cx) - .is_none_or(|panel| panel.inner_padding(cx)) - } } /// State used to move the window when the title bar area is dragged. @@ -965,8 +960,6 @@ impl TabPanel { return Empty {}.into_any_element(); }; - let has_inner_padding = self.inner_padding(cx); - let placeholder = self.drop_placeholder_animation; let placeholder_animation_name = self.drop_placeholder_animation_name.clone(); @@ -974,7 +967,6 @@ impl TabPanel { .id("active-panel") .group("") .flex_1() - .when(has_inner_padding, |this| this.pt_2()) .child( div() .id("tab-content") diff --git a/crates/workspace/Cargo.toml b/crates/workspace/Cargo.toml index facd53e..3bceff5 100644 --- a/crates/workspace/Cargo.toml +++ b/crates/workspace/Cargo.toml @@ -14,6 +14,7 @@ utils = { path = "../utils" } gpui.workspace = true gpui-component.workspace = true +gpui-base.workspace = true gix.workspace = true nostr.workspace = true diff --git a/crates/workspace/src/views/repo_detail/issues.rs b/crates/workspace/src/views/repo_detail/issues.rs index 04d89ce..62069fb 100644 --- a/crates/workspace/src/views/repo_detail/issues.rs +++ b/crates/workspace/src/views/repo_detail/issues.rs @@ -9,8 +9,9 @@ use dock::{Panel, PanelEvent}; use gpui::prelude::*; use gpui::{ AnyElement, App, Context, Entity, EventEmitter, FocusHandle, Focusable, Pixels, Render, - SharedString, Size, Window, div, px, size, + SharedString, Size, Window, div, px, relative, size, }; +use gpui_base::Button as BaseButton; use gpui_component::avatar::Avatar; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; @@ -19,8 +20,7 @@ use gpui_component::input::{Input, InputState, Textarea, TextareaState}; use gpui_component::scroll::Scrollbar; use gpui_component::tooltip::Tooltip; use gpui_component::{ - ActiveTheme, Icon, IconName, Selectable, Sizable, StyledExt, VirtualListScrollHandle, - WindowExt, h_flex, v_flex, v_virtual_list, + ActiveTheme, Icon, Sizable, VirtualListScrollHandle, WindowExt, h_flex, v_flex, v_virtual_list, }; use nostr::prelude::Event; use signed_core::{RepoStatus, activity_subject}; @@ -29,11 +29,11 @@ use utils::relative_time; use super::helpers::placeholder; -/// Height of one issue row in the virtual list: 12px padding on top and -/// bottom, a 14px title line and a 24px meta line (the small avatar is the -/// tallest item). Gpui's default line height is phi (~1.62x), so the title -/// line is ~22.7px; the row totals ~71px. -const ISSUE_ROW_HEIGHT: f32 = 71.; +/// Height of one issue row in the virtual list: 8px vertical padding +/// (`py_2`) on top and bottom, a 32px title line (`h_8`) and a 24px meta +/// line (`h_6`), plus the 1px bottom border; the row totals 73px. The +/// status chip (`size_7`, 28px) is shorter than the content. +const ISSUE_ROW_HEIGHT: f32 = 73.; /// Status filter of the issues list, chosen via the header's filter buttons. #[derive(Debug, Clone, Copy, PartialEq, Eq)] @@ -42,7 +42,8 @@ enum IssueFilter { All, /// Issues whose resolved status is [`RepoStatus::Open`]. Open, - /// Issues whose resolved status is [`RepoStatus::Closed`]. + /// Issues whose resolved status is [`RepoStatus::Closed`] or + /// [`RepoStatus::Applied`] (both are "done" states). Closed, } @@ -52,7 +53,10 @@ impl IssueFilter { match self { Self::All => true, Self::Open => store.status_of(issue) == RepoStatus::Open, - Self::Closed => store.status_of(issue) == RepoStatus::Closed, + Self::Closed => matches!( + store.status_of(issue), + RepoStatus::Closed | RepoStatus::Applied + ), } } } @@ -111,10 +115,10 @@ impl IssuesView { h_flex() .id(ix) - .h(px(ISSUE_ROW_HEIGHT)) .w_full() .gap_4() - .p_3() + .px_4() + .py_2() .border_b_1() .border_color(cx.theme().border) .items_start() @@ -124,6 +128,7 @@ impl IssuesView { .flex_1() .child( div() + .h_8() .min_w_0() .text_ellipsis() .whitespace_nowrap() @@ -133,6 +138,7 @@ impl IssuesView { ) .child( h_flex() + .h_6() .gap_2() .text_xs() .child( @@ -152,9 +158,10 @@ impl IssuesView { .text_color(cx.theme().muted_foreground) .child(SharedString::from(&id_hex[..8])), ) - .child(div().child(age)), + .child(SharedString::from(age)), ), ) + .hover(|this| this.bg(cx.theme().list_hover)) .into_any_element() } @@ -171,8 +178,8 @@ impl IssuesView { CustomIconName::GitIssueClosed, "closed", "Issue is closed", - cx.theme().warning, - cx.theme().warning_foreground, + cx.theme().danger, + cx.theme().danger_foreground, ), RepoStatus::Draft => ( CustomIconName::GitIssueOngoing, @@ -193,76 +200,163 @@ impl IssuesView { v_flex() .id(label) .flex_shrink_0() - .size_6() + .size_7() .items_center() .justify_center() .rounded(cx.theme().radius) .bg(bg) - .child(Icon::new(icon).xsmall().text_color(fg)) + .child(Icon::new(icon).small().text_color(fg)) .tooltip(move |window, cx| Tooltip::new(tooltip).build(window, cx)) .into_any_element() } fn render_header(&self, cx: &mut Context) -> AnyElement { + let store = self.store.read(cx); + let (total, open, closed) = + store + .issues + .iter() + .fold( + (0usize, 0usize, 0usize), + |(total, open, closed), issue| match store.status_of(issue) { + RepoStatus::Open => (total + 1, open + 1, closed), + RepoStatus::Closed => (total + 1, open, closed + 1), + RepoStatus::Draft | RepoStatus::Applied => (total + 1, open, closed), + }, + ); + h_flex() + .px_4() .w_full() - .items_center() .gap_3() - .px_3() - .pb_2() .border_b_1() .border_color(cx.theme().border) - .child( - div() - .text_sm() - .text_color(cx.theme().muted_foreground) - .font_semibold() - .child("Issues"), - ) .child( h_flex() - .gap_1() + .h_12() + .gap_2() .child( - Button::new("all") - .icon(CustomIconName::GitIssueOpen) - .label("All") - .ghost() + BaseButton::new("all") + .flex() + .items_center() + .h_7() + .px_2() + .gap_1() + .child(Icon::new(CustomIconName::GitIssueDone)) + .child(div().text_sm().child("All")) + .child( + h_flex() + .justify_center() + .ml_2() + .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(total.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.filter == IssueFilter::All) + .when(self.filter == IssueFilter::All, |this| { + this.bg(cx.theme().button_active) + }) .on_click(cx.listener(|this, _event, _window, cx| { this.filter = IssueFilter::All; cx.notify(); })), ) .child( - Button::new("open") - .icon(CustomIconName::GitIssueOpen) - .label("Open") - .ghost() + BaseButton::new("open") + .flex() + .items_center() + .h_7() + .px_2() + .gap_1() + .child(Icon::new(CustomIconName::GitIssueOpen)) + .child(div().text_sm().child("Open")) + .child( + h_flex() + .justify_center() + .ml_2() + .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(open.to_string())), + ) + .text_color(cx.theme().button_foreground) + .rounded(cx.theme().radius) + .hover(|this| this.bg(cx.theme().button_hover)) .selected(self.filter == IssueFilter::Open) + .when(self.filter == IssueFilter::Open, |this| { + this.bg(cx.theme().button_active) + }) .on_click(cx.listener(|this, _event, _window, cx| { this.filter = IssueFilter::Open; cx.notify(); })), ) .child( - Button::new("closed") - .icon(CustomIconName::GitIssueClosed) - .label("Closed") - .ghost() + BaseButton::new("closed") + .flex() + .items_center() + .h_7() + .px_2() + .gap_1() + .child(Icon::new(CustomIconName::GitIssueClosed)) + .child(div().text_sm().child("Closed")) + .child( + h_flex() + .justify_center() + .ml_2() + .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(closed.to_string())), + ) + .text_color(cx.theme().button_foreground) + .rounded(cx.theme().radius) + .hover(|this| this.bg(cx.theme().button_hover)) .selected(self.filter == IssueFilter::Closed) + .when(self.filter == IssueFilter::Closed, |this| { + this.bg(cx.theme().button_active) + }) .on_click(cx.listener(|this, _event, _window, cx| { this.filter = IssueFilter::Closed; cx.notify(); })), ), ) - // Spacer: pushes the button to the right edge. .child(div().flex_1()) .child( - Button::new("new-issue") - .icon(IconName::Plus) - .label("New issue") - .primary() + BaseButton::new("new") + .flex() + .items_center() + .h_7() + .px_2() + .gap_1() + .child(Icon::new(CustomIconName::CirclePlus)) + .child(div().text_sm().child("New issue")) + .text_color(cx.theme().button_primary_foreground) + .rounded(cx.theme().radius) + .bg(cx.theme().button_primary) + .hover(|this| this.bg(cx.theme().button_primary_hover)) + .active(|this| this.bg(cx.theme().button_primary_active)) .on_click(cx.listener(|this, _event, window, cx| { open_new_issue_dialog(this.store.clone(), window, cx); })), @@ -283,8 +377,8 @@ fn open_new_issue_dialog(store: Entity, window: &mut Window, cx: &mut let store = store.clone(); dialog - .width(px(520.)) - .margin_top(px(50.)) + .keyboard(true) + .close_button(true) .content(move |body, _window, _cx| { body.child( DialogHeader::new() @@ -383,7 +477,6 @@ impl Render for IssuesView { let sizes = self.item_sizes.clone(); let scroll_handle = self.scroll_handle.clone(); - let view = cx.entity().clone(); v_flex() .size_full() @@ -396,15 +489,20 @@ impl Render for IssuesView { .w_full() .when(count > 0, |this| { this.child( - v_virtual_list(view, "il", sizes, move |this, range, _window, cx| { - let issues = &this.store.read(cx).issues; - range - .map(|ix| { - let issue_ix = this.visible_issues[ix]; - this.render_row(issue_ix, &issues[issue_ix], cx) - }) - .collect() - }) + v_virtual_list( + cx.entity().clone(), + "issues", + sizes, + move |this, range, _window, cx| { + let issues = &this.store.read(cx).issues; + range + .map(|ix| { + let issue = this.visible_issues[ix]; + this.render_row(issue, &issues[issue], cx) + }) + .collect() + }, + ) .track_scroll(&scroll_handle) .size_full(), ) diff --git a/crates/workspace/src/views/repo_detail/mod.rs b/crates/workspace/src/views/repo_detail/mod.rs index 14efe59..e0bd819 100644 --- a/crates/workspace/src/views/repo_detail/mod.rs +++ b/crates/workspace/src/views/repo_detail/mod.rs @@ -18,11 +18,11 @@ use gpui_component::combobox::{ Caret, Combobox, ComboboxEvent, ComboboxState, ComboboxTriggerContext, }; use gpui_component::searchable_list::SearchableVec; -use gpui_component::tab::{Tab, TabBar}; use gpui_component::tag::Tag; use gpui_component::tree::TreeState; use gpui_component::{ - ActiveTheme, Icon, IconName, Sizable, StyledExt, VirtualListScrollHandle, h_flex, v_flex, + ActiveTheme, Icon, IconName, Selectable, Sizable, StyledExt, VirtualListScrollHandle, h_flex, + v_flex, }; use signed_core::Announcement; use signed_git::{CommitList, FileCommit}; @@ -960,7 +960,7 @@ impl RepoDetailView { .child( div() .min_w_0() - .text_xs() + .text_sm() .text_color(cx.theme().muted_foreground) .line_clamp(2) .text_ellipsis() @@ -1034,24 +1034,31 @@ impl RepoDetailView { .child( h_flex() .items_center() + .gap_2() .child( - TabBar::new("repo-tabs") - .segmented() - .selected_index(self.active_tab) - .child(Tab::new().label("Files")) - .child(Tab::new().label("Commits").when_some( - commits_count, - |this, count| { - this.suffix( - Tag::secondary() - .xsmall() - .mr_1() - .child(SharedString::from(count.to_string())), - ) - }, - )) - .on_click(cx.listener(|this, index, _window, cx| { - this.active_tab = *index; + Button::new("files-tab") + .label("Files") + .selected(self.active_tab == 0) + .toggled(self.active_tab == 0) + .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) + .when_some(commits_count, |this, count| { + this.child( + Tag::secondary() + .xsmall() + .child(SharedString::from(count.to_string())), + ) + }) + .on_click(cx.listener(|this, _event, _window, cx| { + this.active_tab = 1; cx.notify(); })), ) diff --git a/crates/workspace/src/views/sidebar/mod.rs b/crates/workspace/src/views/sidebar/mod.rs index f45f46f..1aa1e05 100644 --- a/crates/workspace/src/views/sidebar/mod.rs +++ b/crates/workspace/src/views/sidebar/mod.rs @@ -150,10 +150,6 @@ impl Panel for SidebarPanel { fn closable(&self, _cx: &App) -> bool { false } - - fn inner_padding(&self, _cx: &App) -> bool { - false - } } impl EventEmitter for SidebarPanel {} diff --git a/crates/workspace/src/views/sidebar/passphrase_dialog.rs b/crates/workspace/src/views/sidebar/passphrase_dialog.rs index 1ad2f73..ead19f3 100644 --- a/crates/workspace/src/views/sidebar/passphrase_dialog.rs +++ b/crates/workspace/src/views/sidebar/passphrase_dialog.rs @@ -1,6 +1,6 @@ use assets::CustomIconName; use gpui::prelude::*; -use gpui::{AnyWindowHandle, App, Entity, SharedString, Subscription, Window, div, px}; +use gpui::{AnyWindowHandle, App, Entity, SharedString, Subscription, Window, div}; use gpui_component::button::{Button, ButtonVariants}; use gpui_component::dialog::{DialogDescription, DialogFooter, DialogHeader, DialogTitle}; use gpui_component::form::{field, v_form}; @@ -49,8 +49,9 @@ pub fn open(window: &mut Window, cx: &mut App) { let state = state.clone(); dialog - .width(px(420.)) - .margin_top(px(50.)) + .close_button(false) + .overlay_closable(false) + .keyboard(false) .content(move |content, _window, cx| { let busy = state.read(cx).busy; let error = state.read(cx).error.clone();