feat(diff): restyle the diff overlay and commit detail after the v4 design

Carry the v4 language into the diff overlay and the commit detail view:
0.5px hairlines at 8% ink, 26px row pills with a 6px radius, the rem type
ladder from right_panel.rs, neutral chips instead of accent washes, the
shared git_badge for status letters, and tabular figures on counts.
Layout, spacing, type and colour only; no behaviour or i18n changes.
This commit is contained in:
l0ng-ai
2026-09-24 20:23:22 +08:00
parent c9953761d5
commit 9b6261e644
2 changed files with 289 additions and 165 deletions
+161 -75
View File
@@ -5,7 +5,7 @@ use std::sync::Arc;
use gpui::{
AnyElement, Background, FocusHandle, FontWeight, Hsla, KeyDownEvent, MouseButton,
MouseDownEvent, MouseMoveEvent, Pixels, SharedString, Window, div, prelude::*, px,
MouseDownEvent, MouseMoveEvent, Pixels, SharedString, Window, div, prelude::*, px, rems,
};
use gpui_component::button::Button;
use gpui_component::menu::{ContextMenuExt as _, PopupMenuItem};
@@ -27,7 +27,9 @@ use crate::ui::diff_list::{DiffRow, FileHead, RowAt};
use crate::ui::diff_rows::{DiffSelection, Side, SplitCell, SplitRow, UnifiedRow};
use crate::ui::document_column::DocumentChrome;
use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural};
use crate::ui::right_panel::info_chip;
use crate::ui::right_panel::{
HEADING, META, META_MONO, TAB_TEXT, TEXT, TEXT_MONO, git_badge, info_chip,
};
use crate::ui::rounding;
use crate::ui::scm::path::relative_time;
use crate::ui::scm::status::{status_color, status_glyph};
@@ -633,6 +635,7 @@ impl Tty7App {
let subject_takes_the_slack =
chrome.is_dock() && !subject.is_rev && subject.label.is_none();
let menu_app = cx.entity().downgrade();
let hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().window.hover);
let row = h_flex().id("diff-overlay-header");
let row = if chrome.header_is_title_strip() {
crate::ui::app::title_bar_drag(row, "diff-overlay-header", window, cx)
@@ -642,11 +645,15 @@ impl Tty7App {
row.flex_shrink_0()
.h(px(crate::ui::app::TITLE_BAR_HEIGHT))
.pl(px(lead))
.pr(px(crate::ui::app::tile_trailing_inset()))
// The close tile is a notch smaller than the title bar's own, and
// is pulled in by half the difference so its glyph stays on the
// column every other trailing tile in the window is centred on.
.pr(px(crate::ui::app::tile_trailing_inset()
+ (crate::ui::app::TILE_SIZE - CLOSE_TILE) / 2.))
.gap_2()
.items_center()
.border_b_1()
.border_color(cx.theme().border)
.border_b(HAIRLINE_W)
.border_color(hairline(cx))
.child(
gpui::svg()
.path(subject.icon)
@@ -659,7 +666,8 @@ impl Tty7App {
// same monospace the patch below it is set in.
div()
.flex_shrink_0()
.text_size(px(13.))
.text_size(rems(TEXT_MONO))
.font_weight(FontWeight::MEDIUM)
.font_family(self.font_family.clone())
.child(subject.text)
.into_any_element()
@@ -674,16 +682,19 @@ impl Tty7App {
div()
.when(subject_takes_the_slack, |d| d.flex_1().min_w_0().truncate())
.when(!subject_takes_the_slack, |d| d.flex_shrink_0())
.text_sm()
.text_size(rems(TEXT))
.font_weight(FontWeight::MEDIUM)
.child(subject.text)
.into_any_element()
})
// A faint neutral pill, the one the graph gives HEAD: the chip
// qualifies the branch beside it rather than competing with it,
// and the title bar spends no accent on anything.
.when_some(subject.chip, |bar, text| {
bar.child(info_chip(
text,
cx.theme().accent.opacity(0.16),
cx.theme().foreground,
cx.theme().foreground.opacity(CHIP_FILL),
cx.theme().muted_foreground,
&mono,
))
})
@@ -705,7 +716,7 @@ impl Tty7App {
.flex_auto()
.min_w_0()
.truncate()
.text_sm()
.text_size(rems(TEXT))
.child(SharedString::from(label.subject.clone())),
)
// Yields before the subject does, for the same reason the
@@ -717,7 +728,8 @@ impl Tty7App {
.min_w_0()
.flex_shrink(999.)
.truncate()
.text_xs()
.text_size(rems(META))
.font_features(tabular())
.text_color(cx.theme().muted_foreground)
.child(label_byline(label, now_unix())),
)
@@ -740,11 +752,12 @@ impl Tty7App {
.items_center()
.min_w_0()
.gap_1()
.px_1p5()
.py_0p5()
.rounded_md()
.h(CONTROL_H)
.pl(px(4.))
.pr(ROW_INSET)
.rounded(ROW_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(cx.theme().list_hover))
.hover(|s| s.bg(hover))
.on_click(cx.listener(|this, _, _window, cx| {
let active = this.active;
if let Some(overlay) = this
@@ -765,7 +778,7 @@ impl Tty7App {
.child(
h_flex()
.min_w_0()
.text_xs()
.text_size(rems(META_MONO))
.font_family(self.font_family.clone())
.child(div().min_w_0().flex_shrink(999.).truncate().child(head))
.child(div().min_w_0().flex_shrink(1.).truncate().child(leaf)),
@@ -786,7 +799,8 @@ impl Tty7App {
bar.when(!chrome.is_dock(), |bar| {
bar.child(
div()
.text_xs()
.text_size(rems(META))
.font_features(tabular())
.text_color(cx.theme().muted_foreground)
.child(summary),
)
@@ -794,7 +808,8 @@ impl Tty7App {
.when(added > 0, |bar| {
bar.child(
div()
.text_xs()
.text_size(rems(META))
.font_features(tabular())
.text_color(cx.theme().success)
.child(format!("+{added}")),
)
@@ -802,7 +817,8 @@ impl Tty7App {
.when(removed > 0, |bar| {
bar.child(
div()
.text_xs()
.text_size(rems(META))
.font_features(tabular())
.text_color(cx.theme().danger)
.child(format!("−{removed}")),
)
@@ -814,7 +830,7 @@ impl Tty7App {
|bar| {
bar.child(
div()
.text_xs()
.text_size(rems(META))
.text_color(cx.theme().muted_foreground)
.child(t(L10nKey::Refreshing)),
)
@@ -833,12 +849,12 @@ impl Tty7App {
div().occlude().flex_shrink_0().child(
crate::ui::tab_strip::chrome_tile_sized(
Button::new("diff-overlay-close").icon(Icon::new(IconName::Close)),
crate::ui::app::TILE_SIZE,
crate::ui::app::TILE_GLYPH_LINE,
CLOSE_TILE,
crate::ui::tab_strip::RAIL_TILE_GLYPH,
false,
cx,
)
.rounded_lg()
.rounded(px(crate::ui::tab_strip::RAIL_TILE_RADIUS))
.tooltip(t(L10nKey::DiffCloseTooltip))
.on_click(cx.listener(|this, _, window, cx| {
this.close_diff_overlay(window, cx);
@@ -859,6 +875,10 @@ impl Tty7App {
/// glyph, and so is every tile at the other end of the window — so it read
/// as pasted on. Same two choices, no frame: the live one carries a soft
/// fill, the other is quiet text that lights up under the pointer.
///
/// The cells are the list's own row pills — row height, row radius — set
/// in the right panel's tab type, so the switch reads as two more rows of
/// the same app rather than as a widget.
fn diff_view_switch(&self, cx: &mut Context<Self>) -> AnyElement {
let sf = cx.global::<crate::ui::presets::Surfaces>().window;
let current = view_mode(cx);
@@ -875,10 +895,10 @@ impl Tty7App {
h_flex()
.id(("diff-overlay-view-cell", i))
.items_center()
.h(px(22.))
.px(px(8.))
.h(CONTROL_H)
.px(px(10.))
.rounded(ROW_RADIUS)
.text_sm()
.text_size(rems(TAB_TEXT))
.cursor_pointer()
.when(live, |cell| {
cell.bg(gpui::rgb(sf.selected))
@@ -977,7 +997,7 @@ impl Tty7App {
.flex()
.items_center()
.justify_center()
.text_sm()
.text_size(rems(TEXT))
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
@@ -1101,7 +1121,7 @@ impl Tty7App {
// full width and puts it at its own left edge, so a horizontal
// padding here would be silently ignored. The rows carry their own —
// see `diff_row_element`.
.py_4();
.py(px(LIST_INSET));
// The bar reads the list's own height, and a list only counts the
// rows it has measured. Left at that, a patch of any length would
// report itself as one screen long and the thumb would fill the
@@ -1281,17 +1301,61 @@ fn spliced_range(old: &[DiffRow], new: &[DiffRow]) -> (std::ops::Range<usize>, u
(prefix..old.len() - suffix, new.len() - prefix - suffix)
}
/// The row inset every row of the list shares, matching the source control
/// panel's — the overlay is a second view of that panel's list, and the two
/// stopped looking like one app when this one drew cards.
const ROW_INSET: Pixels = px(10.);
/// How far a row's pill sits in from the edge of the list: the window's
/// `CONTENT_INSET`, the same 12px the rail and the right panel keep, and the
/// same inset the header's subject starts at.
const LIST_INSET: f32 = crate::ui::app::CONTENT_INSET;
/// The padding a row keeps inside its pill — the rail's 8px — so a hovered
/// row's fill reaches past its text on both sides. The overlay is a second
/// view of the Changes tab's list, and the two stopped looking like one app
/// when this one drew cards.
const ROW_INSET: Pixels = px(8.);
/// The height of a row that is a *file* rather than a line of one: the same
/// 26px the panel gives its file rows.
const FILE_ROW_H: Pixels = px(26.);
/// The radius on a row that lights up under the pointer. Matches the panel's.
const ROW_RADIUS: Pixels = px(5.);
/// The radius on a row that lights up under the pointer: the 6px the Changes
/// tab's 26px rows use.
const ROW_RADIUS: Pixels = px(6.);
/// The header's pills — the view switch and the way back to the list — are
/// one row tall, so the title bar's controls are the list's rows in another
/// place rather than a smaller widget of their own.
const CONTROL_H: Pixels = FILE_ROW_H;
/// The header's close tile: the rail header's 26px tile, not the title bar's
/// 32. The diff header already carries a subject, a switch and a path; the
/// way out should be the quietest shape on it.
const CLOSE_TILE: f32 = crate::ui::tab_strip::RAIL_TILE;
/// The fill under a neutral chip, the same faint ink the graph's HEAD chip
/// wears.
const CHIP_FILL: f32 = 0.06;
/// A section label's line in the list — the untracked heading — at the
/// Changes tab's group-header height.
const GROUP_HEADER_H: Pixels = px(22.);
/// The width of every rule the diff views draw: the header's bottom edge,
/// the hunk breaks, the split view's centre and the unified gutter.
pub(crate) const HAIRLINE_W: Pixels = px(0.5);
/// The ink of those rules: a hairline at 8% of the foreground, which reads as
/// an edge on the window fill without ever drawing a box. Derived from the
/// foreground rather than `border`, the control-edge role, because a
/// half-pixel of a colour tuned for 1px outlines reads heavier than the rest
/// of the v4 chrome.
pub(crate) fn hairline(cx: &gpui::App) -> Hsla {
cx.theme().foreground.opacity(0.08)
}
/// Tabular figures for counts set in the interface face, so `+12 −3` and a
/// file count keep their width as they change. Mono text needs none of this.
pub(crate) fn tabular() -> gpui::FontFeatures {
gpui::FontFeatures(Arc::new(vec![("tnum".to_string(), 1)]))
}
/// The height of one line of a patch, in either view.
///
@@ -1307,9 +1371,9 @@ const DIFF_LINE_H: Pixels = px(19.);
///
/// Barely there on purpose: with the cards gone it is the only line left in
/// the list, and it is separating two parts of one file rather than two
/// files.
/// files. It is the header's hairline, so the view has one kind of line.
fn hunk_rule(cx: &gpui::App) -> Hsla {
cx.theme().border.opacity(0.6)
hairline(cx)
}
/// One row, inset the way every row in the list is.
@@ -1334,9 +1398,9 @@ fn diff_row_element(
.px(ROW_INSET)
.py_1()
.when(!leads, |h| {
h.mt_1().border_t_1().border_color(hunk_rule(cx))
h.mt_1().border_t(HAIRLINE_W).border_color(hunk_rule(cx))
})
.text_xs()
.text_size(rems(META_MONO))
.font_family(font.clone())
.text_color(cx.theme().muted_foreground)
.truncate()
@@ -1373,15 +1437,19 @@ fn diff_row_element(
DiffRow::MoreFiles { rest } => {
padded(note_row(t_plural(L10nKey::DiffMoreFiles, *rest, &[]), cx))
}
// A section label, in the shape the sidebar gives its group headings:
// small, quiet, and carried by the space around it rather than a bar
// of its own.
// A section label, in the shape the Changes tab gives its group
// headings: 11.5px, medium, muted, sentence case — small, quiet, and
// carried by the space around it rather than a bar of its own.
DiffRow::UntrackedHeader { total } => padded(
div()
.w_full()
.flex()
.items_center()
.h(GROUP_HEADER_H)
.px(ROW_INSET)
.py_1()
.text_xs()
.text_size(rems(HEADING))
.font_weight(FontWeight::MEDIUM)
.font_features(tabular())
.text_color(cx.theme().muted_foreground)
.child(t_plural(L10nKey::DiffUntrackedHeader, *total, &[]))
.into_any_element(),
@@ -1464,7 +1532,11 @@ fn diff_row_drag<E: InteractiveElement + Styled>(
/// The margin the file rows keep from the edge of the list.
fn padded(row: AnyElement) -> AnyElement {
div().w_full().px_2().child(row).into_any_element()
div()
.w_full()
.px(px(LIST_INSET))
.child(row)
.into_any_element()
}
/// An aside in the list's own voice — a cap that was hit, a tail that was not
@@ -1474,7 +1546,7 @@ fn note_row(text: String, cx: &gpui::App) -> AnyElement {
.w_full()
.px(ROW_INSET)
.py_1()
.text_xs()
.text_size(rems(META))
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
@@ -1486,13 +1558,16 @@ fn diff_oversized_notice(snap: &DiffSnapshot, cx: &gpui::App) -> AnyElement {
L10nKey::DiffOversizedNotice,
&[("summary", &oversized_summary(snap, &stats))],
);
// The row pill at rest, filled with the hover step: a notice sits in the
// list's own shape, one quiet rung off the window, rather than in a card.
let fill = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().window.hover);
div()
.w_full()
.px(ROW_INSET)
.py_2()
.rounded(rounding::CARD_RADIUS)
.bg(cx.theme().secondary)
.text_xs()
.rounded(rounding::ROW_RADIUS)
.bg(fill)
.text_size(rems(META))
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
@@ -1504,7 +1579,8 @@ fn diff_file_header(
app: &gpui::WeakEntity<Tty7App>,
cx: &gpui::App,
) -> AnyElement {
let hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().window.hover);
let sf = cx.global::<crate::ui::presets::Surfaces>().window;
let hover = gpui::rgb(sf.hover);
let deco = deco_status(head.status);
let (glyph, glyph_color) = (status_glyph(deco), status_color(deco, cx));
let mut header = h_flex()
@@ -1542,33 +1618,29 @@ fn diff_file_header(
} else {
IconName::ChevronRight
})
.small()
.size(px(crate::ui::app::TILE_GLYPH_XS))
.text_color(cx.theme().muted_foreground),
)
})
.child(
div()
.flex_shrink_0()
.font_family(font.clone())
.text_xs()
.font_weight(FontWeight::BOLD)
.text_color(glyph_color)
.child(glyph),
)
// The status letter is the Changes tab's badge — same cell, weight
// and ink — so M reads amber and A/U green here exactly as it does
// one click away.
.child(git_badge(glyph, glyph_color, font))
.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_xs()
.text_size(rems(TEXT_MONO))
.font_family(font.clone())
.text_color(gpui::rgb(sf.text_resting))
.child(head.shown_path.clone()),
);
if head.binary {
header = header.child(
div()
.flex_shrink_0()
.text_xs()
.text_size(rems(META))
.text_color(cx.theme().muted_foreground)
.child(t(L10nKey::Binary)),
);
@@ -1577,7 +1649,8 @@ fn diff_file_header(
header = header.child(
div()
.flex_shrink_0()
.text_xs()
.text_size(rems(META_MONO))
.font_family(font.clone())
.text_color(cx.theme().success)
.child(format!("+{}", head.added)),
);
@@ -1586,7 +1659,8 @@ fn diff_file_header(
header = header.child(
div()
.flex_shrink_0()
.text_xs()
.text_size(rems(META_MONO))
.font_family(font.clone())
.text_color(cx.theme().danger)
.child(format!("−{}", head.removed)),
);
@@ -1606,7 +1680,8 @@ fn diff_untracked_row(
app: &gpui::WeakEntity<Tty7App>,
cx: &gpui::App,
) -> AnyElement {
let hover = gpui::rgb(cx.global::<crate::ui::presets::Surfaces>().window.hover);
let sf = cx.global::<crate::ui::presets::Surfaces>().window;
let hover = gpui::rgb(sf.hover);
let for_focus = path.to_string();
let app = app.clone();
h_flex()
@@ -1617,8 +1692,6 @@ fn diff_untracked_row(
.h(FILE_ROW_H)
.px(ROW_INSET)
.rounded(ROW_RADIUS)
.text_xs()
.font_family(font.clone())
.cursor_pointer()
.hover(|s| s.bg(hover))
.on_click(move |_, window, cx| {
@@ -1636,14 +1709,21 @@ fn diff_untracked_row(
})
.ok();
})
.child(git_badge(
status_glyph(DecoStatus::Untracked),
status_color(DecoStatus::Untracked, cx),
font,
))
.child(
div()
.flex_shrink_0()
.font_weight(FontWeight::BOLD)
.text_color(status_color(DecoStatus::Untracked, cx))
.child(status_glyph(DecoStatus::Untracked)),
.flex_1()
.min_w_0()
.truncate()
.text_size(rems(TEXT_MONO))
.font_family(font.clone())
.text_color(gpui::rgb(sf.text_resting))
.child(path.to_string()),
)
.child(div().flex_1().min_w_0().truncate().child(path.to_string()))
.into_any_element()
}
@@ -1669,7 +1749,7 @@ fn diff_split_row(
app,
cx,
))
.child(div().flex_shrink_0().w(px(1.)).bg(hunk_rule(cx)))
.child(div().flex_shrink_0().w(HAIRLINE_W).bg(hunk_rule(cx)))
.child(diff_split_cell(
row.right.as_ref(),
Side::New,
@@ -1723,7 +1803,7 @@ fn diff_split_cell(
.w(px(42.))
.justify_end()
.pr_1p5()
.text_color(cx.theme().muted_foreground.opacity(0.7))
.text_color(cx.theme().muted_foreground)
.child(cell.no.map(|n| n.to_string()).unwrap_or_default()),
)
.child(
@@ -1768,7 +1848,7 @@ fn diff_unified_row(
.w(px(34.))
.justify_end()
.pr_1p5()
.text_color(cx.theme().muted_foreground.opacity(0.7))
.text_color(cx.theme().muted_foreground)
.child(no.map(|n| n.to_string()).unwrap_or_default())
};
let fill = match drag.covers(at, None) {
@@ -1787,7 +1867,13 @@ fn diff_unified_row(
// The split view's centre rule, in the one place it still means the
// same thing: everything left of it is a number, everything right of
// it is the file.
.child(div().flex_shrink_0().w(px(1.)).h_full().bg(hunk_rule(cx)))
.child(
div()
.flex_shrink_0()
.w(HAIRLINE_W)
.h_full()
.bg(hunk_rule(cx)),
)
.child(
div()
.flex_shrink_0()
+128 -90
View File
@@ -46,7 +46,7 @@ use tty7_core::core::git::status::DecoStatus;
use crate::terminal::git_diff::DiffSource;
use crate::ui::app::{CONTENT_INSET, Tty7App};
use crate::ui::i18n::{L10nKey, t, t_plural};
use crate::ui::right_panel::{META, META_MONO, ROW_INSET, TEXT, git_badge, info_chip};
use crate::ui::right_panel::{HEADING, META, META_MONO, ROW_INSET, TEXT, git_badge, info_chip};
use crate::ui::scm::path::{relative_time, split_display_path};
use crate::ui::scm::state::{CommitDetailView, RepoKey};
use crate::ui::scm::status::{status_color, status_glyph};
@@ -80,6 +80,26 @@ const BODY_PAD_T: f32 = 2. / 16.;
/// this repository but a handful, and a cap for the ones that are a paragraph.
const SUBJECT_LINES: usize = 3;
/// The radius on every pill in this view — the rows, the way back, the object
/// ids: the 6px the Changes tab's 26px rows use.
const PILL_RADIUS: gpui::Pixels = px(6.);
/// How tall the header's two affordances and the parent links are: a row's
/// height less a couple of pixels either side, so a hovered pill reads as the
/// target inside its row rather than as the whole row lighting up.
const PILL_H: f32 = ROW_H - 4.;
/// The pause before the file list — the one the Changes tab leaves between
/// two groups — and the height of the summary line that heads it, which is a
/// group header there too.
const GROUP_GAP: f32 = 16.;
const GROUP_HEADER_H: f32 = 22.;
/// A file row's name keeps at least this much before the directory beside it
/// has given up all of its width. `panel.rs`'s floor, restated because the
/// two rows are the same row.
const NAME_FLOOR: f32 = 40.;
// Which step of the right panel's ramp does what in this view.
//
// The names come from `right_panel.rs` and the sizes are not this view's to
@@ -159,7 +179,7 @@ impl Tty7App {
// out a `ROW_INSET` short of it so the fill is wider than the text, and
// an outer inset would have to be undone by every one of them.
let mut body = v_flex()
.py(px(2.))
.pb(px(12.))
.child(self.detail_header_row(detail, &mono, cx));
match detail.commit.as_deref() {
@@ -176,8 +196,10 @@ impl Tty7App {
None => {
body = body.child(
div()
.flex()
.items_center()
.min_h(px(ROW_H))
.px(px(CONTENT_INSET))
.py(px(4.))
.text_size(rems(META))
.text_color(muted)
.child(if detail.loaded {
@@ -268,6 +290,10 @@ impl Tty7App {
/// reader came to read. The oid is set in the same mono at the same token
/// size as the parent links below, so the two read as the same kind of
/// thing.
///
/// Both pills pad themselves by `ROW_INSET` inside a row inset by
/// `CONTENT_INSET - ROW_INSET`, so the chevron and the oid land on the
/// same 12px column as every line of text under them.
fn detail_header_row(
&self,
detail: &CommitDetailView,
@@ -287,9 +313,10 @@ impl Tty7App {
.id("scm-detail-back")
.items_center()
.gap(px(2.))
.px(px(4.))
.py(px(1.))
.rounded(px(4.))
.h(px(PILL_H))
.pl(px(ROW_INSET - 2.))
.pr(px(ROW_INSET))
.rounded(PILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(hover_bg))
.on_click(cx.listener(|this, _, _window, cx| this.close_commit_detail(cx)))
@@ -307,12 +334,13 @@ impl Tty7App {
)
.child(div().flex_1().min_w_0())
.child(
div()
h_flex()
.id("scm-detail-sha")
.flex_none()
.px(px(4.))
.py(px(1.))
.rounded(px(4.))
.items_center()
.h(px(PILL_H))
.px(px(ROW_INSET))
.rounded(PILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(hover_bg))
.text_size(rems(META_MONO))
@@ -342,17 +370,19 @@ impl Tty7App {
let folded = !detail.body_expanded && lines > BODY_LINES;
v_flex()
.px(px(CONTENT_INSET))
.pb(px(4.))
.gap(px(3.))
.pt(px(4.))
.pb(px(6.))
.gap(px(4.))
.child(
// Wrapping, not truncating: this view exists because the graph
// row could only show the first 26 characters. It carries the
// weight and the full foreground while everything under it is
// muted, and that is the whole of its emphasis — it sits on
// the same 12px step as the file rows below it.
// the same step as the file rows below it. Medium, the weight
// the rest of the panel gives a title, not semibold.
div()
.text_size(rems(TEXT))
.font_weight(gpui::FontWeight::SEMIBOLD)
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(cx.theme().foreground)
.line_clamp(SUBJECT_LINES)
.child(SharedString::from(commit.summary.clone())),
@@ -360,6 +390,7 @@ impl Tty7App {
.child(
div()
.text_size(rems(META))
.font_features(crate::ui::diff_overlay::tabular())
.text_color(cx.theme().muted_foreground)
.child(byline(commit, now_unix())),
)
@@ -399,7 +430,11 @@ impl Tty7App {
body.to_string()
})),
)
// A control, not a link: muted like the rest of the chrome,
// lifted to the foreground and medium under the pointer. The
// accent stays with focus rings and the primary actions.
.when(lines > BODY_LINES, |this| {
let fg = cx.theme().foreground;
this.child(
div()
.id("scm-detail-body-fold")
@@ -408,7 +443,9 @@ impl Tty7App {
.py(px(1.))
.cursor_pointer()
.text_size(rems(META))
.text_color(cx.theme().info)
.font_weight(gpui::FontWeight::MEDIUM)
.text_color(cx.theme().muted_foreground)
.hover(move |s| s.text_color(fg))
.on_click(cx.listener(|this, _, _window, cx| {
if let Some(open) = this.scm.detail.as_mut() {
open.body_expanded = !open.body_expanded;
@@ -432,9 +469,10 @@ impl Tty7App {
/// of them once there is a whole column to put them in.
///
/// Exactly two of them get a fill, and they are the two that mean
/// something. HEAD is where you are, washed in `accent` under the full
/// foreground — one emphasised token on the row. A tag is yellow because a
/// tag is yellow everywhere in git. Everything else — the other local
/// something. HEAD is where you are: the faint neutral pill the graph
/// gives it, under the full foreground — one emphasised token on the row,
/// and no accent spent on it. A tag is yellow because a tag is yellow
/// everywhere in git. Everything else — the other local
/// branches, every remote-tracking ref — is a bare muted span: no fill, and
/// therefore no padding either, because padding exists to hold text off a
/// background and there is no background to hold it off. A ref name is
@@ -442,10 +480,10 @@ impl Tty7App {
/// them turns a list of names into a wall of blocks, which is what the
/// panel's language is trying not to be.
///
/// `theme.accent` is a neutral surface tint in tty7 rather than the brand
/// colour, which is exactly why 0.28 of it works: it is a raised patch, not
/// a wash of hue, and the foreground stays legible on it. Do not substitute
/// `theme.ring` here and then have to drop the opacity to compensate.
/// The fill is the foreground at 6%, the same patch the graph's HEAD chip
/// wears, so one ref looks the same in both places. The foreground over
/// it, not the muted ink the graph uses: the graph has a filled bead in
/// the gutter to say "you are here", and this view has only the chip.
///
/// What this must never go back to is the bug that predated all of it: the
/// fallback arm painted `theme.accent` at *full* opacity under muted text,
@@ -461,12 +499,7 @@ impl Tty7App {
return None;
}
let theme = cx.theme();
let (accent, warning, fg, muted) = (
theme.accent,
theme.warning,
theme.foreground,
theme.muted_foreground,
);
let (warning, fg, muted) = (theme.warning, theme.foreground, theme.muted_foreground);
let mut row = h_flex()
.flex_wrap()
.items_center()
@@ -479,7 +512,7 @@ impl Tty7App {
for deco in &commit.refs {
row = row.child(match deco.kind {
RefKind::Tag => info_chip(&deco.short, warning.opacity(0.16), warning, mono),
_ if deco.is_head => info_chip(&deco.short, accent.opacity(0.28), fg, mono),
_ if deco.is_head => info_chip(&deco.short, fg.opacity(0.06), fg, mono),
_ => ref_span(&deco.short, muted, mono),
});
}
@@ -489,11 +522,11 @@ impl Tty7App {
/// The parents, as links. Following one is the only way to walk history
/// backwards from a commit the graph's window does not reach.
///
/// `theme.info` and nothing else at rest — the panel's link ink, the same
/// one the "show more" fold uses a few lines above. A filled pill would
/// make the parents a second block competing with the ref chips, and this
/// is a link, not a state. The hover fill is what says the oid is a target,
/// and it is the same fill, radius and inset the header's two affordances
/// The resting row ink and nothing else at rest — no accent, the way no
/// other chrome in the panel spends one. A filled pill would make the
/// parents a second block competing with the ref chips, and this is a
/// link, not a state. The hover fill is what says the oid is a target, and
/// it is the same fill, radius and height the header's two affordances
/// use. The oids themselves are token-sized mono, matching the sha in the
/// header so that every object id in this view is one recognisable shape.
fn detail_parents(
@@ -506,19 +539,21 @@ impl Tty7App {
if commit.parents.is_empty() {
return None;
}
let hover_bg = gpui::rgb(panel_surface(cx).hover);
let (muted, link) = {
let theme = cx.theme();
(theme.muted_foreground, theme.info)
};
let sf = panel_surface(cx);
let hover_bg = gpui::rgb(sf.hover);
let (muted, link) = (cx.theme().muted_foreground, gpui::rgb(sf.text_resting));
// The label sits on the text column and each oid's pill pads back out
// by `ROW_INSET`, so the row starts one inset short of it: the first
// link's text then lines up with the label's gap, not its pill edge.
let mut row = h_flex()
.flex_wrap()
.items_center()
.gap(px(6.))
.gap(px(2.))
.px(px(CONTENT_INSET))
.pb(px(4.))
.child(
div()
.mr(px(ROW_INSET - 2.))
.text_size(rems(META))
.text_color(muted)
.child(t(L10nKey::ScmCommitParents)),
@@ -527,11 +562,12 @@ impl Tty7App {
let repo = detail.repo.clone();
let oid = parent.clone();
row = row.child(
div()
h_flex()
.id(SharedString::from(format!("scm-detail-parent-{parent}")))
.px(px(4.))
.py(px(1.))
.rounded(px(4.))
.items_center()
.h(px(PILL_H))
.px(px(ROW_INSET))
.rounded(PILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(hover_bg))
.text_size(rems(META_MONO))
@@ -603,30 +639,18 @@ impl Tty7App {
/// in the diff views: the ASCII one sits too high and too short beside a
/// `+` of the same size.
///
/// Still not `panel_subtitle`: that helper uppercases its label and puts
/// anything in its trailing slot hard against the right edge, because the
/// slot was built for a button. Both are wrong here. "3 FILES CHANGED" is
/// a heading's voice and this is a sentence about the commit, and the
/// counts are not a control off in the corner — they qualify the words and
/// have to sit next to them, which is the one thing the layout round got
/// right and the user asked to keep.
/// Still not `panel_subtitle`: that helper puts anything in its trailing
/// slot hard against the right edge, because the slot was built for a
/// button. The counts are not a control off in the corner — they qualify
/// the words and have to sit next to them, which is the one thing the
/// layout round got right and the user asked to keep.
///
/// What the helper *is* copied on is its frame: the hairline and the six
/// above it, so the file list starts on exactly the line the working tree's
/// does. A rule is how this panel divides sections; the round that replaced
/// it with a raised card is the round being undone.
///
/// The two paddings are that frame re-derived rather than copied, because
/// the tallest line in each block is a different size. gpui leads a plain
/// `div` at phi: the helper's 10.5px uppercase label measures
/// `round(10.5 × 1.618) = 17px`, and the tallest thing in this row is the
/// 11px file count at `round(11 × 1.618) = 18`. The helper's block is
/// `6 + 1 + 12 + 17 + 4 = 40px` tall, so this one has 15px of padding to
/// spend instead of 16 — half a pixel off each side, which keeps the total
/// at 40 *and* puts both lines' optical centre 27.5px below the top of the
/// margin, so nothing shifts when the reader opens a commit. Change either
/// side's type and this has to be worked out again on both, or one list
/// quietly starts a pixel or two below the other and nobody can see why.
/// What it *is* copied on is the Changes tab's group header, which is what
/// it is: the line that heads a list of files. 16px of air above it and no
/// rule — the working tree tells its groups apart by the pause before each
/// one, and a hairline here would be the only one left in the tab — then
/// 22px of 11.5px medium, muted, sentence case. "3 files changed" is a
/// sentence about the commit, never "3 FILES CHANGED".
fn detail_summary(
&self,
files: &[CommitFile],
@@ -634,21 +658,21 @@ impl Tty7App {
cx: &mut Context<Self>,
) -> AnyElement {
let theme = cx.theme();
let (muted, border) = (theme.muted_foreground, theme.border);
let muted = theme.muted_foreground;
let (added_ink, removed_ink) = (theme.success, theme.danger);
let counts = diff_totals(files);
h_flex()
.items_center()
.gap(px(6.))
.mt(px(6.))
.border_t_1()
.border_color(border)
.mt(px(GROUP_GAP))
.mb(px(1.))
.min_h(rems(GROUP_HEADER_H / 16.))
.px(px(CONTENT_INSET))
.pt(px(11.5))
.pb(px(3.5))
.child(
div()
.text_size(rems(META))
.text_size(rems(HEADING))
.font_weight(gpui::FontWeight::MEDIUM)
.font_features(crate::ui::diff_overlay::tabular())
.text_color(muted)
.child(t_plural(L10nKey::ScmFilesChanged, files.len(), &[])),
)
@@ -691,10 +715,12 @@ impl Tty7App {
.id(SharedString::from(format!("scm-detail-file-{}", file.path)))
.items_center()
.gap(px(8.))
.h(px(ROW_H))
.min_h(rems(ROW_H / 16.))
.w_full()
.min_w_0()
.px(px(ROW_INSET))
.py(px(3.))
.rounded(px(5.))
.rounded(PILL_RADIUS)
.cursor_pointer()
.hover(|s| s.bg(gpui::rgb(sf.hover)))
.when(selected, |s| s.bg(gpui::rgb(sf.selected)))
@@ -717,9 +743,13 @@ impl Tty7App {
})
})
.child(git_badge(status_glyph(deco), status_color(deco, cx), mono))
// The name takes its own width first and only then shrinks, down
// to a floor; the directory beside it gets whatever is left.
.child(
div()
.flex_none()
.flex_shrink(1.)
.min_w(px(NAME_FLOOR))
.truncate()
// Match the working-tree list: UI names, secondary paths.
.text_size(rems(TEXT))
.text_color(if deco == DecoStatus::Conflict {
@@ -733,26 +763,34 @@ impl Tty7App {
.when(deco == DecoStatus::Deleted, |s| s.line_through())
.child(name.to_string()),
)
.when(!dir.is_empty(), |this| {
this.child(
div()
.flex_1()
.min_w_0()
.truncate()
.text_size(rems(META))
.text_color(cx.theme().muted_foreground.opacity(0.75))
.child(dir.to_string()),
)
})
// Flush right, giving way from its *start*: the folder nearest the
// file is the part that says where it is. The same column the
// working tree's rows keep, in the plain caption ink — no extra
// transparency on top of a colour already floored for legibility.
.child(
div()
.flex_1()
.min_w_0()
.overflow_hidden()
.whitespace_nowrap()
.text_ellipsis_start()
.text_right()
.text_size(rems(META))
.text_color(cx.theme().muted_foreground)
.child(dir.to_string()),
)
.into_any_element()
}
fn detail_note(&self, text: String, cx: &mut Context<Self>) -> AnyElement {
div()
.flex()
.items_center()
.min_h(px(ROW_H))
.mt(px(GROUP_GAP))
.px(px(CONTENT_INSET))
.py(px(3.))
.text_size(rems(META))
.text_color(cx.theme().muted_foreground.opacity(0.75))
.text_color(cx.theme().muted_foreground)
.child(text)
.into_any_element()
}