fix(settings): stack a row instead of squeezing its label to a letter per line

The control never shrinks and the label column must keep min_w_0 or long
descriptions stop wrapping, so on a narrow pane the label was squeezed to
nothing: at 600px the Terminal page rendered "Program" as a vertical
column of single letters, and the SSH form did the same below about
1000px, which 4275bb1 left open as the real remaining bug.

flex_wrap was the wrong answer — it let the label column size to its own
description, which then ran out past the row on every wide page. Measure
instead: render_settings knows the viewport and which page it is drawing,
so it can say how much width a row will actually get, and the row lays
itself out side by side or stacked from that. Nothing moves above the
breakpoint.
This commit is contained in:
l0ng-ai
2026-08-08 12:09:58 +07:00
parent e6240e899c
commit 78501b86aa
2 changed files with 82 additions and 24 deletions
+6
View File
@@ -393,6 +393,11 @@ pub struct Tty7App {
pub(crate) editor: crate::ui::code_editor::EditorPanelState,
pub(crate) sidebar_width: Rc<Cell<f32>>,
pub(crate) sidebar_dragging: Rc<Cell<bool>>,
/// How much width a settings row will actually get, measured once per
/// render. `settings_row` is called from page builders that never see the
/// window, and the answer differs per page — the SSH page spends 500px on
/// its two lists first.
pub(crate) settings_row_width: Cell<f32>,
pub(crate) right_panel_width: Rc<Cell<f32>>,
pub(crate) right_panel_dragging: Rc<Cell<bool>>,
pub(crate) right_panel_visible: bool,
@@ -806,6 +811,7 @@ impl Tty7App {
editor,
sidebar_width: Rc::new(Cell::new(sidebar_width)),
sidebar_dragging: Rc::new(Cell::new(false)),
settings_row_width: Cell::new(f32::MAX),
right_panel_width: Rc::new(Cell::new(right_panel_width)),
right_panel_dragging: Rc::new(Cell::new(false)),
right_panel_visible,
+76 -24
View File
@@ -37,6 +37,29 @@ use crate::ui::presets;
use crate::ui::rounding;
use crate::ui::rounding::RoundedCorners as _;
/// The settings nav, the SSH host list, and the padding each page sets — the
/// chrome a row has to share the window with.
const NAV_W: f32 = 220.;
const SSH_LIST_W: f32 = 280.;
const SSH_DETAIL_PAD: f32 = 64.;
const PAGE_PAD: f32 = 80.;
/// What a row on this page really has to lay out in.
///
/// The nav is always in front of it; the SSH page puts its host list there too,
/// and only the scrolled pages cap the reading column at 640.
fn settings_row_width(section: SettingsSection, viewport: f32) -> f32 {
match section {
SettingsSection::Ssh => (viewport - NAV_W - SSH_LIST_W - SSH_DETAIL_PAD).max(0.),
_ => (viewport - NAV_W - PAGE_PAD).clamp(0., 640.),
}
}
/// Width a settings row needs before its label and its control fit side by
/// side: a 260px control, the `gap_8` between them, and enough left for a
/// description to read as prose rather than as a column of words.
const STACK_ROW_BELOW: f32 = 500.;
fn settings_row_id(label: &str, _desc: &str) -> SharedString {
SharedString::from(format!("settings-row-{label}"))
}
@@ -766,6 +789,9 @@ impl Tty7App {
let query = search.read(cx).value().trim().to_lowercase();
let show_theme_panel = theme_panel_open && section == SettingsSection::Appearance;
self.settings_row_width
.set(settings_row_width(section, window.viewport_size().width.as_f32()));
let prof = crate::ui::perf::enabled()
.then(|| (std::time::Instant::now(), section.profile_label()));
@@ -1078,11 +1104,38 @@ impl Tty7App {
}
None => (false, false),
};
h_flex()
// The control never shrinks, so on a narrow pane it takes the width and
// the label column — which must keep `min_w_0` or long descriptions
// stop wrapping — is squeezed to a letter per line. Below the width
// where both still fit, put the control on its own line instead.
// Measured, not `flex_wrap`: wrapping made the label column size to its
// description, which then ran out past the row on every wide page.
let stacked = self.settings_row_width.get() < STACK_ROW_BELOW;
let labels = v_flex()
.gap_0p5()
.min_w_0()
.child(
div()
.text_sm()
.font_weight(FontWeight::MEDIUM)
.text_color(theme.foreground)
.child(label),
)
.when(!desc.is_empty(), |col| {
col.child(
div()
.text_xs()
.text_color(theme.muted_foreground)
.child(desc),
)
});
div()
.id(element_id)
.items_center()
.justify_between()
.gap_8()
.flex()
.when(stacked, |row| row.flex_col().items_start().gap_2())
.when(!stacked, |row| {
row.flex_row().items_center().justify_between().gap_8()
})
.py_2()
.px_2p5()
.mx_neg_2p5()
@@ -1091,26 +1144,7 @@ impl Tty7App {
.when(miss, |row| row.opacity(0.45))
.hover(|h| h.bg(gpui::rgb(cx.global::<presets::Surfaces>().window.hover)))
.on_hover(cx.listener(|_this, _hovered, _window, cx| cx.notify()))
.child(
v_flex()
.gap_0p5()
.min_w_0()
.child(
div()
.text_sm()
.font_weight(FontWeight::MEDIUM)
.text_color(theme.foreground)
.child(label),
)
.when(!desc.is_empty(), |col| {
col.child(
div()
.text_xs()
.text_color(theme.muted_foreground)
.child(desc),
)
}),
)
.child(labels)
.child(h_flex().flex_shrink_0().child(control))
}
@@ -5448,6 +5482,24 @@ impl Tty7App {
mod tests {
use super::*;
/// The row keeps its side-by-side shape while both halves fit, and stacks
/// once they do not. The SSH page reaches that point first — it spends 500px
/// on two lists before the row gets any.
#[test]
fn a_row_stacks_once_its_label_and_control_stop_fitting() {
use SettingsSection::*;
assert!(settings_row_width(Terminal, 1440.) >= STACK_ROW_BELOW);
assert!(settings_row_width(Terminal, 900.) >= STACK_ROW_BELOW);
assert!(settings_row_width(Terminal, 700.) < STACK_ROW_BELOW);
// Capped at the reading column, so a wider window never widens the row.
assert_eq!(settings_row_width(Terminal, 4000.), 640.);
// SSH crosses over while the window is still wide.
assert!(settings_row_width(Ssh, 1440.) >= STACK_ROW_BELOW);
assert!(settings_row_width(Ssh, 1000.) < STACK_ROW_BELOW);
// And never goes negative on a window narrower than its own chrome.
assert_eq!(settings_row_width(Ssh, 100.), 0.);
}
#[test]
fn a_row_is_marked_by_its_label_or_by_the_keywords_behind_it() {
// Straight label hit.