Collab panel2: Now with scrolling and keyboard (#3455)

Also introducing: .track_scroll() for non-uniform lists.

Release Notes:

- N/A
This commit is contained in:
Conrad Irwin 2023-11-30 00:13:52 -07:00 committed by GitHub
commit 02174084ca
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 541 additions and 402 deletions

View file

@ -17,7 +17,7 @@ mod contact_finder;
// Client, Contact, User, UserStore, // Client, Contact, User, UserStore,
// }; // };
use contact_finder::ContactFinder; use contact_finder::ContactFinder;
use menu::Confirm; use menu::{Cancel, Confirm, SelectNext, SelectPrev};
use rpc::proto; use rpc::proto;
use theme::{ActiveTheme, ThemeSettings}; use theme::{ActiveTheme, ThemeSettings};
// use context_menu::{ContextMenu, ContextMenuItem}; // use context_menu::{ContextMenu, ContextMenuItem};
@ -170,10 +170,10 @@ use feature_flags::{ChannelsAlpha, FeatureFlagAppExt, FeatureFlagViewExt};
use fuzzy::{match_strings, StringMatchCandidate}; use fuzzy::{match_strings, StringMatchCandidate};
use gpui::{ use gpui::{
actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext, actions, div, img, overlay, prelude::*, px, rems, serde_json, Action, AppContext,
AsyncWindowContext, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle, Focusable, AsyncWindowContext, Bounds, ClipboardItem, DismissEvent, Div, EventEmitter, FocusHandle,
FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent, ParentElement, Pixels, Focusable, FocusableView, InteractiveElement, IntoElement, Model, MouseDownEvent,
Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled, Subscription, Task, ParentElement, Pixels, Point, PromptLevel, Render, RenderOnce, ScrollHandle, SharedString,
View, ViewContext, VisualContext, WeakView, Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView,
}; };
use project::{Fs, Project}; use project::{Fs, Project};
use serde_derive::{Deserialize, Serialize}; use serde_derive::{Deserialize, Serialize};
@ -302,7 +302,7 @@ pub struct CollabPanel {
client: Arc<Client>, client: Arc<Client>,
project: Model<Project>, project: Model<Project>,
match_candidates: Vec<StringMatchCandidate>, match_candidates: Vec<StringMatchCandidate>,
// list_state: ListState<Self>, scroll_handle: ScrollHandle,
subscriptions: Vec<Subscription>, subscriptions: Vec<Subscription>,
collapsed_sections: Vec<Section>, collapsed_sections: Vec<Section>,
collapsed_channels: Vec<ChannelId>, collapsed_channels: Vec<ChannelId>,
@ -384,10 +384,6 @@ enum ListEntry {
ContactPlaceholder, ContactPlaceholder,
} }
// impl Entity for CollabPanel {
// type Event = Event;
// }
impl CollabPanel { impl CollabPanel {
pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext<Workspace>) -> View<Self> {
cx.build_view(|cx| { cx.build_view(|cx| {
@ -399,28 +395,28 @@ impl CollabPanel {
editor editor
}); });
// cx.subscribe(&filter_editor, |this, _, event, cx| { cx.subscribe(&filter_editor, |this: &mut Self, _, event, cx| {
// if let editor::Event::BufferEdited = event { if let editor::EditorEvent::BufferEdited = event {
// let query = this.filter_editor.read(cx).text(cx); let query = this.filter_editor.read(cx).text(cx);
// if !query.is_empty() { if !query.is_empty() {
// this.selection.take(); this.selection.take();
// } }
// this.update_entries(true, cx); this.update_entries(true, cx);
// if !query.is_empty() { if !query.is_empty() {
// this.selection = this this.selection = this
// .entries .entries
// .iter() .iter()
// .position(|entry| !matches!(entry, ListEntry::Header(_))); .position(|entry| !matches!(entry, ListEntry::Header(_)));
// } }
// } else if let editor::Event::Blurred = event { } else if let editor::EditorEvent::Blurred = event {
// let query = this.filter_editor.read(cx).text(cx); let query = this.filter_editor.read(cx).text(cx);
// if query.is_empty() { if query.is_empty() {
// this.selection.take(); this.selection.take();
// this.update_entries(true, cx); this.update_entries(true, cx);
// } }
// } }
// }) })
// .detach(); .detach();
let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx)); let channel_name_editor = cx.build_view(|cx| Editor::single_line(cx));
@ -586,13 +582,13 @@ impl CollabPanel {
project: workspace.project().clone(), project: workspace.project().clone(),
subscriptions: Vec::default(), subscriptions: Vec::default(),
match_candidates: Vec::default(), match_candidates: Vec::default(),
scroll_handle: ScrollHandle::new(),
collapsed_sections: vec![Section::Offline], collapsed_sections: vec![Section::Offline],
collapsed_channels: Vec::default(), collapsed_channels: Vec::default(),
workspace: workspace.weak_handle(), workspace: workspace.weak_handle(),
client: workspace.app_state().client.clone(), client: workspace.app_state().client.clone(),
// context_menu_on_selected: true, // context_menu_on_selected: true,
drag_target_channel: ChannelDragTarget::None, drag_target_channel: ChannelDragTarget::None,
// list_state,
}; };
this.update_entries(false, cx); this.update_entries(false, cx);
@ -708,9 +704,9 @@ impl CollabPanel {
let query = self.filter_editor.read(cx).text(cx); let query = self.filter_editor.read(cx).text(cx);
let executor = cx.background_executor().clone(); let executor = cx.background_executor().clone();
// let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned());
let _old_entries = mem::take(&mut self.entries); let old_entries = mem::take(&mut self.entries);
// let mut scroll_to_top = false; let scroll_to_top = false;
// if let Some(room) = ActiveCall::global(cx).read(cx).room() { // if let Some(room) = ActiveCall::global(cx).read(cx).room() {
// self.entries.push(ListEntry::Header(Section::ActiveCall)); // self.entries.push(ListEntry::Header(Section::ActiveCall));
@ -1077,71 +1073,62 @@ impl CollabPanel {
self.entries.push(ListEntry::ContactPlaceholder); self.entries.push(ListEntry::ContactPlaceholder);
} }
// if select_same_item { if select_same_item {
// if let Some(prev_selected_entry) = prev_selected_entry { if let Some(prev_selected_entry) = prev_selected_entry {
// self.selection.take(); self.selection.take();
// for (ix, entry) in self.entries.iter().enumerate() { for (ix, entry) in self.entries.iter().enumerate() {
// if *entry == prev_selected_entry { if *entry == prev_selected_entry {
// self.selection = Some(ix); self.selection = Some(ix);
// break; self.scroll_handle.scroll_to_item(ix);
// } break;
// } }
// } }
// } else { }
// self.selection = self.selection.and_then(|prev_selection| { } else {
// if self.entries.is_empty() { self.selection = self.selection.and_then(|prev_selection| {
// None if self.entries.is_empty() {
// } else { None
// Some(prev_selection.min(self.entries.len() - 1)) } else {
// } let ix = prev_selection.min(self.entries.len() - 1);
// }); self.scroll_handle.scroll_to_item(ix);
// } Some(ix)
}
});
}
// let old_scroll_top = self.list_state.logical_scroll_top(); if scroll_to_top {
self.scroll_handle.scroll_to_item(0)
} else {
let (old_index, old_offset) = self.scroll_handle.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
.entries
.iter()
.position(|entry| entry == old_top_entry)
.map(|item_ix| (item_ix, old_offset))
.or_else(|| {
let entry_after_old_top = old_entries.get(old_index + 1)?;
let item_ix = self
.entries
.iter()
.position(|entry| entry == entry_after_old_top)?;
Some((item_ix, px(0.)))
})
.or_else(|| {
let entry_before_old_top = old_entries.get(old_index.saturating_sub(1))?;
let item_ix = self
.entries
.iter()
.position(|entry| entry == entry_before_old_top)?;
Some((item_ix, px(0.)))
})
.unwrap_or_else(|| (old_index, old_offset));
// self.list_state.reset(self.entries.len()); self.scroll_handle
.set_logical_scroll_top(new_index, new_offset);
// if scroll_to_top { }
// self.list_state.scroll_to(ListOffset::default()); }
// } else {
// // Attempt to maintain the same scroll position.
// if let Some(old_top_entry) = old_entries.get(old_scroll_top.item_ix) {
// let new_scroll_top = self
// .entries
// .iter()
// .position(|entry| entry == old_top_entry)
// .map(|item_ix| ListOffset {
// item_ix,
// offset_in_item: old_scroll_top.offset_in_item,
// })
// .or_else(|| {
// let entry_after_old_top = old_entries.get(old_scroll_top.item_ix + 1)?;
// let item_ix = self
// .entries
// .iter()
// .position(|entry| entry == entry_after_old_top)?;
// Some(ListOffset {
// item_ix,
// offset_in_item: 0.,
// })
// })
// .or_else(|| {
// let entry_before_old_top =
// old_entries.get(old_scroll_top.item_ix.saturating_sub(1))?;
// let item_ix = self
// .entries
// .iter()
// .position(|entry| entry == entry_before_old_top)?;
// Some(ListOffset {
// item_ix,
// offset_in_item: 0.,
// })
// });
// self.list_state
// .scroll_to(new_scroll_top.unwrap_or(old_scroll_top));
// }
// }
cx.notify(); cx.notify();
} }
@ -1685,8 +1672,6 @@ impl CollabPanel {
ix: usize, ix: usize,
cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
) { ) {
// self.context_menu_on_selected = position.is_none();
let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| {
self.channel_store self.channel_store
.read(cx) .read(cx)
@ -1777,138 +1762,131 @@ impl CollabPanel {
}); });
cx.focus_view(&context_menu); cx.focus_view(&context_menu);
let subscription = cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| { let subscription =
this.context_menu.take(); cx.subscribe(&context_menu, |this, _, _: &DismissEvent, cx| {
cx.notify(); if this.context_menu.as_ref().is_some_and(|context_menu| {
}); context_menu.0.focus_handle(cx).contains_focused(cx)
}) {
cx.focus_self();
}
this.context_menu.take();
cx.notify();
});
self.context_menu = Some((context_menu, position, subscription)); self.context_menu = Some((context_menu, position, subscription));
cx.notify(); cx.notify();
} }
// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) { fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
// if self.take_editing_state(cx) { if self.take_editing_state(cx) {
// cx.focus(&self.filter_editor); cx.focus_view(&self.filter_editor);
// } else { } else {
// self.filter_editor.update(cx, |editor, cx| { self.filter_editor.update(cx, |editor, cx| {
// if editor.buffer().read(cx).len(cx) > 0 { if editor.buffer().read(cx).len(cx) > 0 {
// editor.set_text("", cx); editor.set_text("", cx);
// } }
// }); });
// } }
// self.update_entries(false, cx); self.update_entries(false, cx);
// } }
// fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) { fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext<Self>) {
// let ix = self.selection.map_or(0, |ix| ix + 1); let ix = self.selection.map_or(0, |ix| ix + 1);
// if ix < self.entries.len() { if ix < self.entries.len() {
// self.selection = Some(ix); self.selection = Some(ix);
// } }
// self.list_state.reset(self.entries.len()); if let Some(ix) = self.selection {
// if let Some(ix) = self.selection { self.scroll_handle.scroll_to_item(ix)
// self.list_state.scroll_to(ListOffset { }
// item_ix: ix, cx.notify();
// offset_in_item: 0., }
// });
// }
// cx.notify();
// }
// fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) { fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext<Self>) {
// let ix = self.selection.take().unwrap_or(0); let ix = self.selection.take().unwrap_or(0);
// if ix > 0 { if ix > 0 {
// self.selection = Some(ix - 1); self.selection = Some(ix - 1);
// } }
// self.list_state.reset(self.entries.len()); if let Some(ix) = self.selection {
// if let Some(ix) = self.selection { self.scroll_handle.scroll_to_item(ix)
// self.list_state.scroll_to(ListOffset { }
// item_ix: ix, cx.notify();
// offset_in_item: 0., }
// });
// }
// cx.notify();
// }
fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) { fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext<Self>) {
if self.confirm_channel_edit(cx) { if self.confirm_channel_edit(cx) {
return; return;
} }
// if let Some(selection) = self.selection { if let Some(selection) = self.selection {
// if let Some(entry) = self.entries.get(selection) { if let Some(entry) = self.entries.get(selection) {
// match entry { match entry {
// ListEntry::Header(section) => match section { ListEntry::Header(section) => match section {
// Section::ActiveCall => Self::leave_call(cx), Section::ActiveCall => Self::leave_call(cx),
// Section::Channels => self.new_root_channel(cx), Section::Channels => self.new_root_channel(cx),
// Section::Contacts => self.toggle_contact_finder(cx), Section::Contacts => self.toggle_contact_finder(cx),
// Section::ContactRequests Section::ContactRequests
// | Section::Online | Section::Online
// | Section::Offline | Section::Offline
// | Section::ChannelInvites => { | Section::ChannelInvites => {
// self.toggle_section_expanded(*section, cx); self.toggle_section_expanded(*section, cx);
// } }
// }, },
// ListEntry::Contact { contact, calling } => { ListEntry::Contact { contact, calling } => {
// if contact.online && !contact.busy && !calling { if contact.online && !contact.busy && !calling {
// self.call(contact.user.id, Some(self.project.clone()), cx); self.call(contact.user.id, cx);
// } }
// } }
// ListEntry::ParticipantProject { // ListEntry::ParticipantProject {
// project_id, // project_id,
// host_user_id, // host_user_id,
// .. // ..
// } => { // } => {
// if let Some(workspace) = self.workspace.upgrade(cx) { // if let Some(workspace) = self.workspace.upgrade(cx) {
// let app_state = workspace.read(cx).app_state().clone(); // let app_state = workspace.read(cx).app_state().clone();
// workspace::join_remote_project( // workspace::join_remote_project(
// *project_id, // *project_id,
// *host_user_id, // *host_user_id,
// app_state, // app_state,
// cx, // cx,
// ) // )
// .detach_and_log_err(cx); // .detach_and_log_err(cx);
// } // }
// } // }
// ListEntry::ParticipantScreen { peer_id, .. } => { // ListEntry::ParticipantScreen { peer_id, .. } => {
// let Some(peer_id) = peer_id else { // let Some(peer_id) = peer_id else {
// return; // return;
// }; // };
// if let Some(workspace) = self.workspace.upgrade(cx) { // if let Some(workspace) = self.workspace.upgrade(cx) {
// workspace.update(cx, |workspace, cx| { // workspace.update(cx, |workspace, cx| {
// workspace.open_shared_screen(*peer_id, cx) // workspace.open_shared_screen(*peer_id, cx)
// }); // });
// } // }
// } // }
// ListEntry::Channel { channel, .. } => { ListEntry::Channel { channel, .. } => {
// let is_active = maybe!({ let is_active = maybe!({
// let call_channel = ActiveCall::global(cx) let call_channel = ActiveCall::global(cx)
// .read(cx) .read(cx)
// .room()? .room()?
// .read(cx) .read(cx)
// .channel_id()?; .channel_id()?;
// Some(call_channel == channel.id) Some(call_channel == channel.id)
// }) })
// .unwrap_or(false); .unwrap_or(false);
// if is_active { if is_active {
// self.open_channel_notes( self.open_channel_notes(channel.id, cx)
// &OpenChannelNotes { } else {
// channel_id: channel.id, self.join_channel(channel.id, cx)
// }, }
// cx, }
// ) ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
// } else { _ => {}
// self.join_channel(channel.id, cx) }
// } }
// } }
// ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx),
// _ => {}
// }
// }
// }
} }
fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) { fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext<Self>) {
@ -1975,33 +1953,33 @@ impl CollabPanel {
self.update_entries(false, cx); self.update_entries(false, cx);
} }
// fn collapse_selected_channel( fn collapse_selected_channel(
// &mut self, &mut self,
// _: &CollapseSelectedChannel, _: &CollapseSelectedChannel,
// cx: &mut ViewContext<Self>, cx: &mut ViewContext<Self>,
// ) { ) {
// let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else {
// return; return;
// }; };
// if self.is_channel_collapsed(channel_id) { if self.is_channel_collapsed(channel_id) {
// return; return;
// } }
// self.toggle_channel_collapsed(channel_id, cx); self.toggle_channel_collapsed(channel_id, cx);
// } }
// fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) { fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext<Self>) {
// let Some(id) = self.selected_channel().map(|channel| channel.id) else { let Some(id) = self.selected_channel().map(|channel| channel.id) else {
// return; return;
// }; };
// if !self.is_channel_collapsed(id) { if !self.is_channel_collapsed(id) {
// return; return;
// } }
// self.toggle_channel_collapsed(id, cx) self.toggle_channel_collapsed(id, cx)
// } }
// fn toggle_channel_collapsed_action( // fn toggle_channel_collapsed_action(
// &mut self, // &mut self,
@ -2030,11 +2008,11 @@ impl CollabPanel {
self.collapsed_channels.binary_search(&channel_id).is_ok() self.collapsed_channels.binary_search(&channel_id).is_ok()
} }
// fn leave_call(cx: &mut ViewContext<Self>) { fn leave_call(cx: &mut ViewContext<Self>) {
// ActiveCall::global(cx) ActiveCall::global(cx)
// .update(cx, |call, cx| call.hang_up(cx)) .update(cx, |call, cx| call.hang_up(cx))
// .detach_and_log_err(cx); .detach_and_log_err(cx);
// } }
fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) { fn toggle_contact_finder(&mut self, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() { if let Some(workspace) = self.workspace.upgrade() {
@ -2154,13 +2132,20 @@ impl CollabPanel {
} }
} }
// fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) { fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext<Self>) {
// let Some(channel) = self.selected_channel() else { let Some(channel) = self.selected_channel() else {
// return; return;
// }; };
let Some(bounds) = self
.selection
.and_then(|ix| self.scroll_handle.bounds_for_item(ix))
else {
return;
};
// self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); self.deploy_channel_context_menu(bounds.center(), channel.id, self.selection.unwrap(), cx);
// } cx.stop_propagation();
}
fn selected_channel(&self) -> Option<&Arc<Channel>> { fn selected_channel(&self) -> Option<&Arc<Channel>> {
self.selection self.selection
@ -2350,44 +2335,67 @@ impl CollabPanel {
) )
} }
fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> List { fn render_signed_in(&mut self, cx: &mut ViewContext<Self>) -> Div {
let is_selected = false; // todo!() this.selection == Some(ix); v_stack()
.size_full()
List::new().children( .child(
self.entries div()
.clone() .p_2()
.into_iter() .child(div().rounded(px(2.0)).child(self.filter_editor.clone())),
.enumerate() )
.map(|(ix, entry)| match entry { .child(
ListEntry::Header(section) => { v_stack()
let is_collapsed = self.collapsed_sections.contains(&section); .size_full()
self.render_header(section, is_selected, is_collapsed, cx) .id("scroll")
.into_any_element() .overflow_y_scroll()
} .track_scroll(&self.scroll_handle)
ListEntry::Contact { contact, calling } => self .children(
.render_contact(&*contact, calling, is_selected, cx) self.entries
.into_any_element(), .clone()
ListEntry::ContactPlaceholder => self .into_iter()
.render_contact_placeholder(is_selected, cx) .enumerate()
.into_any_element(), .map(|(ix, entry)| {
ListEntry::IncomingRequest(user) => self let is_selected = self.selection == Some(ix);
.render_contact_request(user, true, is_selected, cx) match entry {
.into_any_element(), ListEntry::Header(section) => {
ListEntry::OutgoingRequest(user) => self let is_collapsed =
.render_contact_request(user, false, is_selected, cx) self.collapsed_sections.contains(&section);
.into_any_element(), self.render_header(section, is_selected, is_collapsed, cx)
ListEntry::Channel { .into_any_element()
channel, }
depth, ListEntry::Contact { contact, calling } => self
has_children, .render_contact(&*contact, calling, is_selected, cx)
} => self .into_any_element(),
.render_channel(&*channel, depth, has_children, is_selected, ix, cx) ListEntry::ContactPlaceholder => self
.into_any_element(), .render_contact_placeholder(is_selected, cx)
ListEntry::ChannelEditor { depth } => { .into_any_element(),
self.render_channel_editor(depth, 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()
}
}
}),
),
)
} }
fn render_header( fn render_header(
@ -2494,6 +2502,7 @@ impl CollabPanel {
el.child( el.child(
ListItem::new(text.clone()) ListItem::new(text.clone())
.child(div().w_full().child(Label::new(text))) .child(div().w_full().child(Label::new(text)))
.selected(is_selected)
.toggle(Some(!is_collapsed)) .toggle(Some(!is_collapsed))
.on_click(cx.listener(move |this, _, cx| { .on_click(cx.listener(move |this, _, cx| {
this.toggle_section_expanded(section, cx) this.toggle_section_expanded(section, cx)
@ -3214,23 +3223,36 @@ impl CollabPanel {
// } // }
impl Render for CollabPanel { impl Render for CollabPanel {
type Element = Focusable<Stateful<Div>>; type Element = Focusable<Div>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element { fn render(&mut self, cx: &mut ViewContext<Self>) -> Self::Element {
div() v_stack()
.id("collab-panel")
.key_context("CollabPanel") .key_context("CollabPanel")
.on_action(cx.listener(CollabPanel::cancel))
.on_action(cx.listener(CollabPanel::select_next))
.on_action(cx.listener(CollabPanel::select_prev))
.on_action(cx.listener(CollabPanel::confirm))
.on_action(cx.listener(CollabPanel::insert_space))
// .on_action(cx.listener(CollabPanel::remove))
.on_action(cx.listener(CollabPanel::remove_selected_channel))
.on_action(cx.listener(CollabPanel::show_inline_context_menu))
// .on_action(cx.listener(CollabPanel::new_subchannel))
// .on_action(cx.listener(CollabPanel::invite_members))
// .on_action(cx.listener(CollabPanel::manage_members))
.on_action(cx.listener(CollabPanel::rename_selected_channel))
// .on_action(cx.listener(CollabPanel::rename_channel))
// .on_action(cx.listener(CollabPanel::toggle_channel_collapsed_action))
.on_action(cx.listener(CollabPanel::collapse_selected_channel))
.on_action(cx.listener(CollabPanel::expand_selected_channel))
// .on_action(cx.listener(CollabPanel::open_channel_notes))
// .on_action(cx.listener(CollabPanel::join_channel_chat))
// .on_action(cx.listener(CollabPanel::copy_channel_link))
.track_focus(&self.focus_handle) .track_focus(&self.focus_handle)
.size_full() .size_full()
.overflow_scroll() .child(if self.user_store.read(cx).current_user().is_none() {
.on_action(cx.listener(Self::confirm)) self.render_signed_out(cx)
.on_action(cx.listener(Self::insert_space)) } else {
.map(|el| { self.render_signed_in(cx)
if self.user_store.read(cx).current_user().is_none() {
el.child(self.render_signed_out(cx))
} else {
el.child(self.render_signed_in(cx))
}
}) })
.children(self.context_menu.as_ref().map(|(menu, position, _)| { .children(self.context_menu.as_ref().map(|(menu, position, _)| {
overlay() overlay()
@ -3392,111 +3414,111 @@ impl Panel for CollabPanel {
} }
impl FocusableView for CollabPanel { impl FocusableView for CollabPanel {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone() self.filter_editor.focus_handle(cx).clone()
} }
} }
// impl PartialEq for ListEntry { impl PartialEq for ListEntry {
// fn eq(&self, other: &Self) -> bool { fn eq(&self, other: &Self) -> bool {
// match self { match self {
// ListEntry::Header(section_1) => { ListEntry::Header(section_1) => {
// if let ListEntry::Header(section_2) = other { if let ListEntry::Header(section_2) = other {
// return section_1 == section_2; return section_1 == section_2;
// } }
// } }
// ListEntry::CallParticipant { user: user_1, .. } => { // ListEntry::CallParticipant { user: user_1, .. } => {
// if let ListEntry::CallParticipant { user: user_2, .. } = other { // if let ListEntry::CallParticipant { user: user_2, .. } = other {
// return user_1.id == user_2.id; // return user_1.id == user_2.id;
// } // }
// } // }
// ListEntry::ParticipantProject { // ListEntry::ParticipantProject {
// project_id: project_id_1, // project_id: project_id_1,
// .. // ..
// } => { // } => {
// if let ListEntry::ParticipantProject { // if let ListEntry::ParticipantProject {
// project_id: project_id_2, // project_id: project_id_2,
// .. // ..
// } = other // } = other
// { // {
// return project_id_1 == project_id_2; // return project_id_1 == project_id_2;
// } // }
// } // }
// ListEntry::ParticipantScreen { // ListEntry::ParticipantScreen {
// peer_id: peer_id_1, .. // peer_id: peer_id_1, ..
// } => { // } => {
// if let ListEntry::ParticipantScreen { // if let ListEntry::ParticipantScreen {
// peer_id: peer_id_2, .. // peer_id: peer_id_2, ..
// } = other // } = other
// { // {
// return peer_id_1 == peer_id_2; // return peer_id_1 == peer_id_2;
// } // }
// } // }
// ListEntry::Channel { ListEntry::Channel {
// channel: channel_1, .. channel: channel_1, ..
// } => { } => {
// if let ListEntry::Channel { if let ListEntry::Channel {
// channel: channel_2, .. channel: channel_2, ..
// } = other } = other
// { {
// return channel_1.id == channel_2.id; return channel_1.id == channel_2.id;
// } }
// } }
// ListEntry::ChannelNotes { channel_id } => { // ListEntry::ChannelNotes { channel_id } => {
// if let ListEntry::ChannelNotes { // if let ListEntry::ChannelNotes {
// channel_id: other_id, // channel_id: other_id,
// } = other // } = other
// { // {
// return channel_id == other_id; // return channel_id == other_id;
// } // }
// } // }
// ListEntry::ChannelChat { channel_id } => { // ListEntry::ChannelChat { channel_id } => {
// if let ListEntry::ChannelChat { // if let ListEntry::ChannelChat {
// channel_id: other_id, // channel_id: other_id,
// } = other // } = other
// { // {
// return channel_id == other_id; // return channel_id == other_id;
// } // }
// } // }
// ListEntry::ChannelInvite(channel_1) => { // ListEntry::ChannelInvite(channel_1) => {
// if let ListEntry::ChannelInvite(channel_2) = other { // if let ListEntry::ChannelInvite(channel_2) = other {
// return channel_1.id == channel_2.id; // return channel_1.id == channel_2.id;
// } // }
// } // }
// ListEntry::IncomingRequest(user_1) => { ListEntry::IncomingRequest(user_1) => {
// if let ListEntry::IncomingRequest(user_2) = other { if let ListEntry::IncomingRequest(user_2) = other {
// return user_1.id == user_2.id; return user_1.id == user_2.id;
// } }
// } }
// ListEntry::OutgoingRequest(user_1) => { ListEntry::OutgoingRequest(user_1) => {
// if let ListEntry::OutgoingRequest(user_2) = other { if let ListEntry::OutgoingRequest(user_2) = other {
// return user_1.id == user_2.id; return user_1.id == user_2.id;
// } }
// } }
// ListEntry::Contact { ListEntry::Contact {
// contact: contact_1, .. contact: contact_1, ..
// } => { } => {
// if let ListEntry::Contact { if let ListEntry::Contact {
// contact: contact_2, .. contact: contact_2, ..
// } = other } = other
// { {
// return contact_1.user.id == contact_2.user.id; return contact_1.user.id == contact_2.user.id;
// } }
// } }
// ListEntry::ChannelEditor { depth } => { ListEntry::ChannelEditor { depth } => {
// if let ListEntry::ChannelEditor { depth: other_depth } = other { if let ListEntry::ChannelEditor { depth: other_depth } = other {
// return depth == other_depth; return depth == other_depth;
// } }
// } }
// ListEntry::ContactPlaceholder => { ListEntry::ContactPlaceholder => {
// if let ListEntry::ContactPlaceholder = other { if let ListEntry::ContactPlaceholder = other {
// return true; return true;
// } }
// } }
// } }
// false false
// } }
// } }
// fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> { // fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element<CollabPanel> {
// Svg::new(svg_path) // Svg::new(svg_path)

View file

@ -12,6 +12,7 @@ use smallvec::SmallVec;
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
cell::RefCell, cell::RefCell,
cmp::Ordering,
fmt::Debug, fmt::Debug,
mem, mem,
rc::Rc, rc::Rc,
@ -357,6 +358,11 @@ pub trait StatefulInteractiveElement: InteractiveElement {
self self
} }
fn track_scroll(mut self, scroll_handle: &ScrollHandle) -> Self {
self.interactivity().scroll_handle = Some(scroll_handle.clone());
self
}
fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self fn active(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
where where
Self: Sized, Self: Sized,
@ -626,6 +632,26 @@ impl Element for Div {
let mut child_max = Point::default(); let mut child_max = Point::default();
let content_size = if element_state.child_layout_ids.is_empty() { let content_size = if element_state.child_layout_ids.is_empty() {
bounds.size bounds.size
} else if let Some(scroll_handle) = self.interactivity.scroll_handle.as_ref() {
let mut state = scroll_handle.0.borrow_mut();
state.child_bounds = Vec::with_capacity(element_state.child_layout_ids.len());
state.bounds = bounds;
let requested = state.requested_scroll_top.take();
for (ix, child_layout_id) in element_state.child_layout_ids.iter().enumerate() {
let child_bounds = cx.layout_bounds(*child_layout_id);
child_min = child_min.min(&child_bounds.origin);
child_max = child_max.max(&child_bounds.lower_right());
state.child_bounds.push(child_bounds);
if let Some(requested) = requested.as_ref() {
if requested.0 == ix {
*state.offset.borrow_mut() =
bounds.origin - (child_bounds.origin - point(px(0.), requested.1));
}
}
}
(child_max - child_min).into()
} else { } else {
for child_layout_id in &element_state.child_layout_ids { for child_layout_id in &element_state.child_layout_ids {
let child_bounds = cx.layout_bounds(*child_layout_id); let child_bounds = cx.layout_bounds(*child_layout_id);
@ -696,6 +722,7 @@ pub struct Interactivity {
pub key_context: KeyContext, pub key_context: KeyContext,
pub focusable: bool, pub focusable: bool,
pub tracked_focus_handle: Option<FocusHandle>, pub tracked_focus_handle: Option<FocusHandle>,
pub scroll_handle: Option<ScrollHandle>,
pub focus_listeners: FocusListeners, pub focus_listeners: FocusListeners,
pub group: Option<SharedString>, pub group: Option<SharedString>,
pub base_style: StyleRefinement, pub base_style: StyleRefinement,
@ -754,6 +781,10 @@ impl Interactivity {
}); });
} }
if let Some(scroll_handle) = self.scroll_handle.as_ref() {
element_state.scroll_offset = Some(scroll_handle.0.borrow().offset.clone());
}
let style = self.compute_style(None, &mut element_state, cx); let style = self.compute_style(None, &mut element_state, cx);
let layout_id = f(style, cx); let layout_id = f(style, cx);
(layout_id, element_state) (layout_id, element_state)
@ -1206,6 +1237,7 @@ impl Default for Interactivity {
key_context: KeyContext::default(), key_context: KeyContext::default(),
focusable: false, focusable: false,
tracked_focus_handle: None, tracked_focus_handle: None,
scroll_handle: None,
focus_listeners: SmallVec::default(), focus_listeners: SmallVec::default(),
// scroll_offset: Point::default(), // scroll_offset: Point::default(),
group: None, group: None,
@ -1429,3 +1461,83 @@ where
self.element.children_mut() self.element.children_mut()
} }
} }
#[derive(Default)]
struct ScrollHandleState {
// not great to have the nested rc's...
offset: Rc<RefCell<Point<Pixels>>>,
bounds: Bounds<Pixels>,
child_bounds: Vec<Bounds<Pixels>>,
requested_scroll_top: Option<(usize, Pixels)>,
}
#[derive(Clone)]
pub struct ScrollHandle(Rc<RefCell<ScrollHandleState>>);
impl ScrollHandle {
pub fn new() -> Self {
Self(Rc::default())
}
pub fn offset(&self) -> Point<Pixels> {
self.0.borrow().offset.borrow().clone()
}
pub fn top_item(&self) -> usize {
let state = self.0.borrow();
let top = state.bounds.top() - state.offset.borrow().y;
match state.child_bounds.binary_search_by(|bounds| {
if top < bounds.top() {
Ordering::Greater
} else if top > bounds.bottom() {
Ordering::Less
} else {
Ordering::Equal
}
}) {
Ok(ix) => ix,
Err(ix) => ix.min(state.child_bounds.len().saturating_sub(1)),
}
}
pub fn bounds_for_item(&self, ix: usize) -> Option<Bounds<Pixels>> {
self.0.borrow().child_bounds.get(ix).cloned()
}
/// scroll_to_item scrolls the minimal amount to ensure that the item is
/// fully visible
pub fn scroll_to_item(&self, ix: usize) {
let state = self.0.borrow();
let Some(bounds) = state.child_bounds.get(ix) else {
return;
};
let scroll_offset = state.offset.borrow().y;
if bounds.top() + scroll_offset < state.bounds.top() {
state.offset.borrow_mut().y = state.bounds.top() - bounds.top();
} else if bounds.bottom() + scroll_offset > state.bounds.bottom() {
state.offset.borrow_mut().y = state.bounds.bottom() - bounds.bottom();
}
}
pub fn logical_scroll_top(&self) -> (usize, Pixels) {
let ix = self.top_item();
let state = self.0.borrow();
if let Some(child_bounds) = state.child_bounds.get(ix) {
(
ix,
child_bounds.top() + state.offset.borrow().y - state.bounds.top(),
)
} else {
(ix, px(0.))
}
}
pub fn set_logical_scroll_top(&self, ix: usize, px: Pixels) {
self.0.borrow_mut().requested_scroll_top = Some((ix, px));
}
}

View file

@ -258,7 +258,12 @@ impl RenderOnce for ButtonLike {
.active(|active| active.bg(self.style.active(cx).background)) .active(|active| active.bg(self.style.active(cx).background))
.when_some( .when_some(
self.on_click.filter(|_| !self.disabled), self.on_click.filter(|_| !self.disabled),
|this, on_click| this.on_click(move |event, cx| (on_click)(event, cx)), |this, on_click| {
this.on_click(move |event, cx| {
cx.stop_propagation();
(on_click)(event, cx)
})
},
) )
.when_some(self.tooltip, |this, tooltip| { .when_some(self.tooltip, |this, tooltip| {
this.tooltip(move |cx| tooltip(cx)) this.tooltip(move |cx| tooltip(cx))