mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-11-06 08:15:35 +00:00
revert(settings): keep the v4 Settings design's controls
The earlier commits on this branch moved the settings page's dropdowns, buttons, steppers and fields onto flat outline-less wells, following docs/design-system.md. That paragraph predates the v4 Settings design the page was rebuilt to on 2026-09-26 (raised controls, hairline-ringed fields) and was never updated to it, so the change took the page away from the design rather than toward it. The raised controls, 26px heights and the faded disabled primary are back as v4 drew them, and the design-system edits are withdrawn. Kept, as fixes within the design: - dropdowns and fields share one width (kit::CONTROL_W); - the field's value is sized on the Input, which otherwise beat it; - the field hairline is one device pixel: v4's half point is one pixel on Retina and nothing at all on a 1x display.
This commit is contained in:
@@ -101,9 +101,7 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
页标题立在 48px 标题栏带内;分组标题 28px、11.5/16rem medium 辅助色;分组之间是 0.5px 分隔线,上下各 16px。
|
||||
设置行:标签正文色 regular,描述 12/16rem 辅助色,行最低 28px、内边距 8(hover/搜索命中的底色外扩 8px);
|
||||
搜索命中行用浅中性底,不用强调色。输入框与下拉框 28px,按钮、步进器 26px(圆角 6);
|
||||
输入框/下拉框/次要按钮都用同一浅底(主题 `muted`,与对话框输入井、侧栏搜索同色)且无描边、无阴影;
|
||||
输入框只在聚焦(强调色)或出错(红)时显示 1px 内环。右侧控件列统一 180px 宽(`kit::CONTROL_W`),
|
||||
只有路径/命令类输入加宽到 240px;过长的值省略号截断。控件一义一种:开关管开/关,下拉管单选
|
||||
输入框/下拉框/次要按钮都用同一浅底(surface 半阶)且无描边。控件一义一种:开关管开/关,下拉管单选
|
||||
(不再用分段控件),滑块管连续值,步进器管整数微调。
|
||||
每个视图唯一的主操作(保存主题草稿、连接、安装更新)用反色中性,同 Git 提交按钮;不可执行时回到浅底。
|
||||
开关与滑块仍用强调色。SSH 主机列表:标题行带 26px 图标按钮,28px 浅底搜索,分组标题 22px,
|
||||
@@ -128,8 +126,7 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
- **SFTP 浏览器**:行与本地文件树一致(26px、圆角 6、16px 图标辅助色、文件名侧栏标题色、大小等宽数字);
|
||||
编辑框为无边框淡底井(圆角 7);传输进度为正文色细条压在淡底轨道上,失败才转红。
|
||||
- **面板里的主按钮**(SFTP 确认、添加转发、SSH 断线条的重连):反色中性,同对话框主按钮。
|
||||
- **对话框**(SSH 认证/主机密钥、新建 worktree,以及所有确认框——关闭标签、退出、丢弃更改、删除文件等 `window.prompt`,
|
||||
三个平台都不用系统原生弹窗):`ui::dialog` 统一外壳,同切换器的 12px 圆角、
|
||||
- **对话框**(SSH 认证/主机密钥、新建 worktree):`ui::dialog` 统一外壳,同切换器的 12px 圆角、
|
||||
浮层阴影与遮罩、距顶 112px;标题行 48px(正文字号 medium,右侧 `esc` 键帽,下接细线),
|
||||
内容区左右 18px、字段间距 14px;字段名 11.5/16rem medium 辅助色,输入框为无边框 28px
|
||||
淡底井(`muted`、圆角 7)。页脚 40px 细线分隔,按钮 28px / 圆角 6、右对齐、主操作在最右:
|
||||
@@ -137,9 +134,6 @@ Git 的新增/修改标记种子为绿 `#2F8A52` / `#4CC27A`、琥珀 `#B7791F`
|
||||
退回淡底 + 辅助色且不响应点击。主机名与指纹放在淡底信息井里用等宽字。唯一的红色按钮是
|
||||
「主机密钥已变更」的 Override,该卡片也是唯一带红色描边的对话框。
|
||||
- **浮动提示条**:边缘保持中性细线,严重程度只由行首 6px 状态点表达(警告琥珀、断开红)。
|
||||
文档栏里的「文件已在磁盘上更改」横条同理:中性底 + 底部细线 + 琥珀点,按钮用对话框按钮,安全的「保留我的」为主。
|
||||
- **通知(toast)**:出错时带标题说明哪个操作失败(如「git push 失败」),原因作为次级文字,带状态图标;
|
||||
不直接把一行 stderr 当作全部内容。设置窗口是独立窗口,同样要渲染通知层。
|
||||
- **首页**:快捷操作列表 340px 宽,行高 28px、圆角 7,静止无底、hover 取窗口 hover 阶;
|
||||
快捷键用 18px 淡底键帽。
|
||||
- **终端**:ANSI 是用户内容,不拿来充当 UI 错误/成功配色;应用设计调整不得反转状态含义。
|
||||
|
||||
+77
-70
@@ -47,15 +47,8 @@ pub(crate) struct Tk {
|
||||
pub heading: Hsla,
|
||||
/// A nav item at rest.
|
||||
pub nav: Hsla,
|
||||
/// The one fill every control on the page stands on — field, dropdown,
|
||||
/// secondary button, stepper — the theme's `muted`, the same well the
|
||||
/// dialogs' fields and the sidebar's search sit in. No outline, no drop.
|
||||
pub well: Hsla,
|
||||
/// [`Self::well`] a step firmer, under the pointer or while open.
|
||||
pub well_hover: Hsla,
|
||||
/// The focus ring: the accent, the one colour kept for "the keyboard
|
||||
/// is here".
|
||||
pub focus: Hsla,
|
||||
/// A raised button's fill.
|
||||
pub btn: Hsla,
|
||||
/// A popover's fill.
|
||||
pub menu: Hsla,
|
||||
/// A switch or slider knob at rest.
|
||||
@@ -83,7 +76,6 @@ impl Tk {
|
||||
let page = theme.background;
|
||||
let a = |light: f32, dark_a: f32| fg.opacity(if dark { dark_a } else { light });
|
||||
let white = gpui::white();
|
||||
let well = theme.muted;
|
||||
Self {
|
||||
dark,
|
||||
fg,
|
||||
@@ -102,9 +94,7 @@ impl Tk {
|
||||
k6: a(0.6, 0.65),
|
||||
heading: a(0.9, 0.9),
|
||||
nav: a(0.82, 0.8),
|
||||
well,
|
||||
well_hover: well.blend(fg.opacity(if dark { 0.06 } else { 0.05 })),
|
||||
focus: theme.ring,
|
||||
btn: if dark { fg.opacity(0.1) } else { white },
|
||||
menu: if dark { lift(page, 0.06) } else { white },
|
||||
knob: if dark { fg } else { white },
|
||||
chip: if dark { fg.opacity(0.16) } else { white },
|
||||
@@ -127,6 +117,18 @@ impl Tk {
|
||||
}
|
||||
}
|
||||
|
||||
/// The same, a step firmer, for a hovered raised control.
|
||||
pub(crate) fn raised_hover(&self) -> Vec<BoxShadow> {
|
||||
if self.dark {
|
||||
vec![ring(self.fg.opacity(0.2), 0.5, false)]
|
||||
} else {
|
||||
vec![
|
||||
ring(gpui::black().opacity(0.16), 0.5, false),
|
||||
drop(gpui::black().opacity(0.08), 1., 2., 0.),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn popover(&self) -> Vec<BoxShadow> {
|
||||
if self.dark {
|
||||
vec![
|
||||
@@ -316,9 +318,6 @@ impl RenderOnce for Btn {
|
||||
fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
|
||||
let tk = Tk::of(cx);
|
||||
let kind = self.kind;
|
||||
// A primary with nothing to do yet — disabled, or dimmed with nothing
|
||||
// to save — is drawn on the well rather than faded.
|
||||
let resting_primary = kind == BtnKind::Primary && (self.disabled || self.dimmed);
|
||||
let base = h_flex()
|
||||
.id(self.id)
|
||||
.flex_shrink_0()
|
||||
@@ -331,31 +330,23 @@ impl RenderOnce for Btn {
|
||||
.px(px(12.))
|
||||
.when(self.icon.is_some(), |b| b.pl(px(9.)).pr(px(11.)))
|
||||
.rounded(px(6.))
|
||||
.bg(tk.well)
|
||||
.bg(tk.btn)
|
||||
.shadow(tk.raised())
|
||||
.text_size(self.size.unwrap_or(fs(12.5)))
|
||||
.text_color(tk.fg)
|
||||
.when(!self.disabled, |b| {
|
||||
b.hover(move |s| s.bg(tk.well_hover))
|
||||
.active(move |s| s.bg(tk.well_hover.blend(tk.k05)))
|
||||
b.hover(move |s| s.shadow(tk.raised_hover()))
|
||||
.active(move |s| s.bg(tk.k05))
|
||||
}),
|
||||
// Disabled, the ink fill sinks to the well with secondary text —
|
||||
// the fall the dialogs' Create and the Git panel's Commit take. At
|
||||
// 45% opacity a disabled Save was a washed-out black button that
|
||||
// still looked like the thing to press.
|
||||
BtnKind::Primary => base
|
||||
.h(px(26.))
|
||||
.px(px(12.))
|
||||
.rounded(px(6.))
|
||||
.bg(if resting_primary { tk.well } else { tk.fg })
|
||||
.bg(tk.fg)
|
||||
.text_size(self.size.unwrap_or(fs(12.5)))
|
||||
.font_weight(FontWeight::MEDIUM)
|
||||
.text_color(if resting_primary { tk.k6 } else { tk.page })
|
||||
.when(!self.disabled && !resting_primary, |b| {
|
||||
b.hover(|s| s.opacity(0.88))
|
||||
})
|
||||
.when(resting_primary && !self.disabled, |b| {
|
||||
b.hover(move |s| s.bg(tk.well_hover))
|
||||
}),
|
||||
.text_color(tk.page)
|
||||
.when(!self.disabled, |b| b.hover(|s| s.opacity(0.88))),
|
||||
BtnKind::Link | BtnKind::Danger => base
|
||||
.text_size(self.size.unwrap_or(fs(12.)))
|
||||
.text_color(if kind == BtnKind::Danger {
|
||||
@@ -373,21 +364,16 @@ impl RenderOnce for Btn {
|
||||
})
|
||||
}),
|
||||
};
|
||||
el.when(self.disabled && kind != BtnKind::Primary, |b| {
|
||||
b.opacity(0.45)
|
||||
})
|
||||
.when(
|
||||
self.dimmed && !self.disabled && kind != BtnKind::Primary,
|
||||
|b| b.opacity(0.55),
|
||||
)
|
||||
.when_some(self.icon, |b, path| {
|
||||
b.child(Icon::empty().path(path).size(px(10.)).text_color(tk.k6))
|
||||
})
|
||||
.child(self.label)
|
||||
.when_some(self.handler.filter(|_| !self.disabled), |b, handler| {
|
||||
b.cursor_pointer()
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
})
|
||||
el.when(self.disabled, |b| b.opacity(0.45))
|
||||
.when(self.dimmed && !self.disabled, |b| b.opacity(0.55))
|
||||
.when_some(self.icon, |b, path| {
|
||||
b.child(Icon::empty().path(path).size(px(10.)).text_color(tk.k6))
|
||||
})
|
||||
.child(self.label)
|
||||
.when_some(self.handler.filter(|_| !self.disabled), |b, handler| {
|
||||
b.cursor_pointer()
|
||||
.on_click(move |ev, window, cx| handler(ev, window, cx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,19 +409,38 @@ pub(crate) fn search_glass(size: f32, tk: &Tk) -> Icon {
|
||||
.text_color(tk.k35)
|
||||
}
|
||||
|
||||
/// The height of a field or a dropdown. Buttons and steppers are a step
|
||||
/// lower, at 26.
|
||||
pub(crate) const CONTROL_H: f32 = 28.;
|
||||
/// A hairline: one device pixel. The design draws field edges at half a
|
||||
/// point, which is exactly one pixel on a Retina panel — and on a 1x display
|
||||
/// half a pixel at 15% ink is anti-aliased to nothing, so the shell and proxy
|
||||
/// fields read as loose monospace text beside the boxed dropdowns.
|
||||
///
|
||||
/// Read from the scale the settings window last rendered at: the page's
|
||||
/// controls are built from `cx` alone, and threading a `Window` through every
|
||||
/// row to reach two fields would be a lot of signature for one number. A
|
||||
/// thread-local rather than a global, because setting a global from `render`
|
||||
/// queues an observer effect on every frame.
|
||||
pub(crate) fn hairline() -> f32 {
|
||||
(1. / SCALE.with(std::cell::Cell::get)).max(0.5)
|
||||
}
|
||||
|
||||
/// A search field on the well, for filtering a list on the page.
|
||||
/// Record the scale the page is being drawn at, once per frame.
|
||||
pub(crate) fn note_scale(window: &Window) {
|
||||
SCALE.with(|s| s.set(window.scale_factor().max(1.)));
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
static SCALE: std::cell::Cell<f32> = const { std::cell::Cell::new(2.) };
|
||||
}
|
||||
|
||||
/// A search field with a hairline ring, for filtering a list on the page.
|
||||
pub(crate) fn search_field(input: &gpui::Entity<InputState>, tk: &Tk) -> Div {
|
||||
h_flex()
|
||||
.h(px(CONTROL_H))
|
||||
.h(px(26.))
|
||||
.px(px(8.))
|
||||
.gap(px(6.))
|
||||
.items_center()
|
||||
.rounded(px(7.))
|
||||
.bg(tk.well)
|
||||
.rounded(px(6.))
|
||||
.shadow(vec![ring(tk.k15, hairline(), true)])
|
||||
.child(search_glass(10., tk))
|
||||
.child(
|
||||
// On the input, not around it: `Input` sets its own `text_sm`
|
||||
@@ -460,22 +465,24 @@ pub(crate) fn text_field(
|
||||
tk: &Tk,
|
||||
cx: &App,
|
||||
) -> Div {
|
||||
// At rest a field is only its well. The ring is kept for the two states
|
||||
// that have to be seen at a glance: where the keyboard is, and what is
|
||||
// wrong.
|
||||
let ring_color = match (invalid, focused) {
|
||||
(true, _) => Some(tk.danger),
|
||||
(false, true) => Some(tk.focus),
|
||||
(false, false) => None,
|
||||
let ring_color = if invalid {
|
||||
tk.danger
|
||||
} else if focused {
|
||||
tk.k45
|
||||
} else {
|
||||
tk.k15
|
||||
};
|
||||
div()
|
||||
.h(px(CONTROL_H))
|
||||
.h(px(26.))
|
||||
.px(px(10.))
|
||||
.flex()
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.bg(tk.well)
|
||||
.when_some(ring_color, |f, c| f.shadow(vec![ring(c, 1., true)]))
|
||||
.shadow(vec![ring(
|
||||
ring_color,
|
||||
if focused || invalid { 1. } else { hairline() },
|
||||
true,
|
||||
)])
|
||||
.font_family(Tk::mono(cx))
|
||||
.text_color(tk.fg)
|
||||
.child(
|
||||
@@ -535,7 +542,7 @@ pub(crate) fn select_trigger(
|
||||
) -> Stateful<Div> {
|
||||
h_flex()
|
||||
.id(id)
|
||||
.h(px(CONTROL_H))
|
||||
.h(px(26.))
|
||||
.w(px(CONTROL_W))
|
||||
.flex_shrink_0()
|
||||
.pl(px(if leading.is_some() { 8. } else { 10. }))
|
||||
@@ -543,8 +550,9 @@ pub(crate) fn select_trigger(
|
||||
.gap(px(8.))
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.bg(if open { tk.well_hover } else { tk.well })
|
||||
.hover(move |s| s.bg(tk.well_hover))
|
||||
.bg(if open { tk.k05 } else { tk.btn })
|
||||
.shadow(tk.raised())
|
||||
.hover(move |s| s.shadow(tk.raised_hover()))
|
||||
.cursor_pointer()
|
||||
.text_size(fs(12.5))
|
||||
.text_color(tk.fg)
|
||||
@@ -676,9 +684,7 @@ pub(crate) fn stepper(
|
||||
.text_color(tk.k6)
|
||||
.when(!enabled, |d| d.opacity(0.35))
|
||||
.when(enabled, |d| {
|
||||
d.cursor_pointer()
|
||||
.hover(move |s| s.bg(tk.well_hover))
|
||||
.active(move |s| s.bg(tk.well_hover.blend(tk.k05)))
|
||||
d.cursor_pointer().active(move |s| s.bg(tk.k05))
|
||||
})
|
||||
.child(glyph)
|
||||
};
|
||||
@@ -688,7 +694,8 @@ pub(crate) fn stepper(
|
||||
.h(px(26.))
|
||||
.items_center()
|
||||
.rounded(px(6.))
|
||||
.bg(tk.well)
|
||||
.bg(tk.btn)
|
||||
.shadow(tk.raised())
|
||||
.child(
|
||||
end("dec", "−", s.can_dec)
|
||||
.rounded_l(px(6.))
|
||||
|
||||
@@ -83,6 +83,7 @@ impl Render for SettingsWindow {
|
||||
// The workspace window sets this in its own render; a second window
|
||||
// has to, or the page lays out against the default 16px rem.
|
||||
window.set_rem_size(px(cx.global::<Config>().ui_font_size));
|
||||
crate::ui::settings::kit::note_scale(window);
|
||||
let page = self.app.upgrade().map(|app| {
|
||||
let page = app.update(cx, |this, cx| {
|
||||
// The state can be gone for a frame between closing and the
|
||||
|
||||
Reference in New Issue
Block a user