project panel: Add indent guides for sticky items (#34092)

- Adds new trait `StickyItemsDecoration` in `sticky_items` which is
implemented by `IndentGuides` from `indent_guides`.

<img width="347" alt="image"
src="https://github.com/user-attachments/assets/577748bc-13f6-41b8-9266-6a0b72349a18"
/>

Release Notes:

- N/A
This commit is contained in:
Smit Barmase 2025-07-09 05:28:25 +05:30 committed by GitHub
parent ad8b823555
commit 3a247ee947
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 580 additions and 400 deletions

View file

@ -506,35 +506,6 @@ pub trait UniformListDecoration {
) -> AnyElement; ) -> 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 { impl UniformList {
/// Selects a specific list item for measurement. /// Selects a specific list item for measurement.
pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self { pub fn with_width_from_item(mut self, item_index: Option<usize>) -> Self {

View file

@ -4584,53 +4584,52 @@ impl OutlinePanel {
.track_scroll(self.scroll_handle.clone()) .track_scroll(self.scroll_handle.clone())
.when(show_indent_guides, |list| { .when(show_indent_guides, |list| {
list.with_decoration( list.with_decoration(
ui::indent_guides( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
cx.entity().clone(), .with_compute_indents_fn(
px(indent_size), cx.entity().clone(),
IndentGuideColors::panel(cx), |outline_panel, range, _, _| {
|outline_panel, range, _, _| { let entries = outline_panel.cached_entries.get(range);
let entries = outline_panel.cached_entries.get(range); if let Some(entries) = entries {
if let Some(entries) = entries { entries.into_iter().map(|item| item.depth).collect()
entries.into_iter().map(|item| item.depth).collect() } else {
} else { smallvec::SmallVec::new()
smallvec::SmallVec::new() }
} },
}, )
) .with_render_fn(
.with_render_fn( cx.entity().clone(),
cx.entity().clone(), move |outline_panel, params, _, _| {
move |outline_panel, params, _, _| { const LEFT_OFFSET: Pixels = px(14.);
const LEFT_OFFSET: Pixels = px(14.);
let indent_size = params.indent_size; let indent_size = params.indent_size;
let item_height = params.item_height; let item_height = params.item_height;
let active_indent_guide_ix = find_active_indent_guide_ix( let active_indent_guide_ix = find_active_indent_guide_ix(
outline_panel, outline_panel,
&params.indent_guides, &params.indent_guides,
); );
params params
.indent_guides .indent_guides
.into_iter() .into_iter()
.enumerate() .enumerate()
.map(|(ix, layout)| { .map(|(ix, layout)| {
let bounds = Bounds::new( let bounds = Bounds::new(
point( point(
layout.offset.x * indent_size + LEFT_OFFSET, layout.offset.x * indent_size + LEFT_OFFSET,
layout.offset.y * item_height, layout.offset.y * item_height,
), ),
size(px(1.), layout.length * item_height), size(px(1.), layout.length * item_height),
); );
ui::RenderedIndentGuide { ui::RenderedIndentGuide {
bounds, bounds,
layout, layout,
is_active: active_indent_guide_ix == Some(ix), is_active: active_indent_guide_ix == Some(ix),
hitbox: None, hitbox: None,
} }
}) })
.collect() .collect()
}, },
), ),
) )
}) })
}; };

View file

@ -3947,7 +3947,7 @@ impl ProjectPanel {
false false
} }
}); });
let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.15); let shadow_color_top = hsla(0.0, 0.0, 0.0, 0.1);
let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.); let shadow_color_bottom = hsla(0.0, 0.0, 0.0, 0.);
let sticky_shadow = div() let sticky_shadow = div()
.absolute() .absolute()
@ -4176,6 +4176,16 @@ impl ProjectPanel {
} }
} else if kind.is_dir() { } else if kind.is_dir() {
this.marked_entries.clear(); this.marked_entries.clear();
if is_sticky {
if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
let strategy = sticky_index
.map(ScrollStrategy::ToPosition)
.unwrap_or(ScrollStrategy::Top);
this.scroll_handle.scroll_to_item(index, strategy);
cx.notify();
return;
}
}
if event.modifiers().alt { if event.modifiers().alt {
this.toggle_expand_all(entry_id, window, cx); this.toggle_expand_all(entry_id, window, cx);
} else { } else {
@ -4188,16 +4198,6 @@ impl ProjectPanel {
let allow_preview = preview_tabs_enabled && click_count == 1; let allow_preview = preview_tabs_enabled && click_count == 1;
this.open_entry(entry_id, focus_opened_item, allow_preview, cx); this.open_entry(entry_id, focus_opened_item, allow_preview, cx);
} }
if is_sticky {
if let Some((_, _, index)) = this.index_for_entry(entry_id, worktree_id) {
let strategy = sticky_index
.map(ScrollStrategy::ToPosition)
.unwrap_or(ScrollStrategy::Top);
this.scroll_handle.scroll_to_item(index, strategy);
cx.notify();
}
}
}), }),
) )
.child( .child(
@ -5167,52 +5167,51 @@ impl Render for ProjectPanel {
}) })
.when(show_indent_guides, |list| { .when(show_indent_guides, |list| {
list.with_decoration( list.with_decoration(
ui::indent_guides( ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
cx.entity().clone(), .with_compute_indents_fn(
px(indent_size), cx.entity().clone(),
IndentGuideColors::panel(cx), |this, range, window, cx| {
|this, range, window, cx| { let mut items =
let mut items = SmallVec::with_capacity(range.end - range.start);
SmallVec::with_capacity(range.end - range.start); this.iter_visible_entries(
this.iter_visible_entries( range,
range, window,
window, cx,
cx, |entry, _, entries, _, _| {
|entry, _, entries, _, _| { let (depth, _) =
let (depth, _) = Self::calculate_depth_and_difference( Self::calculate_depth_and_difference(
entry, entries, entry, entries,
); );
items.push(depth); items.push(depth);
}, },
); );
items items
}, },
) )
.on_click(cx.listener( .on_click(cx.listener(
|this, active_indent_guide: &IndentGuideLayout, window, cx| { |this, active_indent_guide: &IndentGuideLayout, window, cx| {
if window.modifiers().secondary() { if window.modifiers().secondary() {
let ix = active_indent_guide.offset.y; let ix = active_indent_guide.offset.y;
let Some((target_entry, worktree)) = maybe!({ let Some((target_entry, worktree)) = maybe!({
let (worktree_id, entry) = this.entry_at_index(ix)?; let (worktree_id, entry) =
let worktree = this this.entry_at_index(ix)?;
.project let worktree = this
.read(cx) .project
.worktree_for_id(worktree_id, cx)?; .read(cx)
let target_entry = worktree .worktree_for_id(worktree_id, cx)?;
.read(cx) let target_entry = worktree
.entry_for_path(&entry.path.parent()?)?; .read(cx)
Some((target_entry, worktree)) .entry_for_path(&entry.path.parent()?)?;
}) else { Some((target_entry, worktree))
return; }) else {
}; return;
};
this.collapse_entry(target_entry.clone(), worktree, cx); this.collapse_entry(target_entry.clone(), worktree, cx);
} }
}, },
)) ))
.with_render_fn( .with_render_fn(cx.entity().clone(), move |this, params, _, cx| {
cx.entity().clone(),
move |this, params, _, cx| {
const LEFT_OFFSET: Pixels = px(14.); const LEFT_OFFSET: Pixels = px(14.);
const PADDING_Y: Pixels = px(4.); const PADDING_Y: Pixels = px(4.);
const HITBOX_OVERDRAW: Pixels = px(3.); const HITBOX_OVERDRAW: Pixels = px(3.);
@ -5260,12 +5259,11 @@ impl Render for ProjectPanel {
} }
}) })
.collect() .collect()
}, }),
),
) )
}) })
.when(show_sticky_scroll, |list| { .when(show_sticky_scroll, |list| {
list.with_decoration(ui::sticky_items( let sticky_items = ui::sticky_items(
cx.entity().clone(), cx.entity().clone(),
|this, range, window, cx| { |this, range, window, cx| {
let mut items = SmallVec::with_capacity(range.end - range.start); let mut items = SmallVec::with_capacity(range.end - range.start);
@ -5286,7 +5284,40 @@ impl Render for ProjectPanel {
|this, marker_entry, window, cx| { |this, marker_entry, window, cx| {
this.render_sticky_entries(marker_entry, window, cx) this.render_sticky_entries(marker_entry, window, cx)
}, },
)) );
list.with_decoration(if show_indent_guides {
sticky_items.with_decoration(
ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
.with_render_fn(cx.entity().clone(), move |_, params, _, _| {
const LEFT_OFFSET: Pixels = px(14.);
let indent_size = params.indent_size;
let item_height = params.item_height;
params
.indent_guides
.into_iter()
.map(|layout| {
let bounds = Bounds::new(
point(
layout.offset.x * indent_size + LEFT_OFFSET,
layout.offset.y * item_height,
),
size(px(1.), layout.length * item_height),
);
ui::RenderedIndentGuide {
bounds,
layout,
is_active: false,
hitbox: None,
}
})
.collect()
}),
)
} else {
sticky_items
})
}) })
.size_full() .size_full()
.with_sizing_behavior(ListSizingBehavior::Infer) .with_sizing_behavior(ListSizingBehavior::Infer)

View file

@ -55,23 +55,27 @@ impl Render for IndentGuidesStory {
}), }),
) )
.with_sizing_behavior(gpui::ListSizingBehavior::Infer) .with_sizing_behavior(gpui::ListSizingBehavior::Infer)
.with_decoration(ui::indent_guides( .with_decoration(
cx.entity().clone(), ui::indent_guides(
px(16.), px(16.),
ui::IndentGuideColors { ui::IndentGuideColors {
default: Color::Info.color(cx), default: Color::Info.color(cx),
hover: Color::Accent.color(cx), hover: Color::Accent.color(cx),
active: Color::Accent.color(cx), active: Color::Accent.color(cx),
}, },
|this, range, _cx, _context| { )
this.depths .with_compute_indents_fn(
.iter() cx.entity().clone(),
.skip(range.start) |this, range, _cx, _context| {
.take(range.end - range.start) this.depths
.cloned() .iter()
.collect() .skip(range.start)
}, .take(range.end - range.start)
)), .cloned()
.collect()
},
),
),
), ),
) )
} }

View file

@ -1,8 +1,7 @@
use std::{cmp::Ordering, ops::Range, rc::Rc}; use std::{cmp::Ordering, ops::Range, rc::Rc};
use gpui::{ use gpui::{AnyElement, App, Bounds, Entity, Hsla, Point, fill, point, size};
AnyElement, App, Bounds, Entity, Hsla, Point, UniformListDecoration, fill, point, size, use gpui::{DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent};
};
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::prelude::*; use crate::prelude::*;
@ -32,7 +31,8 @@ impl IndentGuideColors {
pub struct IndentGuides { pub struct IndentGuides {
colors: IndentGuideColors, colors: IndentGuideColors,
indent_size: Pixels, indent_size: Pixels,
compute_indents_fn: Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[usize; 64]>>, compute_indents_fn:
Option<Box<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>,
render_fn: Option< render_fn: Option<
Box< Box<
dyn Fn( dyn Fn(
@ -45,25 +45,11 @@ pub struct IndentGuides {
on_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>, on_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
} }
pub fn indent_guides<V: Render>( pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides {
entity: Entity<V>,
indent_size: Pixels,
colors: IndentGuideColors,
compute_indents_fn: impl Fn(
&mut V,
Range<usize>,
&mut Window,
&mut Context<V>,
) -> SmallVec<[usize; 64]>
+ 'static,
) -> IndentGuides {
let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| {
entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx))
});
IndentGuides { IndentGuides {
colors, colors,
indent_size, indent_size,
compute_indents_fn, compute_indents_fn: None,
render_fn: None, render_fn: None,
on_click: None, on_click: None,
} }
@ -79,6 +65,25 @@ impl IndentGuides {
self self
} }
/// Sets the function that computes indents for uniform list decoration.
pub fn with_compute_indents_fn<V: Render>(
mut self,
entity: Entity<V>,
compute_indents_fn: impl Fn(
&mut V,
Range<usize>,
&mut Window,
&mut Context<V>,
) -> SmallVec<[usize; 64]>
+ 'static,
) -> Self {
let compute_indents_fn = Box::new(move |range, window: &mut Window, cx: &mut App| {
entity.update(cx, |this, cx| compute_indents_fn(this, range, window, cx))
});
self.compute_indents_fn = Some(compute_indents_fn);
self
}
/// Sets a custom callback that will be called when the indent guides need to be rendered. /// Sets a custom callback that will be called when the indent guides need to be rendered.
pub fn with_render_fn<V: Render>( pub fn with_render_fn<V: Render>(
mut self, mut self,
@ -97,6 +102,53 @@ impl IndentGuides {
self.render_fn = Some(Box::new(render_fn)); self.render_fn = Some(Box::new(render_fn));
self self
} }
fn render_from_layout(
&self,
indent_guides: SmallVec<[IndentGuideLayout; 12]>,
bounds: Bounds<Pixels>,
item_height: Pixels,
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let mut indent_guides = if let Some(ref custom_render) = self.render_fn {
let params = RenderIndentGuideParams {
indent_guides,
indent_size: self.indent_size,
item_height,
};
custom_render(params, window, cx)
} else {
indent_guides
.into_iter()
.map(|layout| RenderedIndentGuide {
bounds: Bounds::new(
point(
layout.offset.x * self.indent_size,
layout.offset.y * item_height,
),
size(px(1.), layout.length * item_height),
),
layout,
is_active: false,
hitbox: None,
})
.collect()
};
for guide in &mut indent_guides {
guide.bounds.origin += bounds.origin;
if let Some(hitbox) = guide.hitbox.as_mut() {
hitbox.origin += bounds.origin;
}
}
let indent_guides = IndentGuidesElement {
indent_guides: Rc::new(indent_guides),
colors: self.colors.clone(),
on_hovered_indent_guide_click: self.on_click.clone(),
};
indent_guides.into_any_element()
}
} }
/// Parameters for rendering indent guides. /// Parameters for rendering indent guides.
@ -136,9 +188,7 @@ pub struct IndentGuideLayout {
/// Implements the necessary functionality for rendering indent guides inside a uniform list. /// Implements the necessary functionality for rendering indent guides inside a uniform list.
mod uniform_list { mod uniform_list {
use gpui::{ use gpui::UniformListDecoration;
DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
};
use super::*; use super::*;
@ -161,227 +211,212 @@ mod uniform_list {
if includes_trailing_indent { if includes_trailing_indent {
visible_range.end += 1; visible_range.end += 1;
} }
let visible_entries = &(self.compute_indents_fn)(visible_range.clone(), window, cx); let Some(ref compute_indents_fn) = self.compute_indents_fn else {
panic!("compute_indents_fn is required for UniformListDecoration");
};
let visible_entries = &compute_indents_fn(visible_range.clone(), window, cx);
let indent_guides = compute_indent_guides( let indent_guides = compute_indent_guides(
&visible_entries, &visible_entries,
visible_range.start, visible_range.start,
includes_trailing_indent, includes_trailing_indent,
); );
let mut indent_guides = if let Some(ref custom_render) = self.render_fn { self.render_from_layout(indent_guides, bounds, item_height, window, cx)
let params = RenderIndentGuideParams {
indent_guides,
indent_size: self.indent_size,
item_height,
};
custom_render(params, window, cx)
} else {
indent_guides
.into_iter()
.map(|layout| RenderedIndentGuide {
bounds: Bounds::new(
point(
layout.offset.x * self.indent_size,
layout.offset.y * item_height,
),
size(px(1.), layout.length * item_height),
),
layout,
is_active: false,
hitbox: None,
})
.collect()
};
for guide in &mut indent_guides {
guide.bounds.origin += bounds.origin;
if let Some(hitbox) = guide.hitbox.as_mut() {
hitbox.origin += bounds.origin;
}
}
let indent_guides = IndentGuidesElement {
indent_guides: Rc::new(indent_guides),
colors: self.colors.clone(),
on_hovered_indent_guide_click: self.on_click.clone(),
};
indent_guides.into_any_element()
} }
} }
}
struct IndentGuidesElement { /// Implements the necessary functionality for rendering indent guides inside a sticky items.
colors: IndentGuideColors, mod sticky_items {
indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>, use crate::StickyItemsDecoration;
on_hovered_indent_guide_click:
Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
}
enum IndentGuidesElementPrepaintState { use super::*;
Static,
Interactive {
hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
},
}
impl Element for IndentGuidesElement { impl StickyItemsDecoration for IndentGuides {
type RequestLayoutState = (); fn compute(
type PrepaintState = IndentGuidesElementPrepaintState; &self,
indents: &SmallVec<[usize; 8]>,
fn id(&self) -> Option<ElementId> { bounds: Bounds<Pixels>,
None _scroll_offset: Point<Pixels>,
} item_height: Pixels,
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
None
}
fn request_layout(
&mut self,
_id: Option<&gpui::GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) { ) -> AnyElement {
(window.request_layout(gpui::Style::default(), [], cx), ()) let indent_guides = compute_indent_guides(&indents, 0, false);
self.render_from_layout(indent_guides, bounds, item_height, window, cx)
} }
}
}
fn prepaint( struct IndentGuidesElement {
&mut self, colors: IndentGuideColors,
_id: Option<&gpui::GlobalElementId>, indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
_inspector_id: Option<&gpui::InspectorElementId>, on_hovered_indent_guide_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
_bounds: Bounds<Pixels>, }
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window, enum IndentGuidesElementPrepaintState {
_cx: &mut App, Static,
) -> Self::PrepaintState { Interactive {
if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
{ on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
let hitboxes = self },
.indent_guides }
.as_ref()
.iter() impl Element for IndentGuidesElement {
.map(|guide| { type RequestLayoutState = ();
window.insert_hitbox( type PrepaintState = IndentGuidesElementPrepaintState;
guide.hitbox.unwrap_or(guide.bounds),
HitboxBehavior::Normal, fn id(&self) -> Option<ElementId> {
) None
}) }
.collect();
Self::PrepaintState::Interactive { fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
hitboxes: Rc::new(hitboxes), None
on_hovered_indent_guide_click, }
}
} else { fn request_layout(
Self::PrepaintState::Static &mut self,
_id: Option<&gpui::GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
window: &mut Window,
cx: &mut App,
) -> (gpui::LayoutId, Self::RequestLayoutState) {
(window.request_layout(gpui::Style::default(), [], cx), ())
}
fn prepaint(
&mut self,
_id: Option<&gpui::GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>,
_bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState,
window: &mut Window,
_cx: &mut App,
) -> Self::PrepaintState {
if let Some(on_hovered_indent_guide_click) = self.on_hovered_indent_guide_click.clone() {
let hitboxes = self
.indent_guides
.as_ref()
.iter()
.map(|guide| {
window
.insert_hitbox(guide.hitbox.unwrap_or(guide.bounds), HitboxBehavior::Normal)
})
.collect();
Self::PrepaintState::Interactive {
hitboxes: Rc::new(hitboxes),
on_hovered_indent_guide_click,
} }
} else {
Self::PrepaintState::Static
} }
}
fn paint( fn paint(
&mut self, &mut self,
_id: Option<&gpui::GlobalElementId>, _id: Option<&gpui::GlobalElementId>,
_inspector_id: Option<&gpui::InspectorElementId>, _inspector_id: Option<&gpui::InspectorElementId>,
_bounds: Bounds<Pixels>, _bounds: Bounds<Pixels>,
_request_layout: &mut Self::RequestLayoutState, _request_layout: &mut Self::RequestLayoutState,
prepaint: &mut Self::PrepaintState, prepaint: &mut Self::PrepaintState,
window: &mut Window, window: &mut Window,
_cx: &mut App, _cx: &mut App,
) { ) {
let current_view = window.current_view(); let current_view = window.current_view();
match prepaint { match prepaint {
IndentGuidesElementPrepaintState::Static => { IndentGuidesElementPrepaintState::Static => {
for indent_guide in self.indent_guides.as_ref() { for indent_guide in self.indent_guides.as_ref() {
let fill_color = if indent_guide.is_active { let fill_color = if indent_guide.is_active {
self.colors.active self.colors.active
} else { } else {
self.colors.default self.colors.default
}; };
window.paint_quad(fill(indent_guide.bounds, fill_color)); window.paint_quad(fill(indent_guide.bounds, fill_color));
}
} }
IndentGuidesElementPrepaintState::Interactive { }
hitboxes, IndentGuidesElementPrepaintState::Interactive {
on_hovered_indent_guide_click, hitboxes,
} => { on_hovered_indent_guide_click,
window.on_mouse_event({ } => {
let hitboxes = hitboxes.clone(); window.on_mouse_event({
let indent_guides = self.indent_guides.clone(); let hitboxes = hitboxes.clone();
let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone(); let indent_guides = self.indent_guides.clone();
move |event: &MouseDownEvent, phase, window, cx| { let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
if phase == DispatchPhase::Bubble && event.button == MouseButton::Left { move |event: &MouseDownEvent, phase, window, cx| {
let mut active_hitbox_ix = None; if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
for (i, hitbox) in hitboxes.iter().enumerate() { let mut active_hitbox_ix = None;
if hitbox.is_hovered(window) { for (i, hitbox) in hitboxes.iter().enumerate() {
active_hitbox_ix = Some(i);
break;
}
}
let Some(active_hitbox_ix) = active_hitbox_ix else {
return;
};
let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
on_hovered_indent_guide_click(active_indent_guide, window, cx);
cx.stop_propagation();
window.prevent_default();
}
}
});
let mut hovered_hitbox_id = None;
for (i, hitbox) in hitboxes.iter().enumerate() {
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
let indent_guide = &self.indent_guides[i];
let fill_color = if hitbox.is_hovered(window) {
hovered_hitbox_id = Some(hitbox.id);
self.colors.hover
} else if indent_guide.is_active {
self.colors.active
} else {
self.colors.default
};
window.paint_quad(fill(indent_guide.bounds, fill_color));
}
window.on_mouse_event({
let prev_hovered_hitbox_id = hovered_hitbox_id;
let hitboxes = hitboxes.clone();
move |_: &MouseMoveEvent, phase, window, cx| {
let mut hovered_hitbox_id = None;
for hitbox in hitboxes.as_ref() {
if hitbox.is_hovered(window) { if hitbox.is_hovered(window) {
hovered_hitbox_id = Some(hitbox.id); active_hitbox_ix = Some(i);
break; break;
} }
} }
if phase == DispatchPhase::Capture {
// If the hovered hitbox has changed, we need to re-paint the indent guides. let Some(active_hitbox_ix) = active_hitbox_ix else {
match (prev_hovered_hitbox_id, hovered_hitbox_id) { return;
(Some(prev_id), Some(id)) => { };
if prev_id != id {
cx.notify(current_view) let active_indent_guide = &indent_guides[active_hitbox_ix].layout;
} on_hovered_indent_guide_click(active_indent_guide, window, cx);
}
(None, Some(_)) => cx.notify(current_view), cx.stop_propagation();
(Some(_), None) => cx.notify(current_view), window.prevent_default();
(None, None) => {} }
} }
});
let mut hovered_hitbox_id = None;
for (i, hitbox) in hitboxes.iter().enumerate() {
window.set_cursor_style(gpui::CursorStyle::PointingHand, hitbox);
let indent_guide = &self.indent_guides[i];
let fill_color = if hitbox.is_hovered(window) {
hovered_hitbox_id = Some(hitbox.id);
self.colors.hover
} else if indent_guide.is_active {
self.colors.active
} else {
self.colors.default
};
window.paint_quad(fill(indent_guide.bounds, fill_color));
}
window.on_mouse_event({
let prev_hovered_hitbox_id = hovered_hitbox_id;
let hitboxes = hitboxes.clone();
move |_: &MouseMoveEvent, phase, window, cx| {
let mut hovered_hitbox_id = None;
for hitbox in hitboxes.as_ref() {
if hitbox.is_hovered(window) {
hovered_hitbox_id = Some(hitbox.id);
break;
} }
} }
}); if phase == DispatchPhase::Capture {
} // If the hovered hitbox has changed, we need to re-paint the indent guides.
match (prev_hovered_hitbox_id, hovered_hitbox_id) {
(Some(prev_id), Some(id)) => {
if prev_id != id {
cx.notify(current_view)
}
}
(None, Some(_)) => cx.notify(current_view),
(Some(_), None) => cx.notify(current_view),
(None, None) => {}
}
}
}
});
} }
} }
} }
}
impl IntoElement for IndentGuidesElement { impl IntoElement for IndentGuidesElement {
type Element = Self; type Element = Self;
fn into_element(self) -> Self::Element { fn into_element(self) -> Self::Element {
self self
}
} }
} }

View file

@ -3,7 +3,7 @@ use std::{ops::Range, rc::Rc};
use gpui::{ use gpui::{
AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId, AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration, InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration,
Window, point, size, Window, point, px, size,
}; };
use smallvec::SmallVec; use smallvec::SmallVec;
@ -11,10 +11,10 @@ pub trait StickyCandidate {
fn depth(&self) -> usize; fn depth(&self) -> usize;
} }
#[derive(Clone)]
pub struct StickyItems<T> { pub struct StickyItems<T> {
compute_fn: Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[T; 8]>>, compute_fn: Rc<dyn Fn(Range<usize>, &mut Window, &mut App) -> SmallVec<[T; 8]>>,
render_fn: Rc<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>, render_fn: Rc<dyn Fn(T, &mut Window, &mut App) -> SmallVec<[AnyElement; 8]>>,
decorations: Vec<Box<dyn StickyItemsDecoration>>,
} }
pub fn sticky_items<V, T>( pub fn sticky_items<V, T>(
@ -44,11 +44,26 @@ where
StickyItems { StickyItems {
compute_fn, compute_fn,
render_fn, render_fn,
decorations: Vec::new(),
}
}
impl<T> StickyItems<T>
where
T: StickyCandidate + Clone + 'static,
{
/// Adds a decoration element to the sticky items.
pub fn with_decoration(mut self, decoration: impl StickyItemsDecoration + 'static) -> Self {
self.decorations.push(Box::new(decoration));
self
} }
} }
struct StickyItemsElement { struct StickyItemsElement {
elements: SmallVec<[AnyElement; 8]>, drifting_element: Option<AnyElement>,
drifting_decoration: Option<AnyElement>,
rest_elements: SmallVec<[AnyElement; 8]>,
rest_decorations: SmallVec<[AnyElement; 1]>,
} }
impl IntoElement for StickyItemsElement { impl IntoElement for StickyItemsElement {
@ -103,8 +118,16 @@ impl Element for StickyItemsElement {
window: &mut Window, window: &mut Window,
cx: &mut App, cx: &mut App,
) { ) {
// reverse so that last item is bottom most among sticky items if let Some(ref mut drifting_element) = self.drifting_element {
for item in self.elements.iter_mut().rev() { drifting_element.paint(window, cx);
}
if let Some(ref mut drifting_decoration) = self.drifting_decoration {
drifting_decoration.paint(window, cx);
}
for item in self.rest_elements.iter_mut().rev() {
item.paint(window, cx);
}
for item in self.rest_decorations.iter_mut() {
item.paint(window, cx); item.paint(window, cx);
} }
} }
@ -125,11 +148,14 @@ where
cx: &mut App, cx: &mut App,
) -> AnyElement { ) -> AnyElement {
let entries = (self.compute_fn)(visible_range.clone(), window, cx); let entries = (self.compute_fn)(visible_range.clone(), window, cx);
let mut elements = SmallVec::new();
let mut anchor_entry = None; struct StickyAnchor<T> {
entry: T,
index: usize,
}
let mut sticky_anchor = None;
let mut last_item_is_drifting = false; let mut last_item_is_drifting = false;
let mut anchor_index = None;
let mut iter = entries.iter().enumerate().peekable(); let mut iter = entries.iter().enumerate().peekable();
while let Some((ix, current_entry)) = iter.next() { while let Some((ix, current_entry)) = iter.next() {
@ -137,7 +163,10 @@ where
let index_in_range = ix; let index_in_range = ix;
if current_depth < index_in_range { if current_depth < index_in_range {
anchor_entry = Some(current_entry.clone()); sticky_anchor = Some(StickyAnchor {
entry: current_entry.clone(),
index: visible_range.start + ix,
});
break; break;
} }
@ -146,44 +175,155 @@ where
if next_depth < current_depth && next_depth < index_in_range { if next_depth < current_depth && next_depth < index_in_range {
last_item_is_drifting = true; last_item_is_drifting = true;
anchor_index = Some(visible_range.start + ix); sticky_anchor = Some(StickyAnchor {
anchor_entry = Some(current_entry.clone()); entry: current_entry.clone(),
index: visible_range.start + ix,
});
break; break;
} }
} }
} }
if let Some(anchor_entry) = anchor_entry { let Some(sticky_anchor) = sticky_anchor else {
elements = (self.render_fn)(anchor_entry, window, cx); return StickyItemsElement {
let items_count = elements.len(); drifting_element: None,
drifting_decoration: None,
rest_elements: SmallVec::new(),
rest_decorations: SmallVec::new(),
}
.into_any_element();
};
for (ix, element) in elements.iter_mut().enumerate() { let anchor_depth = sticky_anchor.entry.depth();
let mut item_y_offset = None; let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx);
if ix == items_count - 1 && last_item_is_drifting { let items_count = elements.len();
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));
};
}
let sticky_origin = bounds.origin let indents: SmallVec<[usize; 8]> = {
+ point( elements
-scroll_offset.x, .iter()
-scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO), .enumerate()
); .map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix)))
.collect()
};
let available_space = size( let mut last_decoration_element = None;
AvailableSpace::Definite(bounds.size.width), let mut rest_decoration_elements = SmallVec::new();
AvailableSpace::Definite(item_height),
let available_space = size(
AvailableSpace::Definite(bounds.size.width),
AvailableSpace::Definite(bounds.size.height),
);
let drifting_y_offset = if last_item_is_drifting {
let scroll_top = -scroll_offset.y;
let anchor_top = item_height * sticky_anchor.index;
let sticky_area_height = item_height * items_count;
(anchor_top - scroll_top - sticky_area_height).min(Pixels::ZERO)
} else {
Pixels::ZERO
};
let (drifting_indent, rest_indents) = if last_item_is_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)
} else {
(None, indents)
};
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
+ point(px(0.), item_height * rest_indents.len() + drifting_y_offset);
let decoration_bounds = Bounds::new(sticky_origin, bounds.size);
let mut drifting_dec = decoration.as_ref().compute(
&drifting_indent_vec,
decoration_bounds,
scroll_offset,
item_height,
window,
cx,
); );
element.layout_as_root(available_space, window, cx); drifting_dec.layout_as_root(available_space, window, cx);
element.prepaint_at(sticky_origin, 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 mut rest_dec = decoration.as_ref().compute(
&rest_indents,
decoration_bounds,
scroll_offset,
item_height,
window,
cx,
);
rest_dec.layout_as_root(available_space, window, cx);
rest_dec.prepaint_at(bounds.origin, window, cx);
rest_decoration_elements.push(rest_dec);
} }
} }
StickyItemsElement { elements }.into_any_element() let (mut drifting_element, mut rest_elements) =
if last_item_is_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);
}
if let Some(ref mut drifting_element) = drifting_element {
let sticky_origin = bounds.origin - scroll_offset
+ 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);
}
StickyItemsElement {
drifting_element,
drifting_decoration: last_decoration_element,
rest_elements,
rest_decorations: rest_decoration_elements,
}
.into_any_element()
} }
} }
/// A decoration for a [`StickyItems`]. This can be used for various things,
/// such as rendering indent guides, or other visual effects.
pub trait StickyItemsDecoration {
/// Compute the decoration element, given the visible range of list items,
/// the bounds of the list, and the height of each item.
fn compute(
&self,
indents: &SmallVec<[usize; 8]>,
bounds: Bounds<Pixels>,
scroll_offset: Point<Pixels>,
item_height: Pixels,
window: &mut Window,
cx: &mut App,
) -> AnyElement;
}