diff --git a/crates/gpui/src/elements/div.rs b/crates/gpui/src/elements/div.rs index 58ce51e95bc707dc7eb7d335bd1dafaf8cb0eb40..06fe5902bf6eca527f5f16d84e88c9c847e3e08a 100644 --- a/crates/gpui/src/elements/div.rs +++ b/crates/gpui/src/elements/div.rs @@ -3034,7 +3034,20 @@ struct ScrollHandleState { child_bounds: Vec>, scroll_to_bottom: bool, overflow: Point, - active_item: Option, + active_item: Option, +} + +#[derive(Default, Debug, Clone, Copy)] +struct ScrollActiveItem { + index: usize, + strategy: ScrollStrategy, +} + +#[derive(Default, Debug, Clone, Copy)] +enum ScrollStrategy { + #[default] + FirstVisible, + Top, } /// A handle to the scrollable aspects of an element. @@ -3084,6 +3097,25 @@ impl ScrollHandle { } } + /// Get the bottom child that's scrolled into view. + pub fn bottom_item(&self) -> usize { + let state = self.0.borrow(); + let bottom = state.bounds.bottom() - state.offset.borrow().y; + + match state.child_bounds.binary_search_by(|bounds| { + if bottom < bounds.top() { + Ordering::Greater + } else if bottom > bounds.bottom() { + Ordering::Less + } else { + Ordering::Equal + } + }) { + Ok(ix) => ix, + Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)), + } + } + /// Return the bounds into which this child is painted pub fn bounds(&self) -> Bounds { self.0.borrow().bounds @@ -3097,26 +3129,48 @@ impl ScrollHandle { /// Update [ScrollHandleState]'s active item for scrolling to in prepaint pub fn scroll_to_item(&self, ix: usize) { let mut state = self.0.borrow_mut(); - state.active_item = Some(ix); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::default(), + }); } - /// Scrolls the minimal amount to ensure that the child is - /// fully visible + /// Update [ScrollHandleState]'s active item for scrolling to in prepaint + /// This scrolls the minimal amount to ensure that the child is the first visible element + pub fn scroll_to_top_of_item(&self, ix: usize) { + let mut state = self.0.borrow_mut(); + state.active_item = Some(ScrollActiveItem { + index: ix, + strategy: ScrollStrategy::Top, + }); + } + + /// Scrolls the minimal amount to either ensure that the child is + /// fully visible or the top element of the view depends on the + /// scroll strategy fn scroll_to_active_item(&self) { let mut state = self.0.borrow_mut(); - let Some(active_item_index) = state.active_item else { + let Some(active_item) = state.active_item else { return; }; - let active_item = match state.child_bounds.get(active_item_index) { + + let active_item = match state.child_bounds.get(active_item.index) { Some(bounds) => { let mut scroll_offset = state.offset.borrow_mut(); - if state.overflow.y == Overflow::Scroll { - if bounds.top() + scroll_offset.y < state.bounds.top() { + match active_item.strategy { + ScrollStrategy::FirstVisible => { + if state.overflow.y == Overflow::Scroll { + if bounds.top() + scroll_offset.y < state.bounds.top() { + scroll_offset.y = state.bounds.top() - bounds.top(); + } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { + scroll_offset.y = state.bounds.bottom() - bounds.bottom(); + } + } + } + ScrollStrategy::Top => { scroll_offset.y = state.bounds.top() - bounds.top(); - } else if bounds.bottom() + scroll_offset.y > state.bounds.bottom() { - scroll_offset.y = state.bounds.bottom() - bounds.bottom(); } } @@ -3129,7 +3183,7 @@ impl ScrollHandle { } None } - None => Some(active_item_index), + None => Some(active_item), }; state.active_item = active_item; } @@ -3163,6 +3217,21 @@ impl ScrollHandle { } } + /// Get the logical scroll bottom, based on a child index and a pixel offset. + pub fn logical_scroll_bottom(&self) -> (usize, Pixels) { + let ix = self.bottom_item(); + let state = self.0.borrow(); + + if let Some(child_bounds) = state.child_bounds.get(ix) { + ( + ix, + child_bounds.bottom() + state.offset.borrow().y - state.bounds.bottom(), + ) + } else { + (ix, px(0.)) + } + } + /// Get the count of children for scrollable item. pub fn children_count(&self) -> usize { self.0.borrow().child_bounds.len() diff --git a/crates/settings_ui/src/settings_ui.rs b/crates/settings_ui/src/settings_ui.rs index 90a5ea532391cb51e43b2f1bc61aca327cfbc90c..6999a754e5b7f1c27ff9d05c9aaf29494f93a750 100644 --- a/crates/settings_ui/src/settings_ui.rs +++ b/crates/settings_ui/src/settings_ui.rs @@ -986,6 +986,29 @@ impl SettingsWindow { cx.notify(); } + fn calculate_navbar_entry_from_scroll_position(&mut self) { + let top = self.scroll_handle.top_item(); + let bottom = self.scroll_handle.bottom_item(); + + let scroll_index = (top + bottom) / 2; + let scroll_index = scroll_index.clamp(top, bottom); + let mut page_index = self.navbar_entry; + + while !self.navbar_entries[page_index].is_root { + page_index -= 1; + } + + if self.navbar_entries[page_index].expanded { + let section_index = self + .page_items() + .take(scroll_index + 1) + .filter(|item| matches!(item, SettingsPageItem::SectionHeader(_))) + .count(); + + self.navbar_entry = section_index + page_index; + } + } + fn fetch_files(&mut self, cx: &mut Context) { let prev_files = self.files.clone(); let settings_store = cx.global::(); @@ -1064,9 +1087,7 @@ impl SettingsWindow { window: &mut Window, cx: &mut Context, ) -> impl IntoElement { - let visible_entries: Vec<_> = self.visible_navbar_entries().collect(); - let visible_count = visible_entries.len(); - + let visible_count = self.visible_navbar_entries().count(); let nav_background = cx.theme().colors().panel_background; v_flex() @@ -1112,6 +1133,36 @@ impl SettingsWindow { .on_click(cx.listener( move |this, evt: &gpui::ClickEvent, window, cx| { this.navbar_entry = ix; + + if !this.navbar_entries[ix].is_root { + let mut selected_page_ix = ix; + + while !this.navbar_entries[selected_page_ix] + .is_root + { + selected_page_ix -= 1; + } + + let section_header = ix - selected_page_ix; + + if let Some(section_index) = this + .page_items() + .enumerate() + .filter(|item| { + matches!( + item.1, + SettingsPageItem::SectionHeader(_) + ) + }) + .take(section_header) + .last() + .map(|pair| pair.0) + { + this.scroll_handle + .scroll_to_top_of_item(section_index); + } + } + if evt.is_keyboard() { // todo(settings_ui): Focus the actual item and scroll to it this.focus_first_content_item(window, cx); @@ -1380,6 +1431,7 @@ impl SettingsWindow { impl Render for SettingsWindow { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { let ui_font = theme::setup_ui_font(window, cx); + self.calculate_navbar_entry_from_scroll_position(); div() .id("settings-window")