project_panel: Fix sticky items horizontal scroll and hover propagation (#34367)

Release Notes:

- Fixed horizontal scrolling not working for sticky items in the Project
Panel.
- Fixed issue where hovering over the last sticky item in the Project
Panel showed a hovered state on the entry behind it.
- Improved behavior when clicking a sticky item in the Project Panel so
it scrolls just enough for the item to no longer be sticky.
This commit is contained in:
Smit Barmase 2025-07-12 17:44:30 -07:00 committed by GitHub
parent 8f6b9f0d65
commit 1cadff9311
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 95 additions and 74 deletions

View file

@ -3961,8 +3961,14 @@ impl ProjectPanel {
linear_color_stop(shadow_color_bottom, 0.),
));
let id: ElementId = if is_sticky {
SharedString::from(format!("project_panel_sticky_item_{}", entry_id.to_usize())).into()
} else {
(entry_id.to_proto() as usize).into()
};
div()
.id(entry_id.to_proto() as usize)
.id(id.clone())
.relative()
.group(GROUP_NAME)
.cursor_pointer()
@ -3973,6 +3979,9 @@ impl ProjectPanel {
.border_color(border_color)
.hover(|style| style.bg(bg_hover_color).border_color(border_hover_color))
.when(show_sticky_shadow, |this| this.child(sticky_shadow))
.when(is_sticky, |this| {
this.block_mouse_except_scroll()
})
.when(!is_sticky, |this| {
this
.when(is_highlighted && folded_directory_drag_target.is_none(), |this| this.border_color(transparent_white()).bg(item_colors.drag_over))
@ -4183,6 +4192,16 @@ impl ProjectPanel {
.unwrap_or(ScrollStrategy::Top);
this.scroll_handle.scroll_to_item(index, strategy);
cx.notify();
// move down by 1px so that clicked item
// don't count as sticky anymore
cx.on_next_frame(window, |_, window, cx| {
cx.on_next_frame(window, |this, _, cx| {
let mut offset = this.scroll_handle.offset();
offset.y += px(1.);
this.scroll_handle.set_offset(offset);
cx.notify();
});
});
return;
}
}
@ -4201,7 +4220,7 @@ impl ProjectPanel {
}),
)
.child(
ListItem::new(entry_id.to_proto() as usize)
ListItem::new(id)
.indent_level(depth)
.indent_step_size(px(settings.indent_size))
.spacing(match settings.entry_spacing {

View file

@ -149,47 +149,7 @@ where
) -> AnyElement {
let entries = (self.compute_fn)(visible_range.clone(), window, cx);
struct StickyAnchor<T> {
entry: T,
index: usize,
}
let mut sticky_anchor = None;
let mut last_item_is_drifting = false;
let mut iter = entries.iter().enumerate().peekable();
while let Some((ix, current_entry)) = iter.next() {
let depth = current_entry.depth();
if depth < ix {
sticky_anchor = Some(StickyAnchor {
entry: current_entry.clone(),
index: visible_range.start + ix,
});
break;
}
if let Some(&(_next_ix, next_entry)) = iter.peek() {
let next_depth = next_entry.depth();
let next_item_outdented = next_depth + 1 == depth;
let depth_same_as_index = depth == ix;
let depth_greater_than_index = depth == ix + 1;
if next_item_outdented && (depth_same_as_index || depth_greater_than_index) {
if depth_greater_than_index {
last_item_is_drifting = true;
}
sticky_anchor = Some(StickyAnchor {
entry: current_entry.clone(),
index: visible_range.start + ix,
});
break;
}
}
}
let Some(sticky_anchor) = sticky_anchor else {
let Some(sticky_anchor) = find_sticky_anchor(&entries, visible_range.start) else {
return StickyItemsElement {
drifting_element: None,
drifting_decoration: None,
@ -203,23 +163,21 @@ where
let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx);
let items_count = elements.len();
let indents: SmallVec<[usize; 8]> = {
elements
.iter()
.enumerate()
.map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix)))
.collect()
};
let indents: SmallVec<[usize; 8]> = (0..items_count)
.map(|ix| anchor_depth.saturating_sub(items_count.saturating_sub(ix)))
.collect();
let mut last_decoration_element = None;
let mut rest_decoration_elements = SmallVec::new();
let available_space = size(
AvailableSpace::Definite(bounds.size.width),
let expanded_width = bounds.size.width + scroll_offset.x.abs();
let decor_available_space = size(
AvailableSpace::Definite(expanded_width),
AvailableSpace::Definite(bounds.size.height),
);
let drifting_y_offset = if last_item_is_drifting {
let drifting_y_offset = if sticky_anchor.drifting {
let scroll_top = -scroll_offset.y;
let anchor_top = item_height * (sticky_anchor.index + 1);
let sticky_area_height = item_height * items_count;
@ -228,7 +186,7 @@ where
Pixels::ZERO
};
let (drifting_indent, rest_indents) = if last_item_is_drifting && !indents.is_empty() {
let (drifting_indent, rest_indents) = if sticky_anchor.drifting && !indents.is_empty() {
let last = indents[indents.len() - 1];
let rest: SmallVec<[usize; 8]> = indents[..indents.len() - 1].iter().copied().collect();
(Some(last), rest)
@ -236,11 +194,14 @@ where
(None, indents)
};
let base_origin = bounds.origin - point(px(0.), scroll_offset.y);
for decoration in &self.decorations {
if let Some(drifting_indent) = drifting_indent {
let drifting_indent_vec: SmallVec<[usize; 8]> =
[drifting_indent].into_iter().collect();
let sticky_origin = bounds.origin - scroll_offset
let sticky_origin = base_origin
+ point(px(0.), item_height * rest_indents.len() + drifting_y_offset);
let decoration_bounds = Bounds::new(sticky_origin, bounds.size);
@ -252,13 +213,13 @@ where
window,
cx,
);
drifting_dec.layout_as_root(available_space, window, cx);
drifting_dec.layout_as_root(decor_available_space, window, cx);
drifting_dec.prepaint_at(sticky_origin, window, cx);
last_decoration_element = Some(drifting_dec);
}
if !rest_indents.is_empty() {
let decoration_bounds = Bounds::new(bounds.origin - scroll_offset, bounds.size);
let decoration_bounds = Bounds::new(base_origin, bounds.size);
let mut rest_dec = decoration.as_ref().compute(
&rest_indents,
decoration_bounds,
@ -267,46 +228,45 @@ where
window,
cx,
);
rest_dec.layout_as_root(available_space, window, cx);
rest_dec.layout_as_root(decor_available_space, window, cx);
rest_dec.prepaint_at(bounds.origin, window, cx);
rest_decoration_elements.push(rest_dec);
}
}
let (mut drifting_element, mut rest_elements) =
if last_item_is_drifting && !elements.is_empty() {
if sticky_anchor.drifting && !elements.is_empty() {
let last = elements.pop().unwrap();
(Some(last), elements)
} else {
(None, elements)
};
for (ix, element) in rest_elements.iter_mut().enumerate() {
let sticky_origin = bounds.origin - scroll_offset + point(px(0.), item_height * ix);
let element_available_space = size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(item_height),
);
element.layout_as_root(element_available_space, window, cx);
element.prepaint_at(sticky_origin, window, cx);
}
let element_available_space = size(
AvailableSpace::Definite(expanded_width),
AvailableSpace::Definite(item_height),
);
// order of prepaint is important here
// mouse events checks hitboxes in reverse insertion order
if let Some(ref mut drifting_element) = drifting_element {
let sticky_origin = bounds.origin - scroll_offset
let sticky_origin = base_origin
+ point(
px(0.),
item_height * rest_elements.len() + drifting_y_offset,
);
let element_available_space = size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(item_height),
);
drifting_element.layout_as_root(element_available_space, window, cx);
drifting_element.prepaint_at(sticky_origin, window, cx);
}
for (ix, element) in rest_elements.iter_mut().enumerate() {
let sticky_origin = base_origin + point(px(0.), item_height * ix);
element.layout_as_root(element_available_space, window, cx);
element.prepaint_at(sticky_origin, window, cx);
}
StickyItemsElement {
drifting_element,
drifting_decoration: last_decoration_element,
@ -317,6 +277,48 @@ where
}
}
struct StickyAnchor<T> {
entry: T,
index: usize,
drifting: bool,
}
fn find_sticky_anchor<T: StickyCandidate + Clone>(
entries: &SmallVec<[T; 8]>,
visible_range_start: usize,
) -> Option<StickyAnchor<T>> {
let mut iter = entries.iter().enumerate().peekable();
while let Some((ix, current_entry)) = iter.next() {
let depth = current_entry.depth();
if depth < ix {
return Some(StickyAnchor {
entry: current_entry.clone(),
index: visible_range_start + ix,
drifting: false,
});
}
if let Some(&(_next_ix, next_entry)) = iter.peek() {
let next_depth = next_entry.depth();
let next_item_outdented = next_depth + 1 == depth;
let depth_same_as_index = depth == ix;
let depth_greater_than_index = depth == ix + 1;
if next_item_outdented && (depth_same_as_index || depth_greater_than_index) {
return Some(StickyAnchor {
entry: current_entry.clone(),
index: visible_range_start + ix,
drifting: depth_greater_than_index,
});
}
}
}
None
}
/// A decoration for a [`StickyItems`]. This can be used for various things,
/// such as rendering indent guides, or other visual effects.
pub trait StickyItemsDecoration {