From 6c10ff8548ccadd6749ab6b5680402942fed4187 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 11:56:03 -0500 Subject: [PATCH 1/6] Render the collab panel using a `gpui::list` --- crates/collab_ui2/src/collab_panel.rs | 287 ++++++++------------------ 1 file changed, 87 insertions(+), 200 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 4edf5ef35ba4495d5f3ef4922414d0f79cd46d7c..3a35c812861c429d5ae24aa45f293296969b433f 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -175,12 +175,12 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, fill, img, impl_actions, overlay, point, prelude::*, px, rems, + actions, canvas, div, fill, img, impl_actions, list, overlay, point, prelude::*, px, rems, serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, - InteractiveElement, IntoElement, Length, Model, MouseDownEvent, ParentElement, Pixels, Point, - PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, Stateful, Styled, - Subscription, Task, View, ViewContext, VisualContext, WeakView, + InteractiveElement, IntoElement, Length, ListState, Model, MouseDownEvent, ParentElement, + Pixels, Point, PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, + Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -303,6 +303,7 @@ pub struct CollabPanel { channel_clipboard: Option, pending_serialization: Task>, context_menu: Option<(View, Point, Subscription)>, + list_state: ListState, filter_editor: View, channel_name_editor: View, channel_editing_state: Option, @@ -398,7 +399,7 @@ enum ListEntry { impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { cx.build_view(|cx| { - // let view_id = cx.view_id(); + let view = cx.view().clone(); let filter_editor = cx.build_view(|cx| { let mut editor = Editor::single_line(cx); @@ -445,136 +446,10 @@ impl CollabPanel { }) .detach(); - // let list_state = - // ListState::::new(0, Orientation::Top, 1000., move |this, ix, cx| { - // let theme = theme::current(cx).clone(); - // let is_selected = this.selection == Some(ix); - // let current_project_id = this.project.read(cx).remote_id(); - - // match &this.entries[ix] { - // ListEntry::Header(section) => { - // let is_collapsed = this.collapsed_sections.contains(section); - // this.render_header(*section, &theme, is_selected, is_collapsed, cx) - // } - // ListEntry::CallParticipant { - // user, - // peer_id, - // is_pending, - // } => Self::render_call_participant( - // user, - // *peer_id, - // this.user_store.clone(), - // *is_pending, - // is_selected, - // &theme, - // cx, - // ), - // ListEntry::ParticipantProject { - // project_id, - // worktree_root_names, - // host_user_id, - // is_last, - // } => Self::render_participant_project( - // *project_id, - // worktree_root_names, - // *host_user_id, - // Some(*project_id) == current_project_id, - // *is_last, - // is_selected, - // &theme, - // cx, - // ), - // ListEntry::ParticipantScreen { peer_id, is_last } => { - // Self::render_participant_screen( - // *peer_id, - // *is_last, - // is_selected, - // &theme.collab_panel, - // cx, - // ) - // } - // ListEntry::Channel { - // channel, - // depth, - // has_children, - // } => { - // let channel_row = this.render_channel( - // &*channel, - // *depth, - // &theme, - // is_selected, - // *has_children, - // ix, - // cx, - // ); - - // if is_selected && this.context_menu_on_selected { - // Stack::new() - // .with_child(channel_row) - // .with_child( - // ChildView::new(&this.context_menu, cx) - // .aligned() - // .bottom() - // .right(), - // ) - // .into_any() - // } else { - // return channel_row; - // } - // } - // ListEntry::ChannelNotes { channel_id } => this.render_channel_notes( - // *channel_id, - // &theme.collab_panel, - // is_selected, - // ix, - // cx, - // ), - // ListEntry::ChannelChat { channel_id } => this.render_channel_chat( - // *channel_id, - // &theme.collab_panel, - // is_selected, - // ix, - // cx, - // ), - // ListEntry::ChannelInvite(channel) => Self::render_channel_invite( - // channel.clone(), - // this.channel_store.clone(), - // &theme.collab_panel, - // is_selected, - // cx, - // ), - // ListEntry::IncomingRequest(user) => Self::render_contact_request( - // user.clone(), - // this.user_store.clone(), - // &theme.collab_panel, - // true, - // is_selected, - // cx, - // ), - // ListEntry::OutgoingRequest(user) => Self::render_contact_request( - // user.clone(), - // this.user_store.clone(), - // &theme.collab_panel, - // false, - // is_selected, - // cx, - // ), - // ListEntry::Contact { contact, calling } => Self::render_contact( - // contact, - // *calling, - // &this.project, - // &theme, - // is_selected, - // cx, - // ), - // ListEntry::ChannelEditor { depth } => { - // this.render_channel_editor(&theme, *depth, cx) - // } - // ListEntry::ContactPlaceholder => { - // this.render_contact_placeholder(&theme.collab_panel, is_selected, cx) - // } - // } - // }); + let list_state = + ListState::new(0, gpui::ListAlignment::Top, px(1000.), move |ix, cx| { + view.update(cx, |view, cx| view.render_list_entry(ix, cx)) + }); let mut this = Self { width: None, @@ -583,6 +458,7 @@ impl CollabPanel { fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: None, + list_state, channel_name_editor, filter_editor, entries: Vec::default(), @@ -1084,6 +960,8 @@ impl CollabPanel { self.entries.push(ListEntry::ContactPlaceholder); } + self.list_state.reset(self.entries.len()); + if select_same_item { if let Some(prev_selected_entry) = prev_selected_entry { self.selection.take(); @@ -2158,77 +2036,86 @@ impl CollabPanel { ) } + fn render_list_entry( + &mut self, + // entry: &ListEntry, + ix: usize, + cx: &mut ViewContext, + ) -> AnyElement { + let entry = &self.entries[ix]; + + let is_selected = self.selection == Some(ix); + match entry { + ListEntry::Header(section) => { + let is_collapsed = self.collapsed_sections.contains(section); + self.render_header(*section, is_selected, is_collapsed, cx) + .into_any_element() + } + ListEntry::Contact { contact, calling } => self + .render_contact(contact, *calling, is_selected, cx) + .into_any_element(), + ListEntry::ContactPlaceholder => self + .render_contact_placeholder(is_selected, cx) + .into_any_element(), + ListEntry::IncomingRequest(user) => self + .render_contact_request(user, true, is_selected, cx) + .into_any_element(), + ListEntry::OutgoingRequest(user) => self + .render_contact_request(user, false, is_selected, cx) + .into_any_element(), + ListEntry::Channel { + channel, + depth, + has_children, + } => self + .render_channel(channel, *depth, *has_children, is_selected, ix, cx) + .into_any_element(), + ListEntry::ChannelEditor { depth } => { + self.render_channel_editor(*depth, cx).into_any_element() + } + ListEntry::CallParticipant { + user, + peer_id, + is_pending, + } => self + .render_call_participant(user, *peer_id, *is_pending, cx) + .into_any_element(), + ListEntry::ParticipantProject { + project_id, + worktree_root_names, + host_user_id, + is_last, + } => self + .render_participant_project( + *project_id, + &worktree_root_names, + *host_user_id, + *is_last, + cx, + ) + .into_any_element(), + ListEntry::ParticipantScreen { peer_id, is_last } => self + .render_participant_screen(*peer_id, *is_last, cx) + .into_any_element(), + ListEntry::ChannelNotes { channel_id } => self + .render_channel_notes(*channel_id, cx) + .into_any_element(), + ListEntry::ChannelChat { channel_id } => { + self.render_channel_chat(*channel_id, cx).into_any_element() + } + } + } + fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { v_stack() .size_full() .child( v_stack() .size_full() - .id("scroll") - .overflow_y_scroll() - .track_scroll(&self.scroll_handle) - .children(self.entries.iter().enumerate().map(|(ix, entry)| { - let is_selected = self.selection == Some(ix); - match entry { - ListEntry::Header(section) => { - let is_collapsed = self.collapsed_sections.contains(section); - self.render_header(*section, is_selected, is_collapsed, cx) - .into_any_element() - } - ListEntry::Contact { contact, calling } => self - .render_contact(contact, *calling, is_selected, cx) - .into_any_element(), - ListEntry::ContactPlaceholder => self - .render_contact_placeholder(is_selected, cx) - .into_any_element(), - ListEntry::IncomingRequest(user) => self - .render_contact_request(user, true, is_selected, cx) - .into_any_element(), - ListEntry::OutgoingRequest(user) => self - .render_contact_request(user, false, is_selected, cx) - .into_any_element(), - ListEntry::Channel { - channel, - depth, - has_children, - } => self - .render_channel(channel, *depth, *has_children, is_selected, ix, cx) - .into_any_element(), - ListEntry::ChannelEditor { depth } => { - self.render_channel_editor(*depth, cx).into_any_element() - } - ListEntry::CallParticipant { - user, - peer_id, - is_pending, - } => self - .render_call_participant(user, *peer_id, *is_pending, cx) - .into_any_element(), - ListEntry::ParticipantProject { - project_id, - worktree_root_names, - host_user_id, - is_last, - } => self - .render_participant_project( - *project_id, - &worktree_root_names, - *host_user_id, - *is_last, - cx, - ) - .into_any_element(), - ListEntry::ParticipantScreen { peer_id, is_last } => self - .render_participant_screen(*peer_id, *is_last, cx) - .into_any_element(), - ListEntry::ChannelNotes { channel_id } => self - .render_channel_notes(*channel_id, cx) - .into_any_element(), - ListEntry::ChannelChat { channel_id } => { - self.render_channel_chat(*channel_id, cx).into_any_element() - } - } - })), + // .id("scroll") + // .overflow_y_scroll() + // .track_scroll(&self.scroll_handle) + .child(list(self.list_state.clone()).full().into_any_element()), ) .child( div().p_2().child( From b478a4c4d5676f1d7e07961d29f41e24cf5e7602 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 12:10:40 -0500 Subject: [PATCH 2/6] Perform scroll interactions through the `ListState` --- crates/collab_ui2/src/collab_panel.rs | 53 ++++++++++++++++----------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 3a35c812861c429d5ae24aa45f293296969b433f..31b1ef0237c9baacb8b40d07c0ad9be72bf0325c 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -178,9 +178,10 @@ use gpui::{ actions, canvas, div, fill, img, impl_actions, list, overlay, point, prelude::*, px, rems, serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, - InteractiveElement, IntoElement, Length, ListState, Model, MouseDownEvent, ParentElement, - Pixels, Point, PromptLevel, Quad, Render, RenderOnce, ScrollHandle, SharedString, Size, - Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, + InteractiveElement, IntoElement, Length, ListOffset, ListState, Model, MouseDownEvent, + ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, ScrollHandle, + SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, + WeakView, }; use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; @@ -314,7 +315,6 @@ pub struct CollabPanel { client: Arc, project: Model, match_candidates: Vec, - scroll_handle: ScrollHandle, subscriptions: Vec, collapsed_sections: Vec
, collapsed_channels: Vec, @@ -469,7 +469,6 @@ impl CollabPanel { project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), - scroll_handle: ScrollHandle::new(), collapsed_sections: vec![Section::Offline], collapsed_channels: Vec::default(), workspace: workspace.weak_handle(), @@ -585,6 +584,13 @@ impl CollabPanel { ); } + fn scroll_to_item(&mut self, ix: usize) { + self.list_state.scroll_to(ListOffset { + item_ix: ix, + offset_in_item: px(0.), + }) + } + fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { let channel_store = self.channel_store.read(cx); let user_store = self.user_store.read(cx); @@ -968,7 +974,7 @@ impl CollabPanel { for (ix, entry) in self.entries.iter().enumerate() { if *entry == prev_selected_entry { self.selection = Some(ix); - self.scroll_handle.scroll_to_item(ix); + self.scroll_to_item(ix); break; } } @@ -979,16 +985,19 @@ impl CollabPanel { None } else { let ix = prev_selection.min(self.entries.len() - 1); - self.scroll_handle.scroll_to_item(ix); + self.scroll_to_item(ix); Some(ix) } }); } if scroll_to_top { - self.scroll_handle.scroll_to_item(0) + self.scroll_to_item(0) } else { - let (old_index, old_offset) = self.scroll_handle.logical_scroll_top(); + let ListOffset { + item_ix: old_index, + offset_in_item: old_offset, + } = self.list_state.logical_scroll_top(); // Attempt to maintain the same scroll position. if let Some(old_top_entry) = old_entries.get(old_index) { let (new_index, new_offset) = self @@ -1014,8 +1023,9 @@ impl CollabPanel { }) .unwrap_or_else(|| (old_index, old_offset)); - self.scroll_handle - .set_logical_scroll_top(new_index, new_offset); + // TODO: How to handle this with `list`? + // self.scroll_handle + // .set_logical_scroll_top(new_index, new_offset); } } @@ -1506,7 +1516,7 @@ impl CollabPanel { } if let Some(ix) = self.selection { - self.scroll_handle.scroll_to_item(ix) + self.scroll_to_item(ix) } cx.notify(); } @@ -1518,7 +1528,7 @@ impl CollabPanel { } if let Some(ix) = self.selection { - self.scroll_handle.scroll_to_item(ix) + self.scroll_to_item(ix) } cx.notify(); } @@ -1841,14 +1851,15 @@ impl CollabPanel { let Some(channel) = self.selected_channel() else { return; }; - let Some(bounds) = self - .selection - .and_then(|ix| self.scroll_handle.bounds_for_item(ix)) - else { - return; - }; - - self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); + // TODO: How to handle now that we're using `list`? + // let Some(bounds) = self + // .selection + // .and_then(|ix| self.scroll_handle.bounds_for_item(ix)) + // else { + // return; + // }; + // + // self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); cx.stop_propagation(); } From 47fc03ab98536b3ba2cbd75cf169ff516a5eb07f Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 13:24:30 -0500 Subject: [PATCH 3/6] Fix positioning of the inline context menu Added a new `bounds_for_item` for `ListState`. Co-authored-by: Max --- crates/collab_ui2/src/collab_panel.rs | 55 ++++++++++----------------- crates/gpui2/src/elements/list.rs | 46 ++++++++++++++++++---- 2 files changed, 60 insertions(+), 41 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 31b1ef0237c9baacb8b40d07c0ad9be72bf0325c..6f21649dd6a07f4d47ecb69e7f7183d4c4b789b3 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -165,7 +165,7 @@ struct ChannelMoveClipboard { const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -use std::{iter::once, mem, sync::Arc}; +use std::{mem, sync::Arc}; use call::ActiveCall; use channel::{Channel, ChannelEvent, ChannelId, ChannelStore}; @@ -175,12 +175,11 @@ use editor::Editor; use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt}; use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ - actions, canvas, div, fill, img, impl_actions, list, overlay, point, prelude::*, px, rems, - serde_json, size, Action, AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, - DismissEvent, Div, EventEmitter, FocusHandle, Focusable, FocusableView, Hsla, - InteractiveElement, IntoElement, Length, ListOffset, ListState, Model, MouseDownEvent, - ParentElement, Pixels, Point, PromptLevel, Quad, Render, RenderOnce, ScrollHandle, - SharedString, Size, Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, + actions, canvas, div, fill, impl_actions, list, overlay, point, prelude::*, px, serde_json, + AnyElement, AppContext, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, + EventEmitter, FocusHandle, Focusable, FocusableView, InteractiveElement, IntoElement, + ListOffset, ListState, Model, MouseDownEvent, ParentElement, Pixels, Point, PromptLevel, + Render, RenderOnce, SharedString, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; use project::{Fs, Project}; @@ -189,7 +188,7 @@ use settings::{Settings, SettingsStore}; use ui::prelude::*; use ui::{ h_stack, v_stack, Avatar, Button, Color, ContextMenu, Icon, IconButton, IconElement, IconSize, - Label, List, ListHeader, ListItem, Tooltip, + Label, ListHeader, ListItem, Tooltip, }; use util::{maybe, ResultExt, TryFutureExt}; use workspace::{ @@ -1023,9 +1022,10 @@ impl CollabPanel { }) .unwrap_or_else(|| (old_index, old_offset)); - // TODO: How to handle this with `list`? - // self.scroll_handle - // .set_logical_scroll_top(new_index, new_offset); + self.list_state.scroll_to(ListOffset { + item_ix: new_index, + offset_in_item: new_offset, + }); } } @@ -1851,15 +1851,14 @@ impl CollabPanel { let Some(channel) = self.selected_channel() else { return; }; - // TODO: How to handle now that we're using `list`? - // let Some(bounds) = self - // .selection - // .and_then(|ix| self.scroll_handle.bounds_for_item(ix)) - // else { - // return; - // }; - // - // self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); + let Some(bounds) = self + .selection + .and_then(|ix| self.list_state.bounds_for_item(ix)) + else { + return; + }; + + self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx); cx.stop_propagation(); } @@ -2047,12 +2046,7 @@ impl CollabPanel { ) } - fn render_list_entry( - &mut self, - // entry: &ListEntry, - ix: usize, - cx: &mut ViewContext, - ) -> AnyElement { + fn render_list_entry(&mut self, ix: usize, cx: &mut ViewContext) -> AnyElement { let entry = &self.entries[ix]; let is_selected = self.selection == Some(ix); @@ -2120,14 +2114,7 @@ impl CollabPanel { fn render_signed_in(&mut self, cx: &mut ViewContext) -> Div { v_stack() .size_full() - .child( - v_stack() - .size_full() - // .id("scroll") - // .overflow_y_scroll() - // .track_scroll(&self.scroll_handle) - .child(list(self.list_state.clone()).full().into_any_element()), - ) + .child(list(self.list_state.clone()).full()) .child( div().p_2().child( div() diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 6818c5c7a2e62baab27aad701dfa27cfa80dbce4..108703370cf4ab12cb7cc2e7bbb6464fab7892f3 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -1,6 +1,7 @@ use crate::{ - px, AnyElement, AvailableSpace, BorrowAppContext, DispatchPhase, Element, IntoElement, Pixels, - Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, WindowContext, + point, px, AnyElement, AvailableSpace, BorrowAppContext, Bounds, DispatchPhase, Element, + IntoElement, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, + WindowContext, }; use collections::VecDeque; use refineable::Refineable as _; @@ -23,7 +24,7 @@ pub struct List { pub struct ListState(Rc>); struct StateInner { - last_layout_width: Option, + last_layout_bounds: Option>, render_item: Box AnyElement>, items: SumTree, logical_scroll_top: Option, @@ -83,7 +84,7 @@ impl ListState { let mut items = SumTree::new(); items.extend((0..element_count).map(|_| ListItem::Unrendered), &()); Self(Rc::new(RefCell::new(StateInner { - last_layout_width: None, + last_layout_bounds: None, render_item: Box::new(render_item), items, logical_scroll_top: None, @@ -152,6 +153,35 @@ impl ListState { } state.logical_scroll_top = Some(scroll_top); } + + /// Get the bounds for the given item in window coordinates. + pub fn bounds_for_item(&self, ix: usize) -> Option> { + let state = &*self.0.borrow(); + let bounds = state.last_layout_bounds.unwrap_or_default(); + let scroll_top = state.logical_scroll_top.unwrap_or_default(); + + if ix < scroll_top.item_ix { + return None; + } + + let mut cursor = state.items.cursor::<(Count, Height)>(); + cursor.seek(&Count(scroll_top.item_ix), Bias::Right, &()); + + let scroll_top = cursor.start().1 .0 + scroll_top.offset_in_item; + + cursor.seek_forward(&Count(ix), Bias::Right, &()); + if let Some(&ListItem::Rendered { height }) = cursor.item() { + let &(Count(count), Height(top)) = cursor.start(); + if count == ix { + let top = bounds.top() + top - scroll_top; + return Some(Bounds::from_corners( + point(bounds.left(), top), + point(bounds.right(), top + height), + )); + } + } + None + } } impl StateInner { @@ -234,7 +264,7 @@ impl std::fmt::Debug for ListItem { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub struct ListOffset { pub item_ix: usize, pub offset_in_item: Pixels, @@ -265,7 +295,9 @@ impl Element for List { let state = &mut *self.state.0.borrow_mut(); // If the width of the list has changed, invalidate all cached item heights - if state.last_layout_width != Some(bounds.size.width) { + if state.last_layout_bounds.map_or(true, |last_bounds| { + last_bounds.size.width != bounds.size.width + }) { state.items = SumTree::from_iter( (0..state.items.summary().count).map(|_| ListItem::Unrendered), &(), @@ -392,7 +424,7 @@ impl Element for List { } state.items = new_items; - state.last_layout_width = Some(bounds.size.width); + state.last_layout_bounds = Some(bounds); let list_state = self.state.clone(); let height = bounds.size.height; From a40f04b71fd72e3818d3b9f23c4e31c30c4d47b6 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 13:50:51 -0500 Subject: [PATCH 4/6] Only scroll enough to reveal the next channel if it isn't visible Co-authored-by: Max --- crates/collab_ui2/src/collab_panel.rs | 5 +--- crates/gpui2/src/elements/list.rs | 33 +++++++++++++++++++++++++-- 2 files changed, 32 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6f21649dd6a07f4d47ecb69e7f7183d4c4b789b3..1ca6101c54a19cfd51f4e05c34a0dcb0bbb4fc3d 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -584,10 +584,7 @@ impl CollabPanel { } fn scroll_to_item(&mut self, ix: usize) { - self.list_state.scroll_to(ListOffset { - item_ix: ix, - offset_in_item: px(0.), - }) + self.list_state.scroll_to_reveal_item(ix) } fn update_entries(&mut self, select_same_item: bool, cx: &mut ViewContext) { diff --git a/crates/gpui2/src/elements/list.rs b/crates/gpui2/src/elements/list.rs index 108703370cf4ab12cb7cc2e7bbb6464fab7892f3..415a3b7368bb6a88049ec5377bb1aafc21b3596e 100644 --- a/crates/gpui2/src/elements/list.rs +++ b/crates/gpui2/src/elements/list.rs @@ -154,11 +154,40 @@ impl ListState { state.logical_scroll_top = Some(scroll_top); } + pub fn scroll_to_reveal_item(&self, ix: usize) { + let state = &mut *self.0.borrow_mut(); + let mut scroll_top = state.logical_scroll_top(); + let height = state + .last_layout_bounds + .map_or(px(0.), |bounds| bounds.size.height); + + if ix <= scroll_top.item_ix { + scroll_top.item_ix = ix; + scroll_top.offset_in_item = px(0.); + } else { + let mut cursor = state.items.cursor::(); + cursor.seek(&Count(ix + 1), Bias::Right, &()); + let bottom = cursor.start().height; + let goal_top = px(0.).max(bottom - height); + + cursor.seek(&Height(goal_top), Bias::Left, &()); + let start_ix = cursor.start().count; + let start_item_top = cursor.start().height; + + if start_ix >= scroll_top.item_ix { + scroll_top.item_ix = start_ix; + scroll_top.offset_in_item = goal_top - start_item_top; + } + } + + state.logical_scroll_top = Some(scroll_top); + } + /// Get the bounds for the given item in window coordinates. pub fn bounds_for_item(&self, ix: usize) -> Option> { let state = &*self.0.borrow(); let bounds = state.last_layout_bounds.unwrap_or_default(); - let scroll_top = state.logical_scroll_top.unwrap_or_default(); + let scroll_top = state.logical_scroll_top(); if ix < scroll_top.item_ix { return None; @@ -264,7 +293,7 @@ impl std::fmt::Debug for ListItem { } } -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct ListOffset { pub item_ix: usize, pub offset_in_item: Pixels, From 541712790295a6950afbf610a44d755b4c9ae143 Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 14:01:24 -0500 Subject: [PATCH 5/6] Fix channel drag and drop Also add the ability to unnest a channel by dragging it to the "Channels" header. This is currently not working due to a collab server issue. Co-authored-by: Max --- crates/collab_ui2/src/collab_panel.rs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 1ca6101c54a19cfd51f4e05c34a0dcb0bbb4fc3d..c2da29ba9f0ceffe488be53c3d1cb17938ccfd23 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2225,14 +2225,14 @@ impl CollabPanel { .selected(is_selected), ) .when(section == Section::Channels, |el| { - el.drag_over::(|style| { + el.drag_over::(|style| { style.bg(cx.theme().colors().ghost_element_hover) }) .on_drop(cx.listener( - move |this, view: &View, cx| { + move |this, dragged_channel: &Channel, cx| { this.channel_store .update(cx, |channel_store, cx| { - channel_store.move_channel(view.read(cx).channel.id, None, cx) + channel_store.move_channel(dragged_channel.id, None, cx) }) .detach_and_log_err(cx) }, @@ -2451,15 +2451,15 @@ impl CollabPanel { width, }) }) - .drag_over::(|style| { + .drag_over::(|style| { style.bg(cx.theme().colors().ghost_element_hover) }) .on_drop( - cx.listener(move |this, view: &View, cx| { + cx.listener(move |this, dragged_channel: &Channel, cx| { this.channel_store .update(cx, |channel_store, cx| { channel_store.move_channel( - view.read(cx).channel.id, + dragged_channel.id, Some(channel_id), cx, ) From 5c060ceb1c2e3ae85503f89c57da34ac8e4446ad Mon Sep 17 00:00:00 2001 From: Marshall Bowers Date: Fri, 15 Dec 2023 14:03:49 -0500 Subject: [PATCH 6/6] Format code --- crates/collab_ui2/src/collab_panel.rs | 34 +++++++++------------------ 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index c2da29ba9f0ceffe488be53c3d1cb17938ccfd23..ec723df25d740c5d020eecb378a35978dd179baf 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -2225,18 +2225,14 @@ impl CollabPanel { .selected(is_selected), ) .when(section == Section::Channels, |el| { - el.drag_over::(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .on_drop(cx.listener( - move |this, dragged_channel: &Channel, cx| { + el.drag_over::(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| { this.channel_store .update(cx, |channel_store, cx| { channel_store.move_channel(dragged_channel.id, None, cx) }) .detach_and_log_err(cx) - }, - )) + })) }); if section == Section::Offline { @@ -2451,22 +2447,14 @@ impl CollabPanel { width, }) }) - .drag_over::(|style| { - style.bg(cx.theme().colors().ghost_element_hover) - }) - .on_drop( - cx.listener(move |this, dragged_channel: &Channel, cx| { - this.channel_store - .update(cx, |channel_store, cx| { - channel_store.move_channel( - dragged_channel.id, - Some(channel_id), - cx, - ) - }) - .detach_and_log_err(cx) - }), - ) + .drag_over::(|style| style.bg(cx.theme().colors().ghost_element_hover)) + .on_drop(cx.listener(move |this, dragged_channel: &Channel, cx| { + this.channel_store + .update(cx, |channel_store, cx| { + channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) + }) + .detach_and_log_err(cx) + })) .child( ListItem::new(channel_id as usize) // Offset the indent depth by one to give us room to show the disclosure.