git_graph: Add some design adjustments (#49899)
- Made hover/active styles, as well as clicks, work for the entire row, capture the graph element, too (needed to make some manual hover and other states management to pull that off) - Used the existing `Chip` component for the branch chip instead of a local recreation - Adjusted spacing and sizing of commit detail panel, including button labels truncation and tooltip content - Added diff stat numbers for the changed files, to match the commit view - Standardized the commit avatar component across the git graph, the commit view, and the file history view - Added scrollbar to the changed files uniform list - Removed author name display redundancy (kept only email) - Made the commit detail UI have a min-width <img width="750" height="1964" alt="Screenshot 2026-02-23 at 11 31@2x" src="https://github.com/user-attachments/assets/d1433bd8-5edb-4829-882b-52b1bffbd6db" /> --- Before you mark this PR as ready for review, make sure that you have: - [x] Added a solid test coverage and/or screenshots from doing manual testing - [x] Done a self-review taking into account security and performance aspects - [x] Aligned any UI changes with the [UI checklist](https://github.com/zed-industries/zed/blob/main/CONTRIBUTING.md#uiux-checklist) Release Notes: - N/A --------- Co-authored-by: Anthony Eid <anthony@zed.dev>
This commit is contained in:
1 parent
b775e5d63f
commit
a3c62de696
10 files changed
+588
-412
No files matched your search
Generated
+1
@@ -7156,6 +7156,7 @@ dependencies = [
|
||||
"git",
|
||||
"git_ui",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"project",
|
||||
"rand 0.9.2",
|
||||
|
||||
@@ -26,6 +26,7 @@ feature_flags.workspace = true
|
||||
git.workspace = true
|
||||
git_ui.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
menu.workspace = true
|
||||
project.workspace = true
|
||||
settings.workspace = true
|
||||
|
||||
+503
-297
File diff suppressed because it is too large.
Load diff
@@ -5,7 +5,7 @@ use git::blame::BlameEntry;
|
||||
use git::repository::CommitSummary;
|
||||
use git::{GitRemote, commit::ParsedCommitMessage};
|
||||
use gpui::{
|
||||
App, Asset, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
AbsoluteLength, App, Asset, Element, Entity, MouseButton, ParentElement, Render, ScrollHandle,
|
||||
StatefulInteractiveElement, WeakEntity, prelude::*,
|
||||
};
|
||||
use markdown::{Markdown, MarkdownElement};
|
||||
@@ -30,7 +30,7 @@ pub struct CommitAvatar<'a> {
|
||||
sha: &'a SharedString,
|
||||
author_email: Option<SharedString>,
|
||||
remote: Option<&'a GitRemote>,
|
||||
size: Option<IconSize>,
|
||||
size: Option<AbsoluteLength>,
|
||||
}
|
||||
|
||||
impl<'a> CommitAvatar<'a> {
|
||||
@@ -59,21 +59,38 @@ impl<'a> CommitAvatar<'a> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn size(mut self, size: IconSize) -> Self {
|
||||
self.size = Some(size);
|
||||
pub fn size(mut self, size: impl Into<AbsoluteLength>) -> Self {
|
||||
self.size = Some(size.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(&'a self, window: &mut Window, cx: &mut App) -> AnyElement {
|
||||
let border_color = cx.theme().colors().border_variant;
|
||||
let border_width = px(1.);
|
||||
|
||||
match self.avatar(window, cx) {
|
||||
// Loading or no avatar found
|
||||
None => Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.when_some(self.size, |this, size| this.size(size))
|
||||
.into_any_element(),
|
||||
// Found
|
||||
None => {
|
||||
let container_size = self
|
||||
.size
|
||||
.map(|s| s.to_pixels(window.rem_size()) + border_width * 2.);
|
||||
|
||||
h_flex()
|
||||
.when_some(container_size, |this, size| this.size(size))
|
||||
.justify_center()
|
||||
.rounded_full()
|
||||
.border(border_width)
|
||||
.border_color(border_color)
|
||||
.bg(cx.theme().colors().element_disabled)
|
||||
.child(
|
||||
Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Small),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
Some(avatar) => avatar
|
||||
.when_some(self.size, |this, size| this.size(size.rems()))
|
||||
.when_some(self.size, |this, size| this.size(size))
|
||||
.border_color(border_color)
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,9 +10,9 @@ use git::{
|
||||
parse_git_remote_url,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context,
|
||||
Element, Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement,
|
||||
ParentElement, PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
AnyElement, App, AppContext as _, AsyncApp, AsyncWindowContext, ClipboardItem, Context, Entity,
|
||||
EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
|
||||
PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
|
||||
};
|
||||
use language::{
|
||||
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, OffsetRangeExt as _,
|
||||
@@ -403,33 +403,13 @@ impl CommitView {
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> AnyElement {
|
||||
let size = size.into();
|
||||
let avatar = CommitAvatar::new(
|
||||
CommitAvatar::new(
|
||||
sha,
|
||||
Some(self.commit.author_email.clone()),
|
||||
self.remote.as_ref(),
|
||||
);
|
||||
|
||||
v_flex()
|
||||
.w(size)
|
||||
.h(size)
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.rounded_full()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.child(
|
||||
avatar
|
||||
.avatar(window, cx)
|
||||
.map(|a| a.size(size).into_any_element())
|
||||
.unwrap_or_else(|| {
|
||||
Icon::new(IconName::Person)
|
||||
.color(Color::Muted)
|
||||
.size(IconSize::Medium)
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
.into_any()
|
||||
)
|
||||
.size(size)
|
||||
.render(window, cx)
|
||||
}
|
||||
|
||||
fn calculate_changed_lines(&self, cx: &App) -> (u32, u32) {
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
use anyhow::Result;
|
||||
use futures::Future;
|
||||
|
||||
use git::repository::{FileHistory, FileHistoryEntry, RepoPath};
|
||||
use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
|
||||
use gpui::{
|
||||
AnyElement, AnyEntity, App, Asset, Context, Entity, EventEmitter, FocusHandle, Focusable,
|
||||
IntoElement, Render, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window,
|
||||
uniform_list,
|
||||
AnyElement, AnyEntity, App, Context, Entity, EventEmitter, FocusHandle, Focusable, IntoElement,
|
||||
Render, ScrollStrategy, Task, UniformListScrollHandle, WeakEntity, Window, uniform_list,
|
||||
};
|
||||
use project::{
|
||||
Project, ProjectPath,
|
||||
@@ -15,13 +14,14 @@ use std::any::{Any, TypeId};
|
||||
use std::sync::Arc;
|
||||
|
||||
use time::OffsetDateTime;
|
||||
use ui::{Avatar, Chip, Divider, ListItem, WithScrollbar, prelude::*};
|
||||
use ui::{Chip, Divider, ListItem, WithScrollbar, prelude::*};
|
||||
use util::ResultExt;
|
||||
use workspace::{
|
||||
Item, Workspace,
|
||||
item::{ItemEvent, SaveOptions},
|
||||
};
|
||||
|
||||
use crate::commit_tooltip::CommitAvatar;
|
||||
use crate::commit_view::CommitView;
|
||||
|
||||
const PAGE_SIZE: usize = 50;
|
||||
@@ -276,20 +276,10 @@ impl FileHistoryView {
|
||||
author_email: Option<SharedString>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> impl IntoElement {
|
||||
let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
|
||||
let size = rems_from_px(20.);
|
||||
|
||||
if let Some(remote) = remote {
|
||||
let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone(), author_email);
|
||||
if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
|
||||
Avatar::new(url.to_string()).size(size)
|
||||
} else {
|
||||
Avatar::new("").size(size)
|
||||
}
|
||||
} else {
|
||||
Avatar::new("").size(size)
|
||||
}
|
||||
) -> AnyElement {
|
||||
CommitAvatar::new(sha, author_email, self.remote.as_ref())
|
||||
.size(rems_from_px(20.))
|
||||
.render(window, cx)
|
||||
}
|
||||
|
||||
fn render_commit_entry(
|
||||
@@ -388,60 +378,6 @@ impl FileHistoryView {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
struct CommitAvatarAsset {
|
||||
sha: SharedString,
|
||||
author_email: Option<SharedString>,
|
||||
remote: GitRemote,
|
||||
}
|
||||
|
||||
impl std::hash::Hash for CommitAvatarAsset {
|
||||
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
|
||||
self.sha.hash(state);
|
||||
self.remote.host.name().hash(state);
|
||||
}
|
||||
}
|
||||
|
||||
impl CommitAvatarAsset {
|
||||
fn new(remote: GitRemote, sha: SharedString, author_email: Option<SharedString>) -> Self {
|
||||
Self {
|
||||
remote,
|
||||
sha,
|
||||
author_email,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Asset for CommitAvatarAsset {
|
||||
type Source = Self;
|
||||
type Output = Option<SharedString>;
|
||||
|
||||
fn load(
|
||||
source: Self::Source,
|
||||
cx: &mut App,
|
||||
) -> impl Future<Output = Self::Output> + Send + 'static {
|
||||
let client = cx.http_client();
|
||||
async move {
|
||||
match source
|
||||
.remote
|
||||
.host
|
||||
.commit_author_avatar_url(
|
||||
&source.remote.owner,
|
||||
&source.remote.repo,
|
||||
source.sha.clone(),
|
||||
source.author_email.clone(),
|
||||
client,
|
||||
)
|
||||
.await
|
||||
{
|
||||
Ok(Some(url)) => Some(SharedString::from(url.to_string())),
|
||||
Ok(None) => None,
|
||||
Err(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<ItemEvent> for FileHistoryView {}
|
||||
|
||||
impl Focusable for FileHistoryView {
|
||||
|
||||
@@ -73,7 +73,7 @@ impl Avatar {
|
||||
impl RenderOnce for Avatar {
|
||||
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let border_width = if self.border_color.is_some() {
|
||||
px(2.)
|
||||
px(1.)
|
||||
} else {
|
||||
px(0.)
|
||||
};
|
||||
|
||||
@@ -434,6 +434,7 @@ impl RenderOnce for Button {
|
||||
|
||||
self.base.child(
|
||||
h_flex()
|
||||
.when(self.truncate, |this| this.min_w_0().overflow_hidden())
|
||||
.gap(DynamicSpacing::Base04.rems(cx))
|
||||
.when(self.icon_position == Some(IconPosition::Start), |this| {
|
||||
this.children(self.icon.map(|icon| {
|
||||
@@ -448,6 +449,7 @@ impl RenderOnce for Button {
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.when(self.truncate, |this| this.min_w_0().overflow_hidden())
|
||||
.when(
|
||||
self.key_binding_position == KeybindingPosition::Start,
|
||||
|this| this.flex_row_reverse(),
|
||||
|
||||
@@ -16,6 +16,8 @@ pub struct Chip {
|
||||
label_color: Color,
|
||||
label_size: LabelSize,
|
||||
bg_color: Option<Hsla>,
|
||||
border_color: Option<Hsla>,
|
||||
height: Option<Pixels>,
|
||||
tooltip: Option<Box<dyn Fn(&mut Window, &mut App) -> AnyView + 'static>>,
|
||||
}
|
||||
|
||||
@@ -27,6 +29,8 @@ impl Chip {
|
||||
label_color: Color::Default,
|
||||
label_size: LabelSize::XSmall,
|
||||
bg_color: None,
|
||||
border_color: None,
|
||||
height: None,
|
||||
tooltip: None,
|
||||
}
|
||||
}
|
||||
@@ -49,6 +53,18 @@ impl Chip {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a custom border color for the chip.
|
||||
pub fn border_color(mut self, color: Hsla) -> Self {
|
||||
self.border_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets a custom height for the chip.
|
||||
pub fn height(mut self, height: Pixels) -> Self {
|
||||
self.height = Some(height);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn tooltip(mut self, tooltip: impl Fn(&mut Window, &mut App) -> AnyView + 'static) -> Self {
|
||||
self.tooltip = Some(Box::new(tooltip));
|
||||
self
|
||||
@@ -61,20 +77,24 @@ impl RenderOnce for Chip {
|
||||
.bg_color
|
||||
.unwrap_or(cx.theme().colors().element_background);
|
||||
|
||||
let border_color = self.border_color.unwrap_or(cx.theme().colors().border);
|
||||
|
||||
h_flex()
|
||||
.when_some(self.height, |this, h| this.h(h))
|
||||
.min_w_0()
|
||||
.flex_initial()
|
||||
.px_1()
|
||||
.border_1()
|
||||
.rounded_sm()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.border_color(border_color)
|
||||
.bg(bg_color)
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
Label::new(self.label.clone())
|
||||
.size(self.label_size)
|
||||
.color(self.label_color)
|
||||
.buffer_font(cx),
|
||||
.buffer_font(cx)
|
||||
.truncate(),
|
||||
)
|
||||
.id(self.label.clone())
|
||||
.when_some(self.tooltip, |this, tooltip| this.tooltip(tooltip))
|
||||
|
||||
@@ -724,6 +724,7 @@ impl TableWidths {
|
||||
pub struct Table {
|
||||
striped: bool,
|
||||
show_row_borders: bool,
|
||||
show_row_hover: bool,
|
||||
width: Option<Length>,
|
||||
headers: Option<TableRow<AnyElement>>,
|
||||
rows: TableContents,
|
||||
@@ -743,6 +744,7 @@ impl Table {
|
||||
cols,
|
||||
striped: false,
|
||||
show_row_borders: true,
|
||||
show_row_hover: true,
|
||||
width: None,
|
||||
headers: None,
|
||||
rows: TableContents::Vec(Vec::new()),
|
||||
@@ -895,6 +897,13 @@ impl Table {
|
||||
self
|
||||
}
|
||||
|
||||
/// Hides the default hover background on table rows.
|
||||
/// Use this when you want to handle row hover styling manually via `map_row`.
|
||||
pub fn hide_row_hover(mut self) -> Self {
|
||||
self.show_row_hover = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Provide a callback that is invoked when the table is rendered without any rows
|
||||
pub fn empty_table_callback(
|
||||
mut self,
|
||||
@@ -948,7 +957,9 @@ pub fn render_table_row(
|
||||
.id(("table_row", row_index))
|
||||
.size_full()
|
||||
.when_some(bg, |row, bg| row.bg(bg))
|
||||
.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
|
||||
.when(table_context.show_row_hover, |row| {
|
||||
row.hover(|s| s.bg(cx.theme().colors().element_hover.opacity(0.6)))
|
||||
})
|
||||
.when(!is_striped && table_context.show_row_borders, |row| {
|
||||
row.border_b_1()
|
||||
.border_color(transparent_black())
|
||||
@@ -1055,6 +1066,7 @@ pub fn render_table_header(
|
||||
pub struct TableRenderContext {
|
||||
pub striped: bool,
|
||||
pub show_row_borders: bool,
|
||||
pub show_row_hover: bool,
|
||||
pub total_row_count: usize,
|
||||
pub column_widths: Option<TableRow<Length>>,
|
||||
pub map_row: Option<Rc<dyn Fn((usize, Stateful<Div>), &mut Window, &mut App) -> AnyElement>>,
|
||||
@@ -1066,6 +1078,7 @@ impl TableRenderContext {
|
||||
Self {
|
||||
striped: table.striped,
|
||||
show_row_borders: table.show_row_borders,
|
||||
show_row_hover: table.show_row_hover,
|
||||
total_row_count: table.rows.len(),
|
||||
column_widths: table.col_widths.as_ref().map(|widths| widths.lengths(cx)),
|
||||
map_row: table.map_row.clone(),
|
||||
|
||||
Reference in new issue
Block a user