From 3a247ee94760541906488b4145148964fce709a3 Mon Sep 17 00:00:00 2001 From: Smit Barmase Date: Wed, 9 Jul 2025 05:28:25 +0530 Subject: [PATCH] project panel: Add indent guides for sticky items (#34092) - Adds new trait `StickyItemsDecoration` in `sticky_items` which is implemented by `IndentGuides` from `indent_guides`. image Release Notes: - N/A --- crates/gpui/src/elements/uniform_list.rs | 29 -- crates/outline_panel/src/outline_panel.rs | 89 ++-- crates/project_panel/src/project_panel.rs | 151 +++--- crates/storybook/src/stories/indent_guides.rs | 38 +- crates/ui/src/components/indent_guides.rs | 461 ++++++++++-------- crates/ui/src/components/sticky_items.rs | 212 ++++++-- 6 files changed, 580 insertions(+), 400 deletions(-) diff --git a/crates/gpui/src/elements/uniform_list.rs b/crates/gpui/src/elements/uniform_list.rs index 342490b882..52e2015c20 100644 --- a/crates/gpui/src/elements/uniform_list.rs +++ b/crates/gpui/src/elements/uniform_list.rs @@ -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, - 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, - item_height: Pixels, - scroll_offset: Point, - padding: crate::Edges, - 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) -> Self { diff --git a/crates/outline_panel/src/outline_panel.rs b/crates/outline_panel/src/outline_panel.rs index 05352e24de..12dcab9e87 100644 --- a/crates/outline_panel/src/outline_panel.rs +++ b/crates/outline_panel/src/outline_panel.rs @@ -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.); + 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, - ); + 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() + }, + ), ) }) }; diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index bd0d5e3919..0ec9bac33f 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -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; - }; + 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| { + 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) diff --git a/crates/storybook/src/stories/indent_guides.rs b/crates/storybook/src/stories/indent_guides.rs index e83c9ed383..e4f9669b1f 100644 --- a/crates/storybook/src/stories/indent_guides.rs +++ b/crates/storybook/src/stories/indent_guides.rs @@ -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() + }, + ), + ), ), ) } diff --git a/crates/ui/src/components/indent_guides.rs b/crates/ui/src/components/indent_guides.rs index 6d4db984f9..e3dc1f35fa 100644 --- a/crates/ui/src/components/indent_guides.rs +++ b/crates/ui/src/components/indent_guides.rs @@ -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, &mut Window, &mut App) -> SmallVec<[usize; 64]>>, + compute_indents_fn: + Option, &mut Window, &mut App) -> SmallVec<[usize; 64]>>>, render_fn: Option< Box< dyn Fn( @@ -45,25 +45,11 @@ pub struct IndentGuides { on_click: Option>, } -pub fn indent_guides( - entity: Entity, - indent_size: Pixels, - colors: IndentGuideColors, - compute_indents_fn: impl Fn( - &mut V, - Range, - &mut Window, - &mut Context, - ) -> 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( + mut self, + entity: Entity, + compute_indents_fn: impl Fn( + &mut V, + Range, + &mut Window, + &mut Context, + ) -> 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( 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, + 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>, - on_hovered_indent_guide_click: - Option>, - } +/// Implements the necessary functionality for rendering indent guides inside a sticky items. +mod sticky_items { + use crate::StickyItemsDecoration; - enum IndentGuidesElementPrepaintState { - Static, - Interactive { - hitboxes: Rc>, - on_hovered_indent_guide_click: Rc, - }, - } + use super::*; - impl Element for IndentGuidesElement { - type RequestLayoutState = (); - type PrepaintState = IndentGuidesElementPrepaintState; - - fn id(&self) -> Option { - None - } - - 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>, + impl StickyItemsDecoration for IndentGuides { + fn compute( + &self, + indents: &SmallVec<[usize; 8]>, + bounds: Bounds, + _scroll_offset: Point, + item_height: Pixels, window: &mut Window, cx: &mut App, - ) -> (gpui::LayoutId, Self::RequestLayoutState) { - (window.request_layout(gpui::Style::default(), [], cx), ()) + ) -> AnyElement { + let indent_guides = compute_indent_guides(&indents, 0, false); + self.render_from_layout(indent_guides, bounds, item_height, window, cx) } + } +} - fn prepaint( - &mut self, - _id: Option<&gpui::GlobalElementId>, - _inspector_id: Option<&gpui::InspectorElementId>, - _bounds: Bounds, - _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 +struct IndentGuidesElement { + colors: IndentGuideColors, + indent_guides: Rc>, + on_hovered_indent_guide_click: Option>, +} + +enum IndentGuidesElementPrepaintState { + Static, + Interactive { + hitboxes: Rc>, + on_hovered_indent_guide_click: Rc, + }, +} + +impl Element for IndentGuidesElement { + type RequestLayoutState = (); + type PrepaintState = IndentGuidesElementPrepaintState; + + fn id(&self) -> Option { + None + } + + 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, + _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, - _request_layout: &mut Self::RequestLayoutState, - prepaint: &mut Self::PrepaintState, - window: &mut Window, - _cx: &mut App, - ) { - let current_view = window.current_view(); + fn paint( + &mut self, + _id: Option<&gpui::GlobalElementId>, + _inspector_id: Option<&gpui::InspectorElementId>, + _bounds: Bounds, + _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 - }; + 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)); - } + 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; - } - } - - 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() { + } + 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) { - hovered_hitbox_id = Some(hitbox.id); + active_hitbox_ix = Some(i); 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) => {} - } + + 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) { + 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 { - type Element = Self; +impl IntoElement for IndentGuidesElement { + type Element = Self; - fn into_element(self) -> Self::Element { - self - } + fn into_element(self) -> Self::Element { + self } } diff --git a/crates/ui/src/components/sticky_items.rs b/crates/ui/src/components/sticky_items.rs index e98e3023d2..da6c14ff09 100644 --- a/crates/ui/src/components/sticky_items.rs +++ b/crates/ui/src/components/sticky_items.rs @@ -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 { compute_fn: Rc, &mut Window, &mut App) -> SmallVec<[T; 8]>>, render_fn: Rc SmallVec<[AnyElement; 8]>>, + decorations: Vec>, } pub fn sticky_items( @@ -44,11 +44,26 @@ where StickyItems { compute_fn, render_fn, + decorations: Vec::new(), + } +} + +impl StickyItems +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, + drifting_decoration: Option, + 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 { + 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(); + 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(); + }; - 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 anchor_depth = sticky_anchor.entry.depth(); + let mut elements = (self.render_fn)(sticky_anchor.entry, window, cx); + let items_count = elements.len(); - let sticky_origin = bounds.origin - + point( - -scroll_offset.x, - -scroll_offset.y + item_height * ix + item_y_offset.unwrap_or(Pixels::ZERO), - ); + let indents: SmallVec<[usize; 8]> = { + elements + .iter() + .enumerate() + .map(|(ix, _)| anchor_depth.saturating_sub(items_count.saturating_sub(ix))) + .collect() + }; - let available_space = size( - AvailableSpace::Definite(bounds.size.width), - AvailableSpace::Definite(item_height), + 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) + }; + + 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); - 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); } } - 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, + scroll_offset: Point, + item_height: Pixels, + window: &mut Window, + cx: &mut App, + ) -> AnyElement; +}