From 5902e27c77eed3004b84d2ad42e182f0227a8f04 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Tue, 29 Sep 2026 18:05:14 +0800 Subject: [PATCH] fix(sidebar): elide branch names from the end only MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The sidebar branch (group header and row git line) was front-elided, but its width budget was a few pixels short: the line paints with tabular figures while it was measured with proportional ones, and the separator was measured as " · " though it is drawn as a bare "·" between two gaps. The front-elided branch then overflowed and CSS truncation cut its tail too, leaving "…nd-error-classificati…". - Measure the git line and header with the same tabular font they paint with, and measure the separator as drawn. - Elide the branch from the end, the plain cut a reader expects; the CSS backstop now cuts in the same direction. --- src/ui/tab_sidebar.rs | 52 +++++++++++++++++++++++-------------------- src/ui/tab_strip.rs | 45 +++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 24 deletions(-) diff --git a/src/ui/tab_sidebar.rs b/src/ui/tab_sidebar.rs index 8f990ecd..6ff93284 100644 --- a/src/ui/tab_sidebar.rs +++ b/src/ui/tab_sidebar.rs @@ -24,8 +24,8 @@ use crate::ui::i18n::{L10nKey, t, t_fmt}; use crate::ui::reorder::{self, Reorder, Surface}; use crate::ui::right_panel::RESIZE_HANDLE_WIDTH; use crate::ui::tab_strip::{ - DragTab, REORDER_SLIDE_MS, abbreviate_home, elide_label, elide_path_keep_tail, - elide_tail_clusters, measure_text, + DragTab, REORDER_SLIDE_MS, abbreviate_home, elide_end_clusters, elide_label, + elide_path_keep_tail, measure_text, }; pub(crate) const MIN_SIDEBAR_WIDTH: f32 = 180.; @@ -68,7 +68,6 @@ const HEAD_CONTROL_HEIGHT: f32 = 28.; const HEAD_CONTROL_RADIUS: f32 = 7.; /// What sits between a group's branch and its diff counts. -const META_SEP: &str = " · "; const META_SEP_TRIMMED: &str = "·"; /// Tabular numerals, so a column of diff counts lines up digit for digit. @@ -375,9 +374,17 @@ impl Tty7App { let rem = window.rem_size().as_f32(); // Measure with the same interface scale used to paint the header. let header_size = rem * META_REM; + // The git lines and the group header paint with `tabular()` figures, + // which set a `1` as wide as an `8`; measuring them proportionally + // came out a few pixels short on counts like `+1565 −124`, and the + // front-elided branch then lost its tail to CSS truncation as well. + let meta_font = gpui::Font { + features: tabular(), + ..font.clone() + }; let header_font = gpui::Font { weight: FontWeight::MEDIUM, - ..font.clone() + ..meta_font.clone() }; // On the rail the diff counts are metadata like the branch beside // them, so they take its caption ink; the hover card, which is where @@ -678,13 +685,12 @@ impl Tty7App { .text_size(px(meta_size)) .text_color(cx.theme().muted_foreground) .font_features(tabular()); - let counts_w = counts_width(&window.text_system(), &font, meta_size, &g); - // Branch: cut from the front, so `…session-auth` keeps - // the part that tells two branches off one prefix apart. + let counts_w = counts_width(&window.text_system(), &meta_font, meta_size, &g); + // Branch: cut from the end, the way any other label is. let branch_avail = (label_avail - counts_w).max(0.); - let shown = elide_tail_clusters( + let shown = elide_end_clusters( &window.text_system(), - &font, + &meta_font, meta_size, &g.branch, branch_avail, @@ -1236,24 +1242,23 @@ impl Tty7App { } let count_label = row_count.to_string(); if folded { - avail -= - measure_text(&ts, &font, header_size, &count_label) + row_metrics::META_GAP; + avail -= measure_text(&ts, &meta_font, header_size, &count_label) + + row_metrics::META_GAP; } let avail = avail.max(HEADER_NAME_FLOOR); // What the shared branch would take if nothing were in its - // way: the icon, the gap after it, the branch itself, the - // counts, and the two gaps the spacer between the name and - // the branch sits in. - // What the shared branch would take if nothing were in its - // way: the branch itself, the ` · ` before its counts, the + // way: the branch itself, the `·` before its counts, the // counts, and the gaps the spacer between the name and the // branch sits in. - let sep_w = measure_text(&ts, &font, header_size, META_SEP); + // Drawn as a bare `·` with a gap on either side; the gap before + // it is the one `counts_width` already reserves. + let sep_w = measure_text(&ts, &meta_font, header_size, META_SEP_TRIMMED) + + row_metrics::META_GAP; let git_want = shared_git.as_ref().map(|shared| { - let counts = counts_width(&ts, &font, header_size, &shared.status); + let counts = counts_width(&ts, &meta_font, header_size, &shared.status); let sep = if counts > 0. { sep_w } else { 0. }; 2. * row_metrics::META_GAP - + measure_text(&ts, &font, header_size, &shared.status.branch) + + measure_text(&ts, &meta_font, header_size, &shared.status.branch) + sep + counts }); @@ -1398,15 +1403,14 @@ impl Tty7App { click, rows, } = shared; - let counts_w = counts_width(&ts, &font, header_size, &status); + let counts_w = counts_width(&ts, &meta_font, header_size, &status); let sep = if counts_w > 0. { sep_w } else { 0. }; let branch_avail = (avail - name_w - 2. * row_metrics::META_GAP - sep - counts_w).max(0.); - // Cut from the front, like a row's: the tail is what - // tells two branches off the same prefix apart. - let branch = elide_tail_clusters( + // Cut from the end, like a row's. + let branch = elide_end_clusters( &ts, - &font, + &meta_font, header_size, &status.branch, branch_avail, diff --git a/src/ui/tab_strip.rs b/src/ui/tab_strip.rs index a3082904..0cda6da9 100644 --- a/src/ui/tab_strip.rs +++ b/src/ui/tab_strip.rs @@ -471,6 +471,38 @@ pub(crate) fn elide_label( } } +/// Keeps the longest head of `text` that fits before a trailing ellipsis — +/// the plain cut a reader expects, for a string read left to right like a +/// branch name. +pub(crate) fn elide_end_clusters( + text_system: &gpui::WindowTextSystem, + font: &gpui::Font, + size: f32, + text: &str, + max_width: f32, +) -> SharedString { + if measure_text(text_system, font, size, text) <= max_width { + return SharedString::from(text.to_string()); + } + let budget = max_width - measure_text(text_system, font, size, "…"); + if budget <= 0. { + return SharedString::from("…"); + } + let cells = clusters(text); + let (mut lo, mut hi) = (0usize, cells.len()); + while lo < hi { + let mid = (lo + hi + 1) / 2; + if measure_text(text_system, font, size, &cells[..mid].concat()) <= budget { + lo = mid; + } else { + hi = mid - 1; + } + } + let mut out = cells[..lo].concat(); + out.push('…'); + SharedString::from(out) +} + /// Keeps the longest tail of `text` that fits after a bare ellipsis. Shared /// by the path and token elisions as their last resort. pub(crate) fn elide_tail_clusters( @@ -3223,6 +3255,19 @@ mod tests { assert!(out.chars().count() < branch.chars().count()); } + #[gpui::test] + fn elide_end_keeps_the_head_of_a_branch(cx: &mut TestAppContext) { + let (ts, font, size) = elide_setup(cx); + let branch = "fix/rpc-proxy-and-error-classification"; + let max = 140.; + let out = elide_end_clusters(&ts, &font, size, branch, max); + assert!(out.starts_with("fix/rpc-"), "head survives: {out}"); + assert!(out.ends_with('…') && out.matches('…').count() == 1, "{out}"); + assert!(measure_text(&ts, &font, size, &out) <= max); + let fits = measure_text(&ts, &font, size, branch); + assert_eq!(elide_end_clusters(&ts, &font, size, branch, fits), branch); + } + #[gpui::test] fn elide_edges_leaves_short_branches_alone(cx: &mut TestAppContext) { let (ts, font, size) = elide_setup(cx);