git_ui: Prevent button overflow due to long names (#25940)

- Fix component preview widths for git panel
- Fix buttons getting pushed off the screen in git  panel

Release Notes:

- N/A

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
This commit is contained in:
Nate Butler 2025-03-03 13:38:15 -05:00 committed by GitHub
parent b2add8c803
commit 16ab8701a2
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
13 changed files with 283 additions and 148 deletions

View file

@ -609,7 +609,7 @@ impl AssistantPanel {
.id("title")
.overflow_x_scroll()
.px(DynamicSpacing::Base08.rems(cx))
.child(Label::new(title).text_ellipsis()),
.child(Label::new(title).truncate()),
)
.child(
h_flex()

View file

@ -260,7 +260,7 @@ impl RenderOnce for PastThread {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()
@ -356,7 +356,7 @@ impl RenderOnce for PastContext {
.start_slot(
div()
.max_w_4_5()
.child(Label::new(summary).size(LabelSize::Small).text_ellipsis()),
.child(Label::new(summary).size(LabelSize::Small).truncate()),
)
.end_slot(
h_flex()

View file

@ -243,7 +243,7 @@ impl PickerDelegate for SlashCommandDelegate {
Label::new(info.description.clone())
.size(LabelSize::Small)
.color(Color::Muted)
.text_ellipsis(),
.truncate(),
),
),
),

View file

@ -226,3 +226,7 @@ impl Item for ComponentPreview {
f(*event)
}
}
// TODO: impl serializable item for component preview so it will restore with the workspace
// ref: https://github.com/zed-industries/zed/blob/32201ac70a501e63dfa2ade9c00f85aea2d4dd94/crates/image_viewer/src/image_viewer.rs#L199
// Use `ImageViewer` as a model for how to do it, except it'll be even simpler

View file

@ -522,7 +522,7 @@ impl ExtensionsPage {
extension.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.child(Label::new("<>").size(LabelSize::Small)),
)
@ -534,7 +534,7 @@ impl ExtensionsPage {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
.truncate()
}))
.children(repository_url.map(|repository_url| {
IconButton::new(
@ -665,7 +665,7 @@ impl ExtensionsPage {
extension.manifest.authors.join(", ")
))
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.child(
Label::new(format!(
@ -683,7 +683,7 @@ impl ExtensionsPage {
Label::new(description.clone())
.size(LabelSize::Small)
.color(Color::Default)
.text_ellipsis()
.truncate()
}))
.child(
h_flex()

View file

@ -2118,7 +2118,7 @@ impl GitPanel {
.child(
Label::new(commit.subject.clone())
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
)
.id("commit-msg-hover")
.hoverable_tooltip(move |window, cx| {
@ -3296,41 +3296,32 @@ impl RenderOnce for PanelRepoFooter {
fn render(self, window: &mut Window, cx: &mut App) -> impl IntoElement {
let active_repo = self.active_repository.clone();
let overflow_menu_id: SharedString = format!("overflow-menu-{}", active_repo).into();
let repo_selector_trigger = Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted);
let repo_selector = if let Some(panel) = self.git_panel.clone() {
let repo_selector = panel.read(cx).repository_selector.clone();
let repo_count = repo_selector.read(cx).repositories_len(cx);
if repo_count > 1 {
RepositorySelectorPopoverMenu::new(
panel.read(cx).repository_selector.clone(),
Button::new("repo-selector", active_repo)
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted),
Tooltip::text("Choose a repository"),
)
.into_any_element()
} else {
Label::new(active_repo)
.size(LabelSize::Small)
.color(Color::Muted)
.line_height_style(LineHeightStyle::UiLabel)
.into_any_element()
}
let single_repo = repo_count == 1;
RepositorySelectorPopoverMenu::new(
panel.read(cx).repository_selector.clone(),
repo_selector_trigger.disabled(single_repo).truncate(true),
Tooltip::text("Switch active repository"),
)
.into_any_element()
} else {
Button::new("repo-selector", active_repo.clone())
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.color(Color::Muted)
.into_any_element()
// for rendering preview, we don't have git_panel there
repo_selector_trigger.into_any_element()
};
let branch = self.branch.clone();
let branch_name = branch
.as_ref()
.map_or("<no branch>".into(), |branch| branch.name.clone());
.map_or(" (no branch)".into(), |branch| branch.name.clone());
let branches = self.branches.clone();
@ -3338,6 +3329,7 @@ impl RenderOnce for PanelRepoFooter {
.style(ButtonStyle::Transparent)
.size(ButtonSize::None)
.label_size(LabelSize::Small)
.truncate(true)
.tooltip(Tooltip::for_action_title(
"Switch Branch",
&zed_actions::git::Branch,
@ -3372,36 +3364,31 @@ impl RenderOnce for PanelRepoFooter {
.justify_between()
.child(
h_flex()
.relative()
.flex_1()
.overflow_hidden()
.items_center()
.gap_0p5()
.child(
div()
// .when(repo_or_branch_has_uppercase, |this| {
// this.relative().pt(px(2.))
// })
.child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
),
div().child(
Icon::new(IconName::GitBranchSmall)
.size(IconSize::Small)
.color(Color::Muted),
),
)
.child(
h_flex()
.gap_0p5()
.child(repo_selector)
.child(
div()
.text_color(cx.theme().colors().text_muted)
.text_sm()
.child("/"),
)
.child(branch_selector),
),
.child(repo_selector)
.when_some(branch.clone(), |this, _| {
this.child(
div()
.text_color(cx.theme().colors().text_muted)
.text_sm()
.child("/"),
)
})
.child(branch_selector),
)
.child(
h_flex()
.gap_1()
.flex_shrink_0()
.children(spinner)
.child(self.render_overflow_menu(overflow_menu_id))
.when_some(branch, |this, branch| {
@ -3462,94 +3449,220 @@ impl ComponentPreview for PanelRepoFooter {
}
}
fn custom(branch_name: &str, upstream: Option<UpstreamTracking>) -> Branch {
Branch {
is_head: true,
name: branch_name.to_string().into(),
upstream: upstream.map(|tracking| Upstream {
ref_name: format!("zed/{}", branch_name).into(),
tracking,
}),
most_recent_commit: Some(CommitSummary {
sha: "abc123".into(),
subject: "Modify stuff".into(),
commit_timestamp: 1710932954,
}),
}
}
fn active_repository(id: usize) -> SharedString {
format!("repo-{}", id).into()
}
let example_width = px(340.);
v_flex()
.gap_6()
.w_full()
.flex_none()
.children(vec![example_group_with_title(
"Action Button States",
vec![
single_example(
"No Branch",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"no-branch",
active_repository(1).clone(),
None,
))
.into_any_element(),
),
)
.grow(),
single_example(
"Remote status unknown",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"unknown-upstream",
active_repository(2).clone(),
Some(branch(unknown_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"No Remote Upstream",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"no-remote-upstream",
active_repository(3).clone(),
Some(branch(no_remote_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Not Ahead or Behind",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"not-ahead-or-behind",
active_repository(4).clone(),
Some(branch(not_ahead_or_behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Behind remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"behind-remote",
active_repository(5).clone(),
Some(branch(behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Ahead of remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"ahead-of-remote",
active_repository(6).clone(),
Some(branch(ahead_of_upstream)),
))
.into_any_element(),
),
)
.grow(),
single_example(
"Ahead and behind remote",
div()
.w(px(180.))
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"ahead-and-behind",
active_repository(7).clone(),
Some(branch(ahead_and_behind_upstream)),
))
.into_any_element(),
),
)
.grow(),
],
)
.grow()
.vertical()])
.children(vec![example_group_with_title(
"Labels",
vec![
single_example(
"Short Branch & Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"short-branch",
SharedString::from("zed"),
Some(custom("main", behind_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-branch",
SharedString::from("zed"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-repo",
SharedString::from("zed-industries-community-examples"),
Some(custom("gpui", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Long Repo & Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"long-repo-and-branch",
SharedString::from("zed-industries-community-examples"),
Some(custom(
"redesign-and-update-git-ui-list-entry-style",
behind_upstream,
)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Repo",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"uppercase-repo",
SharedString::from("LICENSES"),
Some(custom("main", ahead_of_upstream)),
))
.into_any_element(),
)
.grow(),
single_example(
"Uppercase Branch",
div()
.w(example_width)
.overflow_hidden()
.child(PanelRepoFooter::new_preview(
"uppercase-branch",
SharedString::from("zed"),
Some(custom("update-README", behind_upstream)),
))
.into_any_element(),
)
.grow(),
],
)
.grow()
.vertical()])
.into_any_element()
}

View file

@ -401,9 +401,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
.pl_0p5()
.w(px(240.))
.child(
div().max_w_40().child(
Label::new(model_info.model.name().0.clone()).text_ellipsis(),
),
div()
.max_w_40()
.child(Label::new(model_info.model.name().0.clone()).truncate()),
)
.child(
h_flex()

View file

@ -401,7 +401,7 @@ impl TitleBar {
.child(
Label::new(nickname.clone())
.size(LabelSize::Small)
.text_ellipsis(),
.truncate(),
),
)
.tooltip(move |window, cx| {

View file

@ -97,6 +97,7 @@ pub struct Button {
key_binding: Option<KeyBinding>,
key_binding_position: KeybindingPosition,
alpha: Option<f32>,
truncate: bool,
}
impl Button {
@ -123,6 +124,7 @@ impl Button {
key_binding: None,
key_binding_position: KeybindingPosition::default(),
alpha: None,
truncate: false,
}
}
@ -206,6 +208,15 @@ impl Button {
self.alpha = Some(alpha);
self
}
/// Truncates overflowing labels with an ellipsis (`…`) if needed.
///
/// Buttons with static labels should _never_ be truncated, ensure
/// this is only used when the label is dynamic and may overflow.
pub fn truncate(mut self, truncate: bool) -> Self {
self.truncate = truncate;
self
}
}
impl Toggleable for Button {
@ -437,7 +448,8 @@ impl RenderOnce for Button {
.color(label_color)
.size(self.label_size.unwrap_or_default())
.when_some(self.alpha, |this, alpha| this.alpha(alpha))
.line_height_style(LineHeightStyle::UiLabel),
.line_height_style(LineHeightStyle::UiLabel)
.when(self.truncate, |this| this.truncate()),
)
.children(self.key_binding),
)

View file

@ -64,8 +64,8 @@ impl LabelCommon for HighlightedLabel {
self
}
fn text_ellipsis(mut self) -> Self {
self.base = self.base.text_ellipsis();
fn truncate(mut self) -> Self {
self.base = self.base.truncate();
self
}

View file

@ -171,8 +171,9 @@ impl LabelCommon for Label {
self
}
fn text_ellipsis(mut self) -> Self {
self.base = self.base.text_ellipsis();
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn truncate(mut self) -> Self {
self.base = self.base.truncate();
self
}
@ -240,7 +241,7 @@ mod label_preview {
"Special Cases",
vec![
single_example("Single Line", Label::new("Line 1\nLine 2\nLine 3").single_line().into_any_element()),
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").text_ellipsis()).into_any_element()),
single_example("Text Ellipsis", div().max_w_24().child(Label::new("This is a very long file name that should be truncated: very_long_file_name_with_many_words.rs").truncate()).into_any_element()),
],
),
])

View file

@ -57,7 +57,7 @@ pub trait LabelCommon {
fn alpha(self, alpha: f32) -> Self;
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn text_ellipsis(self) -> Self;
fn truncate(self) -> Self;
/// Sets the label to render as a single line.
fn single_line(self) -> Self;
@ -84,7 +84,7 @@ pub struct LabelLike {
alpha: Option<f32>,
underline: bool,
single_line: bool,
text_ellipsis: bool,
truncate: bool,
}
impl Default for LabelLike {
@ -109,7 +109,7 @@ impl LabelLike {
alpha: None,
underline: false,
single_line: false,
text_ellipsis: false,
truncate: false,
}
}
}
@ -166,8 +166,9 @@ impl LabelCommon for LabelLike {
self
}
fn text_ellipsis(mut self) -> Self {
self.text_ellipsis = true;
/// Truncates overflowing text with an ellipsis (`…`) if needed.
fn truncate(mut self) -> Self {
self.truncate = true;
self
}
@ -220,7 +221,7 @@ impl RenderOnce for LabelLike {
})
.when(self.strikethrough, |this| this.line_through())
.when(self.single_line, |this| this.whitespace_nowrap())
.when(self.text_ellipsis, |this| {
.when(self.truncate, |this| {
this.overflow_x_hidden().text_ellipsis()
})
.text_color(color)