settings_ui: Fix page scroll bar lagging behind when jumping to a section (#39897)

The issue was caused by the scroll handle taking a couple of frames to
update its offset correctly after calling
`ScrollHandle::scroll_to_top_of_item`. The fast fix is forcing 3 frames
to render back-to-back.

In the future, we should look into `ScrollHandle` and see if there's any
way to update its state outside of paint.

Release Notes:

- N/A

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>
Co-authored-by: Katie Geer <katie@zed.dev>
Co-authored-by: Ben Kunkle <ben@zed.dev>
This commit is contained in:
Anthony Eid
2025-10-09 19:24:02 +00:00
committed by GitHub
co-authored by Danilo Leal Mikayla Maki Katie Geer Ben Kunkle
parent 98d4c34199
commit e4f0fbbf80
+40 -21
View File
@@ -558,10 +558,10 @@ pub struct SettingsWindow {
/// Index into navbar_entries
navbar_entry: usize,
navbar_entries: Vec<NavBarEntry>,
list_handle: UniformListScrollHandle,
navbar_scroll_handle: UniformListScrollHandle,
search_matches: Vec<Vec<bool>>,
content_handles: Vec<Vec<Entity<NonFocusableHandle>>>,
scroll_handle: ScrollHandle,
page_scroll_handle: ScrollHandle,
focus_handle: FocusHandle,
navbar_focus_handle: Entity<NonFocusableHandle>,
content_focus_handle: Entity<NonFocusableHandle>,
@@ -923,12 +923,12 @@ impl SettingsWindow {
pages: vec![],
navbar_entries: vec![],
navbar_entry: 0,
list_handle: UniformListScrollHandle::default(),
navbar_scroll_handle: UniformListScrollHandle::default(),
search_bar,
search_task: None,
search_matches: vec![],
content_handles: vec![],
scroll_handle: ScrollHandle::new(),
page_scroll_handle: ScrollHandle::new(),
focus_handle: cx.focus_handle(),
navbar_focus_handle: NonFocusableHandle::new(
NAVBAR_CONTAINER_TAB_INDEX,
@@ -1471,7 +1471,7 @@ impl SettingsWindow {
let Some(previous_root_index) = root_index else {
return;
};
this.focus_and_scroll_to_nav_entry(previous_root_index, window);
this.focus_and_scroll_to_nav_entry(previous_root_index, window, cx);
}),
)
.on_action(cx.listener(|this, _: &FocusNextRootNavEntry, window, cx| {
@@ -1491,16 +1491,16 @@ impl SettingsWindow {
let Some(next_root_index) = root_index else {
return;
};
this.focus_and_scroll_to_nav_entry(next_root_index, window);
this.focus_and_scroll_to_nav_entry(next_root_index, window, cx);
}))
.on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, _| {
.on_action(cx.listener(|this, _: &FocusFirstNavEntry, window, cx| {
if let Some((first_entry_index, _)) = this.visible_navbar_entries().next() {
this.focus_and_scroll_to_nav_entry(first_entry_index, window);
this.focus_and_scroll_to_nav_entry(first_entry_index, window, cx);
}
}))
.on_action(cx.listener(|this, _: &FocusLastNavEntry, window, _| {
.on_action(cx.listener(|this, _: &FocusLastNavEntry, window, cx| {
if let Some((last_entry_index, _)) = this.visible_navbar_entries().last() {
this.focus_and_scroll_to_nav_entry(last_entry_index, window);
this.focus_and_scroll_to_nav_entry(last_entry_index, window, cx);
}
}))
.border_color(cx.theme().colors().border)
@@ -1551,9 +1551,9 @@ impl SettingsWindow {
}),
)
.size_full()
.track_scroll(self.list_handle.clone()),
.track_scroll(self.navbar_scroll_handle.clone()),
)
.vertical_scrollbar_for(self.list_handle.clone(), window, cx),
.vertical_scrollbar_for(self.navbar_scroll_handle.clone(), window, cx),
)
.child(
h_flex()
@@ -1592,7 +1592,7 @@ impl SettingsWindow {
return;
};
self.focus_content_element(first_item_index, window, cx);
self.scroll_handle.set_offset(point(px(0.), px(0.)));
self.page_scroll_handle.set_offset(point(px(0.), px(0.)));
} else {
let entry_item_index = self.navbar_entries[navbar_entry_index]
.item_index
@@ -1603,10 +1603,23 @@ impl SettingsWindow {
else {
return;
};
self.scroll_handle
self.page_scroll_handle
.scroll_to_top_of_item(selected_item_index);
self.focus_content_element(entry_item_index, window, cx);
}
// Page scroll handle updates the active item index
// in it's next paint call after using scroll_handle.scroll_to_top_of_item
// The call after that updates the offset of the scroll handle. So to
// ensure the scroll handle doesn't lag behind we need to render three frames
// back to back.
cx.on_next_frame(window, |_, window, cx| {
cx.on_next_frame(window, |_, _, cx| {
cx.notify();
});
cx.notify();
});
cx.notify();
}
fn is_nav_entry_visible(&self, nav_entry_index: usize) -> bool {
@@ -1614,16 +1627,22 @@ impl SettingsWindow {
.any(|(index, _)| index == nav_entry_index)
}
fn focus_and_scroll_to_nav_entry(&self, nav_entry_index: usize, window: &mut Window) {
fn focus_and_scroll_to_nav_entry(
&self,
nav_entry_index: usize,
window: &mut Window,
cx: &mut Context<Self>,
) {
let Some(position) = self
.visible_navbar_entries()
.position(|(index, _)| index == nav_entry_index)
else {
return;
};
self.list_handle
self.navbar_scroll_handle
.scroll_to_item(position, gpui::ScrollStrategy::Top);
window.focus(&self.navbar_entries[nav_entry_index].focus_handle);
cx.notify();
}
fn visible_page_items(&self) -> impl Iterator<Item = (usize, &SettingsPageItem)> {
@@ -1670,7 +1689,7 @@ impl SettingsWindow {
.id("settings-ui-page")
.size_full()
.overflow_y_scroll()
.track_scroll(&self.scroll_handle);
.track_scroll(&self.page_scroll_handle);
let items: Vec<_> = items.collect();
let items_len = items.len();
@@ -1785,7 +1804,7 @@ impl SettingsWindow {
.px_8()
.bg(cx.theme().colors().editor_background)
.child(page_header)
.vertical_scrollbar_for(self.scroll_handle.clone(), window, cx)
.vertical_scrollbar_for(self.page_scroll_handle.clone(), window, cx)
.track_focus(&self.content_focus_handle.focus_handle(cx))
.child(
div()
@@ -2026,7 +2045,7 @@ impl Render for SettingsWindow {
{
this.open_and_scroll_to_navbar_entry(this.navbar_entry, window, cx);
} else {
this.focus_and_scroll_to_nav_entry(this.navbar_entry, window);
this.focus_and_scroll_to_nav_entry(this.navbar_entry, window, cx);
}
}))
.on_action(
@@ -2474,11 +2493,11 @@ mod test {
search_bar: cx.new(|cx| Editor::single_line(window, cx)),
navbar_entry: selected_idx.expect("Must have a selected navbar entry"),
navbar_entries: Vec::default(),
list_handle: UniformListScrollHandle::default(),
navbar_scroll_handle: UniformListScrollHandle::default(),
search_matches: vec![],
content_handles: vec![],
search_task: None,
scroll_handle: ScrollHandle::new(),
page_scroll_handle: ScrollHandle::new(),
focus_handle: cx.focus_handle(),
navbar_focus_handle: NonFocusableHandle::new(
NAVBAR_CONTAINER_TAB_INDEX,