From 6da34ac1488b1ad70879f163b57fbb4b5c9a11ae Mon Sep 17 00:00:00 2001 From: Danilo Leal <67129314+danilo-leal@users.noreply.github.com> Date: Tue, 7 Jul 2026 09:15:07 -0300 Subject: [PATCH] git_graph: Add design adjustments (#60469) This PR adds adjustments to the tree view, making it more consistent with all other tree view displays in the app (e.g., displaying indent guides, removing chevron toggle, etc.), and also fixes an issue where the commit message scrollbar was scrolling up with the message. Release Notes: - N/A --- crates/git_ui/src/git_graph.rs | 200 +++++++++++----------- crates/git_ui/src/git_panel.rs | 46 +---- crates/git_ui/src/project_diff.rs | 6 +- crates/outline_panel/src/outline_panel.rs | 2 +- crates/project_panel/src/project_panel.rs | 6 +- crates/ui/src/components/data_table.rs | 8 +- crates/ui/src/components/indent_guides.rs | 17 +- 7 files changed, 137 insertions(+), 148 deletions(-) diff --git a/crates/git_ui/src/git_graph.rs b/crates/git_ui/src/git_graph.rs index b360e1ec57..90efa57d22 100644 --- a/crates/git_ui/src/git_graph.rs +++ b/crates/git_ui/src/git_graph.rs @@ -52,7 +52,7 @@ use theme::AccentColors; use time::{OffsetDateTime, UtcOffset, format_description::BorrowedFormatItem}; use ui::{ Chip, ColumnWidthConfig, CommonAnimationExt as _, ContextMenu, ContextMenuEntry, DiffStat, - Divider, HeaderResizeInfo, HighlightedLabel, ListItem, ListItemSpacing, + Divider, HeaderResizeInfo, HighlightedLabel, IndentGuideColors, ListItem, ListItemSpacing, RedistributableColumnsState, ScrollableHandle, Table, TableInteractionState, TableRenderContext, TableResizeBehavior, Tooltip, WithScrollbar, bind_redistributable_columns, prelude::*, redistribute_hidden_fractions, redistribute_hidden_widths, @@ -73,9 +73,8 @@ const RESIZE_HANDLE_WIDTH: f32 = 8.0; const COPIED_STATE_DURATION: Duration = Duration::from_secs(2); const COMMIT_TAG_LIST_WIDTH_IN_REMS: Rems = rems(10.); const CUSTOM_GIT_COMMANDS_DOCS_SLUG: &str = "tasks#custom-git-commands"; +const TREE_INDENT: f32 = 20.0; const TABLE_COLUMN_COUNT: usize = 4; -// Extra vertical breathing room added to the UI line height when computing -// the git graph's row height, so commit dots and lines have space around them. const ROW_VERTICAL_PADDING: Pixels = px(4.0); struct CopiedState { @@ -278,8 +277,6 @@ impl ChangedFileEntry { workspace: WeakEntity, _cx: &App, ) -> AnyElement { - const TREE_INDENT: f32 = 12.0; - let file_name = self.file_name.clone(); let dir_path = self.dir_path.clone(); @@ -358,8 +355,6 @@ struct ChangedFileDirectoryEntry { impl ChangedFileDirectoryEntry { fn render(&self, ix: usize, git_graph: WeakEntity, cx: &App) -> AnyElement { - const TREE_INDENT: f32 = 12.0; - let path = self.path.clone(); let expanded = self.expanded; let folder_icon = FileIcons::get_folder_icon(expanded, path.as_std_path(), cx) @@ -381,22 +376,6 @@ impl ChangedFileDirectoryEntry { .spacing(ListItemSpacing::Sparse) .indent_level(self.depth) .indent_step_size(px(TREE_INDENT)) - .toggle(Some(expanded)) - .always_show_disclosure_icon(true) - .on_toggle({ - let path = path.clone(); - let git_graph = git_graph.clone(); - move |_, _, cx| { - git_graph - .update(cx, |git_graph, cx| { - git_graph - .changed_files_expanded_dirs - .insert(path.clone(), !expanded); - cx.notify(); - }) - .ok(); - } - }) .start_slot(folder_icon) .child( Label::new(self.name.clone()) @@ -3018,7 +2997,7 @@ impl GitGraph { }; CommitAvatar::new(&full_sha, author_email_for_avatar, remote.as_ref()) - .size(px(40.)) + .size(px(32.)) .render(window, cx) }; @@ -3056,6 +3035,25 @@ impl GitGraph { Rc::default() }; + let is_tree_view = self.changed_files_view_mode.is_tree(); + let view_toggle = IconButton::new("toggle-changed-files-view", IconName::ListTree) + .icon_size(IconSize::Small) + .toggle_state(self.changed_files_view_mode.is_tree()) + .tooltip({ + let tooltip = if is_tree_view { + "Show Flat View" + } else { + "Show Tree View" + }; + move |_, cx| Tooltip::for_action(tooltip, &ToggleChangedFilesView, cx) + }) + .on_click(cx.listener(|this, _, _window, cx| { + this.changed_files_view_mode = this.changed_files_view_mode.toggled(); + this.changed_files_scroll_handle + .scroll_to_item(0, ScrollStrategy::Top); + cx.notify(); + })); + v_flex() .min_w(px(300.)) .h_full() @@ -3090,17 +3088,12 @@ impl GitGraph { .py_1() .w_full() .items_center() - .gap_1() .child(avatar) + .child(Label::new(author_name).mt_1p5()) .child( - v_flex() - .items_center() - .child(Label::new(author_name)) - .child( - Label::new(date_string) - .color(Color::Muted) - .size(LabelSize::Small), - ), + Label::new(date_string) + .color(Color::Muted) + .size(LabelSize::Small), ), ) .children((!ref_names.is_empty()).then(|| { @@ -3258,69 +3251,40 @@ impl GitGraph { .child( v_flex() .min_w_0() - .p_2() .flex_1() - .gap_1() + .overflow_hidden() .child( h_flex() + .p_2() + .pr_3() + .pb_1() .gap_1() .w_full() .justify_between() - .child( - Label::new(format!( - "{} Changed {}", - changed_files_count, - if changed_files_count == 1 { - "File" - } else { - "Files" - } - )) - .size(LabelSize::Small) - .color(Color::Muted), - ) .child( h_flex() .gap_1() - .child(DiffStat::new( - "commit-diff-stat", - total_lines_added, - total_lines_removed, - )) .child( - IconButton::new( - "toggle-changed-files-view", - IconName::ListTree, - ) - .shape(ui::IconButtonShape::Square) - .icon_size(IconSize::Small) - .toggle_state(self.changed_files_view_mode.is_tree()) - .tooltip({ - let tooltip = if self.changed_files_view_mode.is_tree() - { - "Show Flat View" + Label::new(format!( + "{} Changed {}", + changed_files_count, + if changed_files_count == 1 { + "File" } else { - "Show Tree View" - }; - move |_, cx| { - Tooltip::for_action( - tooltip, - &ToggleChangedFilesView, - cx, - ) + "Files" } - }) - .on_click( - cx.listener(|this, _, _window, cx| { - this.changed_files_view_mode = - this.changed_files_view_mode.toggled(); - this.changed_files_scroll_handle - .scroll_to_item(0, ScrollStrategy::Top); - cx.notify(); - }), - ), - ), - ), + )) + .size(LabelSize::Small) + .color(Color::Muted), + ) + .child(Divider::vertical()) + .child(view_toggle), + ) + .child(DiffStat::new( + "commit-diff-stat", + total_lines_added, + total_lines_removed, + )), ) .child( div() @@ -3329,7 +3293,7 @@ impl GitGraph { .min_h_0() .child({ let flat_entries = changed_file_entries; - let is_tree_view = self.changed_files_view_mode.is_tree(); + let entry_count = if is_tree_view { tree_entries.len() } else { @@ -3339,6 +3303,8 @@ impl GitGraph { let repository = repository.downgrade(); let workspace = self.workspace.clone(); let git_graph = cx.weak_entity(); + let indent_tree_entries = tree_entries.clone(); + uniform_list( "changed-files-list", entry_count, @@ -3381,8 +3347,34 @@ impl GitGraph { .collect() }, ) + .when(is_tree_view, |list| { + list.with_decoration( + ui::indent_guides( + px(TREE_INDENT), + IndentGuideColors::panel(cx), + ) + .with_left_offset( + ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET - px(2.), + ) + .with_compute_indents_fn( + cx.entity(), + move |_, range, _window, _cx| { + range + .map(|ix| match indent_tree_entries.get(ix) { + Some(ChangedFileTreeEntry::Directory( + entry, + )) => entry.depth, + Some(ChangedFileTreeEntry::File(entry)) => { + entry.depth + } + None => 0, + }) + .collect() + }, + ), + ) + }) .size_full() - .ml_neg_1() .track_scroll(&self.changed_files_scroll_handle) }) .vertical_scrollbar_for(&self.changed_files_scroll_handle, window, cx), @@ -3393,6 +3385,11 @@ impl GitGraph { h_flex().p_1p5().w_full().child( Button::new("view-commit", "View Commit") .full_width() + .start_icon( + Icon::new(IconName::GitCommit) + .size(IconSize::Small) + .color(Color::Muted), + ) .style(ButtonStyle::OutlinedGhost) .on_click(cx.listener(|this, _, window, cx| { this.open_selected_commit_view(window, cx); @@ -3912,22 +3909,26 @@ impl GitGraph { // which causes problems with text reflow when using flexbox. // grid, on the other hand, doesn't appear to give taffy the same // problems. - .grid() + .w_full() + .max_h(line_height * 12.) .py_2() .pl_2() - .w_full() - .gap_1() + .grid() .grid_cols(1) - // Value of 12 taken from ./commit_view.rs:725 - .max_h(line_height * 12.) + .gap_1() .child( div() - .id("commit-message") - .text_sm() + .relative() .size_full() - .overflow_y_scroll() - .track_scroll(scroll_handle) - .child(MarkdownElement::new(message.clone(), message_style)) + .child( + div() + .id("commit-message") + .text_sm() + .size_full() + .overflow_y_scroll() + .track_scroll(scroll_handle) + .child(MarkdownElement::new(message.clone(), message_style)), + ) .vertical_scrollbar_for(scroll_handle, window, cx), ) .into_any_element() @@ -3961,11 +3962,10 @@ impl Render for GitGraph { let label = Label::new(message) .color(Color::Muted) .size(LabelSize::Large); - div() + + h_flex() .size_full() - .h_flex() .gap_1() - .items_center() .justify_center() .child(label) .when(is_loading && error.is_none(), |this| { @@ -4013,7 +4013,7 @@ impl Render for GitGraph { h_flex() .size_full() .child( - div() + v_flex() .flex_1() .min_w_0() .size_full() diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 90463f1b9e..b1667adff5 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -39,11 +39,10 @@ use git::{ ViewFile, parse_git_remote_url, }; use gpui::{ - AbsoluteLength, Action, Anchor, AnyElement, AsyncApp, AsyncWindowContext, Bounds, ClickEvent, + AbsoluteLength, Action, Anchor, AnyElement, AsyncApp, AsyncWindowContext, ClickEvent, DismissEvent, Empty, Entity, EventEmitter, FocusHandle, Focusable, KeyContext, MouseButton, MouseDownEvent, Pixels, Point, PromptLevel, ScrollStrategy, Subscription, Task, TaskExt, - TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, point, size, - uniform_list, + TextStyle, UniformListScrollHandle, WeakEntity, actions, anchored, deferred, uniform_list, }; use itertools::Itertools; use language::{Buffer, BufferEvent, File}; @@ -82,9 +81,8 @@ use theme_settings::ThemeSettings; use time::OffsetDateTime; use ui::{ ButtonLike, Checkbox, ContextMenu, ContextMenuEntry, Divider, ElevationIndex, - IndentGuideColors, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, - RenderedIndentGuide, ScrollAxes, Scrollbars, SplitButton, Tab, TintColor, Tooltip, - WithScrollbar, prelude::*, + IndentGuideColors, KeyBinding, PopoverMenu, PopoverMenuHandle, ProjectEmptyState, ScrollAxes, + Scrollbars, SplitButton, Tab, TintColor, Tooltip, WithScrollbar, prelude::*, }; use util::paths::PathStyle; use util::{ResultExt, TryFutureExt, markdown::MarkdownInlineCode, maybe, rel_path::RelPath}; @@ -100,6 +98,8 @@ const GIT_PANEL_KEY: &str = "GitPanel"; const UPDATE_DEBOUNCE: Duration = Duration::from_millis(50); // TODO: We should revise this part. It seems the indentation width is not aligned with the one in project panel const TREE_INDENT: f32 = 16.0; +// Horizontal offset that aligns the tree indent guides with the row icon column. +const INDENT_GUIDE_LEFT_OFFSET: gpui::Pixels = gpui::px(19.); actions!( git_panel, @@ -6381,43 +6381,15 @@ impl GitPanel { }), ) .when(is_tree_view, |list| { - let indent_size = px(TREE_INDENT); list.with_decoration( - ui::indent_guides(indent_size, IndentGuideColors::panel(cx)) + ui::indent_guides(px(TREE_INDENT), IndentGuideColors::panel(cx)) + .with_left_offset(INDENT_GUIDE_LEFT_OFFSET) .with_compute_indents_fn( cx.entity(), |this, range, _window, _cx| { this.compute_visible_depths(range) }, - ) - .with_render_fn(cx.entity(), |_, params, _, _| { - // Magic number to align the tree item is 3 here - // because we're using 12px as the left-side padding - // and 3 makes the alignment work with the bounding box of the icon - let left_offset = px(TREE_INDENT + 3_f32); - let indent_size = params.indent_size; - let item_height = params.item_height; - - params - .indent_guides - .into_iter() - .map(|layout| { - let bounds = Bounds::new( - point( - layout.offset.x * indent_size + left_offset, - layout.offset.y * item_height, - ), - size(px(1.), layout.length * item_height), - ); - RenderedIndentGuide { - bounds, - layout, - is_active: false, - hitbox: None, - } - }) - .collect() - }), + ), ) }) .group("entries") diff --git a/crates/git_ui/src/project_diff.rs b/crates/git_ui/src/project_diff.rs index d674be5576..445eaf1e70 100644 --- a/crates/git_ui/src/project_diff.rs +++ b/crates/git_ui/src/project_diff.rs @@ -799,6 +799,8 @@ impl Render for ProjectDiffToolbar { let (additions, deletions) = project_diff.read(cx).calculate_changed_lines(cx); let is_multibuffer_empty = project_diff.read(cx).multibuffer(cx).read(cx).is_empty(); + let stage_all_button_width = rems(5.); + h_flex() .my_neg_1() .py_1() @@ -894,7 +896,7 @@ impl Render for ProjectDiffToolbar { |this| { this.child( Button::new("unstage-all", "Unstage All") - .width(rems_from_px(80.)) + .width(stage_all_button_width) .tooltip(Tooltip::for_action_title_in( "Unstage All Changes", &UnstageAll, @@ -911,7 +913,7 @@ impl Render for ProjectDiffToolbar { |this| { this.child( Button::new("stage-all", "Stage All") - .width(rems_from_px(80.)) + .width(stage_all_button_width) .disabled(!button_states.stage_all) .tooltip(Tooltip::for_action_title_in( "Stage All Changes", diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index a8786aaa40..7c3845eb98 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -4752,7 +4752,7 @@ impl OutlinePanel { } }) .with_render_fn(cx.entity(), move |outline_panel, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); + const LEFT_OFFSET: Pixels = ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET; let indent_size = params.indent_size; let item_height = params.item_height; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 4dfa07b6b2..8b830513b2 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -7083,7 +7083,8 @@ impl Render for ProjectPanel { .with_render_fn( cx.entity(), move |this, params, _, cx| { - const LEFT_OFFSET: Pixels = px(14.); + const LEFT_OFFSET: Pixels = + ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET; const PADDING_Y: Pixels = px(4.); const HITBOX_OVERDRAW: Pixels = px(3.); @@ -7179,7 +7180,8 @@ impl Render for ProjectPanel { .with_render_fn( cx.entity(), move |_, params, _, _| { - const LEFT_OFFSET: Pixels = px(14.); + const LEFT_OFFSET: Pixels = + ui::LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET; let indent_size = params.indent_size; let item_height = params.item_height; diff --git a/crates/ui/src/components/data_table.rs b/crates/ui/src/components/data_table.rs index 805c93708a..8d785fec8a 100644 --- a/crates/ui/src/components/data_table.rs +++ b/crates/ui/src/components/data_table.rs @@ -750,13 +750,11 @@ pub fn render_table_header( let shared_element_id: SharedString = format!("table-{}", element_id).into(); let pinned_cols = table_context.pinned_cols; - let outer = div() - .flex() - .flex_row() - .items_center() + let outer = h_flex() + .py_1() .w_full() .border_b_1() - .border_color(cx.theme().colors().border); + .border_color(cx.theme().colors().border_variant); let use_ui_font = table_context.use_ui_font; let resize_info_ref = resize_info.as_ref(); diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 9e78b0d4bf..f83dda592f 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -28,9 +28,14 @@ impl IndentGuideColors { } } +/// Horizontal offset that lines an indent guide up with the icon column of a +/// standard [`ListItem`](crate::ListItem)-based row. +pub const LIST_ITEM_INDENT_GUIDE_LEFT_OFFSET: Pixels = px(15.); + pub struct IndentGuides { colors: IndentGuideColors, indent_size: Pixels, + left_offset: Pixels, compute_indents_fn: Option, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>, render_fn: Option< @@ -49,6 +54,7 @@ pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGu IndentGuides { colors, indent_size, + left_offset: px(0.), compute_indents_fn: None, render_fn: None, on_click: None, @@ -56,6 +62,15 @@ pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGu } impl IndentGuides { + /// Sets a horizontal offset applied to every guide, used to line the guides + /// up with the icon column of the list's rows. Ignored when a custom render + /// function is set via [`Self::with_render_fn`], which is responsible for its + /// own positioning. + pub fn with_left_offset(mut self, left_offset: Pixels) -> Self { + self.left_offset = left_offset; + self + } + /// Sets the callback that will be called when the user clicks on an indent guide. pub fn on_click( mut self, @@ -124,7 +139,7 @@ impl IndentGuides { .map(|layout| RenderedIndentGuide { bounds: Bounds::new( point( - layout.offset.x * self.indent_size, + layout.offset.x * self.indent_size + self.left_offset, layout.offset.y * item_height, ), size(px(1.), layout.length * item_height),