diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 9ba8d7f9b20b00cb685d40f5776894977c2ff42f..36fd8bd6382f73929d3073829493ce5d93c4150d 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -118,3123 +118,3142 @@ // to: ChannelId, // } -// actions!( -// collab_panel, -// [ -// ToggleFocus, -// Remove, -// Secondary, -// CollapseSelectedChannel, -// ExpandSelectedChannel, -// StartMoveChannel, -// MoveSelected, -// InsertSpace, -// ] -// ); - -// impl_actions!( -// collab_panel, -// [ -// RemoveChannel, -// NewChannel, -// InviteMembers, -// ManageMembers, -// RenameChannel, -// ToggleCollapse, -// OpenChannelNotes, -// JoinChannelCall, -// JoinChannelChat, -// CopyChannelLink, -// StartMoveChannelFor, -// MoveChannel, -// ToggleSelectedIx -// ] -// ); - -// #[derive(Debug, Copy, Clone, PartialEq, Eq)] -// struct ChannelMoveClipboard { -// channel_id: ChannelId, -// } - -// const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; - -// pub fn init(cx: &mut AppContext) { -// settings::register::(cx); -// contact_finder::init(cx); -// channel_modal::init(cx); -// channel_view::init(cx); - -// cx.add_action(CollabPanel::cancel); -// cx.add_action(CollabPanel::select_next); -// cx.add_action(CollabPanel::select_prev); -// cx.add_action(CollabPanel::confirm); -// cx.add_action(CollabPanel::insert_space); -// cx.add_action(CollabPanel::remove); -// cx.add_action(CollabPanel::remove_selected_channel); -// cx.add_action(CollabPanel::show_inline_context_menu); -// cx.add_action(CollabPanel::new_subchannel); -// cx.add_action(CollabPanel::invite_members); -// cx.add_action(CollabPanel::manage_members); -// cx.add_action(CollabPanel::rename_selected_channel); -// cx.add_action(CollabPanel::rename_channel); -// cx.add_action(CollabPanel::toggle_channel_collapsed_action); -// cx.add_action(CollabPanel::collapse_selected_channel); -// cx.add_action(CollabPanel::expand_selected_channel); -// cx.add_action(CollabPanel::open_channel_notes); -// cx.add_action(CollabPanel::join_channel_chat); -// cx.add_action(CollabPanel::copy_channel_link); - -// cx.add_action( -// |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { -// if panel.selection.take() != Some(action.ix) { -// panel.selection = Some(action.ix) -// } - -// cx.notify(); -// }, -// ); - -// cx.add_action( -// |panel: &mut CollabPanel, -// action: &StartMoveChannelFor, -// _: &mut ViewContext| { -// panel.channel_clipboard = Some(ChannelMoveClipboard { -// channel_id: action.channel_id, -// }); -// }, -// ); - -// cx.add_action( -// |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { -// if let Some(channel) = panel.selected_channel() { -// panel.channel_clipboard = Some(ChannelMoveClipboard { -// channel_id: channel.id, -// }) -// } -// }, -// ); - -// cx.add_action( -// |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { -// let Some(clipboard) = panel.channel_clipboard.take() else { -// return; -// }; -// let Some(selected_channel) = panel.selected_channel() else { -// return; -// }; - -// panel -// .channel_store -// .update(cx, |channel_store, cx| { -// channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) -// }) -// .detach_and_log_err(cx) -// }, -// ); - -// cx.add_action( -// |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { -// if let Some(clipboard) = panel.channel_clipboard.take() { -// panel.channel_store.update(cx, |channel_store, cx| { -// channel_store -// .move_channel(clipboard.channel_id, Some(action.to), cx) -// .detach_and_log_err(cx) -// }) -// } -// }, -// ); -// } - -// #[derive(Debug)] -// pub enum ChannelEditingState { -// Create { -// location: Option, -// pending_name: Option, -// }, -// Rename { -// location: ChannelId, -// pending_name: Option, -// }, -// } - -// impl ChannelEditingState { -// fn pending_name(&self) -> Option<&str> { -// match self { -// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), -// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), -// } -// } -// } - -// pub struct CollabPanel { -// width: Option, -// fs: Arc, -// has_focus: bool, -// channel_clipboard: Option, -// pending_serialization: Task>, -// context_menu: ViewHandle, -// filter_editor: ViewHandle, -// channel_name_editor: ViewHandle, -// channel_editing_state: Option, -// entries: Vec, -// selection: Option, -// user_store: ModelHandle, -// client: Arc, -// channel_store: ModelHandle, -// project: ModelHandle, -// match_candidates: Vec, -// list_state: ListState, -// subscriptions: Vec, -// collapsed_sections: Vec
, -// collapsed_channels: Vec, -// drag_target_channel: ChannelDragTarget, -// workspace: WeakViewHandle, -// context_menu_on_selected: bool, -// } - -// #[derive(PartialEq, Eq)] -// enum ChannelDragTarget { -// None, -// Root, -// Channel(ChannelId), -// } - -// #[derive(Serialize, Deserialize)] -// struct SerializedCollabPanel { -// width: Option, -// collapsed_channels: Option>, -// } - -// #[derive(Debug)] -// pub enum Event { -// DockPositionChanged, -// Focus, -// Dismissed, -// } - -// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -// enum Section { -// ActiveCall, -// Channels, -// ChannelInvites, -// ContactRequests, -// Contacts, -// Online, -// Offline, -// } - -// #[derive(Clone, Debug)] -// enum ListEntry { -// Header(Section), -// CallParticipant { -// user: Arc, -// peer_id: Option, -// is_pending: bool, -// }, -// ParticipantProject { -// project_id: u64, -// worktree_root_names: Vec, -// host_user_id: u64, -// is_last: bool, -// }, -// ParticipantScreen { -// peer_id: Option, -// is_last: bool, -// }, -// IncomingRequest(Arc), -// OutgoingRequest(Arc), -// ChannelInvite(Arc), -// Channel { -// channel: Arc, -// depth: usize, -// has_children: bool, -// }, -// ChannelNotes { -// channel_id: ChannelId, -// }, -// ChannelChat { -// channel_id: ChannelId, -// }, -// ChannelEditor { -// depth: usize, -// }, -// Contact { -// contact: Arc, -// calling: bool, -// }, -// ContactPlaceholder, -// } - -// impl Entity for CollabPanel { -// type Event = Event; -// } - -// impl CollabPanel { -// pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { -// cx.add_view::(|cx| { -// let view_id = cx.view_id(); - -// let filter_editor = cx.add_view(|cx| { -// let mut editor = Editor::single_line( -// Some(Arc::new(|theme| { -// theme.collab_panel.user_query_editor.clone() -// })), -// cx, -// ); -// editor.set_placeholder_text("Filter channels, contacts", cx); -// editor -// }); - -// cx.subscribe(&filter_editor, |this, _, event, cx| { -// if let editor::Event::BufferEdited = event { -// let query = this.filter_editor.read(cx).text(cx); -// if !query.is_empty() { -// this.selection.take(); -// } -// this.update_entries(true, cx); -// if !query.is_empty() { -// this.selection = this -// .entries -// .iter() -// .position(|entry| !matches!(entry, ListEntry::Header(_))); -// } -// } else if let editor::Event::Blurred = event { -// let query = this.filter_editor.read(cx).text(cx); -// if query.is_empty() { -// this.selection.take(); -// this.update_entries(true, cx); -// } -// } -// }) -// .detach(); - -// let channel_name_editor = cx.add_view(|cx| { -// Editor::single_line( -// Some(Arc::new(|theme| { -// theme.collab_panel.user_query_editor.clone() -// })), -// cx, -// ) -// }); - -// cx.subscribe(&channel_name_editor, |this, _, event, cx| { -// if let editor::Event::Blurred = event { -// if let Some(state) = &this.channel_editing_state { -// if state.pending_name().is_some() { -// return; -// } -// } -// this.take_editing_state(cx); -// this.update_entries(false, cx); -// cx.notify(); -// } -// }) -// .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 mut this = Self { -// width: None, -// has_focus: false, -// channel_clipboard: None, -// fs: workspace.app_state().fs.clone(), -// pending_serialization: Task::ready(None), -// context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), -// channel_name_editor, -// filter_editor, -// entries: Vec::default(), -// channel_editing_state: None, -// selection: None, -// user_store: workspace.user_store().clone(), -// channel_store: ChannelStore::global(cx), -// project: workspace.project().clone(), -// subscriptions: Vec::default(), -// match_candidates: Vec::default(), -// collapsed_sections: vec![Section::Offline], -// collapsed_channels: Vec::default(), -// workspace: workspace.weak_handle(), -// client: workspace.app_state().client.clone(), -// context_menu_on_selected: true, -// drag_target_channel: ChannelDragTarget::None, -// list_state, -// }; - -// this.update_entries(false, cx); - -// // Update the dock position when the setting changes. -// let mut old_dock_position = this.position(cx); -// this.subscriptions -// .push( -// cx.observe_global::(move |this: &mut Self, cx| { -// let new_dock_position = this.position(cx); -// if new_dock_position != old_dock_position { -// old_dock_position = new_dock_position; -// cx.emit(Event::DockPositionChanged); -// } -// cx.notify(); -// }), -// ); - -// let active_call = ActiveCall::global(cx); -// this.subscriptions -// .push(cx.observe(&this.user_store, |this, _, cx| { -// this.update_entries(true, cx) -// })); -// this.subscriptions -// .push(cx.observe(&this.channel_store, |this, _, cx| { -// this.update_entries(true, cx) -// })); -// this.subscriptions -// .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); -// this.subscriptions -// .push(cx.observe_flag::(move |_, this, cx| { -// this.update_entries(true, cx) -// })); -// this.subscriptions.push(cx.subscribe( -// &this.channel_store, -// |this, _channel_store, e, cx| match e { -// ChannelEvent::ChannelCreated(channel_id) -// | ChannelEvent::ChannelRenamed(channel_id) => { -// if this.take_editing_state(cx) { -// this.update_entries(false, cx); -// this.selection = this.entries.iter().position(|entry| { -// if let ListEntry::Channel { channel, .. } = entry { -// channel.id == *channel_id -// } else { -// false -// } -// }); -// } -// } -// }, -// )); - -// this -// }) -// } - -// pub fn load( -// workspace: WeakViewHandle, -// cx: AsyncAppContext, -// ) -> Task>> { -// cx.spawn(|mut cx| async move { -// let serialized_panel = if let Some(panel) = cx -// .background() -// .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) -// .await -// .log_err() -// .flatten() -// { -// match serde_json::from_str::(&panel) { -// Ok(panel) => Some(panel), -// Err(err) => { -// log::error!("Failed to deserialize collaboration panel: {}", err); -// None -// } -// } -// } else { -// None -// }; - -// workspace.update(&mut cx, |workspace, cx| { -// let panel = CollabPanel::new(workspace, cx); -// if let Some(serialized_panel) = serialized_panel { -// panel.update(cx, |panel, cx| { -// panel.width = serialized_panel.width; -// panel.collapsed_channels = serialized_panel -// .collapsed_channels -// .unwrap_or_else(|| Vec::new()); -// cx.notify(); -// }); -// } -// panel -// }) -// }) -// } - -// fn serialize(&mut self, cx: &mut ViewContext) { -// let width = self.width; -// let collapsed_channels = self.collapsed_channels.clone(); -// self.pending_serialization = cx.background().spawn( -// async move { -// KEY_VALUE_STORE -// .write_kvp( -// COLLABORATION_PANEL_KEY.into(), -// serde_json::to_string(&SerializedCollabPanel { -// width, -// collapsed_channels: Some(collapsed_channels), -// })?, -// ) -// .await?; -// anyhow::Ok(()) -// } -// .log_err(), -// ); -// } - -// 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); -// let query = self.filter_editor.read(cx).text(cx); -// let executor = cx.background().clone(); - -// let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); -// let old_entries = mem::take(&mut self.entries); -// let mut scroll_to_top = false; - -// if let Some(room) = ActiveCall::global(cx).read(cx).room() { -// self.entries.push(ListEntry::Header(Section::ActiveCall)); -// if !old_entries -// .iter() -// .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) -// { -// scroll_to_top = true; -// } - -// if !self.collapsed_sections.contains(&Section::ActiveCall) { -// let room = room.read(cx); - -// if let Some(channel_id) = room.channel_id() { -// self.entries.push(ListEntry::ChannelNotes { channel_id }); -// self.entries.push(ListEntry::ChannelChat { channel_id }) -// } - -// // Populate the active user. -// if let Some(user) = user_store.current_user() { -// self.match_candidates.clear(); -// self.match_candidates.push(StringMatchCandidate { -// id: 0, -// string: user.github_login.clone(), -// char_bag: user.github_login.chars().collect(), -// }); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// if !matches.is_empty() { -// let user_id = user.id; -// self.entries.push(ListEntry::CallParticipant { -// user, -// peer_id: None, -// is_pending: false, -// }); -// let mut projects = room.local_participant().projects.iter().peekable(); -// while let Some(project) = projects.next() { -// self.entries.push(ListEntry::ParticipantProject { -// project_id: project.id, -// worktree_root_names: project.worktree_root_names.clone(), -// host_user_id: user_id, -// is_last: projects.peek().is_none() && !room.is_screen_sharing(), -// }); -// } -// if room.is_screen_sharing() { -// self.entries.push(ListEntry::ParticipantScreen { -// peer_id: None, -// is_last: true, -// }); -// } -// } -// } - -// // Populate remote participants. -// self.match_candidates.clear(); -// self.match_candidates -// .extend(room.remote_participants().iter().map(|(_, participant)| { -// StringMatchCandidate { -// id: participant.user.id as usize, -// string: participant.user.github_login.clone(), -// char_bag: participant.user.github_login.chars().collect(), -// } -// })); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// for mat in matches { -// let user_id = mat.candidate_id as u64; -// let participant = &room.remote_participants()[&user_id]; -// self.entries.push(ListEntry::CallParticipant { -// user: participant.user.clone(), -// peer_id: Some(participant.peer_id), -// is_pending: false, -// }); -// let mut projects = participant.projects.iter().peekable(); -// while let Some(project) = projects.next() { -// self.entries.push(ListEntry::ParticipantProject { -// project_id: project.id, -// worktree_root_names: project.worktree_root_names.clone(), -// host_user_id: participant.user.id, -// is_last: projects.peek().is_none() -// && participant.video_tracks.is_empty(), -// }); -// } -// if !participant.video_tracks.is_empty() { -// self.entries.push(ListEntry::ParticipantScreen { -// peer_id: Some(participant.peer_id), -// is_last: true, -// }); -// } -// } - -// // Populate pending participants. -// self.match_candidates.clear(); -// self.match_candidates -// .extend(room.pending_participants().iter().enumerate().map( -// |(id, participant)| StringMatchCandidate { -// id, -// string: participant.github_login.clone(), -// char_bag: participant.github_login.chars().collect(), -// }, -// )); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// self.entries -// .extend(matches.iter().map(|mat| ListEntry::CallParticipant { -// user: room.pending_participants()[mat.candidate_id].clone(), -// peer_id: None, -// is_pending: true, -// })); -// } -// } - -// let mut request_entries = Vec::new(); - -// if cx.has_flag::() { -// self.entries.push(ListEntry::Header(Section::Channels)); - -// if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { -// self.match_candidates.clear(); -// self.match_candidates -// .extend(channel_store.ordered_channels().enumerate().map( -// |(ix, (_, channel))| StringMatchCandidate { -// id: ix, -// string: channel.name.clone(), -// char_bag: channel.name.chars().collect(), -// }, -// )); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// if let Some(state) = &self.channel_editing_state { -// if matches!(state, ChannelEditingState::Create { location: None, .. }) { -// self.entries.push(ListEntry::ChannelEditor { depth: 0 }); -// } -// } -// let mut collapse_depth = None; -// for mat in matches { -// let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); -// let depth = channel.parent_path.len(); - -// if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { -// collapse_depth = Some(depth); -// } else if let Some(collapsed_depth) = collapse_depth { -// if depth > collapsed_depth { -// continue; -// } -// if self.is_channel_collapsed(channel.id) { -// collapse_depth = Some(depth); -// } else { -// collapse_depth = None; -// } -// } - -// let has_children = channel_store -// .channel_at_index(mat.candidate_id + 1) -// .map_or(false, |next_channel| { -// next_channel.parent_path.ends_with(&[channel.id]) -// }); - -// match &self.channel_editing_state { -// Some(ChannelEditingState::Create { -// location: parent_id, -// .. -// }) if *parent_id == Some(channel.id) => { -// self.entries.push(ListEntry::Channel { -// channel: channel.clone(), -// depth, -// has_children: false, -// }); -// self.entries -// .push(ListEntry::ChannelEditor { depth: depth + 1 }); -// } -// Some(ChannelEditingState::Rename { -// location: parent_id, -// .. -// }) if parent_id == &channel.id => { -// self.entries.push(ListEntry::ChannelEditor { depth }); -// } -// _ => { -// self.entries.push(ListEntry::Channel { -// channel: channel.clone(), -// depth, -// has_children, -// }); -// } -// } -// } -// } - -// let channel_invites = channel_store.channel_invitations(); -// if !channel_invites.is_empty() { -// self.match_candidates.clear(); -// self.match_candidates -// .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { -// StringMatchCandidate { -// id: ix, -// string: channel.name.clone(), -// char_bag: channel.name.chars().collect(), -// } -// })); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// request_entries.extend(matches.iter().map(|mat| { -// ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) -// })); - -// if !request_entries.is_empty() { -// self.entries -// .push(ListEntry::Header(Section::ChannelInvites)); -// if !self.collapsed_sections.contains(&Section::ChannelInvites) { -// self.entries.append(&mut request_entries); -// } -// } -// } -// } - -// self.entries.push(ListEntry::Header(Section::Contacts)); - -// request_entries.clear(); -// let incoming = user_store.incoming_contact_requests(); -// if !incoming.is_empty() { -// self.match_candidates.clear(); -// self.match_candidates -// .extend( -// incoming -// .iter() -// .enumerate() -// .map(|(ix, user)| StringMatchCandidate { -// id: ix, -// string: user.github_login.clone(), -// char_bag: user.github_login.chars().collect(), -// }), -// ); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// request_entries.extend( -// matches -// .iter() -// .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), -// ); -// } - -// let outgoing = user_store.outgoing_contact_requests(); -// if !outgoing.is_empty() { -// self.match_candidates.clear(); -// self.match_candidates -// .extend( -// outgoing -// .iter() -// .enumerate() -// .map(|(ix, user)| StringMatchCandidate { -// id: ix, -// string: user.github_login.clone(), -// char_bag: user.github_login.chars().collect(), -// }), -// ); -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); -// request_entries.extend( -// matches -// .iter() -// .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), -// ); -// } - -// if !request_entries.is_empty() { -// self.entries -// .push(ListEntry::Header(Section::ContactRequests)); -// if !self.collapsed_sections.contains(&Section::ContactRequests) { -// self.entries.append(&mut request_entries); -// } -// } - -// let contacts = user_store.contacts(); -// if !contacts.is_empty() { -// self.match_candidates.clear(); -// self.match_candidates -// .extend( -// contacts -// .iter() -// .enumerate() -// .map(|(ix, contact)| StringMatchCandidate { -// id: ix, -// string: contact.user.github_login.clone(), -// char_bag: contact.user.github_login.chars().collect(), -// }), -// ); - -// let matches = executor.block(match_strings( -// &self.match_candidates, -// &query, -// true, -// usize::MAX, -// &Default::default(), -// executor.clone(), -// )); - -// let (online_contacts, offline_contacts) = matches -// .iter() -// .partition::, _>(|mat| contacts[mat.candidate_id].online); - -// for (matches, section) in [ -// (online_contacts, Section::Online), -// (offline_contacts, Section::Offline), -// ] { -// if !matches.is_empty() { -// self.entries.push(ListEntry::Header(section)); -// if !self.collapsed_sections.contains(§ion) { -// let active_call = &ActiveCall::global(cx).read(cx); -// for mat in matches { -// let contact = &contacts[mat.candidate_id]; -// self.entries.push(ListEntry::Contact { -// contact: contact.clone(), -// calling: active_call.pending_invites().contains(&contact.user.id), -// }); -// } -// } -// } -// } -// } - -// if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { -// self.entries.push(ListEntry::ContactPlaceholder); -// } - -// if select_same_item { -// if let Some(prev_selected_entry) = prev_selected_entry { -// self.selection.take(); -// for (ix, entry) in self.entries.iter().enumerate() { -// if *entry == prev_selected_entry { -// self.selection = Some(ix); -// break; -// } -// } -// } -// } else { -// self.selection = self.selection.and_then(|prev_selection| { -// if self.entries.is_empty() { -// None -// } else { -// Some(prev_selection.min(self.entries.len() - 1)) -// } -// }); -// } - -// let old_scroll_top = self.list_state.logical_scroll_top(); - -// self.list_state.reset(self.entries.len()); - -// 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(); -// } - -// fn render_call_participant( -// user: &User, -// peer_id: Option, -// user_store: ModelHandle, -// is_pending: bool, -// is_selected: bool, -// theme: &theme::Theme, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum CallParticipant {} -// enum CallParticipantTooltip {} -// enum LeaveCallButton {} -// enum LeaveCallTooltip {} - -// let collab_theme = &theme.collab_panel; - -// let is_current_user = -// user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); - -// let content = MouseEventHandler::new::( -// user.id as usize, -// cx, -// |mouse_state, cx| { -// let style = if is_current_user { -// *collab_theme -// .contact_row -// .in_state(is_selected) -// .style_for(&mut Default::default()) -// } else { -// *collab_theme -// .contact_row -// .in_state(is_selected) -// .style_for(mouse_state) -// }; - -// Flex::row() -// .with_children(user.avatar.clone().map(|avatar| { -// Image::from_data(avatar) -// .with_style(collab_theme.contact_avatar) -// .aligned() -// .left() -// })) -// .with_child( -// Label::new( -// user.github_login.clone(), -// collab_theme.contact_username.text.clone(), -// ) -// .contained() -// .with_style(collab_theme.contact_username.container) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .with_children(if is_pending { -// Some( -// Label::new("Calling", collab_theme.calling_indicator.text.clone()) -// .contained() -// .with_style(collab_theme.calling_indicator.container) -// .aligned() -// .into_any(), -// ) -// } else if is_current_user { -// Some( -// MouseEventHandler::new::(0, cx, |state, _| { -// render_icon_button( -// theme -// .collab_panel -// .leave_call_button -// .style_for(is_selected, state), -// "icons/exit.svg", -// ) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, |_, _, cx| { -// Self::leave_call(cx); -// }) -// .with_tooltip::( -// 0, -// "Leave call", -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .into_any(), -// ) -// } else { -// None -// }) -// .constrained() -// .with_height(collab_theme.row_height) -// .contained() -// .with_style(style) -// }, -// ); - -// if is_current_user || is_pending || peer_id.is_none() { -// return content.into_any(); -// } - -// let tooltip = format!("Follow {}", user.github_login); - -// content -// .on_click(MouseButton::Left, move |_, this, cx| { -// if let Some(workspace) = this.workspace.upgrade(cx) { -// workspace -// .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) -// .map(|task| task.detach_and_log_err(cx)); -// } -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .with_tooltip::( -// user.id as usize, -// tooltip, -// Some(Box::new(FollowNextCollaborator)), -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } - -// fn render_participant_project( -// project_id: u64, -// worktree_root_names: &[String], -// host_user_id: u64, -// is_current: bool, -// is_last: bool, -// is_selected: bool, -// theme: &theme::Theme, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum JoinProject {} -// enum JoinProjectTooltip {} - -// let collab_theme = &theme.collab_panel; -// let host_avatar_width = collab_theme -// .contact_avatar -// .width -// .or(collab_theme.contact_avatar.height) -// .unwrap_or(0.); -// let tree_branch = collab_theme.tree_branch; -// let project_name = if worktree_root_names.is_empty() { -// "untitled".to_string() -// } else { -// worktree_root_names.join(", ") -// }; - -// let content = -// MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { -// let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); -// let row = if is_current { -// collab_theme -// .project_row -// .in_state(true) -// .style_for(&mut Default::default()) -// } else { -// collab_theme -// .project_row -// .in_state(is_selected) -// .style_for(mouse_state) -// }; - -// Flex::row() -// .with_child(render_tree_branch( -// tree_branch, -// &row.name.text, -// is_last, -// vec2f(host_avatar_width, collab_theme.row_height), -// cx.font_cache(), -// )) -// .with_child( -// Svg::new("icons/file_icons/folder.svg") -// .with_color(collab_theme.channel_hash.color) -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new(project_name.clone(), row.name.text.clone()) -// .aligned() -// .left() -// .contained() -// .with_style(row.name.container) -// .flex(1., false), -// ) -// .constrained() -// .with_height(collab_theme.row_height) -// .contained() -// .with_style(row.container) -// }); - -// if is_current { -// return content.into_any(); -// } - -// content -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// if let Some(workspace) = this.workspace.upgrade(cx) { -// let app_state = workspace.read(cx).app_state().clone(); -// workspace::join_remote_project(project_id, host_user_id, app_state, cx) -// .detach_and_log_err(cx); -// } -// }) -// .with_tooltip::( -// project_id as usize, -// format!("Open {}", project_name), -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } - -// fn render_participant_screen( -// peer_id: Option, -// is_last: bool, -// is_selected: bool, -// theme: &theme::CollabPanel, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum OpenSharedScreen {} - -// let host_avatar_width = theme -// .contact_avatar -// .width -// .or(theme.contact_avatar.height) -// .unwrap_or(0.); -// let tree_branch = theme.tree_branch; - -// let handler = MouseEventHandler::new::( -// peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, -// cx, -// |mouse_state, cx| { -// let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); -// let row = theme -// .project_row -// .in_state(is_selected) -// .style_for(mouse_state); - -// Flex::row() -// .with_child(render_tree_branch( -// tree_branch, -// &row.name.text, -// is_last, -// vec2f(host_avatar_width, theme.row_height), -// cx.font_cache(), -// )) -// .with_child( -// Svg::new("icons/desktop.svg") -// .with_color(theme.channel_hash.color) -// .constrained() -// .with_width(theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new("Screen", row.name.text.clone()) -// .aligned() -// .left() -// .contained() -// .with_style(row.name.container) -// .flex(1., false), -// ) -// .constrained() -// .with_height(theme.row_height) -// .contained() -// .with_style(row.container) -// }, -// ); -// if peer_id.is_none() { -// return handler.into_any(); -// } -// handler -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// if let Some(workspace) = this.workspace.upgrade(cx) { -// workspace.update(cx, |workspace, cx| { -// workspace.open_shared_screen(peer_id.unwrap(), cx) -// }); -// } -// }) -// .into_any() -// } - -// fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { -// if let Some(_) = self.channel_editing_state.take() { -// self.channel_name_editor.update(cx, |editor, cx| { -// editor.set_text("", cx); -// }); -// true -// } else { -// false -// } -// } - -// fn render_header( -// &self, -// section: Section, -// theme: &theme::Theme, -// is_selected: bool, -// is_collapsed: bool, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum Header {} -// enum LeaveCallContactList {} -// enum AddChannel {} - -// let tooltip_style = &theme.tooltip; -// let mut channel_link = None; -// let mut channel_tooltip_text = None; -// let mut channel_icon = None; -// let mut is_dragged_over = false; - -// let text = match section { -// Section::ActiveCall => { -// let channel_name = maybe!({ -// let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; - -// let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; - -// channel_link = Some(channel.link()); -// (channel_icon, channel_tooltip_text) = match channel.visibility { -// proto::ChannelVisibility::Public => { -// (Some("icons/public.svg"), Some("Copy public channel link.")) -// } -// proto::ChannelVisibility::Members => { -// (Some("icons/hash.svg"), Some("Copy private channel link.")) -// } -// }; - -// Some(channel.name.as_str()) -// }); - -// if let Some(name) = channel_name { -// Cow::Owned(format!("{}", name)) -// } else { -// Cow::Borrowed("Current Call") -// } -// } -// Section::ContactRequests => Cow::Borrowed("Requests"), -// Section::Contacts => Cow::Borrowed("Contacts"), -// Section::Channels => Cow::Borrowed("Channels"), -// Section::ChannelInvites => Cow::Borrowed("Invites"), -// Section::Online => Cow::Borrowed("Online"), -// Section::Offline => Cow::Borrowed("Offline"), -// }; - -// enum AddContact {} -// let button = match section { -// Section::ActiveCall => channel_link.map(|channel_link| { -// let channel_link_copy = channel_link.clone(); -// MouseEventHandler::new::(0, cx, |state, _| { -// render_icon_button( -// theme -// .collab_panel -// .leave_call_button -// .style_for(is_selected, state), -// "icons/link.svg", -// ) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, _, cx| { -// let item = ClipboardItem::new(channel_link_copy.clone()); -// cx.write_to_clipboard(item) -// }) -// .with_tooltip::( -// 0, -// channel_tooltip_text.unwrap(), -// None, -// tooltip_style.clone(), -// cx, -// ) -// }), -// Section::Contacts => Some( -// MouseEventHandler::new::(0, cx, |state, _| { -// render_icon_button( -// theme -// .collab_panel -// .add_contact_button -// .style_for(is_selected, state), -// "icons/plus.svg", -// ) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, |_, this, cx| { -// this.toggle_contact_finder(cx); -// }) -// .with_tooltip::( -// 0, -// "Search for new contact", -// None, -// tooltip_style.clone(), -// cx, -// ), -// ), -// Section::Channels => { -// if cx -// .global::>() -// .currently_dragged::(cx.window()) -// .is_some() -// && self.drag_target_channel == ChannelDragTarget::Root -// { -// is_dragged_over = true; -// } - -// Some( -// MouseEventHandler::new::(0, cx, |state, _| { -// render_icon_button( -// theme -// .collab_panel -// .add_contact_button -// .style_for(is_selected, state), -// "icons/plus.svg", -// ) -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) -// .with_tooltip::( -// 0, -// "Create a channel", -// None, -// tooltip_style.clone(), -// cx, -// ), -// ) -// } -// _ => None, -// }; - -// let can_collapse = match section { -// Section::ActiveCall | Section::Channels | Section::Contacts => false, -// Section::ChannelInvites -// | Section::ContactRequests -// | Section::Online -// | Section::Offline => true, -// }; -// let icon_size = (&theme.collab_panel).section_icon_size; -// let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { -// let header_style = if can_collapse { -// theme -// .collab_panel -// .subheader_row -// .in_state(is_selected) -// .style_for(state) -// } else { -// &theme.collab_panel.header_row -// }; - -// Flex::row() -// .with_children(if can_collapse { -// Some( -// Svg::new(if is_collapsed { -// "icons/chevron_right.svg" -// } else { -// "icons/chevron_down.svg" -// }) -// .with_color(header_style.text.color) -// .constrained() -// .with_max_width(icon_size) -// .with_max_height(icon_size) -// .aligned() -// .constrained() -// .with_width(icon_size) -// .contained() -// .with_margin_right( -// theme.collab_panel.contact_username.container.margin.left, -// ), -// ) -// } else if let Some(channel_icon) = channel_icon { -// Some( -// Svg::new(channel_icon) -// .with_color(header_style.text.color) -// .constrained() -// .with_max_width(icon_size) -// .with_max_height(icon_size) -// .aligned() -// .constrained() -// .with_width(icon_size) -// .contained() -// .with_margin_right( -// theme.collab_panel.contact_username.container.margin.left, -// ), -// ) -// } else { -// None -// }) -// .with_child( -// Label::new(text, header_style.text.clone()) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .with_children(button.map(|button| button.aligned().right())) -// .constrained() -// .with_height(theme.collab_panel.row_height) -// .contained() -// .with_style(if is_dragged_over { -// theme.collab_panel.dragged_over_header -// } else { -// header_style.container -// }) -// }); - -// result = result -// .on_move(move |_, this, cx| { -// if cx -// .global::>() -// .currently_dragged::(cx.window()) -// .is_some() -// { -// this.drag_target_channel = ChannelDragTarget::Root; -// cx.notify() -// } -// }) -// .on_up(MouseButton::Left, move |_, this, cx| { -// if let Some((_, dragged_channel)) = cx -// .global::>() -// .currently_dragged::(cx.window()) -// { -// this.channel_store -// .update(cx, |channel_store, cx| { -// channel_store.move_channel(dragged_channel.id, None, cx) -// }) -// .detach_and_log_err(cx) -// } -// }); - -// if can_collapse { -// result = result -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// if can_collapse { -// this.toggle_section_expanded(section, cx); -// } -// }) -// } - -// result.into_any() -// } - -// fn render_contact( -// contact: &Contact, -// calling: bool, -// project: &ModelHandle, -// theme: &theme::Theme, -// is_selected: bool, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum ContactTooltip {} - -// let collab_theme = &theme.collab_panel; -// let online = contact.online; -// let busy = contact.busy || calling; -// let user_id = contact.user.id; -// let github_login = contact.user.github_login.clone(); -// let initial_project = project.clone(); - -// let event_handler = -// MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { -// Flex::row() -// .with_children(contact.user.avatar.clone().map(|avatar| { -// let status_badge = if contact.online { -// Some( -// Empty::new() -// .collapsed() -// .contained() -// .with_style(if busy { -// collab_theme.contact_status_busy -// } else { -// collab_theme.contact_status_free -// }) -// .aligned(), -// ) -// } else { -// None -// }; -// Stack::new() -// .with_child( -// Image::from_data(avatar) -// .with_style(collab_theme.contact_avatar) -// .aligned() -// .left(), -// ) -// .with_children(status_badge) -// })) -// .with_child( -// Label::new( -// contact.user.github_login.clone(), -// collab_theme.contact_username.text.clone(), -// ) -// .contained() -// .with_style(collab_theme.contact_username.container) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .with_children(if state.hovered() { -// Some( -// MouseEventHandler::new::( -// contact.user.id as usize, -// cx, -// |mouse_state, _| { -// let button_style = -// collab_theme.contact_button.style_for(mouse_state); -// render_icon_button(button_style, "icons/x.svg") -// .aligned() -// .flex_float() -// }, -// ) -// .with_padding(Padding::uniform(2.)) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.remove_contact(user_id, &github_login, cx); -// }) -// .flex_float(), -// ) -// } else { -// None -// }) -// .with_children(if calling { -// Some( -// Label::new("Calling", collab_theme.calling_indicator.text.clone()) -// .contained() -// .with_style(collab_theme.calling_indicator.container) -// .aligned(), -// ) -// } else { -// None -// }) -// .constrained() -// .with_height(collab_theme.row_height) -// .contained() -// .with_style( -// *collab_theme -// .contact_row -// .in_state(is_selected) -// .style_for(state), -// ) -// }); - -// if online && !busy { -// let room = ActiveCall::global(cx).read(cx).room(); -// let label = if room.is_some() { -// format!("Invite {} to join call", contact.user.github_login) -// } else { -// format!("Call {}", contact.user.github_login) -// }; - -// event_handler -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.call(user_id, Some(initial_project.clone()), cx); -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .with_tooltip::( -// contact.user.id as usize, -// label, -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } else { -// event_handler -// .with_tooltip::( -// contact.user.id as usize, -// format!( -// "{} is {}", -// contact.user.github_login, -// if busy { "on a call" } else { "offline" } -// ), -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } -// } - -// fn render_contact_placeholder( -// &self, -// theme: &theme::CollabPanel, -// is_selected: bool, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum AddContacts {} -// MouseEventHandler::new::(0, cx, |state, _| { -// let style = theme.list_empty_state.style_for(is_selected, state); -// Flex::row() -// .with_child( -// Svg::new("icons/plus.svg") -// .with_color(theme.list_empty_icon.color) -// .constrained() -// .with_width(theme.list_empty_icon.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new("Add a contact", style.text.clone()) -// .contained() -// .with_style(theme.list_empty_label_container), -// ) -// .align_children_center() -// .contained() -// .with_style(style.container) -// .into_any() -// }) -// .on_click(MouseButton::Left, |_, this, cx| { -// this.toggle_contact_finder(cx); -// }) -// .into_any() -// } - -// fn render_channel_editor( -// &self, -// theme: &theme::Theme, -// depth: usize, -// cx: &AppContext, -// ) -> AnyElement { -// Flex::row() -// .with_child( -// Empty::new() -// .constrained() -// .with_width(theme.collab_panel.disclosure.button_space()), -// ) -// .with_child( -// Svg::new("icons/hash.svg") -// .with_color(theme.collab_panel.channel_hash.color) -// .constrained() -// .with_width(theme.collab_panel.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// if let Some(pending_name) = self -// .channel_editing_state -// .as_ref() -// .and_then(|state| state.pending_name()) -// { -// Label::new( -// pending_name.to_string(), -// theme.collab_panel.contact_username.text.clone(), -// ) -// .contained() -// .with_style(theme.collab_panel.contact_username.container) -// .aligned() -// .left() -// .flex(1., true) -// .into_any() -// } else { -// ChildView::new(&self.channel_name_editor, cx) -// .aligned() -// .left() -// .contained() -// .with_style(theme.collab_panel.channel_editor) -// .flex(1.0, true) -// .into_any() -// }, -// ) -// .align_children_center() -// .constrained() -// .with_height(theme.collab_panel.row_height) -// .contained() -// .with_style(ContainerStyle { -// background_color: Some(theme.editor.background), -// ..*theme.collab_panel.contact_row.default_style() -// }) -// .with_padding_left( -// theme.collab_panel.contact_row.default_style().padding.left -// + theme.collab_panel.channel_indent * depth as f32, -// ) -// .into_any() -// } - -// fn render_channel( -// &self, -// channel: &Channel, -// depth: usize, -// theme: &theme::Theme, -// is_selected: bool, -// has_children: bool, -// ix: usize, -// cx: &mut ViewContext, -// ) -> AnyElement { -// let channel_id = channel.id; -// let collab_theme = &theme.collab_panel; -// let is_public = self -// .channel_store -// .read(cx) -// .channel_for_id(channel_id) -// .map(|channel| channel.visibility) -// == Some(proto::ChannelVisibility::Public); -// let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); -// let disclosed = -// has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); - -// let is_active = maybe!({ -// let call_channel = ActiveCall::global(cx) -// .read(cx) -// .room()? -// .read(cx) -// .channel_id()?; -// Some(call_channel == channel_id) -// }) -// .unwrap_or(false); - -// const FACEPILE_LIMIT: usize = 3; - -// enum ChannelCall {} -// enum ChannelNote {} -// enum NotesTooltip {} -// enum ChatTooltip {} -// enum ChannelTooltip {} - -// let mut is_dragged_over = false; -// if cx -// .global::>() -// .currently_dragged::(cx.window()) -// .is_some() -// && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) -// { -// is_dragged_over = true; -// } - -// let has_messages_notification = channel.unseen_message_id.is_some(); - -// MouseEventHandler::new::(ix, cx, |state, cx| { -// let row_hovered = state.hovered(); - -// let mut select_state = |interactive: &Interactive| { -// if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { -// interactive.clicked.as_ref().unwrap().clone() -// } else if state.hovered() || other_selected { -// interactive -// .hovered -// .as_ref() -// .unwrap_or(&interactive.default) -// .clone() -// } else { -// interactive.default.clone() -// } -// }; - -// Flex::::row() -// .with_child( -// Svg::new(if is_public { -// "icons/public.svg" -// } else { -// "icons/hash.svg" -// }) -// .with_color(collab_theme.channel_hash.color) -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child({ -// let style = collab_theme.channel_name.inactive_state(); -// Flex::row() -// .with_child( -// Label::new(channel.name.clone(), style.text.clone()) -// .contained() -// .with_style(style.container) -// .aligned() -// .left() -// .with_tooltip::( -// ix, -// "Join channel", -// None, -// theme.tooltip.clone(), -// cx, -// ), -// ) -// .with_children({ -// let participants = -// self.channel_store.read(cx).channel_participants(channel_id); - -// if !participants.is_empty() { -// let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); - -// let result = FacePile::new(collab_theme.face_overlap) -// .with_children( -// participants -// .iter() -// .filter_map(|user| { -// Some( -// Image::from_data(user.avatar.clone()?) -// .with_style(collab_theme.channel_avatar), -// ) -// }) -// .take(FACEPILE_LIMIT), -// ) -// .with_children((extra_count > 0).then(|| { -// Label::new( -// format!("+{}", extra_count), -// collab_theme.extra_participant_label.text.clone(), -// ) -// .contained() -// .with_style(collab_theme.extra_participant_label.container) -// })); - -// Some(result) -// } else { -// None -// } -// }) -// .with_spacing(8.) -// .align_children_center() -// .flex(1., true) -// }) -// .with_child( -// MouseEventHandler::new::(ix, cx, move |mouse_state, _| { -// let container_style = collab_theme -// .disclosure -// .button -// .style_for(mouse_state) -// .container; - -// if channel.unseen_message_id.is_some() { -// Svg::new("icons/conversations.svg") -// .with_color(collab_theme.channel_note_active_color) -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .contained() -// .with_style(container_style) -// .with_uniform_padding(4.) -// .into_any() -// } else if row_hovered { -// Svg::new("icons/conversations.svg") -// .with_color(collab_theme.channel_hash.color) -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .contained() -// .with_style(container_style) -// .with_uniform_padding(4.) -// .into_any() -// } else { -// Empty::new().into_any() -// } -// }) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.join_channel_chat(&JoinChannelChat { channel_id }, cx); -// }) -// .with_tooltip::( -// ix, -// "Open channel chat", -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .contained() -// .with_margin_right(4.), -// ) -// .with_child( -// MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { -// let container_style = collab_theme -// .disclosure -// .button -// .style_for(mouse_state) -// .container; -// if row_hovered || channel.unseen_note_version.is_some() { -// Svg::new("icons/file.svg") -// .with_color(if channel.unseen_note_version.is_some() { -// collab_theme.channel_note_active_color -// } else { -// collab_theme.channel_hash.color -// }) -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .contained() -// .with_style(container_style) -// .with_uniform_padding(4.) -// .with_margin_right(collab_theme.channel_hash.container.margin.left) -// .with_tooltip::( -// ix as usize, -// "Open channel notes", -// None, -// theme.tooltip.clone(), -// cx, -// ) -// .into_any() -// } else if has_messages_notification { -// Empty::new() -// .constrained() -// .with_width(collab_theme.channel_hash.width) -// .contained() -// .with_uniform_padding(4.) -// .with_margin_right(collab_theme.channel_hash.container.margin.left) -// .into_any() -// } else { -// Empty::new().into_any() -// } -// }) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); -// }), -// ) -// .align_children_center() -// .styleable_component() -// .disclosable( -// disclosed, -// Box::new(ToggleCollapse { -// location: channel.id.clone(), -// }), -// ) -// .with_id(ix) -// .with_style(collab_theme.disclosure.clone()) -// .element() -// .constrained() -// .with_height(collab_theme.row_height) -// .contained() -// .with_style(select_state( -// collab_theme -// .channel_row -// .in_state(is_selected || is_active || is_dragged_over), -// )) -// .with_padding_left( -// collab_theme.channel_row.default_style().padding.left -// + collab_theme.channel_indent * depth as f32, -// ) -// }) -// .on_click(MouseButton::Left, move |_, this, cx| { -// if this.drag_target_channel == ChannelDragTarget::None { -// if is_active { -// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) -// } else { -// this.join_channel(channel_id, cx) -// } -// } -// }) -// .on_click(MouseButton::Right, { -// let channel = channel.clone(); -// move |e, this, cx| { -// this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); -// } -// }) -// .on_up(MouseButton::Left, move |_, this, cx| { -// if let Some((_, dragged_channel)) = cx -// .global::>() -// .currently_dragged::(cx.window()) -// { -// this.channel_store -// .update(cx, |channel_store, cx| { -// channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) -// }) -// .detach_and_log_err(cx) -// } -// }) -// .on_move({ -// let channel = channel.clone(); -// move |_, this, cx| { -// if let Some((_, dragged_channel)) = cx -// .global::>() -// .currently_dragged::(cx.window()) -// { -// if channel.id != dragged_channel.id { -// this.drag_target_channel = ChannelDragTarget::Channel(channel.id); -// } -// cx.notify() -// } -// } -// }) -// .as_draggable::<_, Channel>( -// channel.clone(), -// move |_, channel, cx: &mut ViewContext| { -// let theme = &theme::current(cx).collab_panel; - -// Flex::::row() -// .with_child( -// Svg::new("icons/hash.svg") -// .with_color(theme.channel_hash.color) -// .constrained() -// .with_width(theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new(channel.name.clone(), theme.channel_name.text.clone()) -// .contained() -// .with_style(theme.channel_name.container) -// .aligned() -// .left(), -// ) -// .align_children_center() -// .contained() -// .with_background_color( -// theme -// .container -// .background_color -// .unwrap_or(gpui::color::Color::transparent_black()), -// ) -// .contained() -// .with_padding_left( -// theme.channel_row.default_style().padding.left -// + theme.channel_indent * depth as f32, -// ) -// .into_any() -// }, -// ) -// .with_cursor_style(CursorStyle::PointingHand) -// .into_any() -// } - -// fn render_channel_notes( -// &self, -// channel_id: ChannelId, -// theme: &theme::CollabPanel, -// is_selected: bool, -// ix: usize, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum ChannelNotes {} -// let host_avatar_width = theme -// .contact_avatar -// .width -// .or(theme.contact_avatar.height) -// .unwrap_or(0.); - -// MouseEventHandler::new::(ix as usize, cx, |state, cx| { -// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); -// let row = theme.project_row.in_state(is_selected).style_for(state); - -// Flex::::row() -// .with_child(render_tree_branch( -// tree_branch, -// &row.name.text, -// false, -// vec2f(host_avatar_width, theme.row_height), -// cx.font_cache(), -// )) -// .with_child( -// Svg::new("icons/file.svg") -// .with_color(theme.channel_hash.color) -// .constrained() -// .with_width(theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new("notes", theme.channel_name.text.clone()) -// .contained() -// .with_style(theme.channel_name.container) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .constrained() -// .with_height(theme.row_height) -// .contained() -// .with_style(*theme.channel_row.style_for(is_selected, state)) -// .with_padding_left(theme.channel_row.default_style().padding.left) -// }) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .into_any() -// } - -// fn render_channel_chat( -// &self, -// channel_id: ChannelId, -// theme: &theme::CollabPanel, -// is_selected: bool, -// ix: usize, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum ChannelChat {} -// let host_avatar_width = theme -// .contact_avatar -// .width -// .or(theme.contact_avatar.height) -// .unwrap_or(0.); - -// MouseEventHandler::new::(ix as usize, cx, |state, cx| { -// let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); -// let row = theme.project_row.in_state(is_selected).style_for(state); - -// Flex::::row() -// .with_child(render_tree_branch( -// tree_branch, -// &row.name.text, -// true, -// vec2f(host_avatar_width, theme.row_height), -// cx.font_cache(), -// )) -// .with_child( -// Svg::new("icons/conversations.svg") -// .with_color(theme.channel_hash.color) -// .constrained() -// .with_width(theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new("chat", theme.channel_name.text.clone()) -// .contained() -// .with_style(theme.channel_name.container) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .constrained() -// .with_height(theme.row_height) -// .contained() -// .with_style(*theme.channel_row.style_for(is_selected, state)) -// .with_padding_left(theme.channel_row.default_style().padding.left) -// }) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.join_channel_chat(&JoinChannelChat { channel_id }, cx); -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .into_any() -// } - -// fn render_channel_invite( -// channel: Arc, -// channel_store: ModelHandle, -// theme: &theme::CollabPanel, -// is_selected: bool, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum Decline {} -// enum Accept {} - -// let channel_id = channel.id; -// let is_invite_pending = channel_store -// .read(cx) -// .has_pending_channel_invite_response(&channel); -// let button_spacing = theme.contact_button_spacing; - -// Flex::row() -// .with_child( -// Svg::new("icons/hash.svg") -// .with_color(theme.channel_hash.color) -// .constrained() -// .with_width(theme.channel_hash.width) -// .aligned() -// .left(), -// ) -// .with_child( -// Label::new(channel.name.clone(), theme.contact_username.text.clone()) -// .contained() -// .with_style(theme.contact_username.container) -// .aligned() -// .left() -// .flex(1., true), -// ) -// .with_child( -// MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { -// let button_style = if is_invite_pending { -// &theme.disabled_button -// } else { -// theme.contact_button.style_for(mouse_state) -// }; -// render_icon_button(button_style, "icons/x.svg").aligned() -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.respond_to_channel_invite(channel_id, false, cx); -// }) -// .contained() -// .with_margin_right(button_spacing), -// ) -// .with_child( -// MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { -// let button_style = if is_invite_pending { -// &theme.disabled_button -// } else { -// theme.contact_button.style_for(mouse_state) -// }; -// render_icon_button(button_style, "icons/check.svg") -// .aligned() -// .flex_float() -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.respond_to_channel_invite(channel_id, true, cx); -// }), -// ) -// .constrained() -// .with_height(theme.row_height) -// .contained() -// .with_style( -// *theme -// .contact_row -// .in_state(is_selected) -// .style_for(&mut Default::default()), -// ) -// .with_padding_left( -// theme.contact_row.default_style().padding.left + theme.channel_indent, -// ) -// .into_any() -// } - -// fn render_contact_request( -// user: Arc, -// user_store: ModelHandle, -// theme: &theme::CollabPanel, -// is_incoming: bool, -// is_selected: bool, -// cx: &mut ViewContext, -// ) -> AnyElement { -// enum Decline {} -// enum Accept {} -// enum Cancel {} - -// let mut row = Flex::row() -// .with_children(user.avatar.clone().map(|avatar| { -// Image::from_data(avatar) -// .with_style(theme.contact_avatar) -// .aligned() -// .left() -// })) -// .with_child( -// Label::new( -// user.github_login.clone(), -// theme.contact_username.text.clone(), -// ) -// .contained() -// .with_style(theme.contact_username.container) -// .aligned() -// .left() -// .flex(1., true), -// ); - -// let user_id = user.id; -// let github_login = user.github_login.clone(); -// let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); -// let button_spacing = theme.contact_button_spacing; - -// if is_incoming { -// row.add_child( -// MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { -// let button_style = if is_contact_request_pending { -// &theme.disabled_button -// } else { -// theme.contact_button.style_for(mouse_state) -// }; -// render_icon_button(button_style, "icons/x.svg").aligned() -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.respond_to_contact_request(user_id, false, cx); -// }) -// .contained() -// .with_margin_right(button_spacing), -// ); - -// row.add_child( -// MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { -// let button_style = if is_contact_request_pending { -// &theme.disabled_button -// } else { -// theme.contact_button.style_for(mouse_state) -// }; -// render_icon_button(button_style, "icons/check.svg") -// .aligned() -// .flex_float() -// }) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.respond_to_contact_request(user_id, true, cx); -// }), -// ); -// } else { -// row.add_child( -// MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { -// let button_style = if is_contact_request_pending { -// &theme.disabled_button -// } else { -// theme.contact_button.style_for(mouse_state) -// }; -// render_icon_button(button_style, "icons/x.svg") -// .aligned() -// .flex_float() -// }) -// .with_padding(Padding::uniform(2.)) -// .with_cursor_style(CursorStyle::PointingHand) -// .on_click(MouseButton::Left, move |_, this, cx| { -// this.remove_contact(user_id, &github_login, cx); -// }) -// .flex_float(), -// ); -// } - -// row.constrained() -// .with_height(theme.row_height) -// .contained() -// .with_style( -// *theme -// .contact_row -// .in_state(is_selected) -// .style_for(&mut Default::default()), -// ) -// .into_any() -// } - -// fn has_subchannels(&self, ix: usize) -> bool { -// self.entries.get(ix).map_or(false, |entry| { -// if let ListEntry::Channel { has_children, .. } = entry { -// *has_children -// } else { -// false -// } -// }) -// } - -// fn deploy_channel_context_menu( -// &mut self, -// position: Option, -// channel: &Channel, -// ix: usize, -// cx: &mut ViewContext, -// ) { -// self.context_menu_on_selected = position.is_none(); - -// let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { -// self.channel_store -// .read(cx) -// .channel_for_id(clipboard.channel_id) -// .map(|channel| channel.name.clone()) -// }); - -// self.context_menu.update(cx, |context_menu, cx| { -// context_menu.set_position_mode(if self.context_menu_on_selected { -// OverlayPositionMode::Local -// } else { -// OverlayPositionMode::Window -// }); - -// let mut items = Vec::new(); - -// let select_action_name = if self.selection == Some(ix) { -// "Unselect" -// } else { -// "Select" -// }; - -// items.push(ContextMenuItem::action( -// select_action_name, -// ToggleSelectedIx { ix }, -// )); - -// if self.has_subchannels(ix) { -// let expand_action_name = if self.is_channel_collapsed(channel.id) { -// "Expand Subchannels" -// } else { -// "Collapse Subchannels" -// }; -// items.push(ContextMenuItem::action( -// expand_action_name, -// ToggleCollapse { -// location: channel.id, -// }, -// )); -// } - -// items.push(ContextMenuItem::action( -// "Open Notes", -// OpenChannelNotes { -// channel_id: channel.id, -// }, -// )); - -// items.push(ContextMenuItem::action( -// "Open Chat", -// JoinChannelChat { -// channel_id: channel.id, -// }, -// )); - -// items.push(ContextMenuItem::action( -// "Copy Channel Link", -// CopyChannelLink { -// channel_id: channel.id, -// }, -// )); - -// if self.channel_store.read(cx).is_channel_admin(channel.id) { -// items.extend([ -// ContextMenuItem::Separator, -// ContextMenuItem::action( -// "New Subchannel", -// NewChannel { -// location: channel.id, -// }, -// ), -// ContextMenuItem::action( -// "Rename", -// RenameChannel { -// channel_id: channel.id, -// }, -// ), -// ContextMenuItem::action( -// "Move this channel", -// StartMoveChannelFor { -// channel_id: channel.id, -// }, -// ), -// ]); - -// if let Some(channel_name) = clipboard_channel_name { -// items.push(ContextMenuItem::Separator); -// items.push(ContextMenuItem::action( -// format!("Move '#{}' here", channel_name), -// MoveChannel { to: channel.id }, -// )); -// } - -// items.extend([ -// ContextMenuItem::Separator, -// ContextMenuItem::action( -// "Invite Members", -// InviteMembers { -// channel_id: channel.id, -// }, -// ), -// ContextMenuItem::action( -// "Manage Members", -// ManageMembers { -// channel_id: channel.id, -// }, -// ), -// ContextMenuItem::Separator, -// ContextMenuItem::action( -// "Delete", -// RemoveChannel { -// channel_id: channel.id, -// }, -// ), -// ]); -// } - -// context_menu.show( -// position.unwrap_or_default(), -// if self.context_menu_on_selected { -// gpui::elements::AnchorCorner::TopRight -// } else { -// gpui::elements::AnchorCorner::BottomLeft -// }, -// items, -// cx, -// ); -// }); - -// cx.notify(); -// } - -// fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { -// if self.take_editing_state(cx) { -// cx.focus(&self.filter_editor); -// } else { -// self.filter_editor.update(cx, |editor, cx| { -// if editor.buffer().read(cx).len(cx) > 0 { -// editor.set_text("", cx); -// } -// }); -// } - -// self.update_entries(false, cx); -// } - -// fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { -// let ix = self.selection.map_or(0, |ix| ix + 1); -// if ix < self.entries.len() { -// self.selection = Some(ix); -// } - -// self.list_state.reset(self.entries.len()); -// if let Some(ix) = self.selection { -// self.list_state.scroll_to(ListOffset { -// item_ix: ix, -// offset_in_item: 0., -// }); -// } -// cx.notify(); -// } - -// fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { -// let ix = self.selection.take().unwrap_or(0); -// if ix > 0 { -// self.selection = Some(ix - 1); -// } - -// self.list_state.reset(self.entries.len()); -// if let Some(ix) = self.selection { -// self.list_state.scroll_to(ListOffset { -// item_ix: ix, -// offset_in_item: 0., -// }); -// } -// cx.notify(); -// } - -// fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { -// if self.confirm_channel_edit(cx) { -// return; -// } - -// if let Some(selection) = self.selection { -// if let Some(entry) = self.entries.get(selection) { -// match entry { -// ListEntry::Header(section) => match section { -// Section::ActiveCall => Self::leave_call(cx), -// Section::Channels => self.new_root_channel(cx), -// Section::Contacts => self.toggle_contact_finder(cx), -// Section::ContactRequests -// | Section::Online -// | Section::Offline -// | Section::ChannelInvites => { -// self.toggle_section_expanded(*section, cx); -// } -// }, -// ListEntry::Contact { contact, calling } => { -// if contact.online && !contact.busy && !calling { -// self.call(contact.user.id, Some(self.project.clone()), cx); -// } -// } -// ListEntry::ParticipantProject { -// project_id, -// host_user_id, -// .. -// } => { -// if let Some(workspace) = self.workspace.upgrade(cx) { -// let app_state = workspace.read(cx).app_state().clone(); -// workspace::join_remote_project( -// *project_id, -// *host_user_id, -// app_state, -// cx, -// ) -// .detach_and_log_err(cx); -// } -// } -// ListEntry::ParticipantScreen { peer_id, .. } => { -// let Some(peer_id) = peer_id else { -// return; -// }; -// if let Some(workspace) = self.workspace.upgrade(cx) { -// workspace.update(cx, |workspace, cx| { -// workspace.open_shared_screen(*peer_id, cx) -// }); -// } -// } -// ListEntry::Channel { channel, .. } => { -// let is_active = maybe!({ -// let call_channel = ActiveCall::global(cx) -// .read(cx) -// .room()? -// .read(cx) -// .channel_id()?; - -// Some(call_channel == channel.id) -// }) -// .unwrap_or(false); -// if is_active { -// self.open_channel_notes( -// &OpenChannelNotes { -// channel_id: channel.id, -// }, -// cx, -// ) -// } else { -// self.join_channel(channel.id, cx) -// } -// } -// ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), -// _ => {} -// } -// } -// } -// } - -// fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { -// if self.channel_editing_state.is_some() { -// self.channel_name_editor.update(cx, |editor, cx| { -// editor.insert(" ", cx); -// }); -// } -// } - -// fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { -// if let Some(editing_state) = &mut self.channel_editing_state { -// match editing_state { -// ChannelEditingState::Create { -// location, -// pending_name, -// .. -// } => { -// if pending_name.is_some() { -// return false; -// } -// let channel_name = self.channel_name_editor.read(cx).text(cx); - -// *pending_name = Some(channel_name.clone()); - -// self.channel_store -// .update(cx, |channel_store, cx| { -// channel_store.create_channel(&channel_name, *location, cx) -// }) -// .detach(); -// cx.notify(); -// } -// ChannelEditingState::Rename { -// location, -// pending_name, -// } => { -// if pending_name.is_some() { -// return false; -// } -// let channel_name = self.channel_name_editor.read(cx).text(cx); -// *pending_name = Some(channel_name.clone()); - -// self.channel_store -// .update(cx, |channel_store, cx| { -// channel_store.rename(*location, &channel_name, cx) -// }) -// .detach(); -// cx.notify(); -// } -// } -// cx.focus_self(); -// true -// } else { -// false -// } -// } - -// fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { -// if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { -// self.collapsed_sections.remove(ix); -// } else { -// self.collapsed_sections.push(section); -// } -// self.update_entries(false, cx); -// } - -// fn collapse_selected_channel( -// &mut self, -// _: &CollapseSelectedChannel, -// cx: &mut ViewContext, -// ) { -// let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { -// return; -// }; - -// if self.is_channel_collapsed(channel_id) { -// return; -// } - -// self.toggle_channel_collapsed(channel_id, cx); -// } - -// fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { -// let Some(id) = self.selected_channel().map(|channel| channel.id) else { -// return; -// }; - -// if !self.is_channel_collapsed(id) { -// return; -// } - -// self.toggle_channel_collapsed(id, cx) -// } - -// fn toggle_channel_collapsed_action( -// &mut self, -// action: &ToggleCollapse, -// cx: &mut ViewContext, -// ) { -// self.toggle_channel_collapsed(action.location, cx); -// } - -// fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { -// match self.collapsed_channels.binary_search(&channel_id) { -// Ok(ix) => { -// self.collapsed_channels.remove(ix); -// } -// Err(ix) => { -// self.collapsed_channels.insert(ix, channel_id); -// } -// }; -// self.serialize(cx); -// self.update_entries(true, cx); -// cx.notify(); -// cx.focus_self(); -// } - -// fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { -// self.collapsed_channels.binary_search(&channel_id).is_ok() -// } - -// fn leave_call(cx: &mut ViewContext) { -// ActiveCall::global(cx) -// .update(cx, |call, cx| call.hang_up(cx)) -// .detach_and_log_err(cx); -// } - -// fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { -// if let Some(workspace) = self.workspace.upgrade(cx) { -// workspace.update(cx, |workspace, cx| { -// workspace.toggle_modal(cx, |_, cx| { -// cx.add_view(|cx| { -// let mut finder = ContactFinder::new(self.user_store.clone(), cx); -// finder.set_query(self.filter_editor.read(cx).text(cx), cx); -// finder -// }) -// }); -// }); -// } -// } +actions!( + ToggleFocus, + Remove, + Secondary, + CollapseSelectedChannel, + ExpandSelectedChannel, + StartMoveChannel, + MoveSelected, + InsertSpace, +); -// fn new_root_channel(&mut self, cx: &mut ViewContext) { -// self.channel_editing_state = Some(ChannelEditingState::Create { -// location: None, -// pending_name: None, -// }); -// self.update_entries(false, cx); -// self.select_channel_editor(); -// cx.focus(self.channel_name_editor.as_any()); -// cx.notify(); -// } - -// fn select_channel_editor(&mut self) { -// self.selection = self.entries.iter().position(|entry| match entry { -// ListEntry::ChannelEditor { .. } => true, -// _ => false, -// }); -// } +// impl_actions!( +// collab_panel, +// [ +// RemoveChannel, +// NewChannel, +// InviteMembers, +// ManageMembers, +// RenameChannel, +// ToggleCollapse, +// OpenChannelNotes, +// JoinChannelCall, +// JoinChannelChat, +// CopyChannelLink, +// StartMoveChannelFor, +// MoveChannel, +// ToggleSelectedIx +// ] +// ); -// fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { -// self.collapsed_channels -// .retain(|channel| *channel != action.location); -// self.channel_editing_state = Some(ChannelEditingState::Create { -// location: Some(action.location.to_owned()), -// pending_name: None, -// }); -// self.update_entries(false, cx); -// self.select_channel_editor(); -// cx.focus(self.channel_name_editor.as_any()); -// cx.notify(); -// } +// #[derive(Debug, Copy, Clone, PartialEq, Eq)] +// struct ChannelMoveClipboard { +// channel_id: ChannelId, +// } -// fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext) { -// self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); -// } +// const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; -// fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { -// self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); -// } +use std::sync::Arc; + +use gpui::{ + actions, div, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, Focusable, + InteractiveComponent, ParentComponent, Render, Task, View, ViewContext, VisualContext, + WeakView, +}; +use project::Fs; +use settings::Settings; +use workspace::{ + dock::{DockPosition, Panel, PanelEvent}, + Workspace, +}; + +use crate::CollaborationPanelSettings; + +pub fn init(cx: &mut AppContext) { + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }) + .detach(); + // contact_finder::init(cx); + // channel_modal::init(cx); + // channel_view::init(cx); + + // cx.add_action(CollabPanel::cancel); + // cx.add_action(CollabPanel::select_next); + // cx.add_action(CollabPanel::select_prev); + // cx.add_action(CollabPanel::confirm); + // cx.add_action(CollabPanel::insert_space); + // cx.add_action(CollabPanel::remove); + // cx.add_action(CollabPanel::remove_selected_channel); + // cx.add_action(CollabPanel::show_inline_context_menu); + // cx.add_action(CollabPanel::new_subchannel); + // cx.add_action(CollabPanel::invite_members); + // cx.add_action(CollabPanel::manage_members); + // cx.add_action(CollabPanel::rename_selected_channel); + // cx.add_action(CollabPanel::rename_channel); + // cx.add_action(CollabPanel::toggle_channel_collapsed_action); + // cx.add_action(CollabPanel::collapse_selected_channel); + // cx.add_action(CollabPanel::expand_selected_channel); + // cx.add_action(CollabPanel::open_channel_notes); + // cx.add_action(CollabPanel::join_channel_chat); + // cx.add_action(CollabPanel::copy_channel_link); + + // cx.add_action( + // |panel: &mut CollabPanel, action: &ToggleSelectedIx, cx: &mut ViewContext| { + // if panel.selection.take() != Some(action.ix) { + // panel.selection = Some(action.ix) + // } + + // cx.notify(); + // }, + // ); + + // cx.add_action( + // |panel: &mut CollabPanel, + // action: &StartMoveChannelFor, + // _: &mut ViewContext| { + // panel.channel_clipboard = Some(ChannelMoveClipboard { + // channel_id: action.channel_id, + // }); + // }, + // ); + + // cx.add_action( + // |panel: &mut CollabPanel, _: &StartMoveChannel, _: &mut ViewContext| { + // if let Some(channel) = panel.selected_channel() { + // panel.channel_clipboard = Some(ChannelMoveClipboard { + // channel_id: channel.id, + // }) + // } + // }, + // ); + + // cx.add_action( + // |panel: &mut CollabPanel, _: &MoveSelected, cx: &mut ViewContext| { + // let Some(clipboard) = panel.channel_clipboard.take() else { + // return; + // }; + // let Some(selected_channel) = panel.selected_channel() else { + // return; + // }; + + // panel + // .channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(clipboard.channel_id, Some(selected_channel.id), cx) + // }) + // .detach_and_log_err(cx) + // }, + // ); + + // cx.add_action( + // |panel: &mut CollabPanel, action: &MoveChannel, cx: &mut ViewContext| { + // if let Some(clipboard) = panel.channel_clipboard.take() { + // panel.channel_store.update(cx, |channel_store, cx| { + // channel_store + // .move_channel(clipboard.channel_id, Some(action.to), cx) + // .detach_and_log_err(cx) + // }) + // } + // }, + // ); +} -// fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { -// if let Some(channel) = self.selected_channel() { -// self.remove_channel(channel.id, cx) -// } -// } +// #[derive(Debug)] +// pub enum ChannelEditingState { +// Create { +// location: Option, +// pending_name: Option, +// }, +// Rename { +// location: ChannelId, +// pending_name: Option, +// }, +// } -// fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { -// if let Some(channel) = self.selected_channel() { -// self.rename_channel( -// &RenameChannel { -// channel_id: channel.id, -// }, -// cx, -// ); +// impl ChannelEditingState { +// fn pending_name(&self) -> Option<&str> { +// match self { +// ChannelEditingState::Create { pending_name, .. } => pending_name.as_deref(), +// ChannelEditingState::Rename { pending_name, .. } => pending_name.as_deref(), // } // } +// } -// fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { -// let channel_store = self.channel_store.read(cx); -// if !channel_store.is_channel_admin(action.channel_id) { -// return; -// } -// if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { -// self.channel_editing_state = Some(ChannelEditingState::Rename { -// location: action.channel_id.to_owned(), -// pending_name: None, -// }); -// self.channel_name_editor.update(cx, |editor, cx| { -// editor.set_text(channel.name.clone(), cx); -// editor.select_all(&Default::default(), cx); -// }); -// cx.focus(self.channel_name_editor.as_any()); -// self.update_entries(false, cx); -// self.select_channel_editor(); -// } -// } +pub struct CollabPanel { + width: Option, + fs: Arc, + focus_handle: FocusHandle, + // channel_clipboard: Option, + // pending_serialization: Task>, + // context_menu: ViewHandle, + // filter_editor: ViewHandle, + // channel_name_editor: ViewHandle, + // channel_editing_state: Option, + // entries: Vec, + // selection: Option, + // user_store: ModelHandle, + // client: Arc, + // channel_store: ModelHandle, + // project: ModelHandle, + // match_candidates: Vec, + // list_state: ListState, + // subscriptions: Vec, + // collapsed_sections: Vec
, + // collapsed_channels: Vec, + // drag_target_channel: ChannelDragTarget, + _workspace: WeakView, + // context_menu_on_selected: bool, +} -// fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { -// if let Some(workspace) = self.workspace.upgrade(cx) { -// ChannelView::open(action.channel_id, workspace, cx).detach(); -// } -// } +// #[derive(PartialEq, Eq)] +// enum ChannelDragTarget { +// None, +// Root, +// Channel(ChannelId), +// } -// fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { -// let Some(channel) = self.selected_channel() else { -// return; -// }; +// #[derive(Serialize, Deserialize)] +// struct SerializedCollabPanel { +// width: Option, +// collapsed_channels: Option>, +// } -// self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); -// } +// #[derive(Debug)] +// pub enum Event { +// DockPositionChanged, +// Focus, +// Dismissed, +// } -// fn selected_channel(&self) -> Option<&Arc> { -// self.selection -// .and_then(|ix| self.entries.get(ix)) -// .and_then(|entry| match entry { -// ListEntry::Channel { channel, .. } => Some(channel), -// _ => None, -// }) -// } +// #[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +// enum Section { +// ActiveCall, +// Channels, +// ChannelInvites, +// ContactRequests, +// Contacts, +// Online, +// Offline, +// } -// fn show_channel_modal( -// &mut self, +// #[derive(Clone, Debug)] +// enum ListEntry { +// Header(Section), +// CallParticipant { +// user: Arc, +// peer_id: Option, +// is_pending: bool, +// }, +// ParticipantProject { +// project_id: u64, +// worktree_root_names: Vec, +// host_user_id: u64, +// is_last: bool, +// }, +// ParticipantScreen { +// peer_id: Option, +// is_last: bool, +// }, +// IncomingRequest(Arc), +// OutgoingRequest(Arc), +// ChannelInvite(Arc), +// Channel { +// channel: Arc, +// depth: usize, +// has_children: bool, +// }, +// ChannelNotes { // channel_id: ChannelId, -// mode: channel_modal::Mode, -// cx: &mut ViewContext, -// ) { -// let workspace = self.workspace.clone(); -// let user_store = self.user_store.clone(); -// let channel_store = self.channel_store.clone(); -// let members = self.channel_store.update(cx, |channel_store, cx| { -// channel_store.get_channel_member_details(channel_id, cx) -// }); - -// cx.spawn(|_, mut cx| async move { -// let members = members.await?; -// workspace.update(&mut cx, |workspace, cx| { -// workspace.toggle_modal(cx, |_, cx| { -// cx.add_view(|cx| { -// ChannelModal::new( -// user_store.clone(), -// channel_store.clone(), -// channel_id, -// mode, -// members, -// cx, -// ) -// }) -// }); -// }) -// }) -// .detach(); -// } - -// fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { -// self.remove_channel(action.channel_id, cx) -// } - -// fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { -// let channel_store = self.channel_store.clone(); -// if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { -// let prompt_message = format!( -// "Are you sure you want to remove the channel \"{}\"?", -// channel.name -// ); -// let mut answer = -// cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); -// let window = cx.window(); -// cx.spawn(|this, mut cx| async move { -// if answer.next().await == Some(0) { -// if let Err(e) = channel_store -// .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) -// .await -// { -// window.prompt( -// PromptLevel::Info, -// &format!("Failed to remove channel: {}", e), -// &["Ok"], -// &mut cx, -// ); -// } -// this.update(&mut cx, |_, cx| cx.focus_self()).ok(); -// } -// }) -// .detach(); -// } -// } - -// // Should move to the filter editor if clicking on it -// // Should move selection to the channel editor if activating it - -// fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { -// let user_store = self.user_store.clone(); -// let prompt_message = format!( -// "Are you sure you want to remove \"{}\" from your contacts?", -// github_login -// ); -// let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); -// let window = cx.window(); -// cx.spawn(|_, mut cx| async move { -// if answer.next().await == Some(0) { -// if let Err(e) = user_store -// .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) -// .await -// { -// window.prompt( -// PromptLevel::Info, -// &format!("Failed to remove contact: {}", e), -// &["Ok"], -// &mut cx, -// ); -// } -// } -// }) -// .detach(); -// } - -// fn respond_to_contact_request( -// &mut self, -// user_id: u64, -// accept: bool, -// cx: &mut ViewContext, -// ) { -// self.user_store -// .update(cx, |store, cx| { -// store.respond_to_contact_request(user_id, accept, cx) -// }) -// .detach(); -// } - -// fn respond_to_channel_invite( -// &mut self, -// channel_id: u64, -// accept: bool, -// cx: &mut ViewContext, -// ) { -// self.channel_store -// .update(cx, |store, cx| { -// store.respond_to_channel_invite(channel_id, accept, cx) -// }) -// .detach(); -// } - -// fn call( -// &mut self, -// recipient_user_id: u64, -// initial_project: Option>, -// cx: &mut ViewContext, -// ) { -// ActiveCall::global(cx) -// .update(cx, |call, cx| { -// call.invite(recipient_user_id, initial_project, cx) -// }) -// .detach_and_log_err(cx); -// } - -// fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { -// let Some(workspace) = self.workspace.upgrade(cx) else { -// return; -// }; -// let Some(handle) = cx.window().downcast::() else { -// return; -// }; -// workspace::join_channel( -// channel_id, -// workspace.read(cx).app_state().clone(), -// Some(handle), -// cx, -// ) -// .detach_and_log_err(cx) -// } - -// fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { -// let channel_id = action.channel_id; -// if let Some(workspace) = self.workspace.upgrade(cx) { -// cx.app_context().defer(move |cx| { -// workspace.update(cx, |workspace, cx| { -// if let Some(panel) = workspace.focus_panel::(cx) { -// panel.update(cx, |panel, cx| { -// panel -// .select_channel(channel_id, None, cx) -// .detach_and_log_err(cx); -// }); -// } -// }); -// }); -// } -// } +// }, +// ChannelChat { +// channel_id: ChannelId, +// }, +// ChannelEditor { +// depth: usize, +// }, +// Contact { +// contact: Arc, +// calling: bool, +// }, +// ContactPlaceholder, +// } -// fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { -// let channel_store = self.channel_store.read(cx); -// let Some(channel) = channel_store.channel_for_id(action.channel_id) else { -// return; -// }; -// let item = ClipboardItem::new(channel.link()); -// cx.write_to_clipboard(item) -// } +// impl Entity for CollabPanel { +// type Event = Event; // } +impl CollabPanel { + pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> View { + cx.build_view(|cx| { + // let view_id = cx.view_id(); + + // let filter_editor = cx.add_view(|cx| { + // let mut editor = Editor::single_line( + // Some(Arc::new(|theme| { + // theme.collab_panel.user_query_editor.clone() + // })), + // cx, + // ); + // editor.set_placeholder_text("Filter channels, contacts", cx); + // editor + // }); + + // cx.subscribe(&filter_editor, |this, _, event, cx| { + // if let editor::Event::BufferEdited = event { + // let query = this.filter_editor.read(cx).text(cx); + // if !query.is_empty() { + // this.selection.take(); + // } + // this.update_entries(true, cx); + // if !query.is_empty() { + // this.selection = this + // .entries + // .iter() + // .position(|entry| !matches!(entry, ListEntry::Header(_))); + // } + // } else if let editor::Event::Blurred = event { + // let query = this.filter_editor.read(cx).text(cx); + // if query.is_empty() { + // this.selection.take(); + // this.update_entries(true, cx); + // } + // } + // }) + // .detach(); + + // let channel_name_editor = cx.add_view(|cx| { + // Editor::single_line( + // Some(Arc::new(|theme| { + // theme.collab_panel.user_query_editor.clone() + // })), + // cx, + // ) + // }); + + // cx.subscribe(&channel_name_editor, |this, _, event, cx| { + // if let editor::Event::Blurred = event { + // if let Some(state) = &this.channel_editing_state { + // if state.pending_name().is_some() { + // return; + // } + // } + // this.take_editing_state(cx); + // this.update_entries(false, cx); + // cx.notify(); + // } + // }) + // .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 this = Self { + width: None, + focus_handle: cx.focus_handle(), + // channel_clipboard: None, + fs: workspace.app_state().fs.clone(), + // pending_serialization: Task::ready(None), + // context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), + // channel_name_editor, + // filter_editor, + // entries: Vec::default(), + // channel_editing_state: None, + // selection: None, + // user_store: workspace.user_store().clone(), + // channel_store: ChannelStore::global(cx), + // project: workspace.project().clone(), + // subscriptions: Vec::default(), + // match_candidates: Vec::default(), + // collapsed_sections: vec![Section::Offline], + // collapsed_channels: Vec::default(), + _workspace: workspace.weak_handle(), + // client: workspace.app_state().client.clone(), + // context_menu_on_selected: true, + // drag_target_channel: ChannelDragTarget::None, + // list_state, + }; + + // this.update_entries(false, cx); + + // // Update the dock position when the setting changes. + // let mut old_dock_position = this.position(cx); + // this.subscriptions + // .push( + // cx.observe_global::(move |this: &mut Self, cx| { + // let new_dock_position = this.position(cx); + // if new_dock_position != old_dock_position { + // old_dock_position = new_dock_position; + // cx.emit(Event::DockPositionChanged); + // } + // cx.notify(); + // }), + // ); + + // let active_call = ActiveCall::global(cx); + // this.subscriptions + // .push(cx.observe(&this.user_store, |this, _, cx| { + // this.update_entries(true, cx) + // })); + // this.subscriptions + // .push(cx.observe(&this.channel_store, |this, _, cx| { + // this.update_entries(true, cx) + // })); + // this.subscriptions + // .push(cx.observe(&active_call, |this, _, cx| this.update_entries(true, cx))); + // this.subscriptions + // .push(cx.observe_flag::(move |_, this, cx| { + // this.update_entries(true, cx) + // })); + // this.subscriptions.push(cx.subscribe( + // &this.channel_store, + // |this, _channel_store, e, cx| match e { + // ChannelEvent::ChannelCreated(channel_id) + // | ChannelEvent::ChannelRenamed(channel_id) => { + // if this.take_editing_state(cx) { + // this.update_entries(false, cx); + // this.selection = this.entries.iter().position(|entry| { + // if let ListEntry::Channel { channel, .. } = entry { + // channel.id == *channel_id + // } else { + // false + // } + // }); + // } + // } + // }, + // )); + + this + }) + } + + pub fn load( + workspace: WeakView, + cx: AsyncWindowContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + // todo!() + // let serialized_panel = if let Some(panel) = cx + // .background() + // .spawn(async move { KEY_VALUE_STORE.read_kvp(COLLABORATION_PANEL_KEY) }) + // .await + // .log_err() + // .flatten() + // { + // match serde_json::from_str::(&panel) { + // Ok(panel) => Some(panel), + // Err(err) => { + // log::error!("Failed to deserialize collaboration panel: {}", err); + // None + // } + // } + // } else { + // None + // }; + + workspace.update(&mut cx, |workspace, cx| { + let panel = CollabPanel::new(workspace, cx); + // if let Some(serialized_panel) = serialized_panel { + // panel.update(cx, |panel, cx| { + // panel.width = serialized_panel.width; + // panel.collapsed_channels = serialized_panel + // .collapsed_channels + // .unwrap_or_else(|| Vec::new()); + // cx.notify(); + // }); + // } + panel + }) + }) + } + + // fn serialize(&mut self, cx: &mut ViewContext) { + // let width = self.width; + // let collapsed_channels = self.collapsed_channels.clone(); + // self.pending_serialization = cx.background().spawn( + // async move { + // KEY_VALUE_STORE + // .write_kvp( + // COLLABORATION_PANEL_KEY.into(), + // serde_json::to_string(&SerializedCollabPanel { + // width, + // collapsed_channels: Some(collapsed_channels), + // })?, + // ) + // .await?; + // anyhow::Ok(()) + // } + // .log_err(), + // ); + // } + + // 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); + // let query = self.filter_editor.read(cx).text(cx); + // let executor = cx.background().clone(); + + // let prev_selected_entry = self.selection.and_then(|ix| self.entries.get(ix).cloned()); + // let old_entries = mem::take(&mut self.entries); + // let mut scroll_to_top = false; + + // if let Some(room) = ActiveCall::global(cx).read(cx).room() { + // self.entries.push(ListEntry::Header(Section::ActiveCall)); + // if !old_entries + // .iter() + // .any(|entry| matches!(entry, ListEntry::Header(Section::ActiveCall))) + // { + // scroll_to_top = true; + // } + + // if !self.collapsed_sections.contains(&Section::ActiveCall) { + // let room = room.read(cx); + + // if let Some(channel_id) = room.channel_id() { + // self.entries.push(ListEntry::ChannelNotes { channel_id }); + // self.entries.push(ListEntry::ChannelChat { channel_id }) + // } + + // // Populate the active user. + // if let Some(user) = user_store.current_user() { + // self.match_candidates.clear(); + // self.match_candidates.push(StringMatchCandidate { + // id: 0, + // string: user.github_login.clone(), + // char_bag: user.github_login.chars().collect(), + // }); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // if !matches.is_empty() { + // let user_id = user.id; + // self.entries.push(ListEntry::CallParticipant { + // user, + // peer_id: None, + // is_pending: false, + // }); + // let mut projects = room.local_participant().projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: user_id, + // is_last: projects.peek().is_none() && !room.is_screen_sharing(), + // }); + // } + // if room.is_screen_sharing() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: None, + // is_last: true, + // }); + // } + // } + // } + + // // Populate remote participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.remote_participants().iter().map(|(_, participant)| { + // StringMatchCandidate { + // id: participant.user.id as usize, + // string: participant.user.github_login.clone(), + // char_bag: participant.user.github_login.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // for mat in matches { + // let user_id = mat.candidate_id as u64; + // let participant = &room.remote_participants()[&user_id]; + // self.entries.push(ListEntry::CallParticipant { + // user: participant.user.clone(), + // peer_id: Some(participant.peer_id), + // is_pending: false, + // }); + // let mut projects = participant.projects.iter().peekable(); + // while let Some(project) = projects.next() { + // self.entries.push(ListEntry::ParticipantProject { + // project_id: project.id, + // worktree_root_names: project.worktree_root_names.clone(), + // host_user_id: participant.user.id, + // is_last: projects.peek().is_none() + // && participant.video_tracks.is_empty(), + // }); + // } + // if !participant.video_tracks.is_empty() { + // self.entries.push(ListEntry::ParticipantScreen { + // peer_id: Some(participant.peer_id), + // is_last: true, + // }); + // } + // } + + // // Populate pending participants. + // self.match_candidates.clear(); + // self.match_candidates + // .extend(room.pending_participants().iter().enumerate().map( + // |(id, participant)| StringMatchCandidate { + // id, + // string: participant.github_login.clone(), + // char_bag: participant.github_login.chars().collect(), + // }, + // )); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // self.entries + // .extend(matches.iter().map(|mat| ListEntry::CallParticipant { + // user: room.pending_participants()[mat.candidate_id].clone(), + // peer_id: None, + // is_pending: true, + // })); + // } + // } + + // let mut request_entries = Vec::new(); + + // if cx.has_flag::() { + // self.entries.push(ListEntry::Header(Section::Channels)); + + // if channel_store.channel_count() > 0 || self.channel_editing_state.is_some() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend(channel_store.ordered_channels().enumerate().map( + // |(ix, (_, channel))| StringMatchCandidate { + // id: ix, + // string: channel.name.clone(), + // char_bag: channel.name.chars().collect(), + // }, + // )); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // if let Some(state) = &self.channel_editing_state { + // if matches!(state, ChannelEditingState::Create { location: None, .. }) { + // self.entries.push(ListEntry::ChannelEditor { depth: 0 }); + // } + // } + // let mut collapse_depth = None; + // for mat in matches { + // let channel = channel_store.channel_at_index(mat.candidate_id).unwrap(); + // let depth = channel.parent_path.len(); + + // if collapse_depth.is_none() && self.is_channel_collapsed(channel.id) { + // collapse_depth = Some(depth); + // } else if let Some(collapsed_depth) = collapse_depth { + // if depth > collapsed_depth { + // continue; + // } + // if self.is_channel_collapsed(channel.id) { + // collapse_depth = Some(depth); + // } else { + // collapse_depth = None; + // } + // } + + // let has_children = channel_store + // .channel_at_index(mat.candidate_id + 1) + // .map_or(false, |next_channel| { + // next_channel.parent_path.ends_with(&[channel.id]) + // }); + + // match &self.channel_editing_state { + // Some(ChannelEditingState::Create { + // location: parent_id, + // .. + // }) if *parent_id == Some(channel.id) => { + // self.entries.push(ListEntry::Channel { + // channel: channel.clone(), + // depth, + // has_children: false, + // }); + // self.entries + // .push(ListEntry::ChannelEditor { depth: depth + 1 }); + // } + // Some(ChannelEditingState::Rename { + // location: parent_id, + // .. + // }) if parent_id == &channel.id => { + // self.entries.push(ListEntry::ChannelEditor { depth }); + // } + // _ => { + // self.entries.push(ListEntry::Channel { + // channel: channel.clone(), + // depth, + // has_children, + // }); + // } + // } + // } + // } + + // let channel_invites = channel_store.channel_invitations(); + // if !channel_invites.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend(channel_invites.iter().enumerate().map(|(ix, channel)| { + // StringMatchCandidate { + // id: ix, + // string: channel.name.clone(), + // char_bag: channel.name.chars().collect(), + // } + // })); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // request_entries.extend(matches.iter().map(|mat| { + // ListEntry::ChannelInvite(channel_invites[mat.candidate_id].clone()) + // })); + + // if !request_entries.is_empty() { + // self.entries + // .push(ListEntry::Header(Section::ChannelInvites)); + // if !self.collapsed_sections.contains(&Section::ChannelInvites) { + // self.entries.append(&mut request_entries); + // } + // } + // } + // } + + // self.entries.push(ListEntry::Header(Section::Contacts)); + + // request_entries.clear(); + // let incoming = user_store.incoming_contact_requests(); + // if !incoming.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend( + // incoming + // .iter() + // .enumerate() + // .map(|(ix, user)| StringMatchCandidate { + // id: ix, + // string: user.github_login.clone(), + // char_bag: user.github_login.chars().collect(), + // }), + // ); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // request_entries.extend( + // matches + // .iter() + // .map(|mat| ListEntry::IncomingRequest(incoming[mat.candidate_id].clone())), + // ); + // } + + // let outgoing = user_store.outgoing_contact_requests(); + // if !outgoing.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend( + // outgoing + // .iter() + // .enumerate() + // .map(|(ix, user)| StringMatchCandidate { + // id: ix, + // string: user.github_login.clone(), + // char_bag: user.github_login.chars().collect(), + // }), + // ); + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + // request_entries.extend( + // matches + // .iter() + // .map(|mat| ListEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + // ); + // } + + // if !request_entries.is_empty() { + // self.entries + // .push(ListEntry::Header(Section::ContactRequests)); + // if !self.collapsed_sections.contains(&Section::ContactRequests) { + // self.entries.append(&mut request_entries); + // } + // } + + // let contacts = user_store.contacts(); + // if !contacts.is_empty() { + // self.match_candidates.clear(); + // self.match_candidates + // .extend( + // contacts + // .iter() + // .enumerate() + // .map(|(ix, contact)| StringMatchCandidate { + // id: ix, + // string: contact.user.github_login.clone(), + // char_bag: contact.user.github_login.chars().collect(), + // }), + // ); + + // let matches = executor.block(match_strings( + // &self.match_candidates, + // &query, + // true, + // usize::MAX, + // &Default::default(), + // executor.clone(), + // )); + + // let (online_contacts, offline_contacts) = matches + // .iter() + // .partition::, _>(|mat| contacts[mat.candidate_id].online); + + // for (matches, section) in [ + // (online_contacts, Section::Online), + // (offline_contacts, Section::Offline), + // ] { + // if !matches.is_empty() { + // self.entries.push(ListEntry::Header(section)); + // if !self.collapsed_sections.contains(§ion) { + // let active_call = &ActiveCall::global(cx).read(cx); + // for mat in matches { + // let contact = &contacts[mat.candidate_id]; + // self.entries.push(ListEntry::Contact { + // contact: contact.clone(), + // calling: active_call.pending_invites().contains(&contact.user.id), + // }); + // } + // } + // } + // } + // } + + // if incoming.is_empty() && outgoing.is_empty() && contacts.is_empty() { + // self.entries.push(ListEntry::ContactPlaceholder); + // } + + // if select_same_item { + // if let Some(prev_selected_entry) = prev_selected_entry { + // self.selection.take(); + // for (ix, entry) in self.entries.iter().enumerate() { + // if *entry == prev_selected_entry { + // self.selection = Some(ix); + // break; + // } + // } + // } + // } else { + // self.selection = self.selection.and_then(|prev_selection| { + // if self.entries.is_empty() { + // None + // } else { + // Some(prev_selection.min(self.entries.len() - 1)) + // } + // }); + // } + + // let old_scroll_top = self.list_state.logical_scroll_top(); + + // self.list_state.reset(self.entries.len()); + + // 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(); + // } + + // fn render_call_participant( + // user: &User, + // peer_id: Option, + // user_store: ModelHandle, + // is_pending: bool, + // is_selected: bool, + // theme: &theme::Theme, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum CallParticipant {} + // enum CallParticipantTooltip {} + // enum LeaveCallButton {} + // enum LeaveCallTooltip {} + + // let collab_theme = &theme.collab_panel; + + // let is_current_user = + // user_store.read(cx).current_user().map(|user| user.id) == Some(user.id); + + // let content = MouseEventHandler::new::( + // user.id as usize, + // cx, + // |mouse_state, cx| { + // let style = if is_current_user { + // *collab_theme + // .contact_row + // .in_state(is_selected) + // .style_for(&mut Default::default()) + // } else { + // *collab_theme + // .contact_row + // .in_state(is_selected) + // .style_for(mouse_state) + // }; + + // Flex::row() + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(collab_theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new( + // user.github_login.clone(), + // collab_theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_children(if is_pending { + // Some( + // Label::new("Calling", collab_theme.calling_indicator.text.clone()) + // .contained() + // .with_style(collab_theme.calling_indicator.container) + // .aligned() + // .into_any(), + // ) + // } else if is_current_user { + // Some( + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .leave_call_button + // .style_for(is_selected, state), + // "icons/exit.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, _, cx| { + // Self::leave_call(cx); + // }) + // .with_tooltip::( + // 0, + // "Leave call", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any(), + // ) + // } else { + // None + // }) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(style) + // }, + // ); + + // if is_current_user || is_pending || peer_id.is_none() { + // return content.into_any(); + // } + + // let tooltip = format!("Follow {}", user.github_login); + + // content + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace + // .update(cx, |workspace, cx| workspace.follow(peer_id.unwrap(), cx)) + // .map(|task| task.detach_and_log_err(cx)); + // } + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // user.id as usize, + // tooltip, + // Some(Box::new(FollowNextCollaborator)), + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } + + // fn render_participant_project( + // project_id: u64, + // worktree_root_names: &[String], + // host_user_id: u64, + // is_current: bool, + // is_last: bool, + // is_selected: bool, + // theme: &theme::Theme, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum JoinProject {} + // enum JoinProjectTooltip {} + + // let collab_theme = &theme.collab_panel; + // let host_avatar_width = collab_theme + // .contact_avatar + // .width + // .or(collab_theme.contact_avatar.height) + // .unwrap_or(0.); + // let tree_branch = collab_theme.tree_branch; + // let project_name = if worktree_root_names.is_empty() { + // "untitled".to_string() + // } else { + // worktree_root_names.join(", ") + // }; + + // let content = + // MouseEventHandler::new::(project_id as usize, cx, |mouse_state, cx| { + // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + // let row = if is_current { + // collab_theme + // .project_row + // .in_state(true) + // .style_for(&mut Default::default()) + // } else { + // collab_theme + // .project_row + // .in_state(is_selected) + // .style_for(mouse_state) + // }; + + // Flex::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // is_last, + // vec2f(host_avatar_width, collab_theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/file_icons/folder.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(project_name.clone(), row.name.text.clone()) + // .aligned() + // .left() + // .contained() + // .with_style(row.name.container) + // .flex(1., false), + // ) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(row.container) + // }); + + // if is_current { + // return content.into_any(); + // } + + // content + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // let app_state = workspace.read(cx).app_state().clone(); + // workspace::join_remote_project(project_id, host_user_id, app_state, cx) + // .detach_and_log_err(cx); + // } + // }) + // .with_tooltip::( + // project_id as usize, + // format!("Open {}", project_name), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } + + // fn render_participant_screen( + // peer_id: Option, + // is_last: bool, + // is_selected: bool, + // theme: &theme::CollabPanel, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum OpenSharedScreen {} + + // let host_avatar_width = theme + // .contact_avatar + // .width + // .or(theme.contact_avatar.height) + // .unwrap_or(0.); + // let tree_branch = theme.tree_branch; + + // let handler = MouseEventHandler::new::( + // peer_id.map(|id| id.as_u64()).unwrap_or(0) as usize, + // cx, + // |mouse_state, cx| { + // let tree_branch = *tree_branch.in_state(is_selected).style_for(mouse_state); + // let row = theme + // .project_row + // .in_state(is_selected) + // .style_for(mouse_state); + + // Flex::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // is_last, + // vec2f(host_avatar_width, theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/desktop.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("Screen", row.name.text.clone()) + // .aligned() + // .left() + // .contained() + // .with_style(row.name.container) + // .flex(1., false), + // ) + // .constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style(row.container) + // }, + // ); + // if peer_id.is_none() { + // return handler.into_any(); + // } + // handler + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if let Some(workspace) = this.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.open_shared_screen(peer_id.unwrap(), cx) + // }); + // } + // }) + // .into_any() + // } + + // fn take_editing_state(&mut self, cx: &mut ViewContext) -> bool { + // if let Some(_) = self.channel_editing_state.take() { + // self.channel_name_editor.update(cx, |editor, cx| { + // editor.set_text("", cx); + // }); + // true + // } else { + // false + // } + // } + + // fn render_header( + // &self, + // section: Section, + // theme: &theme::Theme, + // is_selected: bool, + // is_collapsed: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum Header {} + // enum LeaveCallContactList {} + // enum AddChannel {} + + // let tooltip_style = &theme.tooltip; + // let mut channel_link = None; + // let mut channel_tooltip_text = None; + // let mut channel_icon = None; + // let mut is_dragged_over = false; + + // let text = match section { + // Section::ActiveCall => { + // let channel_name = maybe!({ + // let channel_id = ActiveCall::global(cx).read(cx).channel_id(cx)?; + + // let channel = self.channel_store.read(cx).channel_for_id(channel_id)?; + + // channel_link = Some(channel.link()); + // (channel_icon, channel_tooltip_text) = match channel.visibility { + // proto::ChannelVisibility::Public => { + // (Some("icons/public.svg"), Some("Copy public channel link.")) + // } + // proto::ChannelVisibility::Members => { + // (Some("icons/hash.svg"), Some("Copy private channel link.")) + // } + // }; + + // Some(channel.name.as_str()) + // }); + + // if let Some(name) = channel_name { + // Cow::Owned(format!("{}", name)) + // } else { + // Cow::Borrowed("Current Call") + // } + // } + // Section::ContactRequests => Cow::Borrowed("Requests"), + // Section::Contacts => Cow::Borrowed("Contacts"), + // Section::Channels => Cow::Borrowed("Channels"), + // Section::ChannelInvites => Cow::Borrowed("Invites"), + // Section::Online => Cow::Borrowed("Online"), + // Section::Offline => Cow::Borrowed("Offline"), + // }; + + // enum AddContact {} + // let button = match section { + // Section::ActiveCall => channel_link.map(|channel_link| { + // let channel_link_copy = channel_link.clone(); + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .leave_call_button + // .style_for(is_selected, state), + // "icons/link.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, _, cx| { + // let item = ClipboardItem::new(channel_link_copy.clone()); + // cx.write_to_clipboard(item) + // }) + // .with_tooltip::( + // 0, + // channel_tooltip_text.unwrap(), + // None, + // tooltip_style.clone(), + // cx, + // ) + // }), + // Section::Contacts => Some( + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .add_contact_button + // .style_for(is_selected, state), + // "icons/plus.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.toggle_contact_finder(cx); + // }) + // .with_tooltip::( + // 0, + // "Search for new contact", + // None, + // tooltip_style.clone(), + // cx, + // ), + // ), + // Section::Channels => { + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Root + // { + // is_dragged_over = true; + // } + + // Some( + // MouseEventHandler::new::(0, cx, |state, _| { + // render_icon_button( + // theme + // .collab_panel + // .add_contact_button + // .style_for(is_selected, state), + // "icons/plus.svg", + // ) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| this.new_root_channel(cx)) + // .with_tooltip::( + // 0, + // "Create a channel", + // None, + // tooltip_style.clone(), + // cx, + // ), + // ) + // } + // _ => None, + // }; + + // let can_collapse = match section { + // Section::ActiveCall | Section::Channels | Section::Contacts => false, + // Section::ChannelInvites + // | Section::ContactRequests + // | Section::Online + // | Section::Offline => true, + // }; + // let icon_size = (&theme.collab_panel).section_icon_size; + // let mut result = MouseEventHandler::new::(section as usize, cx, |state, _| { + // let header_style = if can_collapse { + // theme + // .collab_panel + // .subheader_row + // .in_state(is_selected) + // .style_for(state) + // } else { + // &theme.collab_panel.header_row + // }; + + // Flex::row() + // .with_children(if can_collapse { + // Some( + // Svg::new(if is_collapsed { + // "icons/chevron_right.svg" + // } else { + // "icons/chevron_down.svg" + // }) + // .with_color(header_style.text.color) + // .constrained() + // .with_max_width(icon_size) + // .with_max_height(icon_size) + // .aligned() + // .constrained() + // .with_width(icon_size) + // .contained() + // .with_margin_right( + // theme.collab_panel.contact_username.container.margin.left, + // ), + // ) + // } else if let Some(channel_icon) = channel_icon { + // Some( + // Svg::new(channel_icon) + // .with_color(header_style.text.color) + // .constrained() + // .with_max_width(icon_size) + // .with_max_height(icon_size) + // .aligned() + // .constrained() + // .with_width(icon_size) + // .contained() + // .with_margin_right( + // theme.collab_panel.contact_username.container.margin.left, + // ), + // ) + // } else { + // None + // }) + // .with_child( + // Label::new(text, header_style.text.clone()) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_children(button.map(|button| button.aligned().right())) + // .constrained() + // .with_height(theme.collab_panel.row_height) + // .contained() + // .with_style(if is_dragged_over { + // theme.collab_panel.dragged_over_header + // } else { + // header_style.container + // }) + // }); + + // result = result + // .on_move(move |_, this, cx| { + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // { + // this.drag_target_channel = ChannelDragTarget::Root; + // cx.notify() + // } + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(dragged_channel.id, None, cx) + // }) + // .detach_and_log_err(cx) + // } + // }); + + // if can_collapse { + // result = result + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if can_collapse { + // this.toggle_section_expanded(section, cx); + // } + // }) + // } + + // result.into_any() + // } + + // fn render_contact( + // contact: &Contact, + // calling: bool, + // project: &ModelHandle, + // theme: &theme::Theme, + // is_selected: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ContactTooltip {} + + // let collab_theme = &theme.collab_panel; + // let online = contact.online; + // let busy = contact.busy || calling; + // let user_id = contact.user.id; + // let github_login = contact.user.github_login.clone(); + // let initial_project = project.clone(); + + // let event_handler = + // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { + // Flex::row() + // .with_children(contact.user.avatar.clone().map(|avatar| { + // let status_badge = if contact.online { + // Some( + // Empty::new() + // .collapsed() + // .contained() + // .with_style(if busy { + // collab_theme.contact_status_busy + // } else { + // collab_theme.contact_status_free + // }) + // .aligned(), + // ) + // } else { + // None + // }; + // Stack::new() + // .with_child( + // Image::from_data(avatar) + // .with_style(collab_theme.contact_avatar) + // .aligned() + // .left(), + // ) + // .with_children(status_badge) + // })) + // .with_child( + // Label::new( + // contact.user.github_login.clone(), + // collab_theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_children(if state.hovered() { + // Some( + // MouseEventHandler::new::( + // contact.user.id as usize, + // cx, + // |mouse_state, _| { + // let button_style = + // collab_theme.contact_button.style_for(mouse_state); + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }, + // ) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ) + // } else { + // None + // }) + // .with_children(if calling { + // Some( + // Label::new("Calling", collab_theme.calling_indicator.text.clone()) + // .contained() + // .with_style(collab_theme.calling_indicator.container) + // .aligned(), + // ) + // } else { + // None + // }) + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style( + // *collab_theme + // .contact_row + // .in_state(is_selected) + // .style_for(state), + // ) + // }); + + // if online && !busy { + // let room = ActiveCall::global(cx).read(cx).room(); + // let label = if room.is_some() { + // format!("Invite {} to join call", contact.user.github_login) + // } else { + // format!("Call {}", contact.user.github_login) + // }; + + // event_handler + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.call(user_id, Some(initial_project.clone()), cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .with_tooltip::( + // contact.user.id as usize, + // label, + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else { + // event_handler + // .with_tooltip::( + // contact.user.id as usize, + // format!( + // "{} is {}", + // contact.user.github_login, + // if busy { "on a call" } else { "offline" } + // ), + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } + // } + + // fn render_contact_placeholder( + // &self, + // theme: &theme::CollabPanel, + // is_selected: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum AddContacts {} + // MouseEventHandler::new::(0, cx, |state, _| { + // let style = theme.list_empty_state.style_for(is_selected, state); + // Flex::row() + // .with_child( + // Svg::new("icons/plus.svg") + // .with_color(theme.list_empty_icon.color) + // .constrained() + // .with_width(theme.list_empty_icon.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("Add a contact", style.text.clone()) + // .contained() + // .with_style(theme.list_empty_label_container), + // ) + // .align_children_center() + // .contained() + // .with_style(style.container) + // .into_any() + // }) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.toggle_contact_finder(cx); + // }) + // .into_any() + // } + + // fn render_channel_editor( + // &self, + // theme: &theme::Theme, + // depth: usize, + // cx: &AppContext, + // ) -> AnyElement { + // Flex::row() + // .with_child( + // Empty::new() + // .constrained() + // .with_width(theme.collab_panel.disclosure.button_space()), + // ) + // .with_child( + // Svg::new("icons/hash.svg") + // .with_color(theme.collab_panel.channel_hash.color) + // .constrained() + // .with_width(theme.collab_panel.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // if let Some(pending_name) = self + // .channel_editing_state + // .as_ref() + // .and_then(|state| state.pending_name()) + // { + // Label::new( + // pending_name.to_string(), + // theme.collab_panel.contact_username.text.clone(), + // ) + // .contained() + // .with_style(theme.collab_panel.contact_username.container) + // .aligned() + // .left() + // .flex(1., true) + // .into_any() + // } else { + // ChildView::new(&self.channel_name_editor, cx) + // .aligned() + // .left() + // .contained() + // .with_style(theme.collab_panel.channel_editor) + // .flex(1.0, true) + // .into_any() + // }, + // ) + // .align_children_center() + // .constrained() + // .with_height(theme.collab_panel.row_height) + // .contained() + // .with_style(ContainerStyle { + // background_color: Some(theme.editor.background), + // ..*theme.collab_panel.contact_row.default_style() + // }) + // .with_padding_left( + // theme.collab_panel.contact_row.default_style().padding.left + // + theme.collab_panel.channel_indent * depth as f32, + // ) + // .into_any() + // } + + // fn render_channel( + // &self, + // channel: &Channel, + // depth: usize, + // theme: &theme::Theme, + // is_selected: bool, + // has_children: bool, + // ix: usize, + // cx: &mut ViewContext, + // ) -> AnyElement { + // let channel_id = channel.id; + // let collab_theme = &theme.collab_panel; + // let is_public = self + // .channel_store + // .read(cx) + // .channel_for_id(channel_id) + // .map(|channel| channel.visibility) + // == Some(proto::ChannelVisibility::Public); + // let other_selected = self.selected_channel().map(|channel| channel.id) == Some(channel.id); + // let disclosed = + // has_children.then(|| !self.collapsed_channels.binary_search(&channel.id).is_ok()); + + // let is_active = maybe!({ + // let call_channel = ActiveCall::global(cx) + // .read(cx) + // .room()? + // .read(cx) + // .channel_id()?; + // Some(call_channel == channel_id) + // }) + // .unwrap_or(false); + + // const FACEPILE_LIMIT: usize = 3; + + // enum ChannelCall {} + // enum ChannelNote {} + // enum NotesTooltip {} + // enum ChatTooltip {} + // enum ChannelTooltip {} + + // let mut is_dragged_over = false; + // if cx + // .global::>() + // .currently_dragged::(cx.window()) + // .is_some() + // && self.drag_target_channel == ChannelDragTarget::Channel(channel_id) + // { + // is_dragged_over = true; + // } + + // let has_messages_notification = channel.unseen_message_id.is_some(); + + // MouseEventHandler::new::(ix, cx, |state, cx| { + // let row_hovered = state.hovered(); + + // let mut select_state = |interactive: &Interactive| { + // if state.clicked() == Some(MouseButton::Left) && interactive.clicked.is_some() { + // interactive.clicked.as_ref().unwrap().clone() + // } else if state.hovered() || other_selected { + // interactive + // .hovered + // .as_ref() + // .unwrap_or(&interactive.default) + // .clone() + // } else { + // interactive.default.clone() + // } + // }; + + // Flex::::row() + // .with_child( + // Svg::new(if is_public { + // "icons/public.svg" + // } else { + // "icons/hash.svg" + // }) + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child({ + // let style = collab_theme.channel_name.inactive_state(); + // Flex::row() + // .with_child( + // Label::new(channel.name.clone(), style.text.clone()) + // .contained() + // .with_style(style.container) + // .aligned() + // .left() + // .with_tooltip::( + // ix, + // "Join channel", + // None, + // theme.tooltip.clone(), + // cx, + // ), + // ) + // .with_children({ + // let participants = + // self.channel_store.read(cx).channel_participants(channel_id); + + // if !participants.is_empty() { + // let extra_count = participants.len().saturating_sub(FACEPILE_LIMIT); + + // let result = FacePile::new(collab_theme.face_overlap) + // .with_children( + // participants + // .iter() + // .filter_map(|user| { + // Some( + // Image::from_data(user.avatar.clone()?) + // .with_style(collab_theme.channel_avatar), + // ) + // }) + // .take(FACEPILE_LIMIT), + // ) + // .with_children((extra_count > 0).then(|| { + // Label::new( + // format!("+{}", extra_count), + // collab_theme.extra_participant_label.text.clone(), + // ) + // .contained() + // .with_style(collab_theme.extra_participant_label.container) + // })); + + // Some(result) + // } else { + // None + // } + // }) + // .with_spacing(8.) + // .align_children_center() + // .flex(1., true) + // }) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, _| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + + // if channel.unseen_message_id.is_some() { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_note_active_color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else if row_hovered { + // Svg::new("icons/conversations.svg") + // .with_color(collab_theme.channel_hash.color) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + // }) + // .with_tooltip::( + // ix, + // "Open channel chat", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .contained() + // .with_margin_right(4.), + // ) + // .with_child( + // MouseEventHandler::new::(ix, cx, move |mouse_state, cx| { + // let container_style = collab_theme + // .disclosure + // .button + // .style_for(mouse_state) + // .container; + // if row_hovered || channel.unseen_note_version.is_some() { + // Svg::new("icons/file.svg") + // .with_color(if channel.unseen_note_version.is_some() { + // collab_theme.channel_note_active_color + // } else { + // collab_theme.channel_hash.color + // }) + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_style(container_style) + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .with_tooltip::( + // ix as usize, + // "Open channel notes", + // None, + // theme.tooltip.clone(), + // cx, + // ) + // .into_any() + // } else if has_messages_notification { + // Empty::new() + // .constrained() + // .with_width(collab_theme.channel_hash.width) + // .contained() + // .with_uniform_padding(4.) + // .with_margin_right(collab_theme.channel_hash.container.margin.left) + // .into_any() + // } else { + // Empty::new().into_any() + // } + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + // }), + // ) + // .align_children_center() + // .styleable_component() + // .disclosable( + // disclosed, + // Box::new(ToggleCollapse { + // location: channel.id.clone(), + // }), + // ) + // .with_id(ix) + // .with_style(collab_theme.disclosure.clone()) + // .element() + // .constrained() + // .with_height(collab_theme.row_height) + // .contained() + // .with_style(select_state( + // collab_theme + // .channel_row + // .in_state(is_selected || is_active || is_dragged_over), + // )) + // .with_padding_left( + // collab_theme.channel_row.default_style().padding.left + // + collab_theme.channel_indent * depth as f32, + // ) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // if this.drag_target_channel == ChannelDragTarget::None { + // if is_active { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx) + // } else { + // this.join_channel(channel_id, cx) + // } + // } + // }) + // .on_click(MouseButton::Right, { + // let channel = channel.clone(); + // move |e, this, cx| { + // this.deploy_channel_context_menu(Some(e.position), &channel, ix, cx); + // } + // }) + // .on_up(MouseButton::Left, move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // this.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.move_channel(dragged_channel.id, Some(channel_id), cx) + // }) + // .detach_and_log_err(cx) + // } + // }) + // .on_move({ + // let channel = channel.clone(); + // move |_, this, cx| { + // if let Some((_, dragged_channel)) = cx + // .global::>() + // .currently_dragged::(cx.window()) + // { + // if channel.id != dragged_channel.id { + // this.drag_target_channel = ChannelDragTarget::Channel(channel.id); + // } + // cx.notify() + // } + // } + // }) + // .as_draggable::<_, Channel>( + // channel.clone(), + // move |_, channel, cx: &mut ViewContext| { + // let theme = &theme::current(cx).collab_panel; + + // Flex::::row() + // .with_child( + // Svg::new("icons/hash.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(channel.name.clone(), theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left(), + // ) + // .align_children_center() + // .contained() + // .with_background_color( + // theme + // .container + // .background_color + // .unwrap_or(gpui::color::Color::transparent_black()), + // ) + // .contained() + // .with_padding_left( + // theme.channel_row.default_style().padding.left + // + theme.channel_indent * depth as f32, + // ) + // .into_any() + // }, + // ) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() + // } + + // fn render_channel_notes( + // &self, + // channel_id: ChannelId, + // theme: &theme::CollabPanel, + // is_selected: bool, + // ix: usize, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ChannelNotes {} + // let host_avatar_width = theme + // .contact_avatar + // .width + // .or(theme.contact_avatar.height) + // .unwrap_or(0.); + + // MouseEventHandler::new::(ix as usize, cx, |state, cx| { + // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + // let row = theme.project_row.in_state(is_selected).style_for(state); + + // Flex::::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // false, + // vec2f(host_avatar_width, theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/file.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("notes", theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style(*theme.channel_row.style_for(is_selected, state)) + // .with_padding_left(theme.channel_row.default_style().padding.left) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.open_channel_notes(&OpenChannelNotes { channel_id }, cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() + // } + + // fn render_channel_chat( + // &self, + // channel_id: ChannelId, + // theme: &theme::CollabPanel, + // is_selected: bool, + // ix: usize, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum ChannelChat {} + // let host_avatar_width = theme + // .contact_avatar + // .width + // .or(theme.contact_avatar.height) + // .unwrap_or(0.); + + // MouseEventHandler::new::(ix as usize, cx, |state, cx| { + // let tree_branch = *theme.tree_branch.in_state(is_selected).style_for(state); + // let row = theme.project_row.in_state(is_selected).style_for(state); + + // Flex::::row() + // .with_child(render_tree_branch( + // tree_branch, + // &row.name.text, + // true, + // vec2f(host_avatar_width, theme.row_height), + // cx.font_cache(), + // )) + // .with_child( + // Svg::new("icons/conversations.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new("chat", theme.channel_name.text.clone()) + // .contained() + // .with_style(theme.channel_name.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style(*theme.channel_row.style_for(is_selected, state)) + // .with_padding_left(theme.channel_row.default_style().padding.left) + // }) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.join_channel_chat(&JoinChannelChat { channel_id }, cx); + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .into_any() + // } + + // fn render_channel_invite( + // channel: Arc, + // channel_store: ModelHandle, + // theme: &theme::CollabPanel, + // is_selected: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum Decline {} + // enum Accept {} + + // let channel_id = channel.id; + // let is_invite_pending = channel_store + // .read(cx) + // .has_pending_channel_invite_response(&channel); + // let button_spacing = theme.contact_button_spacing; + + // Flex::row() + // .with_child( + // Svg::new("icons/hash.svg") + // .with_color(theme.channel_hash.color) + // .constrained() + // .with_width(theme.channel_hash.width) + // .aligned() + // .left(), + // ) + // .with_child( + // Label::new(channel.name.clone(), theme.contact_username.text.clone()) + // .contained() + // .with_style(theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { + // let button_style = if is_invite_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg").aligned() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_channel_invite(channel_id, false, cx); + // }) + // .contained() + // .with_margin_right(button_spacing), + // ) + // .with_child( + // MouseEventHandler::new::(channel.id as usize, cx, |mouse_state, _| { + // let button_style = if is_invite_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/check.svg") + // .aligned() + // .flex_float() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_channel_invite(channel_id, true, cx); + // }), + // ) + // .constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style( + // *theme + // .contact_row + // .in_state(is_selected) + // .style_for(&mut Default::default()), + // ) + // .with_padding_left( + // theme.contact_row.default_style().padding.left + theme.channel_indent, + // ) + // .into_any() + // } + + // fn render_contact_request( + // user: Arc, + // user_store: ModelHandle, + // theme: &theme::CollabPanel, + // is_incoming: bool, + // is_selected: bool, + // cx: &mut ViewContext, + // ) -> AnyElement { + // enum Decline {} + // enum Accept {} + // enum Cancel {} + + // let mut row = Flex::row() + // .with_children(user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.contact_avatar) + // .aligned() + // .left() + // })) + // .with_child( + // Label::new( + // user.github_login.clone(), + // theme.contact_username.text.clone(), + // ) + // .contained() + // .with_style(theme.contact_username.container) + // .aligned() + // .left() + // .flex(1., true), + // ); + + // let user_id = user.id; + // let github_login = user.github_login.clone(); + // let is_contact_request_pending = user_store.read(cx).is_contact_request_pending(&user); + // let button_spacing = theme.contact_button_spacing; + + // if is_incoming { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg").aligned() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, false, cx); + // }) + // .contained() + // .with_margin_right(button_spacing), + // ); + + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/check.svg") + // .aligned() + // .flex_float() + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.respond_to_contact_request(user_id, true, cx); + // }), + // ); + // } else { + // row.add_child( + // MouseEventHandler::new::(user.id as usize, cx, |mouse_state, _| { + // let button_style = if is_contact_request_pending { + // &theme.disabled_button + // } else { + // theme.contact_button.style_for(mouse_state) + // }; + // render_icon_button(button_style, "icons/x.svg") + // .aligned() + // .flex_float() + // }) + // .with_padding(Padding::uniform(2.)) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, move |_, this, cx| { + // this.remove_contact(user_id, &github_login, cx); + // }) + // .flex_float(), + // ); + // } + + // row.constrained() + // .with_height(theme.row_height) + // .contained() + // .with_style( + // *theme + // .contact_row + // .in_state(is_selected) + // .style_for(&mut Default::default()), + // ) + // .into_any() + // } + + // fn has_subchannels(&self, ix: usize) -> bool { + // self.entries.get(ix).map_or(false, |entry| { + // if let ListEntry::Channel { has_children, .. } = entry { + // *has_children + // } else { + // false + // } + // }) + // } + + // fn deploy_channel_context_menu( + // &mut self, + // position: Option, + // channel: &Channel, + // ix: usize, + // cx: &mut ViewContext, + // ) { + // self.context_menu_on_selected = position.is_none(); + + // let clipboard_channel_name = self.channel_clipboard.as_ref().and_then(|clipboard| { + // self.channel_store + // .read(cx) + // .channel_for_id(clipboard.channel_id) + // .map(|channel| channel.name.clone()) + // }); + + // self.context_menu.update(cx, |context_menu, cx| { + // context_menu.set_position_mode(if self.context_menu_on_selected { + // OverlayPositionMode::Local + // } else { + // OverlayPositionMode::Window + // }); + + // let mut items = Vec::new(); + + // let select_action_name = if self.selection == Some(ix) { + // "Unselect" + // } else { + // "Select" + // }; + + // items.push(ContextMenuItem::action( + // select_action_name, + // ToggleSelectedIx { ix }, + // )); + + // if self.has_subchannels(ix) { + // let expand_action_name = if self.is_channel_collapsed(channel.id) { + // "Expand Subchannels" + // } else { + // "Collapse Subchannels" + // }; + // items.push(ContextMenuItem::action( + // expand_action_name, + // ToggleCollapse { + // location: channel.id, + // }, + // )); + // } + + // items.push(ContextMenuItem::action( + // "Open Notes", + // OpenChannelNotes { + // channel_id: channel.id, + // }, + // )); + + // items.push(ContextMenuItem::action( + // "Open Chat", + // JoinChannelChat { + // channel_id: channel.id, + // }, + // )); + + // items.push(ContextMenuItem::action( + // "Copy Channel Link", + // CopyChannelLink { + // channel_id: channel.id, + // }, + // )); + + // if self.channel_store.read(cx).is_channel_admin(channel.id) { + // items.extend([ + // ContextMenuItem::Separator, + // ContextMenuItem::action( + // "New Subchannel", + // NewChannel { + // location: channel.id, + // }, + // ), + // ContextMenuItem::action( + // "Rename", + // RenameChannel { + // channel_id: channel.id, + // }, + // ), + // ContextMenuItem::action( + // "Move this channel", + // StartMoveChannelFor { + // channel_id: channel.id, + // }, + // ), + // ]); + + // if let Some(channel_name) = clipboard_channel_name { + // items.push(ContextMenuItem::Separator); + // items.push(ContextMenuItem::action( + // format!("Move '#{}' here", channel_name), + // MoveChannel { to: channel.id }, + // )); + // } + + // items.extend([ + // ContextMenuItem::Separator, + // ContextMenuItem::action( + // "Invite Members", + // InviteMembers { + // channel_id: channel.id, + // }, + // ), + // ContextMenuItem::action( + // "Manage Members", + // ManageMembers { + // channel_id: channel.id, + // }, + // ), + // ContextMenuItem::Separator, + // ContextMenuItem::action( + // "Delete", + // RemoveChannel { + // channel_id: channel.id, + // }, + // ), + // ]); + // } + + // context_menu.show( + // position.unwrap_or_default(), + // if self.context_menu_on_selected { + // gpui::elements::AnchorCorner::TopRight + // } else { + // gpui::elements::AnchorCorner::BottomLeft + // }, + // items, + // cx, + // ); + // }); + + // cx.notify(); + // } + + // fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + // if self.take_editing_state(cx) { + // cx.focus(&self.filter_editor); + // } else { + // self.filter_editor.update(cx, |editor, cx| { + // if editor.buffer().read(cx).len(cx) > 0 { + // editor.set_text("", cx); + // } + // }); + // } + + // self.update_entries(false, cx); + // } + + // fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + // let ix = self.selection.map_or(0, |ix| ix + 1); + // if ix < self.entries.len() { + // self.selection = Some(ix); + // } + + // self.list_state.reset(self.entries.len()); + // if let Some(ix) = self.selection { + // self.list_state.scroll_to(ListOffset { + // item_ix: ix, + // offset_in_item: 0., + // }); + // } + // cx.notify(); + // } + + // fn select_prev(&mut self, _: &SelectPrev, cx: &mut ViewContext) { + // let ix = self.selection.take().unwrap_or(0); + // if ix > 0 { + // self.selection = Some(ix - 1); + // } + + // self.list_state.reset(self.entries.len()); + // if let Some(ix) = self.selection { + // self.list_state.scroll_to(ListOffset { + // item_ix: ix, + // offset_in_item: 0., + // }); + // } + // cx.notify(); + // } + + // fn confirm(&mut self, _: &Confirm, cx: &mut ViewContext) { + // if self.confirm_channel_edit(cx) { + // return; + // } + + // if let Some(selection) = self.selection { + // if let Some(entry) = self.entries.get(selection) { + // match entry { + // ListEntry::Header(section) => match section { + // Section::ActiveCall => Self::leave_call(cx), + // Section::Channels => self.new_root_channel(cx), + // Section::Contacts => self.toggle_contact_finder(cx), + // Section::ContactRequests + // | Section::Online + // | Section::Offline + // | Section::ChannelInvites => { + // self.toggle_section_expanded(*section, cx); + // } + // }, + // ListEntry::Contact { contact, calling } => { + // if contact.online && !contact.busy && !calling { + // self.call(contact.user.id, Some(self.project.clone()), cx); + // } + // } + // ListEntry::ParticipantProject { + // project_id, + // host_user_id, + // .. + // } => { + // if let Some(workspace) = self.workspace.upgrade(cx) { + // let app_state = workspace.read(cx).app_state().clone(); + // workspace::join_remote_project( + // *project_id, + // *host_user_id, + // app_state, + // cx, + // ) + // .detach_and_log_err(cx); + // } + // } + // ListEntry::ParticipantScreen { peer_id, .. } => { + // let Some(peer_id) = peer_id else { + // return; + // }; + // if let Some(workspace) = self.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.open_shared_screen(*peer_id, cx) + // }); + // } + // } + // ListEntry::Channel { channel, .. } => { + // let is_active = maybe!({ + // let call_channel = ActiveCall::global(cx) + // .read(cx) + // .room()? + // .read(cx) + // .channel_id()?; + + // Some(call_channel == channel.id) + // }) + // .unwrap_or(false); + // if is_active { + // self.open_channel_notes( + // &OpenChannelNotes { + // channel_id: channel.id, + // }, + // cx, + // ) + // } else { + // self.join_channel(channel.id, cx) + // } + // } + // ListEntry::ContactPlaceholder => self.toggle_contact_finder(cx), + // _ => {} + // } + // } + // } + // } + + // fn insert_space(&mut self, _: &InsertSpace, cx: &mut ViewContext) { + // if self.channel_editing_state.is_some() { + // self.channel_name_editor.update(cx, |editor, cx| { + // editor.insert(" ", cx); + // }); + // } + // } + + // fn confirm_channel_edit(&mut self, cx: &mut ViewContext) -> bool { + // if let Some(editing_state) = &mut self.channel_editing_state { + // match editing_state { + // ChannelEditingState::Create { + // location, + // pending_name, + // .. + // } => { + // if pending_name.is_some() { + // return false; + // } + // let channel_name = self.channel_name_editor.read(cx).text(cx); + + // *pending_name = Some(channel_name.clone()); + + // self.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.create_channel(&channel_name, *location, cx) + // }) + // .detach(); + // cx.notify(); + // } + // ChannelEditingState::Rename { + // location, + // pending_name, + // } => { + // if pending_name.is_some() { + // return false; + // } + // let channel_name = self.channel_name_editor.read(cx).text(cx); + // *pending_name = Some(channel_name.clone()); + + // self.channel_store + // .update(cx, |channel_store, cx| { + // channel_store.rename(*location, &channel_name, cx) + // }) + // .detach(); + // cx.notify(); + // } + // } + // cx.focus_self(); + // true + // } else { + // false + // } + // } + + // fn toggle_section_expanded(&mut self, section: Section, cx: &mut ViewContext) { + // if let Some(ix) = self.collapsed_sections.iter().position(|s| *s == section) { + // self.collapsed_sections.remove(ix); + // } else { + // self.collapsed_sections.push(section); + // } + // self.update_entries(false, cx); + // } + + // fn collapse_selected_channel( + // &mut self, + // _: &CollapseSelectedChannel, + // cx: &mut ViewContext, + // ) { + // let Some(channel_id) = self.selected_channel().map(|channel| channel.id) else { + // return; + // }; + + // if self.is_channel_collapsed(channel_id) { + // return; + // } + + // self.toggle_channel_collapsed(channel_id, cx); + // } + + // fn expand_selected_channel(&mut self, _: &ExpandSelectedChannel, cx: &mut ViewContext) { + // let Some(id) = self.selected_channel().map(|channel| channel.id) else { + // return; + // }; + + // if !self.is_channel_collapsed(id) { + // return; + // } + + // self.toggle_channel_collapsed(id, cx) + // } + + // fn toggle_channel_collapsed_action( + // &mut self, + // action: &ToggleCollapse, + // cx: &mut ViewContext, + // ) { + // self.toggle_channel_collapsed(action.location, cx); + // } + + // fn toggle_channel_collapsed<'a>(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + // match self.collapsed_channels.binary_search(&channel_id) { + // Ok(ix) => { + // self.collapsed_channels.remove(ix); + // } + // Err(ix) => { + // self.collapsed_channels.insert(ix, channel_id); + // } + // }; + // self.serialize(cx); + // self.update_entries(true, cx); + // cx.notify(); + // cx.focus_self(); + // } + + // fn is_channel_collapsed(&self, channel_id: ChannelId) -> bool { + // self.collapsed_channels.binary_search(&channel_id).is_ok() + // } + + // fn leave_call(cx: &mut ViewContext) { + // ActiveCall::global(cx) + // .update(cx, |call, cx| call.hang_up(cx)) + // .detach_and_log_err(cx); + // } + + // fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + // if let Some(workspace) = self.workspace.upgrade(cx) { + // workspace.update(cx, |workspace, cx| { + // workspace.toggle_modal(cx, |_, cx| { + // cx.add_view(|cx| { + // let mut finder = ContactFinder::new(self.user_store.clone(), cx); + // finder.set_query(self.filter_editor.read(cx).text(cx), cx); + // finder + // }) + // }); + // }); + // } + // } + + // fn new_root_channel(&mut self, cx: &mut ViewContext) { + // self.channel_editing_state = Some(ChannelEditingState::Create { + // location: None, + // pending_name: None, + // }); + // self.update_entries(false, cx); + // self.select_channel_editor(); + // cx.focus(self.channel_name_editor.as_any()); + // cx.notify(); + // } + + // fn select_channel_editor(&mut self) { + // self.selection = self.entries.iter().position(|entry| match entry { + // ListEntry::ChannelEditor { .. } => true, + // _ => false, + // }); + // } + + // fn new_subchannel(&mut self, action: &NewChannel, cx: &mut ViewContext) { + // self.collapsed_channels + // .retain(|channel| *channel != action.location); + // self.channel_editing_state = Some(ChannelEditingState::Create { + // location: Some(action.location.to_owned()), + // pending_name: None, + // }); + // self.update_entries(false, cx); + // self.select_channel_editor(); + // cx.focus(self.channel_name_editor.as_any()); + // cx.notify(); + // } + + // fn invite_members(&mut self, action: &InviteMembers, cx: &mut ViewContext) { + // self.show_channel_modal(action.channel_id, channel_modal::Mode::InviteMembers, cx); + // } + + // fn manage_members(&mut self, action: &ManageMembers, cx: &mut ViewContext) { + // self.show_channel_modal(action.channel_id, channel_modal::Mode::ManageMembers, cx); + // } + + // fn remove(&mut self, _: &Remove, cx: &mut ViewContext) { + // if let Some(channel) = self.selected_channel() { + // self.remove_channel(channel.id, cx) + // } + // } + + // fn rename_selected_channel(&mut self, _: &menu::SecondaryConfirm, cx: &mut ViewContext) { + // if let Some(channel) = self.selected_channel() { + // self.rename_channel( + // &RenameChannel { + // channel_id: channel.id, + // }, + // cx, + // ); + // } + // } + + // fn rename_channel(&mut self, action: &RenameChannel, cx: &mut ViewContext) { + // let channel_store = self.channel_store.read(cx); + // if !channel_store.is_channel_admin(action.channel_id) { + // return; + // } + // if let Some(channel) = channel_store.channel_for_id(action.channel_id).cloned() { + // self.channel_editing_state = Some(ChannelEditingState::Rename { + // location: action.channel_id.to_owned(), + // pending_name: None, + // }); + // self.channel_name_editor.update(cx, |editor, cx| { + // editor.set_text(channel.name.clone(), cx); + // editor.select_all(&Default::default(), cx); + // }); + // cx.focus(self.channel_name_editor.as_any()); + // self.update_entries(false, cx); + // self.select_channel_editor(); + // } + // } + + // fn open_channel_notes(&mut self, action: &OpenChannelNotes, cx: &mut ViewContext) { + // if let Some(workspace) = self.workspace.upgrade(cx) { + // ChannelView::open(action.channel_id, workspace, cx).detach(); + // } + // } + + // fn show_inline_context_menu(&mut self, _: &menu::ShowContextMenu, cx: &mut ViewContext) { + // let Some(channel) = self.selected_channel() else { + // return; + // }; + + // self.deploy_channel_context_menu(None, &channel.clone(), self.selection.unwrap(), cx); + // } + + // fn selected_channel(&self) -> Option<&Arc> { + // self.selection + // .and_then(|ix| self.entries.get(ix)) + // .and_then(|entry| match entry { + // ListEntry::Channel { channel, .. } => Some(channel), + // _ => None, + // }) + // } + + // fn show_channel_modal( + // &mut self, + // channel_id: ChannelId, + // mode: channel_modal::Mode, + // cx: &mut ViewContext, + // ) { + // let workspace = self.workspace.clone(); + // let user_store = self.user_store.clone(); + // let channel_store = self.channel_store.clone(); + // let members = self.channel_store.update(cx, |channel_store, cx| { + // channel_store.get_channel_member_details(channel_id, cx) + // }); + + // cx.spawn(|_, mut cx| async move { + // let members = members.await?; + // workspace.update(&mut cx, |workspace, cx| { + // workspace.toggle_modal(cx, |_, cx| { + // cx.add_view(|cx| { + // ChannelModal::new( + // user_store.clone(), + // channel_store.clone(), + // channel_id, + // mode, + // members, + // cx, + // ) + // }) + // }); + // }) + // }) + // .detach(); + // } + + // fn remove_selected_channel(&mut self, action: &RemoveChannel, cx: &mut ViewContext) { + // self.remove_channel(action.channel_id, cx) + // } + + // fn remove_channel(&mut self, channel_id: ChannelId, cx: &mut ViewContext) { + // let channel_store = self.channel_store.clone(); + // if let Some(channel) = channel_store.read(cx).channel_for_id(channel_id) { + // let prompt_message = format!( + // "Are you sure you want to remove the channel \"{}\"?", + // channel.name + // ); + // let mut answer = + // cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + // let window = cx.window(); + // cx.spawn(|this, mut cx| async move { + // if answer.next().await == Some(0) { + // if let Err(e) = channel_store + // .update(&mut cx, |channels, _| channels.remove_channel(channel_id)) + // .await + // { + // window.prompt( + // PromptLevel::Info, + // &format!("Failed to remove channel: {}", e), + // &["Ok"], + // &mut cx, + // ); + // } + // this.update(&mut cx, |_, cx| cx.focus_self()).ok(); + // } + // }) + // .detach(); + // } + // } + + // // Should move to the filter editor if clicking on it + // // Should move selection to the channel editor if activating it + + // fn remove_contact(&mut self, user_id: u64, github_login: &str, cx: &mut ViewContext) { + // let user_store = self.user_store.clone(); + // let prompt_message = format!( + // "Are you sure you want to remove \"{}\" from your contacts?", + // github_login + // ); + // let mut answer = cx.prompt(PromptLevel::Warning, &prompt_message, &["Remove", "Cancel"]); + // let window = cx.window(); + // cx.spawn(|_, mut cx| async move { + // if answer.next().await == Some(0) { + // if let Err(e) = user_store + // .update(&mut cx, |store, cx| store.remove_contact(user_id, cx)) + // .await + // { + // window.prompt( + // PromptLevel::Info, + // &format!("Failed to remove contact: {}", e), + // &["Ok"], + // &mut cx, + // ); + // } + // } + // }) + // .detach(); + // } + + // fn respond_to_contact_request( + // &mut self, + // user_id: u64, + // accept: bool, + // cx: &mut ViewContext, + // ) { + // self.user_store + // .update(cx, |store, cx| { + // store.respond_to_contact_request(user_id, accept, cx) + // }) + // .detach(); + // } + + // fn respond_to_channel_invite( + // &mut self, + // channel_id: u64, + // accept: bool, + // cx: &mut ViewContext, + // ) { + // self.channel_store + // .update(cx, |store, cx| { + // store.respond_to_channel_invite(channel_id, accept, cx) + // }) + // .detach(); + // } + + // fn call( + // &mut self, + // recipient_user_id: u64, + // initial_project: Option>, + // cx: &mut ViewContext, + // ) { + // ActiveCall::global(cx) + // .update(cx, |call, cx| { + // call.invite(recipient_user_id, initial_project, cx) + // }) + // .detach_and_log_err(cx); + // } + + // fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { + // let Some(workspace) = self.workspace.upgrade(cx) else { + // return; + // }; + // let Some(handle) = cx.window().downcast::() else { + // return; + // }; + // workspace::join_channel( + // channel_id, + // workspace.read(cx).app_state().clone(), + // Some(handle), + // cx, + // ) + // .detach_and_log_err(cx) + // } + + // fn join_channel_chat(&mut self, action: &JoinChannelChat, cx: &mut ViewContext) { + // let channel_id = action.channel_id; + // if let Some(workspace) = self.workspace.upgrade(cx) { + // cx.app_context().defer(move |cx| { + // workspace.update(cx, |workspace, cx| { + // if let Some(panel) = workspace.focus_panel::(cx) { + // panel.update(cx, |panel, cx| { + // panel + // .select_channel(channel_id, None, cx) + // .detach_and_log_err(cx); + // }); + // } + // }); + // }); + // } + // } + + // fn copy_channel_link(&mut self, action: &CopyChannelLink, cx: &mut ViewContext) { + // let channel_store = self.channel_store.read(cx); + // let Some(channel) = channel_store.channel_for_id(action.channel_id) else { + // return; + // }; + // let item = ClipboardItem::new(channel.link()); + // cx.write_to_clipboard(item) + // } +} + // fn render_tree_branch( // branch_style: theme::TreeBranch, // row_style: &TextStyle, @@ -3280,6 +3299,17 @@ // .with_width(size.x()) // } +impl Render for CollabPanel { + type Element = Focusable>; + + fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + div() + .key_context("CollabPanel") + .track_focus(&self.focus_handle) + .child("COLLAB PANEL") + } +} + // impl View for CollabPanel { // fn ui_name() -> &'static str { // "CollabPanel" @@ -3379,59 +3409,59 @@ // } // } -// impl Panel for CollabPanel { -// fn position(&self, cx: &gpui::WindowContext) -> DockPosition { -// settings::get::(cx).dock -// } - -// fn position_is_valid(&self, position: DockPosition) -> bool { -// matches!(position, DockPosition::Left | DockPosition::Right) -// } - -// fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { -// settings::update_settings_file::( -// self.fs.clone(), -// cx, -// move |settings| settings.dock = Some(position), -// ); -// } - -// fn size(&self, cx: &gpui::WindowContext) -> f32 { -// self.width -// .unwrap_or_else(|| settings::get::(cx).default_width) -// } - -// fn set_size(&mut self, size: Option, cx: &mut ViewContext) { -// self.width = size; -// self.serialize(cx); -// cx.notify(); -// } - -// fn icon_path(&self, cx: &gpui::WindowContext) -> Option<&'static str> { -// settings::get::(cx) -// .button -// .then(|| "icons/user_group_16.svg") -// } - -// fn icon_tooltip(&self) -> (String, Option>) { -// ( -// "Collaboration Panel".to_string(), -// Some(Box::new(ToggleFocus)), -// ) -// } - -// fn should_change_position_on_event(event: &Self::Event) -> bool { -// matches!(event, Event::DockPositionChanged) -// } - -// fn has_focus(&self, _cx: &gpui::WindowContext) -> bool { -// self.has_focus -// } - -// fn is_focus_event(event: &Self::Event) -> bool { -// matches!(event, Event::Focus) -// } -// } +impl EventEmitter for CollabPanel {} + +impl Panel for CollabPanel { + fn position(&self, cx: &gpui::WindowContext) -> DockPosition { + CollaborationPanelSettings::get_global(cx).dock + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| settings.dock = Some(position), + ); + } + + fn size(&self, cx: &gpui::WindowContext) -> f32 { + self.width + .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width) + } + + fn set_size(&mut self, size: Option, cx: &mut ViewContext) { + self.width = size; + // todo!() + // self.serialize(cx); + cx.notify(); + } + + fn icon(&self, cx: &gpui::WindowContext) -> Option { + CollaborationPanelSettings::get_global(cx) + .button + .then(|| ui::Icon::Collab) + } + + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) + } + + fn has_focus(&self, cx: &gpui::WindowContext) -> bool { + self.focus_handle.contains_focused(cx) + } + + fn persistent_name(&self) -> &'static str { + "Collaboration Panel" + } + + fn focus_handle(&self, _cx: &ui::prelude::WindowContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} // impl PartialEq for ListEntry { // fn eq(&self, other: &Self) -> bool { diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index c1b7928209f2bbc3f5f889163ca08383a5d744db..c9d16c7a5de68a7004980c4abb1a9dd0f25a8279 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -37,7 +37,7 @@ use gpui::{ }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, TextTooltip}; +use ui::{h_stack, Button, ButtonVariant, KeyBinding, Label, TextColor, Tooltip}; use workspace::Workspace; // const MAX_PROJECT_NAME_LENGTH: usize = 40; @@ -111,18 +111,14 @@ impl Render for CollabTitlebarItem { .variant(ButtonVariant::Ghost) .color(Some(TextColor::Player(0))), ) - .tooltip(move |_, cx| { - cx.build_view(|_| TextTooltip::new("Toggle following")) - }), + .tooltip(move |_, cx| Tooltip::text("Toggle following", cx)), ) // TODO - Add project menu .child( div() .id("titlebar_project_menu_button") .child(Button::new("project_name").variant(ButtonVariant::Ghost)) - .tooltip(move |_, cx| { - cx.build_view(|_| TextTooltip::new("Recent Projects")) - }), + .tooltip(move |_, cx| Tooltip::text("Recent Projects", cx)), ) // TODO - Add git menu .child( @@ -137,9 +133,8 @@ impl Render for CollabTitlebarItem { // todo!() Replace with real action. #[gpui::action] struct NoAction {} - cx.build_view(|_| { - TextTooltip::new("Recent Branches") + Tooltip::new("Recent Branches") .key_binding(KeyBinding::new(gpui::KeyBinding::new( "cmd-b", NoAction {}, @@ -147,6 +142,7 @@ impl Render for CollabTitlebarItem { ))) .meta("Only local branches shown") }) + .into() }), ), ) // self.titlebar_item diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index 7dc96c0e5965e7fc27f1d497370efc2f164d3c92..d2e6b28115dacf147d1a2ac7dff25e9de9dd8de8 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -9,6 +9,7 @@ mod panel_settings; use std::sync::Arc; +pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; use gpui::AppContext; pub use panel_settings::{ @@ -29,7 +30,7 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { // vcs_menu::init(cx); collab_titlebar_item::init(cx); - // collab_panel::init(cx); + collab_panel::init(cx); // chat_panel::init(cx); // notifications::init(&app_state, cx); diff --git a/crates/editor2/src/editor.rs b/crates/editor2/src/editor.rs index af7da8e83719d1f959854c002b7056247f39cb94..ba02d567faf79355726e362e7507d2506ecd99e2 100644 --- a/crates/editor2/src/editor.rs +++ b/crates/editor2/src/editor.rs @@ -97,7 +97,7 @@ use text::{OffsetUtf16, Rope}; use theme::{ ActiveTheme, DiagnosticStyle, PlayerColor, SyntaxTheme, Theme, ThemeColors, ThemeSettings, }; -use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, TextTooltip}; +use ui::{v_stack, HighlightedLabel, IconButton, StyledExt, Tooltip}; use util::{post_inc, RangeExt, ResultExt, TryFutureExt}; use workspace::{ item::{ItemEvent, ItemHandle}, @@ -9985,7 +9985,7 @@ pub fn diagnostic_block_renderer(diagnostic: Diagnostic, is_valid: bool) -> Rend .on_click(move |_, _, cx| { cx.write_to_clipboard(ClipboardItem::new(message.clone())); }) - .tooltip(|_, cx| cx.build_view(|cx| TextTooltip::new("Copy diagnostic message"))) + .tooltip(|_, cx| Tooltip::text("Copy diagnostic message", cx)) .render() }) } diff --git a/crates/editor2/src/element.rs b/crates/editor2/src/element.rs index de1b6f062244445960479710aeccddcfcd8e2bc6..aa54bed73a9fbb5883fc30651b752c02240f4eb4 100644 --- a/crates/editor2/src/element.rs +++ b/crates/editor2/src/element.rs @@ -12,8 +12,8 @@ use crate::{ }, scroll::scroll_amount::ScrollAmount, CursorShape, DisplayPoint, Editor, EditorMode, EditorSettings, EditorSnapshot, EditorStyle, - HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, PageDown, PageUp, Point, SelectPhase, - Selection, SoftWrap, ToPoint, MAX_LINE_LEN, + HalfPageDown, HalfPageUp, LineDown, LineUp, MoveDown, OpenExcerpts, PageDown, PageUp, Point, + SelectPhase, Selection, SoftWrap, ToPoint, MAX_LINE_LEN, }; use anyhow::Result; use collections::{BTreeMap, HashMap}; @@ -45,7 +45,7 @@ use std::{ }; use sum_tree::Bias; use theme::{ActiveTheme, PlayerColor}; -use ui::{h_stack, IconButton}; +use ui::{h_stack, IconButton, Tooltip}; use util::ResultExt; use workspace::item::Item; @@ -2036,7 +2036,9 @@ impl EditorElement { .on_click(move |editor: &mut Editor, cx| { editor.jump(jump_path.clone(), jump_position, jump_anchor, cx); }) - .tooltip("Jump to Buffer") // todo!(pass an action as well to show key binding) + .tooltip(move |_, cx| { + Tooltip::for_action("Jump to Buffer", &OpenExcerpts, cx) + }) }); let element = if *starts_new_buffer { diff --git a/crates/gpui2/src/elements/div.rs b/crates/gpui2/src/elements/div.rs index e098e8ef1aaf184cb3f126fd8df5db06d8942089..31a8827109d922546f312b507844d3996388e009 100644 --- a/crates/gpui2/src/elements/div.rs +++ b/crates/gpui2/src/elements/div.rs @@ -22,7 +22,6 @@ use util::ResultExt; const DRAG_THRESHOLD: f64 = 2.; const TOOLTIP_DELAY: Duration = Duration::from_millis(500); -const TOOLTIP_OFFSET: Point = Point::new(px(10.0), px(8.0)); pub struct GroupStyle { pub group: SharedString, @@ -408,21 +407,19 @@ pub trait StatefulInteractiveComponent>: InteractiveCo self } - fn tooltip( + fn tooltip( mut self, - build_tooltip: impl Fn(&mut V, &mut ViewContext) -> View + 'static, + build_tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, ) -> Self where Self: Sized, - W: 'static + Render, { debug_assert!( self.interactivity().tooltip_builder.is_none(), "calling tooltip more than once on the same element is not supported" ); - self.interactivity().tooltip_builder = Some(Rc::new(move |view_state, cx| { - build_tooltip(view_state, cx).into() - })); + self.interactivity().tooltip_builder = + Some(Rc::new(move |view_state, cx| build_tooltip(view_state, cx))); self } @@ -966,7 +963,7 @@ where waiting: None, tooltip: Some(AnyTooltip { view: tooltip_builder(view_state, cx), - cursor_offset: cx.mouse_position() + TOOLTIP_OFFSET, + cursor_offset: cx.mouse_position(), }), }); cx.notify(); diff --git a/crates/gpui2/src/elements/mod.rs b/crates/gpui2/src/elements/mod.rs index eb061f7d34c8e77c1a366123818b253d33620c7a..12c57958eaaf1829664ee73500985d05037f9786 100644 --- a/crates/gpui2/src/elements/mod.rs +++ b/crates/gpui2/src/elements/mod.rs @@ -1,11 +1,13 @@ mod div; mod img; +mod overlay; mod svg; mod text; mod uniform_list; pub use div::*; pub use img::*; +pub use overlay::*; pub use svg::*; pub use text::*; pub use uniform_list::*; diff --git a/crates/gpui2/src/elements/overlay.rs b/crates/gpui2/src/elements/overlay.rs new file mode 100644 index 0000000000000000000000000000000000000000..a190337f04dbfe5a4acd908aaf4a646c33a49240 --- /dev/null +++ b/crates/gpui2/src/elements/overlay.rs @@ -0,0 +1,203 @@ +use smallvec::SmallVec; + +use crate::{ + point, AnyElement, BorrowWindow, Bounds, Element, LayoutId, ParentComponent, Pixels, Point, + Size, Style, +}; + +pub struct OverlayState { + child_layout_ids: SmallVec<[LayoutId; 4]>, +} + +pub struct Overlay { + children: SmallVec<[AnyElement; 2]>, + anchor_corner: AnchorCorner, + fit_mode: OverlayFitMode, + // todo!(); + // anchor_position: Option, + // position_mode: OverlayPositionMode, +} + +/// overlay gives you a floating element that will avoid overflowing the window bounds. +/// Its children should have no margin to avoid measurement issues. +pub fn overlay() -> Overlay { + Overlay { + children: SmallVec::new(), + anchor_corner: AnchorCorner::TopLeft, + fit_mode: OverlayFitMode::SwitchAnchor, + } +} + +impl Overlay { + /// Sets which corner of the overlay should be anchored to the current position. + pub fn anchor(mut self, anchor: AnchorCorner) -> Self { + self.anchor_corner = anchor; + self + } + + /// Snap to window edge instead of switching anchor corner when an overflow would occur. + pub fn snap_to_window(mut self) -> Self { + self.fit_mode = OverlayFitMode::SnapToWindow; + self + } +} + +impl ParentComponent for Overlay { + fn children_mut(&mut self) -> &mut SmallVec<[AnyElement; 2]> { + &mut self.children + } +} + +impl Element for Overlay { + type ElementState = OverlayState; + + fn element_id(&self) -> Option { + None + } + + fn layout( + &mut self, + view_state: &mut V, + _: Option, + cx: &mut crate::ViewContext, + ) -> (crate::LayoutId, Self::ElementState) { + let child_layout_ids = self + .children + .iter_mut() + .map(|child| child.layout(view_state, cx)) + .collect::>(); + let layout_id = cx.request_layout(&Style::default(), child_layout_ids.iter().copied()); + + (layout_id, OverlayState { child_layout_ids }) + } + + fn paint( + &mut self, + bounds: crate::Bounds, + view_state: &mut V, + element_state: &mut Self::ElementState, + cx: &mut crate::ViewContext, + ) { + if element_state.child_layout_ids.is_empty() { + return; + } + + let mut child_min = point(Pixels::MAX, Pixels::MAX); + let mut child_max = Point::default(); + for child_layout_id in &element_state.child_layout_ids { + 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()); + } + let size: Size = (child_max - child_min).into(); + let origin = bounds.origin; + + let mut desired = self.anchor_corner.get_bounds(origin, size); + let limits = Bounds { + origin: Point::zero(), + size: cx.viewport_size(), + }; + + match self.fit_mode { + OverlayFitMode::SnapToWindow => { + // Snap the horizontal edges of the overlay to the horizontal edges of the window if + // its horizontal bounds overflow + if desired.right() > limits.right() { + desired.origin.x -= desired.right() - limits.right(); + } else if desired.left() < limits.left() { + desired.origin.x = limits.origin.x; + } + + // Snap the vertical edges of the overlay to the vertical edges of the window if + // its vertical bounds overflow. + if desired.bottom() > limits.bottom() { + desired.origin.y -= desired.bottom() - limits.bottom(); + } else if desired.top() < limits.top() { + desired.origin.y = limits.origin.y; + } + } + OverlayFitMode::SwitchAnchor => { + let mut anchor_corner = self.anchor_corner; + + if desired.left() < limits.left() || desired.right() > limits.right() { + anchor_corner = anchor_corner.switch_axis(Axis::Horizontal); + } + + if bounds.top() < limits.top() || bounds.bottom() > limits.bottom() { + anchor_corner = anchor_corner.switch_axis(Axis::Vertical); + } + + // Update bounds if needed + if anchor_corner != self.anchor_corner { + desired = anchor_corner.get_bounds(origin, size) + } + } + OverlayFitMode::None => {} + } + + cx.with_element_offset(desired.origin - bounds.origin, |cx| { + for child in &mut self.children { + child.paint(view_state, cx); + } + }) + } +} + +enum Axis { + Horizontal, + Vertical, +} + +#[derive(Copy, Clone)] +pub enum OverlayFitMode { + SnapToWindow, + SwitchAnchor, + None, +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub enum AnchorCorner { + TopLeft, + TopRight, + BottomLeft, + BottomRight, +} + +impl AnchorCorner { + fn get_bounds(&self, origin: Point, size: Size) -> Bounds { + let origin = match self { + Self::TopLeft => origin, + Self::TopRight => Point { + x: origin.x - size.width, + y: origin.y, + }, + Self::BottomLeft => Point { + x: origin.x, + y: origin.y - size.height, + }, + Self::BottomRight => Point { + x: origin.x - size.width, + y: origin.y - size.height, + }, + }; + + Bounds { origin, size } + } + + fn switch_axis(self, axis: Axis) -> Self { + match axis { + Axis::Vertical => match self { + AnchorCorner::TopLeft => AnchorCorner::BottomLeft, + AnchorCorner::TopRight => AnchorCorner::BottomRight, + AnchorCorner::BottomLeft => AnchorCorner::TopLeft, + AnchorCorner::BottomRight => AnchorCorner::TopRight, + }, + Axis::Horizontal => match self { + AnchorCorner::TopLeft => AnchorCorner::TopRight, + AnchorCorner::TopRight => AnchorCorner::TopLeft, + AnchorCorner::BottomLeft => AnchorCorner::BottomRight, + AnchorCorner::BottomRight => AnchorCorner::BottomLeft, + }, + } + } +} diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index e07300951ec61429ba617927649406409e74b531..854453101eeb1de0808565cdaf3414475f0123a5 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -421,6 +421,22 @@ impl Bounds where T: Add + Clone + Default + Debug, { + pub fn top(&self) -> T { + self.origin.y.clone() + } + + pub fn bottom(&self) -> T { + self.origin.y.clone() + self.size.height.clone() + } + + pub fn left(&self) -> T { + self.origin.x.clone() + } + + pub fn right(&self) -> T { + self.origin.x.clone() + self.size.width.clone() + } + pub fn upper_right(&self) -> Point { Point { x: self.origin.x.clone() + self.size.width.clone(), diff --git a/crates/project_panel2/src/project_panel.rs b/crates/project_panel2/src/project_panel.rs index 79a0f344aebb0191a9bebc3b640c18ce2d33fb17..03d8f54353d57d690a8fa67ef7853dfbbc5e6123 100644 --- a/crates/project_panel2/src/project_panel.rs +++ b/crates/project_panel2/src/project_panel.rs @@ -130,6 +130,13 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(assets: impl AssetSource, cx: &mut AppContext) { init_settings(cx); file_associations::init(assets, cx); + + cx.observe_new_views(|workspace: &mut Workspace, _| { + workspace.register_action(|workspace, _: &ToggleFocus, cx| { + workspace.toggle_panel_focus::(cx); + }); + }) + .detach(); } #[derive(Debug)] @@ -1516,12 +1523,12 @@ impl workspace::dock::Panel for ProjectPanel { cx.notify(); } - fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/project.svg") + fn icon(&self, _: &WindowContext) -> Option { + Some(ui::Icon::FileTree) } - fn icon_tooltip(&self) -> (String, Option>) { - ("Project Panel".into(), Some(Box::new(ToggleFocus))) + fn toggle_action(&self) -> Box { + Box::new(ToggleFocus) } // fn should_change_position_on_event(event: &Self::Event) -> bool { diff --git a/crates/storybook2/src/stories/scroll.rs b/crates/storybook2/src/stories/scroll.rs index f9530269d5cdcdd73de522263003c88bd2500290..f1bb7b4e7cacb05b0e2cd43a7eae3cfe3275092c 100644 --- a/crates/storybook2/src/stories/scroll.rs +++ b/crates/storybook2/src/stories/scroll.rs @@ -1,5 +1,6 @@ use gpui::{div, prelude::*, px, Div, Render, SharedString, Stateful, Styled, View, WindowContext}; use theme2::ActiveTheme; +use ui::Tooltip; pub struct ScrollStory; @@ -35,16 +36,18 @@ impl Render for ScrollStory { } else { color_2 }; - div().id(id).bg(bg).size(px(100. as f32)).when( - row >= 5 && column >= 5, - |d| { + div() + .id(id) + .tooltip(move |_, cx| Tooltip::text(format!("{}, {}", row, column), cx)) + .bg(bg) + .size(px(100. as f32)) + .when(row >= 5 && column >= 5, |d| { d.overflow_scroll() .child(div().size(px(50.)).bg(color_1)) .child(div().size(px(50.)).bg(color_2)) .child(div().size(px(50.)).bg(color_1)) .child(div().size(px(50.)).bg(color_2)) - }, - ) + }) })) })) } diff --git a/crates/ui2/src/components/button.rs b/crates/ui2/src/components/button.rs index 8eff92f11aac89d26020972f1586e1625b0862ed..397ce4f4c48314339e340f717ebccddda63453e4 100644 --- a/crates/ui2/src/components/button.rs +++ b/crates/ui2/src/components/button.rs @@ -61,7 +61,7 @@ impl ButtonVariant { } } -pub type ClickHandler = Arc) + Send + Sync>; +pub type ClickHandler = Arc)>; struct ButtonHandlers { click: Option>, diff --git a/crates/ui2/src/components/icon.rs b/crates/ui2/src/components/icon.rs index a0ef496d18cea94f9423ac181806dd4b5fd4a9fc..61aa23497856c001d521394233db20b9ee1ed4d6 100644 --- a/crates/ui2/src/components/icon.rs +++ b/crates/ui2/src/components/icon.rs @@ -25,6 +25,7 @@ pub enum Icon { ChevronRight, ChevronUp, Close, + Collab, Dash, Exit, ExclamationTriangle, @@ -83,6 +84,7 @@ impl Icon { Icon::ChevronRight => "icons/chevron_right.svg", Icon::ChevronUp => "icons/chevron_up.svg", Icon::Close => "icons/x.svg", + Icon::Collab => "icons/user_group_16.svg", Icon::Dash => "icons/dash.svg", Icon::Exit => "icons/exit.svg", Icon::ExclamationTriangle => "icons/warning.svg", diff --git a/crates/ui2/src/components/icon_button.rs b/crates/ui2/src/components/icon_button.rs index 7afaa1224343cc872ac8aac0cc6f8a0b5a245198..4408c51f62128cbe27b76dcd28f976b225abedda 100644 --- a/crates/ui2/src/components/icon_button.rs +++ b/crates/ui2/src/components/icon_button.rs @@ -1,5 +1,5 @@ -use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement, TextTooltip}; -use gpui::{prelude::*, MouseButton, VisualContext}; +use crate::{h_stack, prelude::*, ClickHandler, Icon, IconElement}; +use gpui::{prelude::*, AnyView, MouseButton}; use std::sync::Arc; struct IconButtonHandlers { @@ -19,7 +19,7 @@ pub struct IconButton { color: TextColor, variant: ButtonVariant, state: InteractionState, - tooltip: Option, + tooltip: Option) -> AnyView + 'static>>, handlers: IconButtonHandlers, } @@ -56,22 +56,23 @@ impl IconButton { self } - pub fn tooltip(mut self, tooltip: impl Into) -> Self { - self.tooltip = Some(tooltip.into()); + pub fn tooltip( + mut self, + tooltip: impl Fn(&mut V, &mut ViewContext) -> AnyView + 'static, + ) -> Self { + self.tooltip = Some(Box::new(tooltip)); self } - pub fn on_click( - mut self, - handler: impl 'static + Fn(&mut V, &mut ViewContext) + Send + Sync, - ) -> Self { + pub fn on_click(mut self, handler: impl 'static + Fn(&mut V, &mut ViewContext)) -> Self { self.handlers.click = Some(Arc::new(handler)); self } - fn render(self, _view: &mut V, cx: &mut ViewContext) -> impl Component { + fn render(mut self, _view: &mut V, cx: &mut ViewContext) -> impl Component { let icon_color = match (self.state, self.color) { (InteractionState::Disabled, _) => TextColor::Disabled, + (InteractionState::Active, _) => TextColor::Error, _ => self.color, }; @@ -99,15 +100,16 @@ impl IconButton { .child(IconElement::new(self.icon).color(icon_color)); if let Some(click_handler) = self.handlers.click.clone() { - button = button.on_mouse_down(MouseButton::Left, move |state, event, cx| { - cx.stop_propagation(); - click_handler(state, cx); - }); + button = button + .on_mouse_down(MouseButton::Left, move |state, event, cx| { + cx.stop_propagation(); + click_handler(state, cx); + }) + .cursor_pointer(); } - if let Some(tooltip) = self.tooltip.clone() { - button = - button.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(tooltip.clone()))); + if let Some(tooltip) = self.tooltip.take() { + button = button.tooltip(move |view: &mut V, cx| (tooltip)(view, cx)) } button diff --git a/crates/ui2/src/components/tooltip.rs b/crates/ui2/src/components/tooltip.rs index 8463ed7ba4a34a1ccb110499db8dbe0c61d0f9b8..a8dae6c97fd4486d848905784b1db83cedf56143 100644 --- a/crates/ui2/src/components/tooltip.rs +++ b/crates/ui2/src/components/tooltip.rs @@ -1,17 +1,53 @@ -use gpui::{Div, Render}; +use gpui::{overlay, Action, AnyView, Overlay, Render, VisualContext}; use settings2::Settings; use theme2::{ActiveTheme, ThemeSettings}; use crate::prelude::*; use crate::{h_stack, v_stack, KeyBinding, Label, LabelSize, StyledExt, TextColor}; -pub struct TextTooltip { +pub struct Tooltip { title: SharedString, meta: Option, key_binding: Option, } -impl TextTooltip { +impl Tooltip { + pub fn text(title: impl Into, cx: &mut WindowContext) -> AnyView { + cx.build_view(|cx| Self { + title: title.into(), + meta: None, + key_binding: None, + }) + .into() + } + + pub fn for_action( + title: impl Into, + action: &dyn Action, + cx: &mut WindowContext, + ) -> AnyView { + cx.build_view(|cx| Self { + title: title.into(), + meta: None, + key_binding: KeyBinding::for_action(action, cx), + }) + .into() + } + + pub fn with_meta( + title: impl Into, + action: Option<&dyn Action>, + meta: impl Into, + cx: &mut WindowContext, + ) -> AnyView { + cx.build_view(|cx| Self { + title: title.into(), + meta: Some(meta.into()), + key_binding: action.and_then(|action| KeyBinding::for_action(action, cx)), + }) + .into() + } + pub fn new(title: impl Into) -> Self { Self { title: title.into(), @@ -31,31 +67,36 @@ impl TextTooltip { } } -impl Render for TextTooltip { - type Element = Div; +impl Render for Tooltip { + type Element = Overlay; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { let ui_font = ThemeSettings::get_global(cx).ui_font.family.clone(); - v_stack() - .elevation_2(cx) - .font(ui_font) - .text_ui_sm() - .text_color(cx.theme().colors().text) - .py_1() - .px_2() - .child( - h_stack() - .child(self.title.clone()) - .when_some(self.key_binding.clone(), |this, key_binding| { - this.justify_between().child(key_binding) + overlay().child( + // padding to avoid mouse cursor + div().pl_2().pt_2p5().child( + v_stack() + .elevation_2(cx) + .font(ui_font) + .text_ui_sm() + .text_color(cx.theme().colors().text) + .py_1() + .px_2() + .child( + h_stack() + .child(self.title.clone()) + .when_some(self.key_binding.clone(), |this, key_binding| { + this.justify_between().child(key_binding) + }), + ) + .when_some(self.meta.clone(), |this, meta| { + this.child( + Label::new(meta) + .size(LabelSize::Small) + .color(TextColor::Muted), + ) }), - ) - .when_some(self.meta.clone(), |this, meta| { - this.child( - Label::new(meta) - .size(LabelSize::Small) - .color(TextColor::Muted), - ) - }) + ), + ) } } diff --git a/crates/workspace2/src/dock.rs b/crates/workspace2/src/dock.rs index 455148391bb4c470e822711266a169fb4f2403d0..0ebf0cd5164bb196b578662c0439fcdfb01f05c2 100644 --- a/crates/workspace2/src/dock.rs +++ b/crates/workspace2/src/dock.rs @@ -7,6 +7,7 @@ use gpui::{ use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use std::sync::Arc; +use ui::{h_stack, IconButton, InteractionState, Tooltip}; pub enum PanelEvent { ChangePosition, @@ -24,8 +25,8 @@ pub trait Panel: Render + EventEmitter { fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&mut self, size: Option, cx: &mut ViewContext); - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; - fn icon_tooltip(&self) -> (String, Option>); + fn icon(&self, cx: &WindowContext) -> Option; + fn toggle_action(&self) -> Box; fn icon_label(&self, _: &WindowContext) -> Option { None } @@ -49,8 +50,8 @@ pub trait PanelHandle: Send + Sync { fn set_active(&self, active: bool, cx: &mut WindowContext); fn size(&self, cx: &WindowContext) -> f32; fn set_size(&self, size: Option, cx: &mut WindowContext); - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str>; - fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>); + fn icon(&self, cx: &WindowContext) -> Option; + fn toggle_action(&self, cx: &WindowContext) -> Box; fn icon_label(&self, cx: &WindowContext) -> Option; fn has_focus(&self, cx: &WindowContext) -> bool; fn focus_handle(&self, cx: &WindowContext) -> FocusHandle; @@ -101,12 +102,12 @@ where self.update(cx, |this, cx| this.set_size(size, cx)) } - fn icon_path(&self, cx: &WindowContext) -> Option<&'static str> { - self.read(cx).icon_path(cx) + fn icon(&self, cx: &WindowContext) -> Option { + self.read(cx).icon(cx) } - fn icon_tooltip(&self, cx: &WindowContext) -> (String, Option>) { - self.read(cx).icon_tooltip() + fn toggle_action(&self, cx: &WindowContext) -> Box { + self.read(cx).toggle_action() } fn icon_label(&self, cx: &WindowContext) -> Option { @@ -214,11 +215,11 @@ impl Dock { // .find_map(|entry| entry.panel.as_any().clone().downcast()) // } - // pub fn panel_index_for_type(&self) -> Option { - // self.panel_entries - // .iter() - // .position(|entry| entry.panel.as_any().is::()) - // } + pub fn panel_index_for_type(&self) -> Option { + self.panel_entries + .iter() + .position(|entry| entry.panel.to_any().downcast::().is_ok()) + } pub fn panel_index_for_ui_name(&self, _ui_name: &str, _cx: &AppContext) -> Option { todo!() @@ -644,11 +645,28 @@ impl Render for PanelButtons { fn render(&mut self, cx: &mut ViewContext) -> Self::Element { // todo!() let dock = self.dock.read(cx); - div().children( - dock.panel_entries - .iter() - .map(|panel| panel.panel.persistent_name(cx)), - ) + let active_index = dock.active_panel_index; + let is_open = dock.is_open; + + let buttons = dock + .panel_entries + .iter() + .enumerate() + .filter_map(|(i, panel)| { + let icon = panel.panel.icon(cx)?; + let name = panel.panel.persistent_name(cx); + let action = panel.panel.toggle_action(cx); + let action2 = action.boxed_clone(); + + let mut button = IconButton::new(panel.panel.persistent_name(cx), icon) + .when(i == active_index, |el| el.state(InteractionState::Active)) + .on_click(move |this, cx| cx.dispatch_action(action.boxed_clone())) + .tooltip(move |_, cx| Tooltip::for_action(name, &*action2, cx)); + + Some(button) + }); + + h_stack().children(buttons) } } @@ -665,7 +683,7 @@ impl StatusItemView for PanelButtons { #[cfg(any(test, feature = "test-support"))] pub mod test { use super::*; - use gpui::{div, Div, ViewContext, WindowContext}; + use gpui::{actions, div, Div, ViewContext, WindowContext}; pub struct TestPanel { pub position: DockPosition, @@ -674,6 +692,7 @@ pub mod test { pub has_focus: bool, pub size: f32, } + actions!(ToggleTestPanel); impl EventEmitter for TestPanel {} @@ -723,12 +742,12 @@ pub mod test { self.size = size.unwrap_or(300.); } - fn icon_path(&self, _: &WindowContext) -> Option<&'static str> { - Some("icons/test_panel.svg") + fn icon(&self, _: &WindowContext) -> Option { + None } - fn icon_tooltip(&self) -> (String, Option>) { - ("Test Panel".into(), None) + fn toggle_action(&self) -> Box { + ToggleTestPanel.boxed_clone() } fn is_zoomed(&self, _: &WindowContext) -> bool { diff --git a/crates/workspace2/src/pane.rs b/crates/workspace2/src/pane.rs index 668ce2f20706f2e7e5e96bdd080ca11121d50903..e55d4509612874c276fcf87330ed9f5852be1790 100644 --- a/crates/workspace2/src/pane.rs +++ b/crates/workspace2/src/pane.rs @@ -25,7 +25,7 @@ use std::{ }, }; use ui::v_stack; -use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, TextTooltip}; +use ui::{prelude::*, Icon, IconButton, IconElement, TextColor, Tooltip}; use util::truncate_and_remove_front; #[derive(PartialEq, Clone, Copy, Deserialize, Debug)] @@ -1396,7 +1396,7 @@ impl Pane { .id(item.id()) .cursor_pointer() .when_some(item.tab_tooltip_text(cx), |div, text| { - div.tooltip(move |_, cx| cx.build_view(|cx| TextTooltip::new(text.clone()))) + div.tooltip(move |_, cx| cx.build_view(|cx| Tooltip::new(text.clone())).into()) }) // .on_drag(move |pane, cx| pane.render_tab(ix, item.boxed_clone(), detail, cx)) // .drag_over::(|d| d.bg(cx.theme().colors().element_drop_target)) diff --git a/crates/workspace2/src/status_bar.rs b/crates/workspace2/src/status_bar.rs index 5dccac243f47c0ec169c3bec9436d7c0a998b22f..38356dd3d1f7af04202755fbb69dfe42f184bc6a 100644 --- a/crates/workspace2/src/status_bar.rs +++ b/crates/workspace2/src/status_bar.rs @@ -6,6 +6,7 @@ use gpui::{ WindowContext, }; use theme2::ActiveTheme; +use ui::h_stack; use util::ResultExt; pub trait StatusItemView: Render { @@ -53,16 +54,14 @@ impl Render for StatusBar { impl StatusBar { fn render_left_tools(&self, cx: &mut ViewContext) -> impl Component { - div() - .flex() + h_stack() .items_center() .gap_1() .children(self.left_items.iter().map(|item| item.to_any())) } fn render_right_tools(&self, cx: &mut ViewContext) -> impl Component { - div() - .flex() + h_stack() .items_center() .gap_2() .children(self.right_items.iter().map(|item| item.to_any())) diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5213a1f83da472011872302885abee980943b62c..22fe342bfcf870f9a375217b8135cc9b6a800281 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -29,7 +29,7 @@ use client2::{ Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; -use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle as _}; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; use futures::{ channel::{mpsc, oneshot}, future::try_join_all, @@ -1599,52 +1599,52 @@ impl Workspace { // .downcast() // } - // /// Focus the panel of the given type if it isn't already focused. If it is - // /// already focused, then transfer focus back to the workspace center. - // pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { - // self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); - // } + /// Focus the panel of the given type if it isn't already focused. If it is + /// already focused, then transfer focus back to the workspace center. + pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { + self.focus_or_unfocus_panel::(cx, |panel, cx| !panel.has_focus(cx)); + } - // /// Focus or unfocus the given panel type, depending on the given callback. - // fn focus_or_unfocus_panel( - // &mut self, - // cx: &mut ViewContext, - // should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, - // ) -> Option> { - // for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { - // if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { - // let mut focus_center = false; - // let mut reveal_dock = false; - // let panel = dock.update(cx, |dock, cx| { - // dock.activate_panel(panel_index, cx); - - // let panel = dock.active_panel().cloned(); - // if let Some(panel) = panel.as_ref() { - // if should_focus(&**panel, cx) { - // dock.set_open(true, cx); - // cx.focus(panel.as_any()); - // reveal_dock = true; - // } else { - // // if panel.is_zoomed(cx) { - // // dock.set_open(false, cx); - // // } - // focus_center = true; - // } - // } - // panel - // }); + /// Focus or unfocus the given panel type, depending on the given callback. + fn focus_or_unfocus_panel( + &mut self, + cx: &mut ViewContext, + should_focus: impl Fn(&dyn PanelHandle, &mut ViewContext) -> bool, + ) -> Option> { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { + let mut focus_center = false; + let mut reveal_dock = false; + let panel = dock.update(cx, |dock, cx| { + dock.activate_panel(panel_index, cx); + + let panel = dock.active_panel().cloned(); + if let Some(panel) = panel.as_ref() { + if should_focus(&**panel, cx) { + dock.set_open(true, cx); + panel.focus_handle(cx).focus(cx); + reveal_dock = true; + } else { + // if panel.is_zoomed(cx) { + // dock.set_open(false, cx); + // } + focus_center = true; + } + } + panel + }); - // if focus_center { - // cx.focus_self(); - // } + if focus_center { + self.active_pane.update(cx, |pane, cx| pane.focus(cx)) + } - // self.serialize_workspace(cx); - // cx.notify(); - // return panel; - // } - // } - // None - // } + self.serialize_workspace(cx); + cx.notify(); + return panel; + } + } + None + } // pub fn panel(&self, cx: &WindowContext) -> Option> { // for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { diff --git a/crates/zed2/src/zed2.rs b/crates/zed2/src/zed2.rs index 8e9079ed6b8ac1afcead135b33d69b4e15bf4d61..e45ce46542292c632c83945591968c1669a86c90 100644 --- a/crates/zed2/src/zed2.rs +++ b/crates/zed2/src/zed2.rs @@ -383,8 +383,8 @@ pub fn initialize_workspace( let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); // let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); // let assistant_panel = AssistantPanel::load(workspace_handle.clone(), cx.clone()); - // let channels_panel = - // collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); + let channels_panel = + collab_ui::collab_panel::CollabPanel::load(workspace_handle.clone(), cx.clone()); // let chat_panel = // collab_ui::chat_panel::ChatPanel::load(workspace_handle.clone(), cx.clone()); // let notification_panel = collab_ui::notification_panel::NotificationPanel::load( @@ -395,16 +395,16 @@ pub fn initialize_workspace( project_panel, // terminal_panel, // assistant_panel, - // channels_panel, + channels_panel, // chat_panel, // notification_panel, ) = futures::try_join!( project_panel, // terminal_panel, // assistant_panel, - // channels_panel, + channels_panel, // chat_panel, - // notification_panel, + // notification_panel,/ )?; workspace_handle.update(&mut cx, |workspace, cx| { @@ -412,7 +412,7 @@ pub fn initialize_workspace( workspace.add_panel(project_panel, cx); // workspace.add_panel(terminal_panel, cx); // workspace.add_panel(assistant_panel, cx); - // workspace.add_panel(channels_panel, cx); + workspace.add_panel(channels_panel, cx); // workspace.add_panel(chat_panel, cx); // workspace.add_panel(notification_panel, cx);