From 270598e1f181f85dc815bffaeb039d83c7bebdf8 Mon Sep 17 00:00:00 2001 From: l0ng-ai <24760907+l0ng-ai@users.noreply.github.com> Date: Sat, 8 Aug 2026 14:32:10 +0700 Subject: [PATCH] fix(settings): give the settings pages the scrollbar the rest of the app has MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The tab sidebar, the file tree, the right panel and the SFTP browser all run their scroll area through `with_vertical_scrollbar`. The settings pages — the longest scrolling surfaces tty7 has, several viewports of them on Appearance and Terminal — had no bar at all: no thumb, no sense of how far down the page you were or how much was left. The SSH host list, the SSH form and the theme picker's list were the same. All four now carry the shared bar. It follows the OS "show scroll bars" preference like every other one, so nothing appears for anyone who asked for scrollbars to stay hidden. The scroll areas move inside a wrapper that holds the constraints they used to hold themselves, and two of those do not survive the move. `min_w_0` on a column child means the *cross* axis, and carrying it inside let the SSH form's label column shrink to nothing while the toggles stayed put; it belongs on the wrapper, which is still the row item it always was. And the wrapper has to take its width explicitly rather than by stretching, or the `w_full` inside has no width to be a percentage of — which is how the settings reading column lost its 640px cap on the Chinese page, exactly the way 00ad4eb first found it. --- src/ui/app.rs | 3 ++ src/ui/scrollbar.rs | 6 +++ src/ui/settings.rs | 115 +++++++++++++++++++++++++++++--------------- 3 files changed, 84 insertions(+), 40 deletions(-) diff --git a/src/ui/app.rs b/src/ui/app.rs index 8dde9eb2..31a8dbdb 100644 --- a/src/ui/app.rs +++ b/src/ui/app.rs @@ -3816,6 +3816,9 @@ impl Tty7App { section: SettingsSection::Appearance, search: settings_search, content_scroll, + ssh_master_scroll: gpui::ScrollHandle::new(), + ssh_detail_scroll: gpui::ScrollHandle::new(), + theme_list_scroll: gpui::ScrollHandle::new(), search_anchor, reveal_first_hit: Cell::new(false), font_select, diff --git a/src/ui/scrollbar.rs b/src/ui/scrollbar.rs index 6a25b919..194c641d 100644 --- a/src/ui/scrollbar.rs +++ b/src/ui/scrollbar.rs @@ -11,6 +11,12 @@ pub(crate) fn with_vertical_scrollbar( .relative() .flex_1() .min_h_0() + // Stretching would size this the same, but not *definitely*: a `w_full` + // inside the scroll area would then have no width to be a percentage + // of, and would fall back to its content. That is how the settings + // reading column lost its 640px cap on the Chinese page — one wide row + // measured wider, and every row followed it. + .w_full() .child(scroll_area) .child( div() diff --git a/src/ui/settings.rs b/src/ui/settings.rs index 2d1123c1..219428eb 100644 --- a/src/ui/settings.rs +++ b/src/ui/settings.rs @@ -474,6 +474,9 @@ pub(crate) struct SettingsState { /// claims. Searching tells you "Appearance (2)"; these are what carry you /// to the two, which on a long page start well below the fold. pub(crate) content_scroll: gpui::ScrollHandle, + pub(crate) ssh_master_scroll: gpui::ScrollHandle, + pub(crate) ssh_detail_scroll: gpui::ScrollHandle, + pub(crate) theme_list_scroll: gpui::ScrollHandle, pub(crate) search_anchor: gpui::ScrollAnchor, /// Set when the query or the section changes, and spent by the next render /// that has somewhere to go. A `Cell` because that render only holds `&self`. @@ -961,13 +964,11 @@ impl Tty7App { .h_full() .bg(background) .child(content) + .into_any_element() } else { - v_flex() + let body = v_flex() .id("settings-content") - .flex_1() - .min_w_0() - .h_full() - .bg(background) + .size_full() .overflow_y_scroll() .when_some(self.active_settings(), |pane, s| { pane.track_scroll(&s.content_scroll) @@ -987,7 +988,20 @@ impl Tty7App { .children(no_match_note) .child(content), ), - ) + ); + v_flex() + .flex_1() + .min_w_0() + .h_full() + .bg(background) + .when_some(self.active_settings(), |pane, s| { + pane.child(crate::ui::scrollbar::with_vertical_scrollbar( + "settings-content-scrollbar", + body, + &s.content_scroll, + )) + }) + .into_any_element() }; let root = div() @@ -1704,43 +1718,58 @@ impl Tty7App { fn render_settings_ssh(&self, cx: &mut Context) -> AnyElement { let border = cx.theme().border; + let Some((master_scroll, detail_scroll)) = self + .active_settings() + .map(|s| (s.ssh_master_scroll.clone(), s.ssh_detail_scroll.clone())) + else { + return div().into_any_element(); + }; + let master = v_flex() + .id("ssh-master") + .size_full() + .overflow_y_scroll() + .track_scroll(&master_scroll) + .child(self.render_ssh_master(cx)); + let detail = v_flex() + .id("ssh-detail") + .size_full() + .overflow_y_scroll() + .track_scroll(&detail_scroll) + .child( + div() + .pt(px(crate::ui::app::TITLE_BAR_HEIGHT)) + .px_8() + .pb_8() + .child( + div() + .w_full() + .max_w(px(720.)) + .child(self.render_ssh_detail(cx)), + ), + ); h_flex() .size_full() .items_start() .child( v_flex() - .id("ssh-master") .flex_shrink_0() .w(px(280.)) .h_full() .border_r_1() .border_color(border) - .overflow_y_scroll() - .child(self.render_ssh_master(cx)), - ) - .child( - v_flex() - .id("ssh-detail") - .flex_1() - // Without this the flex default `min-width: auto` keeps the - // column at its content width, so a narrow window pushes the - // form off the right edge instead of squeezing the labels. - .min_w_0() - .h_full() - .overflow_y_scroll() - .child( - div() - .pt(px(crate::ui::app::TITLE_BAR_HEIGHT)) - .px_8() - .pb_8() - .child( - div() - .w_full() - .max_w(px(720.)) - .child(self.render_ssh_detail(cx)), - ), - ), + .child(crate::ui::scrollbar::with_vertical_scrollbar( + "ssh-master-scrollbar", + master, + &master_scroll, + )), ) + .child(v_flex().flex_1().min_w_0().h_full().child( + crate::ui::scrollbar::with_vertical_scrollbar( + "ssh-detail-scrollbar", + detail, + &detail_scroll, + ), + )) .into_any_element() } @@ -4602,6 +4631,7 @@ impl Tty7App { ), None => return div().into_any_element(), }; + let list_scroll = self.active_settings().map(|s| s.theme_list_scroll.clone()); let config = cx.global::(); let slot = match (config.theme_follow_system, slot) { (false, _) => ThemeSlot::Manual, @@ -4769,14 +4799,19 @@ impl Tty7App { .child(header) .child(subtitle) .child(search_box) - .child( - v_flex() - .id("theme-panel-list") - .flex_1() - .overflow_y_scroll() - .children(rejected_note) - .child(list), - ) + .when_some(list_scroll, |panel, scroll| { + panel.child(crate::ui::scrollbar::with_vertical_scrollbar( + "theme-panel-scrollbar", + v_flex() + .id("theme-panel-list") + .size_full() + .overflow_y_scroll() + .track_scroll(&scroll) + .children(rejected_note) + .child(list), + &scroll, + )) + }) .into_any_element() }