mirror of
https://github.com/l0ng-ai/tty7.git
synced 2026-10-03 08:02:02 +00:00
fix(settings): even out the page rhythm and line up the columns
- Nav header: drop the min_h(ROW_H)/min_h(0) pair on the heading, which measured ~46pt taller than it painted and opened a hole under the search. - Page titles sit under the title-bar band, level with the nav heading, instead of jammed against the window's top edge. - Headings get a 22pt group-header row and hug their rows; rules keep more air, so a heading reads as its rows' rather than floating between. - SSH: the host list gives width before the nav, so the nav no longer narrows on that page; its header, search well and detail title run level with the nav's; the empty note starts on the host-title column. - Window & Tabs no longer opens on a stray rule. - Integrations: status leads the buttons on one line, in the meta ink. - Terminal: the shell footnote stays close to its rows.
This commit is contained in:
+98
-49
@@ -169,14 +169,20 @@ fn settings_columns_scaled(
|
|||||||
only_when(ssh, SSH_LIST_W - SSH_LIST_W_MIN),
|
only_when(ssh, SSH_LIST_W - SSH_LIST_W_MIN),
|
||||||
only_when(beside, THEME_PANEL_W - THEME_PANEL_W_MIN),
|
only_when(beside, THEME_PANEL_W - THEME_PANEL_W_MIN),
|
||||||
);
|
);
|
||||||
let slack = nav_slack + list_slack + panel_slack;
|
// The page's own columns give first, and the nav only once they stand on
|
||||||
|
// their floors. The nav is the one column every page shares: squeezed
|
||||||
|
// together with the host list it came out a few points narrower on SSH
|
||||||
|
// than on every other page, and jumped each time the reader moved there.
|
||||||
let short = (nav + ssh_list + theme_panel + page - viewport).max(0.);
|
let short = (nav + ssh_list + theme_panel + page - viewport).max(0.);
|
||||||
if short > 0. && slack > 0. {
|
let page_slack = list_slack + panel_slack;
|
||||||
let give = (short / slack).min(1.);
|
let from_page = short.min(page_slack);
|
||||||
nav -= nav_slack * give;
|
if from_page > 0. {
|
||||||
|
let give = from_page / page_slack;
|
||||||
ssh_list -= list_slack * give;
|
ssh_list -= list_slack * give;
|
||||||
theme_panel -= panel_slack * give;
|
theme_panel -= panel_slack * give;
|
||||||
}
|
}
|
||||||
|
let from_nav = (short - from_page).min(nav_slack);
|
||||||
|
nav -= from_nav;
|
||||||
if panel_overlays {
|
if panel_overlays {
|
||||||
// Covering the page, not replacing it: leave a strip of the page in
|
// Covering the page, not replacing it: leave a strip of the page in
|
||||||
// view so the panel reads as something laid on top and dismissible.
|
// view so the panel reads as something laid on top and dismissible.
|
||||||
@@ -256,6 +262,10 @@ const CONTROL_RADIUS: f32 = crate::ui::tab_strip::RAIL_TILE_RADIUS;
|
|||||||
/// One line of navigation — a nav item, a host, a section heading — at the
|
/// One line of navigation — a nav item, a host, a section heading — at the
|
||||||
/// right panel's Session row height.
|
/// right panel's Session row height.
|
||||||
const ROW_H: f32 = 28.;
|
const ROW_H: f32 = 28.;
|
||||||
|
/// A heading's row: the design's group-header height, shorter than a content
|
||||||
|
/// row so the heading hugs the rows it names rather than floating between
|
||||||
|
/// them and the rule above.
|
||||||
|
const HEADING_H: f32 = 22.;
|
||||||
|
|
||||||
/// How far a row pads its text in from its own edge, which is how far its
|
/// How far a row pads its text in from its own edge, which is how far its
|
||||||
/// hover fill bleeds past that text. The list around it sits `CONTENT_INSET`
|
/// hover fill bleeds past that text. The list around it sits `CONTENT_INSET`
|
||||||
@@ -272,6 +282,9 @@ const SEGMENT_INSET: f32 = 2.;
|
|||||||
/// every row — the Defaults row and the group headings keep the column, so
|
/// every row — the Defaults row and the group headings keep the column, so
|
||||||
/// every title starts on the same one.
|
/// every title starts on the same one.
|
||||||
const SSH_DOT: f32 = 6.;
|
const SSH_DOT: f32 = 6.;
|
||||||
|
/// Where an SSH list row's title starts: the row's padding, the dot gutter and
|
||||||
|
/// the 8px gap after it.
|
||||||
|
const SSH_TITLE_INSET: f32 = ROW_PAD + SSH_DOT + 8.;
|
||||||
|
|
||||||
/// The page's type ladder, in rems: the right panel's, so a label here is the
|
/// The page's type ladder, in rems: the right panel's, so a label here is the
|
||||||
/// size of a label there.
|
/// size of a label there.
|
||||||
@@ -2116,7 +2129,7 @@ fn heading_text(title: impl Into<SharedString>, cx: &App) -> Div {
|
|||||||
div()
|
div()
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.min_h(px(ROW_H))
|
.min_h(px(HEADING_H))
|
||||||
.text_size(gpui::rems(HEADING))
|
.text_size(gpui::rems(HEADING))
|
||||||
.font_weight(FontWeight::MEDIUM)
|
.font_weight(FontWeight::MEDIUM)
|
||||||
.text_color(cx.theme().muted_foreground)
|
.text_color(cx.theme().muted_foreground)
|
||||||
@@ -2876,15 +2889,15 @@ impl Tty7App {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.w_full()
|
.w_full()
|
||||||
.flex_shrink_0()
|
.flex_shrink_0()
|
||||||
.gap(px(10.))
|
.gap(px(4.))
|
||||||
.px(px(CONTENT_INSET))
|
.px(px(CONTENT_INSET))
|
||||||
.pt(px(TITLE_BAR_HEIGHT))
|
.pt(px(TITLE_BAR_HEIGHT))
|
||||||
.pb(px(SECTION_GAP))
|
.pb(px(12.))
|
||||||
.child(
|
// The heading keeps its full row. Cancelling the row's
|
||||||
heading_text(t(L10nKey::SettingsHeader), cx)
|
// floor with a later `min_h(0)` left the column measuring
|
||||||
.min_h(px(0.))
|
// it some 46pt taller than it painted, and that phantom
|
||||||
.px(px(ROW_PAD)),
|
// height opened a hole between the search and the nav.
|
||||||
)
|
.child(heading_text(t(L10nKey::SettingsHeader), cx).px(px(ROW_PAD)))
|
||||||
.child(nav_search),
|
.child(nav_search),
|
||||||
)
|
)
|
||||||
.child(nav_body)
|
.child(nav_body)
|
||||||
@@ -3013,9 +3026,10 @@ impl Tty7App {
|
|||||||
// outside the scroll range. `flex_shrink_0` does not buy
|
// outside the scroll range. `flex_shrink_0` does not buy
|
||||||
// its way out of that; only staying a block does.
|
// its way out of that; only staying a block does.
|
||||||
//
|
//
|
||||||
// The page's title stands in the window's title-bar band,
|
// The page's title sits under the window's title-bar band,
|
||||||
// level with the close tile at the other end, so the page
|
// on the line the nav's "Settings" heading does, and keeps
|
||||||
// under it starts where the nav's search does.
|
// a section's air from the first row. In the band itself
|
||||||
|
// it was jammed against the window's top edge.
|
||||||
div().w_full().px_10().pb_8().child(
|
div().w_full().px_10().pb_8().child(
|
||||||
div()
|
div()
|
||||||
.w_full()
|
.w_full()
|
||||||
@@ -3032,7 +3046,9 @@ impl Tty7App {
|
|||||||
)
|
)
|
||||||
.flex()
|
.flex()
|
||||||
.items_center()
|
.items_center()
|
||||||
.h(px(TITLE_BAR_HEIGHT)),
|
.mt(px(TITLE_BAR_HEIGHT))
|
||||||
|
.h(px(HEADING_H))
|
||||||
|
.mb(px(SECTION_GAP)),
|
||||||
)
|
)
|
||||||
.children(no_match_note)
|
.children(no_match_note)
|
||||||
.when(self.theme_draft_dirty(), |v| {
|
.when(self.theme_draft_dirty(), |v| {
|
||||||
@@ -3384,8 +3400,10 @@ impl Tty7App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn section_header(&self, title: &str, cx: &Context<Self>) -> Stateful<Div> {
|
pub(crate) fn section_header(&self, title: &str, cx: &Context<Self>) -> Stateful<Div> {
|
||||||
|
// Pulled into the first row's top padding: about 9pt from its rows,
|
||||||
|
// against the 16 between rows and the 27 from the rule above.
|
||||||
self.header_text(title, cx)
|
self.header_text(title, cx)
|
||||||
.mb_1()
|
.mb(px(-ROW_PAD / 4.))
|
||||||
.id(settings_header_id(title))
|
.id(settings_header_id(title))
|
||||||
.anchor_scroll(self.first_hit_anchor(title, cx))
|
.anchor_scroll(self.first_hit_anchor(title, cx))
|
||||||
}
|
}
|
||||||
@@ -3410,12 +3428,15 @@ impl Tty7App {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// The seam between two sections: a half-pixel hairline in the divider
|
/// The seam between two sections: a half-pixel hairline in the divider
|
||||||
/// ink with `SECTION_GAP` of air either side — a pane edge's line, not a
|
/// ink with a little more than `SECTION_GAP` of air either side — a pane
|
||||||
/// table rule, so a page reads as one surface broken into bands.
|
/// edge's line, not a table rule, so a page reads as one surface broken
|
||||||
|
/// into bands. The heading under it sits closer to its own rows than to
|
||||||
|
/// this, which is what makes it read as theirs.
|
||||||
pub(crate) fn section_rule(&self, cx: &Context<Self>) -> Div {
|
pub(crate) fn section_rule(&self, cx: &Context<Self>) -> Div {
|
||||||
div()
|
div()
|
||||||
.h(px(0.5))
|
.h(px(0.5))
|
||||||
.my(px(SECTION_GAP))
|
.mt(px(SECTION_GAP + 4.))
|
||||||
|
.mb(px(SECTION_GAP + 8.))
|
||||||
.bg(cx.theme().sidebar_border)
|
.bg(cx.theme().sidebar_border)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4333,7 +4354,12 @@ impl Tty7App {
|
|||||||
.track_scroll(&detail_scroll)
|
.track_scroll(&detail_scroll)
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.pt(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
// Its title row is a control row tall, for a host's
|
||||||
|
// buttons; lifted by the difference so the title still
|
||||||
|
// centres on the line every other page's title does.
|
||||||
|
.pt(px(
|
||||||
|
crate::ui::app::TITLE_BAR_HEIGHT - (ROW_H - HEADING_H) / 2.
|
||||||
|
))
|
||||||
.px_8()
|
.px_8()
|
||||||
.pb_8()
|
.pb_8()
|
||||||
.child(
|
.child(
|
||||||
@@ -4387,10 +4413,14 @@ impl Tty7App {
|
|||||||
|
|
||||||
// The rail's header, rebuilt for a list of hosts: a heading with its
|
// The rail's header, rebuilt for a list of hosts: a heading with its
|
||||||
// two tiles at the trailing end, then the 28px filled search well.
|
// two tiles at the trailing end, then the 28px filled search well.
|
||||||
|
// Built to the nav's measures beside it — a heading row, 4pt, the
|
||||||
|
// well — so the headings and the wells run level across both columns;
|
||||||
|
// the tiles overhang the short heading row rather than push it down.
|
||||||
let header = v_flex()
|
let header = v_flex()
|
||||||
.gap(px(10.))
|
.gap(px(4.))
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
|
.h(px(HEADING_H))
|
||||||
.items_center()
|
.items_center()
|
||||||
.justify_between()
|
.justify_between()
|
||||||
.pl(px(ROW_PAD))
|
.pl(px(ROW_PAD))
|
||||||
@@ -4479,11 +4509,12 @@ impl Tty7App {
|
|||||||
list = list.child(
|
list = list.child(
|
||||||
div()
|
div()
|
||||||
.py_4()
|
.py_4()
|
||||||
// px_2 is what `render_ssh_row` insets its title by: a note
|
// Where `render_ssh_row` starts its title — its padding,
|
||||||
// standing in for the rows starts on their column, not on
|
// then the dot gutter every row keeps: a note standing in
|
||||||
// the list's own edge.
|
// for the rows starts on their column, not on the list's
|
||||||
.px_2()
|
// own edge.
|
||||||
.text_sm()
|
.px(px(SSH_TITLE_INSET))
|
||||||
|
.text_size(gpui::rems(META))
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(t(L10nKey::SettingsNoSavedHosts)),
|
.child(t(L10nKey::SettingsNoSavedHosts)),
|
||||||
);
|
);
|
||||||
@@ -4491,8 +4522,8 @@ impl Tty7App {
|
|||||||
list = list.child(
|
list = list.child(
|
||||||
div()
|
div()
|
||||||
.py_4()
|
.py_4()
|
||||||
.px_2()
|
.px(px(SSH_TITLE_INSET))
|
||||||
.text_sm()
|
.text_size(gpui::rems(META))
|
||||||
.text_color(muted)
|
.text_color(muted)
|
||||||
.child(t_fmt(L10nKey::SettingsNothingMatches, &[("query", &query)])),
|
.child(t_fmt(L10nKey::SettingsNothingMatches, &[("query", &query)])),
|
||||||
);
|
);
|
||||||
@@ -4525,7 +4556,7 @@ impl Tty7App {
|
|||||||
v_flex()
|
v_flex()
|
||||||
.px(px(CONTENT_INSET))
|
.px(px(CONTENT_INSET))
|
||||||
.pb(px(CONTENT_INSET))
|
.pb(px(CONTENT_INSET))
|
||||||
.gap(px(SECTION_GAP))
|
.gap(px(12.))
|
||||||
.pt(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
.pt(px(crate::ui::app::TITLE_BAR_HEIGHT))
|
||||||
.child(header)
|
.child(header)
|
||||||
.child(list)
|
.child(list)
|
||||||
@@ -7355,10 +7386,11 @@ impl Tty7App {
|
|||||||
))
|
))
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
// A footnote to the rows above, so it stays within a row's padding
|
||||||
|
// of them: 12pt out, it read as a paragraph of its own.
|
||||||
.child(
|
.child(
|
||||||
div()
|
div()
|
||||||
.mt_3()
|
.text_size(gpui::rems(META))
|
||||||
.text_xs()
|
|
||||||
.text_color(muted_fg)
|
.text_color(muted_fg)
|
||||||
.child(t(L10nKey::SettingsShellFooter)),
|
.child(t(L10nKey::SettingsShellFooter)),
|
||||||
)
|
)
|
||||||
@@ -7810,7 +7842,7 @@ impl Tty7App {
|
|||||||
use crate::core::agent_hooks::HooksState;
|
use crate::core::agent_hooks::HooksState;
|
||||||
|
|
||||||
let theme = cx.theme();
|
let theme = cx.theme();
|
||||||
let (foreground, muted_fg) = (theme.foreground, theme.muted_foreground);
|
let muted_fg = theme.muted_foreground;
|
||||||
let (success, warning) = (theme.success, theme.warning);
|
let (success, warning) = (theme.success, theme.warning);
|
||||||
let (view, note, selected_host) = match self.active_settings() {
|
let (view, note, selected_host) = match self.active_settings() {
|
||||||
Some(s) => (
|
Some(s) => (
|
||||||
@@ -7869,19 +7901,33 @@ impl Tty7App {
|
|||||||
// `settings_row` gives the control column the whole row
|
// `settings_row` gives the control column the whole row
|
||||||
// once it stacks, and buttons flush to the far edge of a
|
// once it stacks, and buttons flush to the far edge of a
|
||||||
// row whose label starts at the near one read as unrelated.
|
// row whose label starts at the near one read as unrelated.
|
||||||
|
//
|
||||||
|
// The status leads the buttons on their own line: stacked
|
||||||
|
// over them it made every row two control lines tall, its
|
||||||
|
// dot floating level with the label and the buttons with
|
||||||
|
// nothing. It is the row's metadata, so it takes the
|
||||||
|
// description's size and ink.
|
||||||
let control = v_flex()
|
let control = v_flex()
|
||||||
.gap_2()
|
.gap_1()
|
||||||
.when(!stacked, |c| c.items_end())
|
.when(!stacked, |c| c.items_end())
|
||||||
.child(
|
.child(
|
||||||
h_flex()
|
h_flex()
|
||||||
.gap_2()
|
.gap_2()
|
||||||
.items_center()
|
.items_center()
|
||||||
.child(div().size(px(SSH_DOT)).rounded_full().bg(dot_color))
|
.child(
|
||||||
.child(div().text_sm().text_color(foreground).child(status_text)),
|
h_flex()
|
||||||
)
|
.mr_2()
|
||||||
.child(
|
.gap(px(6.))
|
||||||
h_flex()
|
.items_center()
|
||||||
.gap_2()
|
.child(div().size(px(SSH_DOT)).rounded_full().bg(dot_color))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(gpui::rems(META))
|
||||||
|
.text_color(muted_fg)
|
||||||
|
.whitespace_nowrap()
|
||||||
|
.child(status_text),
|
||||||
|
),
|
||||||
|
)
|
||||||
.child(
|
.child(
|
||||||
quiet_button(
|
quiet_button(
|
||||||
Button::new(("agent-hooks-install", i))
|
Button::new(("agent-hooks-install", i))
|
||||||
@@ -8205,9 +8251,10 @@ impl Tty7App {
|
|||||||
cx,
|
cx,
|
||||||
))
|
))
|
||||||
})
|
})
|
||||||
|
// The first thing on its own page: a rule here only divided the
|
||||||
|
// page title from nothing.
|
||||||
.when(!general, |v| {
|
.when(!general, |v| {
|
||||||
v.child(self.section_rule(cx))
|
v.child(self.section_header(t(L10nKey::SettingsTabs), cx))
|
||||||
.child(self.section_header(t(L10nKey::SettingsTabs), cx))
|
|
||||||
.child(self.settings_row(
|
.child(self.settings_row(
|
||||||
t(L10nKey::SettingsNewTabPosition),
|
t(L10nKey::SettingsNewTabPosition),
|
||||||
t(L10nKey::SettingsNewTabPositionDesc),
|
t(L10nKey::SettingsNewTabPositionDesc),
|
||||||
@@ -9996,20 +10043,22 @@ mod tests {
|
|||||||
assert_eq!(settings_row_width(Appearance, true, REPORTED, 1.), page);
|
assert_eq!(settings_row_width(Appearance, true, REPORTED, 1.), page);
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The list that has to give the most is the one the window has the least
|
/// The page's own lists give before the nav does, so the nav keeps one
|
||||||
/// room for, and no list is ever asked for more than it has to spare.
|
/// width from page to page, and no list is ever asked for more than it has
|
||||||
|
/// to spare.
|
||||||
#[test]
|
#[test]
|
||||||
fn the_lists_shrink_together_and_stop_at_their_floors() {
|
fn the_page_lists_shrink_before_the_nav_and_stop_at_their_floors() {
|
||||||
use SettingsSection::*;
|
use SettingsSection::*;
|
||||||
// Wide enough for everyone: nothing moves.
|
// Wide enough for everyone: nothing moves.
|
||||||
let wide = settings_columns(Ssh, false, 1440.);
|
let wide = settings_columns(Ssh, false, 1440.);
|
||||||
assert_eq!((wide.nav, wide.ssh_list), (NAV_W, SSH_LIST_W));
|
assert_eq!((wide.nav, wide.ssh_list), (NAV_W, SSH_LIST_W));
|
||||||
// The reported window — half of a 1440pt screen, three columns on SSH.
|
// The reported window — half of a 1440pt screen, three columns on SSH.
|
||||||
// Both lists give, neither past its floor, and the detail comes out at
|
// The host list gives, not past its floor, the nav stays the width it
|
||||||
// its preferred width instead of the 336 it used to be left with.
|
// is on every other page, and the detail comes out at its preferred
|
||||||
|
// width instead of the 336 it used to be left with.
|
||||||
let half = settings_columns(Ssh, false, 900.);
|
let half = settings_columns(Ssh, false, 900.);
|
||||||
assert!(half.nav < NAV_W && half.ssh_list < SSH_LIST_W);
|
assert_eq!(half.nav, settings_columns(General, false, 900.).nav);
|
||||||
assert!(half.nav >= NAV_W_MIN && half.ssh_list >= SSH_LIST_W_MIN);
|
assert!(half.ssh_list < SSH_LIST_W && half.ssh_list >= SSH_LIST_W_MIN);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
settings_row_width(Ssh, false, 900., 1.).round(),
|
settings_row_width(Ssh, false, 900., 1.).round(),
|
||||||
CONTENT_W,
|
CONTENT_W,
|
||||||
|
|||||||
Reference in New Issue
Block a user