Detailed changes
@@ -506,35 +506,6 @@ 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 {
@@ -4584,53 +4584,52 @@ impl OutlinePanel {
.track_scroll(self.scroll_handle.clone())
.when(show_indent_guides, |list| {
list.with_decoration(
- ui::indent_guides(
- cx.entity().clone(),
- px(indent_size),
- IndentGuideColors::panel(cx),
- |outline_panel, range, _, _| {
- let entries = outline_panel.cached_entries.get(range);
- if let Some(entries) = entries {
- entries.into_iter().map(|item| item.depth).collect()
- } else {
- smallvec::SmallVec::new()
- }
- },
- )
- .with_render_fn(
- cx.entity().clone(),
- move |outline_panel, params, _, _| {
- const LEFT_OFFSET: Pixels = px(14.);
-
- let indent_size = params.indent_size;
- let item_height = params.item_height;
- let active_indent_guide_ix = find_active_indent_guide_ix(
- outline_panel,
- ¶ms.indent_guides,
- );
+ ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
+ .with_compute_indents_fn(
+ cx.entity().clone(),
+ |outline_panel, range, _, _| {
+ let entries = outline_panel.cached_entries.get(range);
+ if let Some(entries) = entries {
+ entries.into_iter().map(|item| item.depth).collect()
+ } else {
+ smallvec::SmallVec::new()
+ }
+ },
+ )
+ .with_render_fn(
+ cx.entity().clone(),
+ move |outline_panel, params, _, _| {
+ const LEFT_OFFSET: Pixels = px(14.);
+
+ let indent_size = params.indent_size;
+ let item_height = params.item_height;
+ let active_indent_guide_ix = find_active_indent_guide_ix(
+ outline_panel,
+ ¶ms.indent_guides,
+ );
- params
- .indent_guides
- .into_iter()
- .enumerate()
- .map(|(ix, 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: active_indent_guide_ix == Some(ix),
- hitbox: None,
- }
- })
- .collect()
- },
- ),
+ params
+ .indent_guides
+ .into_iter()
+ .enumerate()
+ .map(|(ix, 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: active_indent_guide_ix == Some(ix),
+ hitbox: None,
+ }
+ })
+ .collect()
+ },
+ ),
)
})
};
@@ -3947,7 +3947,7 @@ impl ProjectPanel {
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 sticky_shadow = div()
.absolute()
@@ -4176,6 +4176,16 @@ impl ProjectPanel {
}
} else if kind.is_dir() {
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 {
this.toggle_expand_all(entry_id, window, cx);
} else {
@@ -4188,16 +4198,6 @@ impl ProjectPanel {
let allow_preview = preview_tabs_enabled && click_count == 1;
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(
@@ -5167,52 +5167,51 @@ impl Render for ProjectPanel {
})
.when(show_indent_guides, |list| {
list.with_decoration(
- ui::indent_guides(
- cx.entity().clone(),
- px(indent_size),
- IndentGuideColors::panel(cx),
- |this, range, window, cx| {
- let mut items =
- SmallVec::with_capacity(range.end - range.start);
- this.iter_visible_entries(
- range,
- window,
- cx,
- |entry, _, entries, _, _| {
- let (depth, _) = Self::calculate_depth_and_difference(
- entry, entries,
- );
- items.push(depth);
- },
- );
- items
- },
- )
- .on_click(cx.listener(
- |this, active_indent_guide: &IndentGuideLayout, window, cx| {
- if window.modifiers().secondary() {
- let ix = active_indent_guide.offset.y;
- let Some((target_entry, worktree)) = maybe!({
- let (worktree_id, entry) = this.entry_at_index(ix)?;
- let worktree = this
- .project
- .read(cx)
- .worktree_for_id(worktree_id, cx)?;
- let target_entry = worktree
- .read(cx)
- .entry_for_path(&entry.path.parent()?)?;
- Some((target_entry, worktree))
- }) else {
- return;
- };
-
- this.collapse_entry(target_entry.clone(), worktree, cx);
- }
- },
- ))
- .with_render_fn(
- cx.entity().clone(),
- move |this, params, _, cx| {
+ ui::indent_guides(px(indent_size), IndentGuideColors::panel(cx))
+ .with_compute_indents_fn(
+ 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, _, entries, _, _| {
+ let (depth, _) =
+ Self::calculate_depth_and_difference(
+ entry, entries,
+ );
+ items.push(depth);
+ },
+ );
+ items
+ },
+ )
+ .on_click(cx.listener(
+ |this, active_indent_guide: &IndentGuideLayout, window, cx| {
+ if window.modifiers().secondary() {
+ let ix = active_indent_guide.offset.y;
+ let Some((target_entry, worktree)) = maybe!({
+ let (worktree_id, entry) =
+ this.entry_at_index(ix)?;
+ let worktree = this
+ .project
+ .read(cx)
+ .worktree_for_id(worktree_id, cx)?;
+ let target_entry = worktree
+ .read(cx)
+ .entry_for_path(&entry.path.parent()?)?;
+ Some((target_entry, worktree))
+ }) else {
+ return;
+ };
+
+ this.collapse_entry(target_entry.clone(), worktree, cx);
+ }
+ },
+ ))
+ .with_render_fn(cx.entity().clone(), move |this, params, _, cx| {
const LEFT_OFFSET: Pixels = px(14.);
const PADDING_Y: Pixels = px(4.);
const HITBOX_OVERDRAW: Pixels = px(3.);
@@ -5260,12 +5259,11 @@ impl Render for ProjectPanel {
}
})
.collect()
- },
- ),
+ }),
)
})
.when(show_sticky_scroll, |list| {
- list.with_decoration(ui::sticky_items(
+ let sticky_items = ui::sticky_items(
cx.entity().clone(),
|this, range, window, cx| {
let mut items = SmallVec::with_capacity(range.end - range.start);
@@ -5286,7 +5284,40 @@ impl Render for ProjectPanel {
|this, 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()
.with_sizing_behavior(ListSizingBehavior::Infer)
@@ -55,23 +55,27 @@ impl Render for IndentGuidesStory {
}),
)
.with_sizing_behavior(gpui::ListSizingBehavior::Infer)
- .with_decoration(ui::indent_guides(
- cx.entity().clone(),
- px(16.),
- ui::IndentGuideColors {
- default: Color::Info.color(cx),
- hover: Color::Accent.color(cx),
- active: Color::Accent.color(cx),
- },
- |this, range, _cx, _context| {
- this.depths
- .iter()
- .skip(range.start)
- .take(range.end - range.start)
- .cloned()
- .collect()
- },
- )),
+ .with_decoration(
+ ui::indent_guides(
+ px(16.),
+ ui::IndentGuideColors {
+ default: Color::Info.color(cx),
+ hover: Color::Accent.color(cx),
+ active: Color::Accent.color(cx),
+ },
+ )
+ .with_compute_indents_fn(
+ cx.entity().clone(),
+ |this, range, _cx, _context| {
+ this.depths
+ .iter()
+ .skip(range.start)
+ .take(range.end - range.start)
+ .cloned()
+ .collect()
+ },
+ ),
+ ),
),
)
}
@@ -1,8 +1,7 @@
use std::{cmp::Ordering, ops::Range, rc::Rc};
-use gpui::{
- AnyElement, App, Bounds, Entity, Hsla, Point, UniformListDecoration, fill, point, size,
-};
+use gpui::{AnyElement, App, Bounds, Entity, Hsla, Point, fill, point, size};
+use gpui::{DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent};
use smallvec::SmallVec;
use crate::prelude::*;
@@ -32,7 +31,8 @@ impl IndentGuideColors {
pub struct IndentGuides {
colors: IndentGuideColors,
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<
Box<
dyn Fn(
@@ -45,25 +45,11 @@ pub struct IndentGuides {
on_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
}
-pub fn indent_guides<V: Render>(
- 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))
- });
+pub fn indent_guides(indent_size: Pixels, colors: IndentGuideColors) -> IndentGuides {
IndentGuides {
colors,
indent_size,
- compute_indents_fn,
+ compute_indents_fn: None,
render_fn: None,
on_click: None,
}
@@ -79,6 +65,25 @@ impl IndentGuides {
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.
pub fn with_render_fn<V: Render>(
mut self,
@@ -97,6 +102,53 @@ impl IndentGuides {
self.render_fn = Some(Box::new(render_fn));
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.
@@ -136,9 +188,7 @@ pub struct IndentGuideLayout {
/// Implements the necessary functionality for rendering indent guides inside a uniform list.
mod uniform_list {
- use gpui::{
- DispatchPhase, Hitbox, HitboxBehavior, MouseButton, MouseDownEvent, MouseMoveEvent,
- };
+ use gpui::UniformListDecoration;
use super::*;
@@ -161,227 +211,212 @@ mod uniform_list {
if includes_trailing_indent {
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(
&visible_entries,
visible_range.start,
includes_trailing_indent,
);
- 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()
+ self.render_from_layout(indent_guides, bounds, item_height, window, cx)
}
}
+}
- struct IndentGuidesElement {
- colors: IndentGuideColors,
- indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
- on_hovered_indent_guide_click:
- Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
- }
+/// Implements the necessary functionality for rendering indent guides inside a sticky items.
+mod sticky_items {
+ use crate::StickyItemsDecoration;
- enum IndentGuidesElementPrepaintState {
- Static,
- Interactive {
- hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
- on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
- },
+ use super::*;
+
+ impl StickyItemsDecoration for IndentGuides {
+ fn compute(
+ &self,
+ indents: &SmallVec<[usize; 8]>,
+ bounds: Bounds<Pixels>,
+ _scroll_offset: Point<Pixels>,
+ item_height: Pixels,
+ window: &mut Window,
+ cx: &mut App,
+ ) -> AnyElement {
+ let indent_guides = compute_indent_guides(&indents, 0, false);
+ self.render_from_layout(indent_guides, bounds, item_height, window, cx)
+ }
}
+}
- impl Element for IndentGuidesElement {
- type RequestLayoutState = ();
- type PrepaintState = IndentGuidesElementPrepaintState;
+struct IndentGuidesElement {
+ colors: IndentGuideColors,
+ indent_guides: Rc<SmallVec<[RenderedIndentGuide; 12]>>,
+ on_hovered_indent_guide_click: Option<Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>>,
+}
- fn id(&self) -> Option<ElementId> {
- None
- }
+enum IndentGuidesElementPrepaintState {
+ Static,
+ Interactive {
+ hitboxes: Rc<SmallVec<[Hitbox; 12]>>,
+ on_hovered_indent_guide_click: Rc<dyn Fn(&IndentGuideLayout, &mut Window, &mut App)>,
+ },
+}
- fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
- None
- }
+impl Element for IndentGuidesElement {
+ type RequestLayoutState = ();
+ type PrepaintState = IndentGuidesElementPrepaintState;
- fn request_layout(
- &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 id(&self) -> Option<ElementId> {
+ None
+ }
- 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 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,
+ 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(
- &mut self,
- _id: Option<&gpui::GlobalElementId>,
- _inspector_id: Option<&gpui::InspectorElementId>,
- _bounds: Bounds<Pixels>,
- _request_layout: &mut Self::RequestLayoutState,
- prepaint: &mut Self::PrepaintState,
- window: &mut Window,
- _cx: &mut App,
- ) {
- let current_view = window.current_view();
-
- match prepaint {
- IndentGuidesElementPrepaintState::Static => {
- for indent_guide in self.indent_guides.as_ref() {
- let fill_color = if indent_guide.is_active {
- self.colors.active
- } else {
- self.colors.default
- };
-
- window.paint_quad(fill(indent_guide.bounds, fill_color));
- }
+ fn paint(
+ &mut self,
+ _id: Option<&gpui::GlobalElementId>,
+ _inspector_id: Option<&gpui::InspectorElementId>,
+ _bounds: Bounds<Pixels>,
+ _request_layout: &mut Self::RequestLayoutState,
+ prepaint: &mut Self::PrepaintState,
+ window: &mut Window,
+ _cx: &mut App,
+ ) {
+ let current_view = window.current_view();
+
+ match prepaint {
+ IndentGuidesElementPrepaintState::Static => {
+ for indent_guide in self.indent_guides.as_ref() {
+ let fill_color = if indent_guide.is_active {
+ self.colors.active
+ } else {
+ self.colors.default
+ };
+
+ window.paint_quad(fill(indent_guide.bounds, fill_color));
}
- IndentGuidesElementPrepaintState::Interactive {
- hitboxes,
- on_hovered_indent_guide_click,
- } => {
- window.on_mouse_event({
- let hitboxes = hitboxes.clone();
- let indent_guides = self.indent_guides.clone();
- let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
- move |event: &MouseDownEvent, phase, window, cx| {
- if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
- let mut active_hitbox_ix = None;
- for (i, hitbox) in hitboxes.iter().enumerate() {
- if hitbox.is_hovered(window) {
- active_hitbox_ix = Some(i);
- break;
- }
+ }
+ IndentGuidesElementPrepaintState::Interactive {
+ hitboxes,
+ on_hovered_indent_guide_click,
+ } => {
+ window.on_mouse_event({
+ let hitboxes = hitboxes.clone();
+ let indent_guides = self.indent_guides.clone();
+ let on_hovered_indent_guide_click = on_hovered_indent_guide_click.clone();
+ move |event: &MouseDownEvent, phase, window, cx| {
+ if phase == DispatchPhase::Bubble && event.button == MouseButton::Left {
+ let mut active_hitbox_ix = None;
+ for (i, hitbox) in hitboxes.iter().enumerate() {
+ if hitbox.is_hovered(window) {
+ active_hitbox_ix = Some(i);
+ break;
}
+ }
- let Some(active_hitbox_ix) = active_hitbox_ix else {
- return;
- };
+ 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);
+ 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();
- }
+ 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));
}
+ });
+ 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;
- }
+ 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)
- }
+ }
+ 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) => {}
}
+ (None, Some(_)) => cx.notify(current_view),
+ (Some(_), None) => cx.notify(current_view),
+ (None, None) => {}
}
}
- });
- }
+ }
+ });
}
}
}
+}
- impl IntoElement for IndentGuidesElement {
- type Element = Self;
+impl IntoElement for IndentGuidesElement {
+ type Element = Self;
- fn into_element(self) -> Self::Element {
- self
- }
+ fn into_element(self) -> Self::Element {
+ self
}
}
@@ -3,7 +3,7 @@ use std::{ops::Range, rc::Rc};
use gpui::{
AnyElement, App, AvailableSpace, Bounds, Context, Element, ElementId, Entity, GlobalElementId,
InspectorElementId, IntoElement, LayoutId, Pixels, Point, Render, Style, UniformListDecoration,
- Window, point, size,
+ Window, point, px, size,
};
use smallvec::SmallVec;
@@ -11,10 +11,10 @@ pub trait StickyCandidate {
fn depth(&self) -> usize;
}
-#[derive(Clone)]
pub struct StickyItems<T> {
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]>>,
+ decorations: Vec<Box<dyn StickyItemsDecoration>>,
}
pub fn sticky_items<V, T>(
@@ -44,11 +44,26 @@ where
StickyItems {
compute_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 {
- 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 {
@@ -103,8 +118,16 @@ impl Element for StickyItemsElement {
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() {
+ if let Some(ref mut drifting_element) = self.drifting_element {
+ 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);
}
}
@@ -125,11 +148,14 @@ where
cx: &mut App,
) -> AnyElement {
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 anchor_index = None;
let mut iter = entries.iter().enumerate().peekable();
while let Some((ix, current_entry)) = iter.next() {
@@ -137,7 +163,10 @@ where
let index_in_range = ix;
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;
}
@@ -146,44 +175,155 @@ where
if next_depth < current_depth && next_depth < index_in_range {
last_item_is_drifting = true;
- anchor_index = Some(visible_range.start + ix);
- anchor_entry = Some(current_entry.clone());
+ sticky_anchor = Some(StickyAnchor {
+ entry: current_entry.clone(),
+ index: visible_range.start + ix,
+ });
break;
}
}
}
- if let Some(anchor_entry) = anchor_entry {
- elements = (self.render_fn)(anchor_entry, window, cx);
- let items_count = elements.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));
- };
- }
+ let Some(sticky_anchor) = sticky_anchor else {
+ return StickyItemsElement {
+ drifting_element: None,
+ drifting_decoration: None,
+ rest_elements: SmallVec::new(),
+ rest_decorations: SmallVec::new(),
+ }
+ .into_any_element();
+ };
+
+ let anchor_depth = sticky_anchor.entry.depth();
+ 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 mut last_decoration_element = None;
+ let mut rest_decoration_elements = SmallVec::new();
+
+ 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)
+ };
- let sticky_origin = bounds.origin
- + point(
- -scroll_offset.x,
- -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO),
- );
+ 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 available_space = size(
- AvailableSpace::Definite(bounds.size.width),
- AvailableSpace::Definite(item_height),
+ 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);
- element.prepaint_at(sticky_origin, window, cx);
+ drifting_dec.layout_as_root(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 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);
+ }
+ }
+
+ 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 { elements }.into_any_element()
+ 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;
+}