diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 1122d1e6..cc952130 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -808,9 +808,11 @@ impl Tty7App { div() .px_10() .py_8() - // Fill the pane edge-to-edge; cap only on very wide windows so - // rows never stretch to an unreadable width. - .child(div().w_full().max_w(px(860.)).child(content)), + // Cap the column tight enough (640px) that a row's + // right-aligned control stays visually paired with its + // label — the cap is what makes `settings_row`'s + // space-between layout safe. + .child(div().w_full().max_w(px(640.)).child(content)), ) }; @@ -933,10 +935,13 @@ impl Tty7App { } /// One labelled settings row, shared by every section: title + description - /// in a fixed-width left column, control immediately beside it. A fixed - /// column (not space-between) keeps label and control visually paired - /// regardless of window width — space-between on a wide pane stretched the - /// two apart into a dead gap. + /// on the left, control right-aligned. Space-between is safe here because + /// the content column is capped (640px), so the two never stretch apart + /// into a dead gap the way they did on an uncapped pane. A soft full-row + /// hover fill makes each row read as one scannable unit — the same quiet + /// highlight the sidebar and menus use; negative side margins let that fill + /// bleed past the text edge while labels stay aligned with the section + /// headers above. pub(crate) fn settings_row( &self, label: impl Into, @@ -945,15 +950,20 @@ impl Tty7App { cx: &Context, ) -> Div { let theme = cx.theme(); + let desc = desc.into(); h_flex() .items_center() + .justify_between() .gap_8() .py_2() + .px_2p5() + .mx_neg_2p5() + .rounded_lg() + .hover(|h| h.bg(theme.secondary.opacity(0.2))) .child( v_flex() .gap_0p5() - .w(px(260.)) - .flex_shrink_0() + .min_w_0() .child( div() .text_sm() @@ -961,14 +971,18 @@ impl Tty7App { .text_color(theme.foreground) .child(label.into()), ) - .child( - div() - .text_xs() - .text_color(theme.muted_foreground) - .child(desc.into()), - ), + // Rows without a description (the theme color editor) stay + // single-line instead of carrying an empty text child. + .when(!desc.is_empty(), |col| { + col.child( + div() + .text_xs() + .text_color(theme.muted_foreground) + .child(desc), + ) + }), ) - .child(control) + .child(h_flex().flex_shrink_0().child(control)) } /// A segmented control (gpui-component's `ButtonGroup`, outline) for a small @@ -1050,13 +1064,14 @@ impl Tty7App { let control_h = px(24.); // The −│value│+ pill plus its quiet Reset — one shape shared by the // font-size and line-height rows; callers hand in the wired buttons. + // Reset sits *before* the pill: with controls right-aligned, the pill + // holds the row's hard right edge and the quiet action tucks inboard. let stepper_row = move |dec: Stateful
, value: String, inc: Stateful
, reset: Button| { h_flex() .items_center() - .justify_start() - .w(px(240.)) .gap_3() + .child(reset) .child( h_flex() .items_center() @@ -1082,7 +1097,6 @@ impl Tty7App { ) .child(inc), ) - .child(reset) .into_any_element() }; let font_size_control = stepper_row( @@ -1117,21 +1131,16 @@ impl Tty7App { // One font dropdown, shared shape for primary / bold / italic pickers. let font_dropdown = |state: &Entity>>| { - h_flex() - .justify_start() - .w(px(240.)) - .child( - Select::new(state) - .small() - .w(px(180.)) - .h(control_h) - .search_placeholder("Search fonts…") - // Cap the popup's own height so browsing doesn't dump the - // OS's entire font catalog on screen at once — it just - // scrolls from here. Every font is still in the list and - // reachable by typing; this only trims what's shown. - .menu_max_h(px(224.)), - ) + Select::new(state) + .small() + .w(px(180.)) + .h(control_h) + .search_placeholder("Search fonts…") + // Cap the popup's own height so browsing doesn't dump the + // OS's entire font catalog on screen at once — it just + // scrolls from here. Every font is still in the list and + // reachable by typing; this only trims what's shown. + .menu_max_h(px(224.)) .into_any_element() }; let font_family_control = font_dropdown(&font_select); @@ -1316,11 +1325,7 @@ impl Tty7App { state: Entity, cx: &mut Context, ) -> impl IntoElement + use<> { - let control = h_flex() - .items_center() - .w(px(240.)) - .child(ColorPicker::new(&state).small()) - .into_any_element(); + let control = ColorPicker::new(&state).small().into_any_element(); self.settings_row(label, "", control, cx) } @@ -2048,16 +2053,23 @@ impl Tty7App { .child(self.settings_row( "Name", "A label for this connection.", - Input::new(&form.name).small().into_any_element(), + // Explicit widths on every text control: `settings_row` right-aligns + // the control in a shrink-to-fit slot, so a bare Input has no + // definite width to fill. 260px matches the Shell section's inputs. + div() + .w(px(260.)) + .child(Input::new(&form.name).small()) + .into_any_element(), cx, )) .child( self.settings_row( "Host", "Hostname or IP address.", + // Host + port split the shared 260px control width. h_flex() .gap_2() - .child(Input::new(&form.host).small()) + .child(div().w(px(172.)).child(Input::new(&form.host).small())) .child(div().w(px(80.)).child(Input::new(&form.port).small())) .into_any_element(), cx, @@ -2066,7 +2078,10 @@ impl Tty7App { .child(self.settings_row( "User", "Login user (blank = resolve at connect).", - Input::new(&form.user).small().into_any_element(), + div() + .w(px(260.)) + .child(Input::new(&form.user).small()) + .into_any_element(), cx, )) .child(self.settings_row( @@ -2166,7 +2181,10 @@ impl Tty7App { section = section.child(self.settings_row( "Jump host", "Name of another profile to tunnel through (blank = direct).", - Input::new(&form.jump).small().into_any_element(), + div() + .w(px(260.)) + .child(Input::new(&form.jump).small()) + .into_any_element(), cx, )); } @@ -2237,7 +2255,12 @@ impl Tty7App { this.settings_row( label.to_string(), desc.to_string(), - Input::new(input).small().into_any_element(), + // Same explicit control width as the core fields above — a bare + // Input has nothing to fill in the row's right-aligned slot. + div() + .w(px(260.)) + .child(Input::new(input).small()) + .into_any_element(), cx, ) }; @@ -3083,6 +3106,23 @@ impl Tty7App { let active = presets::by_id(cx, &active_id); let name = active.name.clone(); let mode = if active.dark { "Dark" } else { "Light" }; + // A user file (duplicated or dropped in the themes folder) vs a built-in. + let kind = if active.path.is_some() { + "Custom" + } else { + "Built-in" + }; + // The six chromatic ANSI slots (red…cyan) as tiny swatches — the part of + // a theme the mini preview's few bars can't show, and what actually + // distinguishes two same-background themes at a glance. + let to_u32 = |(r, g, b): (u8, u8, u8)| (r as u32) << 16 | (g as u32) << 8 | b as u32; + let swatches = h_flex().gap_1().mt_1p5().children((1..=6).map(|i| { + div() + .w(px(10.)) + .h(px(10.)) + .rounded(px(3.)) + .bg(rgb(to_u32(active.ansi16[i]))) + })); let preview = self.theme_preview(&active); let open = self.active_settings().is_some_and(|s| s.theme_panel_open); @@ -3090,7 +3130,7 @@ impl Tty7App { .id("current-theme") .mt_1() .mb_2() - .max_w(px(520.)) + .w_full() .cursor_pointer() .on_click(cx.listener(|this, _, _w, cx| this.toggle_theme_panel(cx))) .child( @@ -3111,6 +3151,12 @@ impl Tty7App { .child( v_flex() .gap_0p5() + .child( + div() + .text_xs() + .text_color(muted_fg) + .child(format!("{kind} · {mode}")), + ) .child( div() .text_sm() @@ -3118,7 +3164,7 @@ impl Tty7App { .text_color(foreground) .child(name), ) - .child(div().text_xs().text_color(muted_fg).child(mode)), + .child(swatches), ) .child(div().flex_1()) .child(