mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
git_ui: Show uncommitted change count badge on git panel icon (#49624)
## Summary - Implements `icon_label` on `GitPanel` to return the total count of uncommitted changes (`new_count + changes_count`) when non-zero, capped at `"99+"` for large repos. - Updates `PanelButtons::render()` to render that label as a small green badge overlaid on the panel's sidebar icon, using absolute positioning within a `div().relative()` wrapper. - The badge uses `version_control_added` theme color and `LabelSize::XSmall` text with `LineHeightStyle::UiLabel` for accurate vertical centering, positioned at the top-right corner of the icon button. The `icon_label` method already existed on the `Panel`/`PanelHandle` traits with a default `None` impl, and was already implemented by `NotificationPanel` (unread notification count) and `TerminalPanel` (open terminal count) — but was never rendered. This wires it up for all three panels at once. ## Notes - Badge is positioned with non-negative offsets (`top(0)`, `right(0)`) to stay within the parent container's bounds. The status bar's `render_left_tools()` uses `.overflow_x_hidden()`, which in GPUI clips both axes (the `overflow_mask` returns a full content mask whenever any axis is non-`Visible`), so negative offsets would be clipped. - `LineHeightStyle::UiLabel` collapses line height to `relative(1.)` so flex centering aligns the visual glyph rather than a taller-than-necessary line box. - No new data tracking logic — `GitPanel` already maintains `new_count` and `changes_count` reactively. - No feature flag or settings added per YAGNI. ## Suggested .rules additions The following pattern came up repeatedly and would prevent future sessions from hitting the same issue: ``` ## GPUI overflow clipping `overflow_x_hidden()` (and any single-axis overflow setter) clips **both** axes in GPUI. The `overflow_mask()` implementation in `style.rs` returns a full `ContentMask` (bounding box) whenever any axis is non-`Visible`. Absolute-positioned children that extend outside the element bounds will be clipped even if only the X axis is set to Hidden. Avoid negative `top`/`right`/`bottom`/`left` offsets on absolute children of containers that have any overflow hidden — keep badge/overlay elements within the parent's bounds instead. ``` Release Notes: - Added a numeric badge to the git panel sidebar icon showing the count of uncommitted changes. --------- Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
parent
b338a69933
commit
ae445634e0
15 changed files with 221 additions and 9 deletions
|
|
@ -922,6 +922,10 @@
|
|||
///
|
||||
/// Default: false
|
||||
"tree_view": false,
|
||||
// Whether to show a badge on the git panel icon with the count of uncommitted changes.
|
||||
//
|
||||
// Default: false
|
||||
"show_count_badge": false,
|
||||
"scrollbar": {
|
||||
// When to show the scrollbar in the git panel.
|
||||
//
|
||||
|
|
@ -946,6 +950,8 @@
|
|||
"dock": "right",
|
||||
// Default width of the notification panel.
|
||||
"default_width": 380,
|
||||
// Whether to show a badge on the notification panel icon with the count of unread notifications.
|
||||
"show_count_badge": false,
|
||||
},
|
||||
"agent": {
|
||||
// Whether the inline assistant should use streaming tools, when available
|
||||
|
|
@ -1867,6 +1873,8 @@
|
|||
// Timeout for hover and Cmd-click path hyperlink discovery in milliseconds. Specifying a
|
||||
// timeout of `0` will disable path hyperlinking in terminal.
|
||||
"path_hyperlink_timeout_ms": 1,
|
||||
// Whether to show a badge on the terminal panel icon with the count of open terminals.
|
||||
"show_count_badge": false,
|
||||
},
|
||||
"code_actions_on_format": {},
|
||||
// Settings related to running tasks.
|
||||
|
|
|
|||
|
|
@ -677,6 +677,9 @@ impl Panel for NotificationPanel {
|
|||
}
|
||||
|
||||
fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
|
||||
if !NotificationPanelSettings::get_global(cx).show_count_badge {
|
||||
return None;
|
||||
}
|
||||
let count = self.notification_store.read(cx).unread_notification_count();
|
||||
if count == 0 {
|
||||
None
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ pub struct NotificationPanelSettings {
|
|||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub show_count_badge: bool,
|
||||
}
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
|
|
@ -36,6 +37,7 @@ impl Settings for NotificationPanelSettings {
|
|||
button: panel.button.unwrap(),
|
||||
dock: panel.dock.unwrap().into(),
|
||||
default_width: panel.default_width.map(px).unwrap(),
|
||||
show_count_badge: panel.show_count_badge.unwrap(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5797,6 +5797,14 @@ impl Panel for GitPanel {
|
|||
Some("Git Panel")
|
||||
}
|
||||
|
||||
fn icon_label(&self, _: &Window, cx: &App) -> Option<String> {
|
||||
if !GitPanelSettings::get_global(cx).show_count_badge {
|
||||
return None;
|
||||
}
|
||||
let total = self.changes_count;
|
||||
(total > 0).then(|| total.to_string())
|
||||
}
|
||||
|
||||
fn toggle_action(&self) -> Box<dyn Action> {
|
||||
Box::new(ToggleFocus)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -28,6 +28,7 @@ pub struct GitPanelSettings {
|
|||
pub collapse_untracked_diff: bool,
|
||||
pub tree_view: bool,
|
||||
pub diff_stats: bool,
|
||||
pub show_count_badge: bool,
|
||||
}
|
||||
|
||||
impl ScrollbarVisibility for GitPanelSettings {
|
||||
|
|
@ -64,6 +65,7 @@ impl Settings for GitPanelSettings {
|
|||
collapse_untracked_diff: git_panel.collapse_untracked_diff.unwrap(),
|
||||
tree_view: git_panel.tree_view.unwrap(),
|
||||
diff_stats: git_panel.diff_stats.unwrap(),
|
||||
show_count_badge: git_panel.show_count_badge.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -877,6 +877,7 @@ impl VsCodeSettings {
|
|||
scrollbar: None,
|
||||
scroll_multiplier: None,
|
||||
toolbar: None,
|
||||
show_count_badge: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -635,6 +635,11 @@ pub struct GitPanelSettingsContent {
|
|||
///
|
||||
/// Default: true
|
||||
pub diff_stats: Option<bool>,
|
||||
|
||||
/// Whether to show a badge on the git panel icon with the count of uncommitted changes.
|
||||
///
|
||||
/// Default: false
|
||||
pub show_count_badge: Option<bool>,
|
||||
}
|
||||
|
||||
#[derive(
|
||||
|
|
@ -682,6 +687,10 @@ pub struct NotificationPanelSettingsContent {
|
|||
/// Default: 300
|
||||
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
||||
pub default_width: Option<f32>,
|
||||
/// Whether to show a badge on the notification panel icon with the count of unread notifications.
|
||||
///
|
||||
/// Default: false
|
||||
pub show_count_badge: Option<bool>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
|
|
|
|||
|
|
@ -171,6 +171,10 @@ pub struct TerminalSettingsContent {
|
|||
/// Default: 45
|
||||
#[serde(serialize_with = "crate::serialize_optional_f32_with_two_decimal_places")]
|
||||
pub minimum_contrast: Option<f32>,
|
||||
/// Whether to show a badge on the terminal panel icon with the count of open terminals.
|
||||
///
|
||||
/// Default: false
|
||||
pub show_count_badge: Option<bool>,
|
||||
}
|
||||
|
||||
/// Shell configuration to open the terminal with.
|
||||
|
|
|
|||
|
|
@ -4820,7 +4820,7 @@ fn panels_page() -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn terminal_panel_section() -> [SettingsPageItem; 2] {
|
||||
fn terminal_panel_section() -> [SettingsPageItem; 3] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Terminal Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
|
|
@ -4836,6 +4836,28 @@ fn panels_page() -> SettingsPage {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Count Badge",
|
||||
description: "Show a badge on the terminal panel icon with the count of open terminals.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("terminal.show_count_badge"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.terminal
|
||||
.as_ref()?
|
||||
.show_count_badge
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.terminal
|
||||
.get_or_insert_default()
|
||||
.show_count_badge = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
@ -5048,7 +5070,7 @@ fn panels_page() -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn git_panel_section() -> [SettingsPageItem; 13] {
|
||||
fn git_panel_section() -> [SettingsPageItem; 14] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Git Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
|
|
@ -5244,6 +5266,28 @@ fn panels_page() -> SettingsPage {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Count Badge",
|
||||
description: "Whether to show a badge on the git panel icon with the count of uncommitted changes.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("git_panel.show_count_badge"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.git_panel
|
||||
.as_ref()?
|
||||
.show_count_badge
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.git_panel
|
||||
.get_or_insert_default()
|
||||
.show_count_badge = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Scroll Bar",
|
||||
description: "How and when the scrollbar should be displayed.",
|
||||
|
|
@ -5294,7 +5338,7 @@ fn panels_page() -> SettingsPage {
|
|||
]
|
||||
}
|
||||
|
||||
fn notification_panel_section() -> [SettingsPageItem; 4] {
|
||||
fn notification_panel_section() -> [SettingsPageItem; 5] {
|
||||
[
|
||||
SettingsPageItem::SectionHeader("Notification Panel"),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
|
|
@ -5359,6 +5403,28 @@ fn panels_page() -> SettingsPage {
|
|||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
SettingsPageItem::SettingItem(SettingItem {
|
||||
title: "Show Count Badge",
|
||||
description: "Show a badge on the notification panel icon with the count of unread notifications.",
|
||||
field: Box::new(SettingField {
|
||||
json_path: Some("notification_panel.show_count_badge"),
|
||||
pick: |settings_content| {
|
||||
settings_content
|
||||
.notification_panel
|
||||
.as_ref()?
|
||||
.show_count_badge
|
||||
.as_ref()
|
||||
},
|
||||
write: |settings_content, value| {
|
||||
settings_content
|
||||
.notification_panel
|
||||
.get_or_insert_default()
|
||||
.show_count_badge = value;
|
||||
},
|
||||
}),
|
||||
metadata: None,
|
||||
files: USER,
|
||||
}),
|
||||
]
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -50,6 +50,7 @@ pub struct TerminalSettings {
|
|||
pub minimum_contrast: f32,
|
||||
pub path_hyperlink_regexes: Vec<String>,
|
||||
pub path_hyperlink_timeout_ms: u64,
|
||||
pub show_count_badge: bool,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)]
|
||||
|
|
@ -129,6 +130,7 @@ impl settings::Settings for TerminalSettings {
|
|||
})
|
||||
.collect(),
|
||||
path_hyperlink_timeout_ms: project_content.path_hyperlink_timeout_ms.unwrap(),
|
||||
show_count_badge: user_content.show_count_badge.unwrap(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1606,6 +1606,9 @@ impl Panel for TerminalPanel {
|
|||
}
|
||||
|
||||
fn icon_label(&self, _window: &Window, cx: &App) -> Option<String> {
|
||||
if !TerminalSettings::get_global(cx).show_count_badge {
|
||||
return None;
|
||||
}
|
||||
let count = self
|
||||
.center
|
||||
.panes()
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ mod callout;
|
|||
mod chip;
|
||||
mod collab;
|
||||
mod context_menu;
|
||||
mod count_badge;
|
||||
mod data_table;
|
||||
mod diff_stat;
|
||||
mod disclosure;
|
||||
|
|
@ -49,6 +50,7 @@ pub use callout::*;
|
|||
pub use chip::*;
|
||||
pub use collab::*;
|
||||
pub use context_menu::*;
|
||||
pub use count_badge::*;
|
||||
pub use data_table::*;
|
||||
pub use diff_stat::*;
|
||||
pub use disclosure::*;
|
||||
|
|
|
|||
93
crates/ui/src/components/count_badge.rs
Normal file
93
crates/ui/src/components/count_badge.rs
Normal file
|
|
@ -0,0 +1,93 @@
|
|||
use gpui::FontWeight;
|
||||
|
||||
use crate::prelude::*;
|
||||
|
||||
/// A small, pill-shaped badge that displays a numeric count.
|
||||
///
|
||||
/// The count is capped at 99 and displayed as "99+" beyond that.
|
||||
#[derive(IntoElement, RegisterComponent)]
|
||||
pub struct CountBadge {
|
||||
count: usize,
|
||||
}
|
||||
|
||||
impl CountBadge {
|
||||
pub fn new(count: usize) -> Self {
|
||||
Self { count }
|
||||
}
|
||||
}
|
||||
|
||||
impl RenderOnce for CountBadge {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let label = if self.count > 99 {
|
||||
"99+".to_string()
|
||||
} else {
|
||||
self.count.to_string()
|
||||
};
|
||||
|
||||
let bg = cx
|
||||
.theme()
|
||||
.colors()
|
||||
.editor_background
|
||||
.blend(cx.theme().status().error.opacity(0.4));
|
||||
|
||||
h_flex()
|
||||
.absolute()
|
||||
.top_0()
|
||||
.right_0()
|
||||
.p_px()
|
||||
.h_3p5()
|
||||
.min_w_3p5()
|
||||
.rounded_full()
|
||||
.justify_center()
|
||||
.text_center()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(bg)
|
||||
.shadow_sm()
|
||||
.child(
|
||||
Label::new(label)
|
||||
.size(LabelSize::Custom(rems_from_px(9.)))
|
||||
.weight(FontWeight::MEDIUM),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for CountBadge {
|
||||
fn scope() -> ComponentScope {
|
||||
ComponentScope::Status
|
||||
}
|
||||
|
||||
fn description() -> Option<&'static str> {
|
||||
Some("A small, pill-shaped badge that displays a numeric count.")
|
||||
}
|
||||
|
||||
fn preview(_window: &mut Window, cx: &mut App) -> Option<AnyElement> {
|
||||
let container = || {
|
||||
div()
|
||||
.relative()
|
||||
.size_8()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(cx.theme().colors().background)
|
||||
};
|
||||
|
||||
Some(
|
||||
v_flex()
|
||||
.gap_6()
|
||||
.child(example_group_with_title(
|
||||
"Count Badge",
|
||||
vec![
|
||||
single_example(
|
||||
"Basic Count",
|
||||
container().child(CountBadge::new(3)).into_any_element(),
|
||||
),
|
||||
single_example(
|
||||
"Capped Count",
|
||||
container().child(CountBadge::new(150)).into_any_element(),
|
||||
),
|
||||
],
|
||||
))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
|
@ -12,8 +12,10 @@ use gpui::{
|
|||
};
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use ui::{ContextMenu, Divider, DividerColor, IconButton, Tooltip, h_flex};
|
||||
use ui::{prelude::*, right_click_menu};
|
||||
use ui::{
|
||||
ContextMenu, CountBadge, Divider, DividerColor, IconButton, Tooltip, prelude::*,
|
||||
right_click_menu,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub(crate) const RESIZE_HANDLE_SIZE: Pixels = px(6.);
|
||||
|
|
@ -940,6 +942,7 @@ impl Render for PanelButtons {
|
|||
};
|
||||
|
||||
let focus_handle = dock.focus_handle(cx);
|
||||
let icon_label = entry.panel.icon_label(window, cx);
|
||||
|
||||
Some(
|
||||
right_click_menu(name)
|
||||
|
|
@ -973,7 +976,7 @@ impl Render for PanelButtons {
|
|||
.trigger(move |is_active, _window, _cx| {
|
||||
// Include active state in element ID to invalidate the cached
|
||||
// tooltip when panel state changes (e.g., via keyboard shortcut)
|
||||
IconButton::new((name, is_active_button as u64), icon)
|
||||
let button = IconButton::new((name, is_active_button as u64), icon)
|
||||
.icon_size(IconSize::Small)
|
||||
.toggle_state(is_active_button)
|
||||
.on_click({
|
||||
|
|
@ -987,7 +990,15 @@ impl Render for PanelButtons {
|
|||
this.tooltip(move |_window, cx| {
|
||||
Tooltip::for_action(tooltip.clone(), &*action, cx)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
div().relative().child(button).when_some(
|
||||
icon_label
|
||||
.clone()
|
||||
.filter(|_| !is_active_button)
|
||||
.and_then(|label| label.parse::<usize>().ok()),
|
||||
|this, count| this.child(CountBadge::new(count)),
|
||||
)
|
||||
}),
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -7868,7 +7868,6 @@ impl Render for Workspace {
|
|||
window,
|
||||
cx,
|
||||
)),
|
||||
|
||||
BottomDockLayout::RightAligned => div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
|
|
@ -7927,7 +7926,6 @@ impl Render for Workspace {
|
|||
.children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
|
||||
),
|
||||
),
|
||||
|
||||
BottomDockLayout::Contained => div()
|
||||
.flex()
|
||||
.flex_row()
|
||||
|
|
|
|||
Loading…
Reference in a new issue