mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-05 08:02:13 +00:00
feat(scm): restyle the Changes tab after the v4 design
- Pinned block keeps 8/10/14 rhythm; branch name medium, 26px sync tile. - Commit message box rests at 56px with a 7px radius. - Split commit control: inverted neutral fill when committable, faint fill otherwise; 6px radius and an inset 0.5px seam. - Change groups sit 16px apart under 22px sentence-case medium headers; file names take width first and directories right-align, eliding from the start. - History: 32px header, 26px rows inset with rounded hover, 1px lines and 7px beads (HEAD filled, others hollow), neutral inks on a single-lane page, age column always shown, faint HEAD pill.
This commit is contained in:
@@ -78,7 +78,15 @@
|
||||
页签栏底部的细分隔线上;「更改」后附改动数。
|
||||
- **右侧文字层级**:文件树名称使用侧栏标题色,不能整体降为辅助灰;当前项使用 selected 文字色与 medium 字重,根目录以字重区分。Git 普通文件名使用 resting 文字色与界面字体,路径、数量与说明保持次级;信息页的路径前缀弱化,末级名称突出。
|
||||
- **文件树**:文件名中性;A/M/? 等尾部字符承载 Git 状态;目录用小点表示子项变化;冲突才整行提升。
|
||||
- **Git 面板**:改动列表是主体。可执行的提交按钮才突出;未填写信息或不可提交时保持次级。
|
||||
- **Git 面板**:改动列表是主体。顶部固定块(分支行 28px / 提交信息框 / 提交行)上 8、块间 10、下 14。
|
||||
分支名 medium,末尾同步按钮 26px。提交信息框静止 56px、淡底、`ROW_RADIUS`。
|
||||
提交按钮是分体控件(26px、圆角 6、0.5px 内嵌分隔):可提交时整体反色中性(正文色底 + 表面色字),
|
||||
这是面板里唯一的实心形状;不可提交时回到信息框同款淡底,不宣传不能执行的动作。
|
||||
分组之间空 16px,组标题 22px、11.5/16rem medium 辅助色、句首大写(不再全大写),数量右对齐。
|
||||
文件行 26px、圆角 6:状态字母等宽 semibold 按状态着色,文件名正文色优先占宽(下限 40px),
|
||||
目录右对齐、从开头省略。History 区以细线分隔,标题 32px;提交行 26px,1px 连线 + 7px 节点,
|
||||
HEAD 实心、其余空心环;整页只有一条 lane 时连线与节点改用中性色;时间列固定宽度右对齐,
|
||||
HEAD 引用用淡底胶囊。
|
||||
- **设置**:章节标题 > 标签 > 描述;开关/滑块/主操作共享强调色;分隔线弱于控件边界。
|
||||
- **菜单与命令面板**:亮度抬升、有限阴影;当前键盘行清楚,普通项不着色;小窗口内可滚动。
|
||||
- **终端**:ANSI 是用户内容,不拿来充当 UI 错误/成功配色;应用设计调整不得反转状态含义。
|
||||
|
||||
+118
-65
@@ -78,7 +78,10 @@ use crate::ui::scm::state::RepoKey;
|
||||
/// A taller row against an unchanged [`GRAPH_LANE_W`] steepens a merge's
|
||||
/// diagonal, which is the safe direction: the failure this pitch is guarding
|
||||
/// against is an angle read as *shallower* than the jump it draws.
|
||||
const GRAPH_ROW_H: f32 = 24.;
|
||||
///
|
||||
/// Now 26, the file list's own pitch: the section reads as one more list of
|
||||
/// rows under the groups rather than as a denser instrument bolted on below.
|
||||
const GRAPH_ROW_H: f32 = 26.;
|
||||
/// Rows materialized above and below the visible band, so a fast scroll never
|
||||
/// outruns the window into blank space, and the "load more" band — laid out
|
||||
/// one row past the window's end — stays below the fold until it is real.
|
||||
@@ -92,7 +95,10 @@ const GRAPH_WINDOW_MARGIN: usize = 4;
|
||||
/// `round(12 × 1.618) = 20px` — so 24 is that line plus 2px of air on each
|
||||
/// side. The resize constants below are counted against it, which is why it
|
||||
/// has to stay honest.
|
||||
const GRAPH_HEADER_H: f32 = 24.;
|
||||
///
|
||||
/// 32 since the section took the panel's own band rhythm: the header is where
|
||||
/// the section starts, and it wants more air than a row of it.
|
||||
const GRAPH_HEADER_H: f32 = 32.;
|
||||
|
||||
/// The header's controls: the filter tile and the scope picker beside it.
|
||||
///
|
||||
@@ -102,6 +108,15 @@ const GRAPH_HEADER_H: f32 = 24.;
|
||||
const GRAPH_TILE: f32 = 18.;
|
||||
const GRAPH_TILE_GLYPH: f32 = 12.;
|
||||
|
||||
/// Space after the age column, inside the section's own inset.
|
||||
const GRAPH_ROW_PAD_R: f32 = 8.;
|
||||
|
||||
/// The section's rows sit inside this much of the panel's edge on each side,
|
||||
/// so a hovered or selected commit wears a rounded band like a file row does
|
||||
/// rather than a full-bleed stripe. Added to the row's own padding it lands on
|
||||
/// `CONTENT_INSET`.
|
||||
const GRAPH_SIDE: f32 = CONTENT_INSET - 6.;
|
||||
|
||||
/// Horizontal distance between lane centres.
|
||||
const GRAPH_LANE_W: f32 = 12.;
|
||||
|
||||
@@ -118,8 +133,12 @@ const GRAPH_PAD_R: f32 = 6.;
|
||||
/// It stays 3 while the row grows, because what a bead is measured against is
|
||||
/// the string it is on — `GRAPH_LANE_W`, which has not moved. A quarter is the
|
||||
/// floor the test below holds it to.
|
||||
const GRAPH_DOT_R: f32 = 3.;
|
||||
const GRAPH_LINE_W: f32 = 1.5;
|
||||
///
|
||||
/// Since the section went hairline: a 7px node on a 1px string, and the ring a
|
||||
/// node wears is a touch heavier than the string so it closes cleanly.
|
||||
const GRAPH_DOT_R: f32 = 3.5;
|
||||
const GRAPH_LINE_W: f32 = 1.;
|
||||
const GRAPH_RING_W: f32 = 1.2;
|
||||
|
||||
/// Most of the panel's width belongs to the message. Thirty percent is what
|
||||
/// leaves five lanes at the 260px default and still keeps a readable column.
|
||||
@@ -138,17 +157,17 @@ const _: () = assert!(GRAPH_MAX_LANES <= LANE_SLOTS);
|
||||
/// file list has to keep a usable part of a short one.
|
||||
///
|
||||
/// Both of the fixed ones are counted in rows against the header: what they
|
||||
/// mean is a number of commits, not a number of pixels. 260 is `24 + 9.8 × 24`
|
||||
/// mean is a number of commits, not a number of pixels. 287 is `32 + 9.8 × 26`
|
||||
/// — nine commits and most of a tenth, and the fraction is deliberate, because
|
||||
/// a row cut by the bottom edge is the only honest way a fixed-height list says
|
||||
/// there is more below it. 100 is `24 + 3.2 × 24`, three and a bit, which is
|
||||
/// there is more below it. 116 is `32 + 3.2 × 26`, three and a bit, which is
|
||||
/// the least that still looks like history rather than like a mistake.
|
||||
///
|
||||
/// They grew with [`GRAPH_ROW_H`] — 220 and 88 around a 20px row — precisely
|
||||
/// They grew with [`GRAPH_ROW_H`] — 220 and 88 around a 20px row, 260 and 100 around 24 — precisely
|
||||
/// because they are counted in commits: holding the pixels would have quietly
|
||||
/// bought the file list 40px by showing two fewer commits.
|
||||
const GRAPH_H_DEFAULT: f32 = 260.;
|
||||
const GRAPH_H_MIN: f32 = 100.;
|
||||
const GRAPH_H_DEFAULT: f32 = 287.;
|
||||
const GRAPH_H_MIN: f32 = 116.;
|
||||
const GRAPH_H_MAX_RATIO: f32 = 0.65;
|
||||
|
||||
/// The divider's grab area, matching `RESIZE_HANDLE_WIDTH` on the other axis.
|
||||
@@ -343,6 +362,18 @@ struct GraphPaint {
|
||||
selected_surface: Hsla,
|
||||
/// Whether a "load more" band follows the last row.
|
||||
more: bool,
|
||||
/// Set when the whole page is one lane. A straight line of history has no
|
||||
/// branch identity to colour, so it is drawn in neutrals: a hairline
|
||||
/// string and muted rings, with only HEAD filled in ink.
|
||||
linear: Option<Neutral>,
|
||||
}
|
||||
|
||||
/// The inks a single-lane page is painted in.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Neutral {
|
||||
line: Hsla,
|
||||
ring: Hsla,
|
||||
head: Hsla,
|
||||
}
|
||||
|
||||
/// Paint the whole gutter in one pass.
|
||||
@@ -369,6 +400,12 @@ fn paint_graph(p: &GraphPaint, bounds: Bounds<Pixels>, window: &mut Window) {
|
||||
.max(0) as usize;
|
||||
|
||||
let cx_of = |column: Lane| left + lane_center_x(column, scale);
|
||||
let line_ink = |ink: u32| -> Hsla {
|
||||
match p.linear {
|
||||
Some(n) => n.line,
|
||||
None => gpui::rgb(ink).into(),
|
||||
}
|
||||
};
|
||||
let vline = |x: f32, y0: f32, y1: f32| {
|
||||
Bounds::from_corners(
|
||||
point(px(x - GRAPH_LINE_W / 2.), px(y0)),
|
||||
@@ -388,14 +425,14 @@ fn paint_graph(p: &GraphPaint, bounds: Bounds<Pixels>, window: &mut Window) {
|
||||
|
||||
for (column, ink) in band.top.iter().enumerate() {
|
||||
if let Some(ink) = ink {
|
||||
window.paint_quad(fill(vline(cx_of(column as Lane), y0, mid), gpui::rgb(*ink)));
|
||||
window.paint_quad(fill(vline(cx_of(column as Lane), y0, mid), line_ink(*ink)));
|
||||
}
|
||||
}
|
||||
for (column, ink) in band.bottom.iter().enumerate() {
|
||||
if let Some(ink) = ink {
|
||||
window.paint_quad(fill(
|
||||
vline(cx_of(column as Lane), mid, y0 + GRAPH_ROW_H),
|
||||
gpui::rgb(*ink),
|
||||
line_ink(*ink),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -420,18 +457,28 @@ fn paint_graph(p: &GraphPaint, bounds: Bounds<Pixels>, window: &mut Window) {
|
||||
px(mid + GRAPH_LINE_W / 2.),
|
||||
),
|
||||
),
|
||||
gpui::rgb(*ink),
|
||||
line_ink(*ink),
|
||||
));
|
||||
}
|
||||
|
||||
let node = project(row.node, p.max_lanes);
|
||||
let ink = gpui::rgb(column_ink(
|
||||
node,
|
||||
row.color,
|
||||
p.max_lanes,
|
||||
p.overflowing,
|
||||
&p.lanes,
|
||||
));
|
||||
let is_head = p
|
||||
.page
|
||||
.commits
|
||||
.get(i)
|
||||
.is_some_and(|c| c.refs.iter().any(|r| r.kind == RefKind::Head));
|
||||
let ink: Hsla = match p.linear {
|
||||
Some(n) if is_head => n.head,
|
||||
Some(n) => n.ring,
|
||||
None => gpui::rgb(column_ink(
|
||||
node,
|
||||
row.color,
|
||||
p.max_lanes,
|
||||
p.overflowing,
|
||||
&p.lanes,
|
||||
))
|
||||
.into(),
|
||||
};
|
||||
let cx = cx_of(node);
|
||||
let dot = |r: f32| {
|
||||
Bounds::from_corners(
|
||||
@@ -452,32 +499,34 @@ fn paint_graph(p: &GraphPaint, bounds: Bounds<Pixels>, window: &mut Window) {
|
||||
p.surface
|
||||
};
|
||||
if row.parents > 1 {
|
||||
// A merge is a ring. It is the one row shape a reader scans for,
|
||||
// and an outline reads at 8px where a second fill colour does not.
|
||||
// A merge is the larger ring. It is the one row shape a reader
|
||||
// scans for, and an outline reads at 9px where a second fill
|
||||
// colour does not.
|
||||
let r = GRAPH_DOT_R + 1.;
|
||||
window.paint_quad(quad(
|
||||
dot(r),
|
||||
Corners::all(px(r)),
|
||||
hole,
|
||||
Edges::all(px(GRAPH_LINE_W)),
|
||||
Edges::all(px(GRAPH_RING_W)),
|
||||
ink,
|
||||
BorderStyle::Solid,
|
||||
));
|
||||
} else if row.parents == 0 {
|
||||
// A root has nothing below it; hollow says "the line stops here"
|
||||
// without needing a second glyph.
|
||||
} else if is_head {
|
||||
// Where you are is the one filled bead on the string.
|
||||
window.paint_quad(
|
||||
fill(dot(GRAPH_DOT_R), ink).corner_radii(Corners::all(px(GRAPH_DOT_R))),
|
||||
);
|
||||
} else {
|
||||
// Every other commit is a hollow bead, so HEAD is found by shape
|
||||
// before it is found by the chip beside it.
|
||||
window.paint_quad(quad(
|
||||
dot(GRAPH_DOT_R),
|
||||
Corners::all(px(GRAPH_DOT_R)),
|
||||
hole,
|
||||
Edges::all(px(GRAPH_LINE_W)),
|
||||
Edges::all(px(GRAPH_RING_W)),
|
||||
ink,
|
||||
BorderStyle::Solid,
|
||||
));
|
||||
} else {
|
||||
window.paint_quad(
|
||||
fill(dot(GRAPH_DOT_R), ink).corner_radii(Corners::all(px(GRAPH_DOT_R))),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,21 +540,16 @@ fn paint_graph(p: &GraphPaint, bounds: Bounds<Pixels>, window: &mut Window) {
|
||||
let column = project(*lane, p.max_lanes);
|
||||
let ink = column_ink(column, *lane, p.max_lanes, p.overflowing, &p.lanes);
|
||||
let x = cx_of(column);
|
||||
let ink = line_ink(ink);
|
||||
if p.more {
|
||||
window.paint_quad(fill(
|
||||
vline(x, y0, y0 + GRAPH_ROW_H),
|
||||
Hsla::from(gpui::rgb(ink)).opacity(0.3),
|
||||
));
|
||||
window.paint_quad(fill(vline(x, y0, y0 + GRAPH_ROW_H), ink.opacity(0.3)));
|
||||
} else {
|
||||
// Three steps rather than a gradient: a gradient would be a
|
||||
// second `Background` kind for four pixels of ink.
|
||||
const STEP: f32 = 3.;
|
||||
for (step, alpha) in [0.5f32, 0.3, 0.15].into_iter().enumerate() {
|
||||
let a = y0 + step as f32 * STEP;
|
||||
window.paint_quad(fill(
|
||||
vline(x, a, a + STEP),
|
||||
Hsla::from(gpui::rgb(ink)).opacity(alpha),
|
||||
));
|
||||
window.paint_quad(fill(vline(x, a, a + STEP), ink.opacity(alpha)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,8 +581,8 @@ impl Tty7App {
|
||||
return Some(
|
||||
div()
|
||||
.flex_none()
|
||||
.pt(px(2.))
|
||||
.pb(px(4.))
|
||||
.pt(px(4.))
|
||||
.pb(px(12.))
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().border)
|
||||
.child(self.graph_header(repo, None, cx))
|
||||
@@ -826,6 +870,11 @@ impl Tty7App {
|
||||
.and_then(|oid| page.commits.iter().position(|c| c.oid == oid)),
|
||||
selected_surface: gpui::rgb(sf.selected).into(),
|
||||
more,
|
||||
linear: (page.max_lanes <= 1).then(|| Neutral {
|
||||
line: cx.theme().border,
|
||||
ring: cx.theme().muted_foreground,
|
||||
head: cx.theme().foreground,
|
||||
}),
|
||||
};
|
||||
stack = stack.child(
|
||||
canvas(
|
||||
@@ -846,7 +895,7 @@ impl Tty7App {
|
||||
.min_h_0()
|
||||
.overflow_y_scroll()
|
||||
.track_scroll(&self.scm.graph.scroll)
|
||||
.child(stack);
|
||||
.child(div().px(px(GRAPH_SIDE)).child(stack));
|
||||
crate::ui::scrollbar::with_vertical_scrollbar(
|
||||
"scm-graph-scrollbar",
|
||||
scroller,
|
||||
@@ -858,9 +907,9 @@ impl Tty7App {
|
||||
///
|
||||
/// Column order is fixed and every optional part has a hard cap, so the
|
||||
/// worst case cannot squeeze the message to nothing: type prefix, message,
|
||||
/// ref chip, age. The age goes when a ref chip is present — a chip says
|
||||
/// where a branch is, which is worth more here than three characters of
|
||||
/// "3d", and the full timestamp is in the tooltip either way.
|
||||
/// ref chip, age. The age is a fixed right-aligned column on every row,
|
||||
/// chip or not, so the times stack into one ragged-left column down the
|
||||
/// section and the eye can run down it.
|
||||
fn graph_row(
|
||||
&self,
|
||||
repo: &RepoKey,
|
||||
@@ -883,10 +932,11 @@ impl Tty7App {
|
||||
h_flex()
|
||||
.id(SharedString::from(format!("scm-graph-row-{i}")))
|
||||
.items_center()
|
||||
.gap(px(4.))
|
||||
.gap(px(6.))
|
||||
.h(px(GRAPH_ROW_H))
|
||||
.pl(px(gutter))
|
||||
.pr(px(CONTENT_INSET))
|
||||
.pr(px(GRAPH_ROW_PAD_R))
|
||||
.rounded(px(6.))
|
||||
.cursor_pointer()
|
||||
// The panel's own neutral selection and hover fills, the same two
|
||||
// the file list above uses. A tinted band would make this one list
|
||||
@@ -903,7 +953,7 @@ impl Tty7App {
|
||||
h_flex()
|
||||
.flex_1()
|
||||
.min_w(px(0.))
|
||||
.gap(px(2.))
|
||||
.gap(px(4.))
|
||||
.children(
|
||||
prefix.map(|(kind, breaking)| self.graph_type_prefix(kind, breaking, cx)),
|
||||
)
|
||||
@@ -924,21 +974,21 @@ impl Tty7App {
|
||||
// The age is a mono token, so its column is measured in characters:
|
||||
// the widest it ever prints is four (`12mo`), and four of the mono
|
||||
// face's advances at `META_MONO` is `4 × 11 × 0.6` = 26.4, rounded
|
||||
// up to 27.
|
||||
.when(deco.is_none(), |d| {
|
||||
d.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.min_w(px(27.))
|
||||
.text_size(rems(META_MONO))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(relative_time(
|
||||
now,
|
||||
commit.author.at.unix,
|
||||
))),
|
||||
)
|
||||
})
|
||||
// up to 27. Right-aligned inside it, so `3d` and `12mo` end on the
|
||||
// same edge.
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(px(27.))
|
||||
.text_right()
|
||||
.text_size(rems(META_MONO))
|
||||
.font_family(mono.clone())
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(SharedString::from(relative_time(
|
||||
now,
|
||||
commit.author.at.unix,
|
||||
))),
|
||||
)
|
||||
.tooltip({
|
||||
let text = commit_tooltip(commit, now);
|
||||
move |window, cx| {
|
||||
@@ -1093,10 +1143,13 @@ impl Tty7App {
|
||||
// weight; everything else is context. The fill is grey, not brand:
|
||||
// `theme.accent` is a neutral surface tint here, and the only
|
||||
// saturated colour the section spends is a lane's.
|
||||
//
|
||||
// A faint neutral pill in muted ink: the filled bead in the gutter
|
||||
// already says "you are here", so the chip only has to name it.
|
||||
RefKind::Head => (
|
||||
theme.accent.opacity(0.28),
|
||||
theme.foreground,
|
||||
gpui::FontWeight::SEMIBOLD,
|
||||
theme.foreground.opacity(0.06),
|
||||
theme.muted_foreground,
|
||||
gpui::FontWeight::NORMAL,
|
||||
),
|
||||
// Tags are yellow because tags are yellow — in git's own output,
|
||||
// in every other client, and in the reader's memory.
|
||||
|
||||
+151
-73
@@ -32,9 +32,9 @@ use crate::ui::app::{CONTENT_INSET, TILE_GLYPH_XS, TILE_SIZE_XS, Tty7App};
|
||||
use crate::ui::host_ops::{HostId, SharedHost};
|
||||
use crate::ui::i18n::{L10nKey, t, t_fmt, t_plural};
|
||||
use crate::ui::right_panel::{
|
||||
HEADING, META, META_MONO, ROW_INSET, SEARCH_H, action_strip, git_badge, info_chip,
|
||||
META, META_MONO, ROW_INSET, SEARCH_H, action_strip, git_badge, info_chip,
|
||||
};
|
||||
use crate::ui::rounding::{CARD_RADIUS, HAIRLINE, RoundedCorners as _, segment_corners};
|
||||
use crate::ui::rounding::{HAIRLINE, ROW_RADIUS, RoundedCorners as _, segment_corners};
|
||||
use crate::ui::scm::ScmIntent;
|
||||
use crate::ui::scm::path::split_display_path;
|
||||
use crate::ui::scm::state::{RepoKey, ScmGroup};
|
||||
@@ -65,25 +65,21 @@ const BRANCHES_IN_MENU: usize = 12;
|
||||
/// changes the user came to look at.
|
||||
const UNTRACKED_AUTO_COLLAPSE: usize = 20;
|
||||
|
||||
/// Message fields share the search field's 30px height and grow with content.
|
||||
/// The message box rests at 56px — room for a subject and the start of a body
|
||||
/// — and grows with content from there.
|
||||
const MSG_LINE: f32 = 20.;
|
||||
const MSG_PAD_X: f32 = 9.;
|
||||
const MSG_PAD_Y: f32 = 5.;
|
||||
const MSG_MIN_H: f32 = MSG_PAD_Y + MSG_LINE + MSG_PAD_Y;
|
||||
const MSG_PAD_X: f32 = 10.;
|
||||
const MSG_PAD_Y: f32 = 8.;
|
||||
const MSG_MIN_H: f32 = 56.;
|
||||
/// The ceiling, as `MSG_ROWS_MAX` bare lines. It is a rail, not a border-box
|
||||
/// sum: the wrapper's own hairline and padding are not in it, so at the very
|
||||
/// top of the box's growth the last row is clipped rather than framed.
|
||||
const MSG_MAX_H: f32 = MSG_ROWS_MAX as f32 * MSG_LINE;
|
||||
|
||||
/// The invariant the comment above describes, made unbreakable: the resting
|
||||
/// message box and the panel's search strip are the same height, or this does
|
||||
/// not build.
|
||||
const _: () = assert!(MSG_MIN_H == SEARCH_H);
|
||||
|
||||
/// One line at rest and it grows into the message. The box is one row in a
|
||||
/// column of rows, and a box that stands four lines tall before anything has
|
||||
/// been typed pushes the file list — the thing the panel is for — off the
|
||||
/// bottom of a 260px-wide sidebar.
|
||||
/// One line of input at rest inside the 56px box, growing into the message.
|
||||
/// The box's resting height is the wrapper's floor, not the row count: a box
|
||||
/// that stood six lines tall before anything was typed would push the file
|
||||
/// list — the thing the panel is for — off the bottom of the panel.
|
||||
const MSG_ROWS: usize = 1;
|
||||
const MSG_ROWS_MAX: usize = 6;
|
||||
|
||||
@@ -110,6 +106,27 @@ const MSG_ROWS_MAX: usize = 6;
|
||||
const COMMIT_H: f32 = ROW_H;
|
||||
const COMMIT_CHEVRON_W: f32 = COMMIT_H - 2.;
|
||||
const COMMIT_GLYPH: f32 = 11.;
|
||||
const COMMIT_RADIUS: gpui::Pixels = px(6.);
|
||||
|
||||
/// The block pinned above the file list — branch, message, commit — reads as
|
||||
/// one unit: 8px under the tab row, 10px between its three parts and 14px
|
||||
/// before the first group.
|
||||
const PINNED_TOP: f32 = 8.;
|
||||
const PINNED_GAP: f32 = 10.;
|
||||
const PINNED_BOTTOM: f32 = 14.;
|
||||
|
||||
/// The branch row's trailing refresh/sync tile.
|
||||
const BRANCH_TILE: f32 = 26.;
|
||||
|
||||
/// Space between two change groups, and the height of a group's header.
|
||||
const GROUP_GAP: f32 = 16.;
|
||||
const GROUP_HEADER_H: f32 = 22.;
|
||||
/// Group headers are band labels: a half step under `META`, medium weight,
|
||||
/// muted — the file names under them are what the panel is for.
|
||||
const GROUP_HEADING: f32 = 11.5 / 16.;
|
||||
/// A file row's name keeps at least this much before the directory beside it
|
||||
/// has given up all of its width.
|
||||
const NAME_FLOOR: f32 = 40.;
|
||||
|
||||
/// How long to wait before asking git again about a directory that answered
|
||||
/// with nothing.
|
||||
@@ -316,6 +333,7 @@ impl Tty7App {
|
||||
// it is the same 24, so the row has one interior height and the
|
||||
// type sits inside it rather than setting it.
|
||||
.min_h(rems(28. / 16.))
|
||||
.mt(px(PINNED_TOP))
|
||||
.pl(px(CONTENT_INSET))
|
||||
.pr(px(crate::ui::app::tile_trailing_inset_sm()))
|
||||
.child(
|
||||
@@ -367,6 +385,7 @@ impl Tty7App {
|
||||
.min_w(px(0.))
|
||||
.line_height(relative(1.))
|
||||
.truncate()
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.child(head_label(&status.head)),
|
||||
)
|
||||
.w_full()
|
||||
@@ -409,12 +428,12 @@ impl Tty7App {
|
||||
} else {
|
||||
Icon::empty().path("icons/git-sync.svg")
|
||||
}),
|
||||
crate::ui::app::TILE_SIZE_SM,
|
||||
BRANCH_TILE,
|
||||
crate::ui::app::TILE_GLYPH_SM,
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
.rounded_md()
|
||||
.rounded(px(6.))
|
||||
// With no branch to move the tile could only ever lose (#545):
|
||||
// upstream is None at a detached or unborn HEAD by definition,
|
||||
// so sync degenerates into scm_push, which has nothing to
|
||||
@@ -797,7 +816,7 @@ impl Tty7App {
|
||||
.key_context(COMMIT_KEY_CONTEXT)
|
||||
.flex_none()
|
||||
.px(px(CONTENT_INSET))
|
||||
.pt(px(6.))
|
||||
.pt(px(PINNED_GAP))
|
||||
.child(
|
||||
div()
|
||||
// `MSG_MIN_H` is `panel_search`'s height and the paddings
|
||||
@@ -808,7 +827,7 @@ impl Tty7App {
|
||||
// ever measures itself differently.
|
||||
.min_h(px(MSG_MIN_H))
|
||||
.max_h(px(MSG_MAX_H))
|
||||
.rounded(CARD_RADIUS)
|
||||
.rounded(ROW_RADIUS)
|
||||
// Half a rung, not a whole one. The hover step is what a
|
||||
// row wears when the pointer is on it — a transient state —
|
||||
// and the message box wears its fill all the time, so at
|
||||
@@ -928,16 +947,15 @@ impl Tty7App {
|
||||
let repo_for_button = repo.clone();
|
||||
let live = plan.enabled;
|
||||
let staged = status.staged().count();
|
||||
let theme = cx.theme();
|
||||
let (muted, fg) = (theme.muted_foreground, theme.primary_foreground);
|
||||
let sf = cx.global::<crate::ui::presets::Surfaces>().sidebar;
|
||||
let muted = cx.theme().muted_foreground;
|
||||
let paint = commit_paint(live, cx);
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.px(px(CONTENT_INSET))
|
||||
.pt(px(6.))
|
||||
.pb(px(8.))
|
||||
.pt(px(PINNED_GAP))
|
||||
.pb(px(PINNED_BOTTOM))
|
||||
// The reading the button acts on, in the row it acts from. It
|
||||
// gives way first: a long count is still a count, and the control
|
||||
// beside it is the thing that has to keep its shape.
|
||||
@@ -965,13 +983,18 @@ impl Tty7App {
|
||||
// `segment_corners` so their own hit shapes stay inside the
|
||||
// frame's radius — `overflow_hidden` would do that too, and
|
||||
// would take the chevron's popup menu with it.
|
||||
//
|
||||
// Committable, the whole frame inverts — ink fill, surface
|
||||
// label — which is the one prominent shape on the panel. With
|
||||
// nothing to commit it sinks back to the message box's faint
|
||||
// fill, so it never advertises an action that cannot run.
|
||||
h_flex()
|
||||
.flex_none()
|
||||
.items_center()
|
||||
.h(px(COMMIT_H))
|
||||
.rounded(CARD_RADIUS)
|
||||
.bg(gpui::rgb(field_fill(sf)))
|
||||
.hover(|s| s.bg(gpui::rgb(sf.hover)))
|
||||
.rounded(COMMIT_RADIUS)
|
||||
.bg(paint.fill)
|
||||
.hover(move |s| s.bg(paint.hover))
|
||||
.child(
|
||||
// `xsmall` is the token that sets a `Button`'s label to
|
||||
// `META`, and a control's label is what `META` is for:
|
||||
@@ -985,19 +1008,19 @@ impl Tty7App {
|
||||
// ghost hovers to a colour close enough to it that
|
||||
// the pointer would get no answer. This walks the
|
||||
// same two rungs the file rows walk.
|
||||
.custom(commit_half(cx))
|
||||
.when(live, |button| button.primary())
|
||||
.custom(commit_half(paint.ink, cx))
|
||||
.xsmall()
|
||||
.label(t(plan.label))
|
||||
.h_full()
|
||||
.px(px(10.))
|
||||
.rounded_corners(segment_corners(0, 2, CARD_RADIUS, HAIRLINE))
|
||||
.px(px(12.))
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.rounded_corners(segment_corners(0, 2, COMMIT_RADIUS, HAIRLINE))
|
||||
// The ink is named here rather than left to the
|
||||
// variant, so the disabled half greys out: it lands
|
||||
// over the variant's own paint because
|
||||
// `refine_style` runs after everything the variant
|
||||
// does.
|
||||
.text_color(if live { fg } else { muted })
|
||||
.text_color(if live { paint.ink } else { muted })
|
||||
.disabled(!live)
|
||||
.when(!live, |b| b.tooltip(t(plan.reason)))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
@@ -1010,20 +1033,20 @@ impl Tty7App {
|
||||
);
|
||||
})),
|
||||
)
|
||||
// The seam between the halves is the panel showing through
|
||||
// the fill, not a line drawn on top of it — a rule would be
|
||||
// the one stroke left on the panel. A div rather than
|
||||
// The seam is a short rule in the label's own ink, inset
|
||||
// from both edges so it reads as a divider inside one shape
|
||||
// rather than a cut through it. A div rather than
|
||||
// `border_l_1` on the chevron, because a ghost `Button`
|
||||
// repaints its border colour transparent in every state it
|
||||
// has and the seam would vanish under the pointer.
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.w(HAIRLINE)
|
||||
.h_full()
|
||||
.bg(gpui::rgb(sf.base)),
|
||||
.w(px(0.5))
|
||||
.h(px(COMMIT_H - 12.))
|
||||
.bg(paint.seam),
|
||||
)
|
||||
.child(self.scm_commit_menu(repo, cx)),
|
||||
.child(self.scm_commit_menu(repo, paint.ink, cx)),
|
||||
)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1033,10 +1056,15 @@ impl Tty7App {
|
||||
/// Never disabled, whatever the button beside it is doing: stash and amend
|
||||
/// still mean something with nothing staged, and this is the only way to
|
||||
/// reach them.
|
||||
fn scm_commit_menu(&self, repo: &RepoKey, cx: &mut Context<Self>) -> AnyElement {
|
||||
fn scm_commit_menu(
|
||||
&self,
|
||||
repo: &RepoKey,
|
||||
ink: gpui::Hsla,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let amend = self.scm.amend;
|
||||
Button::new("scm-commit-menu")
|
||||
.custom(commit_half(cx))
|
||||
.custom(commit_half(ink, cx))
|
||||
.icon(Icon::new(IconName::ChevronDown))
|
||||
// `Button` sizes an icon off its own `Size`, so the glyph is asked
|
||||
// for by way of the size that produces it — the same conversion
|
||||
@@ -1044,7 +1072,7 @@ impl Tty7App {
|
||||
.with_size(px(COMMIT_GLYPH / crate::ui::tab_strip::BUTTON_ICON_SCALE))
|
||||
.w(px(COMMIT_CHEVRON_W))
|
||||
.h_full()
|
||||
.rounded_corners(segment_corners(1, 2, CARD_RADIUS, HAIRLINE))
|
||||
.rounded_corners(segment_corners(1, 2, COMMIT_RADIUS, HAIRLINE))
|
||||
.dropdown_menu_with_anchor(gpui::Anchor::TopRight, {
|
||||
let app = cx.entity().downgrade();
|
||||
let repo = repo.clone();
|
||||
@@ -1291,7 +1319,8 @@ impl Tty7App {
|
||||
status: &Arc<WorkingTreeStatus>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let mut list = v_flex().px(px(CONTENT_INSET - ROW_INSET)).py(px(2.));
|
||||
let mut list = v_flex().px(px(CONTENT_INSET - ROW_INSET)).pb(px(12.));
|
||||
let mut first = true;
|
||||
for group in ScmGroup::ORDER {
|
||||
let entries: Vec<&StatusEntry> = status
|
||||
.entries
|
||||
@@ -1302,6 +1331,12 @@ impl Tty7App {
|
||||
continue;
|
||||
}
|
||||
let collapsed = self.scm.group_collapsed(group, entries.len());
|
||||
// Groups are told apart by the pause before each one, not by a
|
||||
// rule: 16px, and none above the first.
|
||||
if !first {
|
||||
list = list.child(div().flex_none().h(px(GROUP_GAP)));
|
||||
}
|
||||
first = false;
|
||||
list = list.child(self.scm_group_header(repo, group, &entries, collapsed, cx));
|
||||
if collapsed {
|
||||
continue;
|
||||
@@ -1356,7 +1391,6 @@ impl Tty7App {
|
||||
) -> AnyElement {
|
||||
let count = entries.len();
|
||||
let sf = cx.global::<crate::ui::presets::Surfaces>().sidebar;
|
||||
let mono = cx.theme().mono_font_family.clone();
|
||||
let id = SharedString::from(format!("scm-group-{group:?}"));
|
||||
let actions = self.scm_group_actions(&id, repo, group, entries, sf.hover, cx);
|
||||
h_flex()
|
||||
@@ -1364,10 +1398,11 @@ impl Tty7App {
|
||||
.group(id)
|
||||
.relative()
|
||||
.items_center()
|
||||
.gap(px(8.))
|
||||
.min_h(rems(ROW_H / 16.))
|
||||
.gap(px(6.))
|
||||
.min_h(rems(GROUP_HEADER_H / 16.))
|
||||
.mb(px(1.))
|
||||
.px(px(ROW_INSET))
|
||||
.rounded(px(5.))
|
||||
.rounded(px(6.))
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(gpui::rgb(sf.hover)))
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
@@ -1388,24 +1423,25 @@ impl Tty7App {
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
})
|
||||
.xsmall(),
|
||||
.size(px(COMMIT_GLYPH)),
|
||||
),
|
||||
)
|
||||
// Sentence case, medium, muted: a band label over the rows rather
|
||||
// than a heading competing with them.
|
||||
.child(
|
||||
div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(rems(HEADING))
|
||||
.font_weight(gpui::FontWeight::SEMIBOLD)
|
||||
.text_size(rems(GROUP_HEADING))
|
||||
.font_weight(gpui::FontWeight::MEDIUM)
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(t(group_label(group)).to_uppercase()),
|
||||
.child(t(group_label(group))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.text_size(rems(META_MONO))
|
||||
.font_family(mono)
|
||||
.text_size(rems(GROUP_HEADING))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(count.to_string()),
|
||||
)
|
||||
@@ -1449,7 +1485,7 @@ impl Tty7App {
|
||||
.min_w_0()
|
||||
.px(px(ROW_INSET))
|
||||
.py(px(3.))
|
||||
.rounded(px(5.))
|
||||
.rounded(px(6.))
|
||||
.cursor_pointer()
|
||||
.hover(|s| s.bg(gpui::rgb(sf.hover)))
|
||||
.when(selected, |s| s.bg(gpui::rgb(sf.selected)))
|
||||
@@ -1483,10 +1519,12 @@ impl Tty7App {
|
||||
})
|
||||
.child(git_badge(letter, status_color(deco, cx), &mono))
|
||||
// Names use the same resting/selected hierarchy as sidebar rows.
|
||||
// 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_1()
|
||||
.min_w_0()
|
||||
.flex_shrink(1.)
|
||||
.min_w(px(NAME_FLOOR))
|
||||
.truncate()
|
||||
.text_size(rems(crate::ui::right_panel::TEXT))
|
||||
.text_color(if deco == DecoStatus::Conflict {
|
||||
@@ -1500,21 +1538,22 @@ impl Tty7App {
|
||||
.when(deco == DecoStatus::Deleted, |s| s.line_through())
|
||||
.child(name.to_string()),
|
||||
)
|
||||
// Cap the secondary directory while the filename fills the rest.
|
||||
// A rem cap also gives short paths their intrinsic width inside
|
||||
// the context-menu wrapper's flex layout.
|
||||
.when(!dir.is_empty(), |this| {
|
||||
this.child(
|
||||
div()
|
||||
.flex_none()
|
||||
.max_w(rems(3.))
|
||||
.min_w_0()
|
||||
.truncate()
|
||||
.text_size(rems(META))
|
||||
.text_color(cx.theme().muted_foreground)
|
||||
.child(dir.to_string()),
|
||||
)
|
||||
})
|
||||
// The directory is flush right and gives way from its *start*:
|
||||
// the folder nearest the file is the part that says where it is,
|
||||
// and `src/ui/…` cut from the end keeps the part that says least.
|
||||
// The row tooltip has the whole path either way.
|
||||
.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()),
|
||||
)
|
||||
.child(actions)
|
||||
.into_any_element()
|
||||
}
|
||||
@@ -1954,15 +1993,54 @@ fn field_fill(sf: crate::ui::presets::Surface) -> u32 {
|
||||
/// `active` slot, and a dropdown marks its trigger selected for as long as
|
||||
/// the menu is open. Anything but transparent there parks a block on the
|
||||
/// chevron for the whole time the user is reading the menu.
|
||||
fn commit_half(cx: &gpui::App) -> ButtonCustomVariant {
|
||||
fn commit_half(ink: gpui::Hsla, cx: &gpui::App) -> ButtonCustomVariant {
|
||||
let clear = gpui::transparent_black();
|
||||
ButtonCustomVariant::new(cx)
|
||||
.color(clear)
|
||||
.foreground(cx.theme().foreground)
|
||||
.foreground(ink)
|
||||
.hover(clear)
|
||||
.active(clear)
|
||||
}
|
||||
|
||||
/// How the split commit control is painted.
|
||||
#[derive(Clone, Copy)]
|
||||
struct CommitPaint {
|
||||
fill: gpui::Hsla,
|
||||
hover: gpui::Hsla,
|
||||
ink: gpui::Hsla,
|
||||
seam: gpui::Hsla,
|
||||
}
|
||||
|
||||
/// Committable: an inverted neutral — the panel's ink as the fill and its own
|
||||
/// surface as the label, so it is the one solid shape in a panel of rows, and
|
||||
/// it stays neutral rather than borrowing the accent the sidebar and focus
|
||||
/// rings already spend. Not committable: the message box's faint fill and the
|
||||
/// panel's ink, a control that is there but is not asking for anything.
|
||||
///
|
||||
/// The label ink is the opaque surface rather than `theme.background`, which
|
||||
/// carries the window's transparency when one is configured.
|
||||
fn commit_paint(live: bool, cx: &gpui::App) -> CommitPaint {
|
||||
let sf = cx.global::<crate::ui::presets::Surfaces>().sidebar;
|
||||
let fg = cx.theme().foreground;
|
||||
match live {
|
||||
true => {
|
||||
let ink: gpui::Hsla = gpui::rgb(sf.base).into();
|
||||
CommitPaint {
|
||||
fill: fg,
|
||||
hover: fg.blend(ink.opacity(0.14)),
|
||||
ink,
|
||||
seam: ink.opacity(0.24),
|
||||
}
|
||||
}
|
||||
false => CommitPaint {
|
||||
fill: gpui::rgb(field_fill(sf)).into(),
|
||||
hover: gpui::rgb(sf.hover).into(),
|
||||
ink: fg,
|
||||
seam: fg.opacity(0.18),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn branch_note(text: &str, ink: gpui::Hsla, mono: &SharedString) -> AnyElement {
|
||||
div()
|
||||
.flex_none()
|
||||
|
||||
Reference in New Issue
Block a user