project_panel: Add Sticky Scroll (#33994)

Closes #7243

- Adds `top_slot_items` to `uniform_list` component to offset list
items.
- Adds `ToPosition` scroll strategy to `uniform_list` to scroll list to
specified index.
- Adds `sticky_items` component which can be used along with
`uniform_list` to add sticky functionality to any view that implements
uniform list.


https://github.com/user-attachments/assets/eb508fa4-167e-4595-911b-52651537284c

Release Notes:

- Added sticky scroll to the project panel, which keeps parent
directories visible while scrolling. This feature is enabled by default.
To disable it, toggle `sticky_scroll` in settings.
This commit is contained in:
Smit Barmase 2025-07-07 08:32:42 +05:30 committed by GitHub
parent 2246b01c4b
commit 6efc5ecefe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 742 additions and 287 deletions

View file

@ -7,8 +7,8 @@
use crate::{
AnyElement, App, AvailableSpace, Bounds, ContentMask, Element, ElementId, GlobalElementId,
Hitbox, InspectorElementId, InteractiveElement, Interactivity, IntoElement, IsZero, LayoutId,
ListSizingBehavior, Overflow, Pixels, ScrollHandle, Size, StyleRefinement, Styled, Window,
point, size,
ListSizingBehavior, Overflow, Pixels, Point, ScrollHandle, Size, StyleRefinement, Styled,
Window, point, size,
};
use smallvec::SmallVec;
use std::{cell::RefCell, cmp, ops::Range, rc::Rc};
@ -42,6 +42,7 @@ where
item_count,
item_to_measure_index: 0,
render_items: Box::new(render_range),
top_slot: None,
decorations: Vec::new(),
interactivity: Interactivity {
element_id: Some(id),
@ -61,6 +62,7 @@ pub struct UniformList {
render_items: Box<
dyn for<'a> Fn(Range<usize>, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>,
>,
top_slot: Option<Box<dyn UniformListTopSlot>>,
decorations: Vec<Box<dyn UniformListDecoration>>,
interactivity: Interactivity,
scroll_handle: Option<UniformListScrollHandle>,
@ -71,6 +73,7 @@ pub struct UniformList {
/// Frame state used by the [UniformList].
pub struct UniformListFrameState {
items: SmallVec<[AnyElement; 32]>,
top_slot_items: SmallVec<[AnyElement; 8]>,
decorations: SmallVec<[AnyElement; 1]>,
}
@ -88,6 +91,8 @@ pub enum ScrollStrategy {
/// May not be possible if there's not enough list items above the item scrolled to:
/// in this case, the element will be placed at the closest possible position.
Center,
/// Scrolls the element to be at the given item index from the top of the viewport.
ToPosition(usize),
}
#[derive(Clone, Debug, Default)]
@ -212,6 +217,7 @@ impl Element for UniformList {
UniformListFrameState {
items: SmallVec::new(),
decorations: SmallVec::new(),
top_slot_items: SmallVec::new(),
},
)
}
@ -345,6 +351,15 @@ impl Element for UniformList {
}
}
}
ScrollStrategy::ToPosition(sticky_index) => {
let target_y_in_viewport = item_height * sticky_index;
let target_scroll_top = item_top - target_y_in_viewport;
let max_scroll_top =
(content_height - list_height).max(Pixels::ZERO);
let new_scroll_top =
target_scroll_top.clamp(Pixels::ZERO, max_scroll_top);
updated_scroll_offset.y = -new_scroll_top;
}
}
scroll_offset = *updated_scroll_offset
}
@ -354,7 +369,17 @@ impl Element for UniformList {
let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height)
/ item_height)
.ceil() as usize;
let visible_range = first_visible_element_ix
let initial_range = first_visible_element_ix
..cmp::min(last_visible_element_ix, self.item_count);
let mut top_slot_elements = if let Some(ref mut top_slot) = self.top_slot {
top_slot.compute(initial_range, window, cx)
} else {
SmallVec::new()
};
let top_slot_offset = top_slot_elements.len();
let visible_range = (top_slot_offset + first_visible_element_ix)
..cmp::min(last_visible_element_ix, self.item_count);
let items = if y_flipped {
@ -393,6 +418,20 @@ impl Element for UniformList {
frame_state.items.push(item);
}
if let Some(ref top_slot) = self.top_slot {
top_slot.prepaint(
&mut top_slot_elements,
padded_bounds,
item_height,
scroll_offset,
padding,
can_scroll_horizontally,
window,
cx,
);
}
frame_state.top_slot_items = top_slot_elements;
let bounds = Bounds::new(
padded_bounds.origin
+ point(
@ -454,6 +493,9 @@ impl Element for UniformList {
for decoration in &mut request_layout.decorations {
decoration.paint(window, cx);
}
if let Some(ref top_slot) = self.top_slot {
top_slot.paint(&mut request_layout.top_slot_items, window, cx);
}
},
)
}
@ -483,6 +525,35 @@ pub trait UniformListDecoration {
) -> AnyElement;
}
/// A trait for implementing top slots in a [`UniformList`].
/// Top slots are elements that appear at the top of the list and can adjust
/// the visible range of list items.
pub trait UniformListTopSlot {
/// Returns elements to render at the top slot for the given visible range.
fn compute(
&mut self,
visible_range: Range<usize>,
window: &mut Window,
cx: &mut App,
) -> SmallVec<[AnyElement; 8]>;
/// Layout and prepaint the top slot elements.
fn prepaint(
&self,
elements: &mut SmallVec<[AnyElement; 8]>,
bounds: Bounds<Pixels>,
item_height: Pixels,
scroll_offset: Point<Pixels>,
padding: crate::Edges<Pixels>,
can_scroll_horizontally: bool,
window: &mut Window,
cx: &mut App,
);
/// Paint the top slot elements.
fn paint(&self, elements: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App);
}
impl UniformList {
/// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {
@ -521,6 +592,12 @@ impl UniformList {
self
}
/// Sets a top slot for the list.
pub fn with_top_slot(mut self, top_slot: impl UniformListTopSlot + 'static) -> Self {
self.top_slot = Some(Box::new(top_slot));
self
}
fn measure_item(
&self,
list_width: Option<Pixels>,