diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index f32ecfc20c..342490b882 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -42,7 +42,6 @@ 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), @@ -62,7 +61,6 @@ pub struct UniformList { render_items: Box< dyn for<'a> Fn(Range, &'a mut Window, &'a mut App) -> SmallVec<[AnyElement; 64]>, >, - top_slot: Option>, decorations: Vec>, interactivity: Interactivity, scroll_handle: Option, @@ -73,7 +71,6 @@ 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]>, } @@ -145,6 +142,15 @@ impl UniformListScrollHandle { .map(|(ix, _)| ix) .unwrap_or_else(|| this.base_handle.logical_scroll_top().0) } + + /// Checks if the list can be scrolled vertically. + pub fn is_scrollable(&self) -> bool { + if let Some(size) = self.0.borrow().last_item_size { + size.contents.height > size.item.height + } else { + false + } + } } impl Styled for UniformList { @@ -217,7 +223,6 @@ impl Element for UniformList { UniformListFrameState { items: SmallVec::new(), decorations: SmallVec::new(), - top_slot_items: SmallVec::new(), }, ) } @@ -369,17 +374,8 @@ impl Element for UniformList { let last_visible_element_ix = ((-scroll_offset.y + padded_bounds.size.height) / item_height) .ceil() as usize; - 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) + let visible_range = first_visible_element_ix ..cmp::min(last_visible_element_ix, self.item_count); let items = if y_flipped { @@ -418,20 +414,6 @@ 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( @@ -448,6 +430,7 @@ impl Element for UniformList { let mut decoration = decoration.as_ref().compute( visible_range.clone(), bounds, + scroll_offset, item_height, self.item_count, window, @@ -493,9 +476,6 @@ 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); - } }, ) } @@ -518,6 +498,7 @@ pub trait UniformListDecoration { &self, visible_range: Range, bounds: Bounds, + scroll_offset: Point, item_height: Pixels, item_count: usize, window: &mut Window, @@ -592,12 +573,6 @@ 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, diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index a5861250a4..bd0d5e3919 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -56,8 +56,8 @@ use std::{ use theme::ThemeSettings; use ui::{ Color, ContextMenu, DecoratedIcon, Icon, IconDecoration, IconDecorationKind, IndentGuideColors, - IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, Scrollbar, - ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, + IndentGuideLayout, KeyBinding, Label, LabelSize, ListItem, ListItemSpacing, ScrollableHandle, + Scrollbar, ScrollbarState, StickyCandidate, Tooltip, prelude::*, v_flex, }; use util::{ResultExt, TakeUntilExt, TryFutureExt, maybe, paths::compare_paths}; use workspace::{ @@ -3327,7 +3327,13 @@ impl ProjectPanel { range: Range, window: &mut Window, cx: &mut Context, - mut callback: impl FnMut(&Entry, &HashSet>, &mut Window, &mut Context), + mut callback: impl FnMut( + &Entry, + usize, + &HashSet>, + &mut Window, + &mut Context, + ), ) { let mut ix = 0; for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { @@ -3348,8 +3354,10 @@ impl ProjectPanel { .map(|e| (e.path.clone())) .collect() }); - for entry in visible_worktree_entries[entry_range].iter() { - callback(&entry, entries, window, cx); + let base_index = ix + entry_range.start; + for (i, entry) in visible_worktree_entries[entry_range].iter().enumerate() { + let global_index = base_index + i; + callback(&entry, global_index, entries, window, cx); } ix = end_ix; } @@ -3930,7 +3938,15 @@ impl ProjectPanel { } }; - let last_sticky_item = details.sticky.as_ref().map_or(false, |item| item.is_last); + let show_sticky_shadow = details.sticky.as_ref().map_or(false, |item| { + if item.is_last { + let is_scrollable = self.scroll_handle.is_scrollable(); + let is_scrolled = self.scroll_handle.offset().y < px(0.); + is_scrollable && is_scrolled + } else { + false + } + }); let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.15); let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); let sticky_shadow = div() @@ -3956,7 +3972,7 @@ impl ProjectPanel { .border_r_2() .border_color(border_color) .hover(|style| style.bg(bg_hover_color).border_color(border_hover_color)) - .when(is_sticky && last_sticky_item, |this| this.child(sticky_shadow)) + .when(show_sticky_shadow, |this| this.child(sticky_shadow)) .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)) @@ -4828,54 +4844,6 @@ impl ProjectPanel { None } - fn candidate_entries_in_range_for_sticky( - &self, - range: Range, - _window: &mut Window, - _cx: &mut Context, - ) -> Vec { - let mut result = Vec::new(); - let mut current_offset = 0; - - for (_, visible_worktree_entries, entries_paths) in &self.visible_entries { - let worktree_len = visible_worktree_entries.len(); - let worktree_end_offset = current_offset + worktree_len; - - if current_offset >= range.end { - break; - } - - if worktree_end_offset > range.start { - let local_start = range.start.saturating_sub(current_offset); - let local_end = range.end.saturating_sub(current_offset).min(worktree_len); - - let paths = entries_paths.get_or_init(|| { - visible_worktree_entries - .iter() - .map(|e| e.path.clone()) - .collect() - }); - - let entries_from_this_worktree = visible_worktree_entries[local_start..local_end] - .iter() - .enumerate() - .map(|(i, entry)| { - let (depth, _) = Self::calculate_depth_and_difference(&entry.entry, paths); - StickyProjectPanelCandidate { - index: current_offset + local_start + i, - depth, - } - }); - - result.extend(entries_from_this_worktree); - } - - current_offset = worktree_end_offset; - } - - result - } - fn render_sticky_entries( &self, child: StickyProjectPanelCandidate, @@ -4926,6 +4894,10 @@ impl ProjectPanel { break 'outer; } + if sticky_parents.is_empty() { + return SmallVec::new(); + } + sticky_parents.reverse(); let git_status_enabled = ProjectPanelSettings::get_global(cx).git_status; @@ -4940,6 +4912,8 @@ impl ProjectPanel { Default::default() }; + // already checked if non empty above + let last_item_index = sticky_parents.len() - 1; sticky_parents .iter() .enumerate() @@ -4950,7 +4924,7 @@ impl ProjectPanel { .unwrap_or_default(); let sticky_details = Some(StickyDetails { sticky_index: index, - is_last: index == sticky_parents.len() - 1, + is_last: index == last_item_index, }); let details = self.details_for_entry( entry, @@ -5191,17 +5165,6 @@ impl Render for ProjectPanel { items }) }) - .when(show_sticky_scroll, |list| { - list.with_top_slot(ui::sticky_items( - cx.entity().clone(), - |this, range, window, cx| { - this.candidate_entries_in_range_for_sticky(range, window, cx) - }, - |this, marker_entry, window, cx| { - this.render_sticky_entries(marker_entry, window, cx) - }, - )) - }) .when(show_indent_guides, |list| { list.with_decoration( ui::indent_guides( @@ -5215,7 +5178,7 @@ impl Render for ProjectPanel { range, window, cx, - |entry, entries, _, _| { + |entry, _, entries, _, _| { let (depth, _) = Self::calculate_depth_and_difference( entry, entries, ); @@ -5301,6 +5264,30 @@ impl Render for ProjectPanel { ), ) }) + .when(show_sticky_scroll, |list| { + list.with_decoration(ui::sticky_items( + cx.entity().clone(), + |this, range, window, cx| { + let mut items = SmallVec::with_capacity(range.end - range.start); + this.iter_visible_entries( + range, + window, + cx, + |entry, index, entries, _, _| { + let (depth, _) = + Self::calculate_depth_and_difference(entry, entries); + let candidate = + StickyProjectPanelCandidate { index, depth }; + items.push(candidate); + }, + ); + items + }, + |this, marker_entry, window, cx| { + this.render_sticky_entries(marker_entry, window, cx) + }, + )) + }) .size_full() .with_sizing_behavior(ListSizingBehavior::Infer) .with_horizontal_sizing_behavior(ListHorizontalSizingBehavior::Unconstrained) diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 01b3e2cf74..6d4db984f9 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -147,6 +147,7 @@ mod uniform_list { &self, visible_range: Range, bounds: Bounds, + _scroll_offset: Point, item_height: Pixels, item_count: usize, window: &mut Window, diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index e5ef0cdf27..e98e3023d2 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -1,7 +1,8 @@ -use std::ops::Range; +use std::{ops::Range, rc::Rc}; use gpui::{ - AnyElement, App, AvailableSpace, Bounds, Context, Entity, Pixels, Render, UniformListTopSlot, + AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, + InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, Window, point, size, }; use smallvec::SmallVec; @@ -10,16 +11,16 @@ pub trait StickyCandidate { fn depth(&self) -> usize; } +#[derive(Clone)] pub struct StickyItems { - compute_fn: Box, &mut Window, &mut App) -> Vec>, - render_fn: Box SmallVec<[AnyElement; 8]>>, - last_item_is_drifting: bool, - anchor_index: Option, + compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, + render_fn: Rc SmallVec<[AnyElement; 8]>>, } pub fn sticky_items( entity: Entity, - compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> Vec + 'static, + compute_fn: impl Fn(&mut V, Range, &mut Window, &mut Context) -> SmallVec<[T; 8]> + + 'static, render_fn: impl Fn(&mut V, T, &mut Window, &mut Context) -> SmallVec<[AnyElement; 8]> + 'static, ) -> StickyItems where @@ -29,37 +30,106 @@ where let entity_compute = entity.clone(); let entity_render = entity.clone(); - let compute_fn = Box::new( - move |range: Range, window: &mut Window, cx: &mut App| -> Vec { + let compute_fn = Rc::new( + move |range: Range, window: &mut Window, cx: &mut App| -> SmallVec<[T; 8]> { entity_compute.update(cx, |view, cx| compute_fn(view, range, window, cx)) }, ); - let render_fn = Box::new( + let render_fn = Rc::new( move |entry: T, window: &mut Window, cx: &mut App| -> SmallVec<[AnyElement; 8]> { entity_render.update(cx, |view, cx| render_fn(view, entry, window, cx)) }, ); + StickyItems { compute_fn, render_fn, - last_item_is_drifting: false, - anchor_index: None, } } -impl UniformListTopSlot for StickyItems +struct StickyItemsElement { + elements: SmallVec<[AnyElement; 8]>, +} + +impl IntoElement for StickyItemsElement { + type Element = Self; + + fn into_element(self) -> Self::Element { + self + } +} + +impl Element for StickyItemsElement { + type RequestLayoutState = (); + type PrepaintState = (); + + fn id(&self) -> Option { + None + } + + fn source_location(&self) -> Option<&'static core::panic::Location<'static>> { + None + } + + fn request_layout( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + window: &mut Window, + cx: &mut App, + ) -> (LayoutId, Self::RequestLayoutState) { + (window.request_layout(Style::default(), [], cx), ()) + } + + fn prepaint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _window: &mut Window, + _cx: &mut App, + ) -> Self::PrepaintState { + () + } + + fn paint( + &mut self, + _id: Option<&GlobalElementId>, + _inspector_id: Option<&InspectorElementId>, + _bounds: Bounds, + _request_layout: &mut Self::RequestLayoutState, + _prepaint: &mut Self::PrepaintState, + window: &mut Window, + cx: &mut App, + ) { + // reverse so that last item is bottom most among sticky items + for item in self.elements.iter_mut().rev() { + item.paint(window, cx); + } + } +} + +impl UniformListDecoration for StickyItems where T: StickyCandidate + Clone + 'static, { fn compute( - &mut self, + &self, visible_range: Range, + bounds: Bounds, + scroll_offset: Point, + item_height: Pixels, + _item_count: usize, window: &mut Window, cx: &mut App, - ) -> SmallVec<[AnyElement; 8]> { + ) -> AnyElement { let entries = (self.compute_fn)(visible_range.clone(), window, cx); + let mut elements = SmallVec::new(); let mut anchor_entry = None; + let mut last_item_is_drifting = false; + let mut anchor_index = None; let mut iter = entries.iter().enumerate().peekable(); while let Some((ix, current_entry)) = iter.next() { @@ -75,8 +145,8 @@ where let next_depth = next_entry.depth(); if next_depth < current_depth && next_depth < index_in_range { - self.last_item_is_drifting = true; - self.anchor_index = Some(visible_range.start + ix); + last_item_is_drifting = true; + anchor_index = Some(visible_range.start + ix); anchor_entry = Some(current_entry.clone()); break; } @@ -84,67 +154,36 @@ where } if let Some(anchor_entry) = anchor_entry { - (self.render_fn)(anchor_entry, window, cx) - } else { - SmallVec::new() - } - } + elements = (self.render_fn)(anchor_entry, window, cx); + let items_count = elements.len(); - fn prepaint( - &self, - items: &mut SmallVec<[AnyElement; 8]>, - bounds: Bounds, - item_height: Pixels, - scroll_offset: gpui::Point, - padding: gpui::Edges, - can_scroll_horizontally: bool, - window: &mut Window, - cx: &mut App, - ) { - let items_count = items.len(); + for (ix, element) in elements.iter_mut().enumerate() { + let mut item_y_offset = None; + if ix == items_count - 1 && last_item_is_drifting { + if let Some(anchor_index) = anchor_index { + let scroll_top = -scroll_offset.y; + let anchor_top = item_height * anchor_index; + let sticky_area_height = item_height * items_count; + item_y_offset = + Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); + }; + } - for (ix, item) in items.iter_mut().enumerate() { - let mut item_y_offset = None; - if ix == items_count - 1 && self.last_item_is_drifting { - if let Some(anchor_index) = self.anchor_index { - let scroll_top = -scroll_offset.y; - let anchor_top = item_height * anchor_index; - let sticky_area_height = item_height * items_count; - item_y_offset = - Some((anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)); - }; - } + let sticky_origin = bounds.origin + + point( + -scroll_offset.x, + -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO), + ); - let sticky_origin = bounds.origin - + point( - if can_scroll_horizontally { - scroll_offset.x + padding.left - } else { - scroll_offset.x - }, - item_height * ix + padding.top + item_y_offset.unwrap_or(Pixels::ZERO), + let available_space = size( + AvailableSpace::Definite(bounds.size.width), + AvailableSpace::Definite(item_height), ); - - let available_width = if can_scroll_horizontally { - bounds.size.width + scroll_offset.x.abs() - } else { - bounds.size.width - }; - - let available_space = size( - AvailableSpace::Definite(available_width), - AvailableSpace::Definite(item_height), - ); - - item.layout_as_root(available_space, window, cx); - item.prepaint_at(sticky_origin, window, cx); + element.layout_as_root(available_space, window, cx); + element.prepaint_at(sticky_origin, window, cx); + } } - } - fn paint(&self, items: &mut SmallVec<[AnyElement; 8]>, window: &mut Window, cx: &mut App) { - // reverse so that last item is bottom most among sticky items - for item in items.iter_mut().rev() { - item.paint(window, cx); - } + StickyItemsElement { elements }.into_any_element() } }