From 969ecfcfa234ee150eebb87507cebec48afdf53f Mon Sep 17 00:00:00 2001 From: Max Brunsfeld Date: Mon, 24 Jul 2023 20:00:31 -0700 Subject: [PATCH] Reinstate all of the contacts popovers' functionality in the new collaboration panel --- crates/collab_ui/src/panel.rs | 1377 +++++++++++++++- .../panel/{contacts => }/contact_finder.rs | 0 crates/collab_ui/src/panel/contacts.rs | 140 -- .../src/panel/contacts/contacts_list.rs | 1384 ----------------- crates/gpui/src/elements.rs | 12 +- styles/src/style_tree/contacts_popover.ts | 6 +- 6 files changed, 1342 insertions(+), 1577 deletions(-) rename crates/collab_ui/src/panel/{contacts => }/contact_finder.rs (100%) delete mode 100644 crates/collab_ui/src/panel/contacts.rs delete mode 100644 crates/collab_ui/src/panel/contacts/contacts_list.rs diff --git a/crates/collab_ui/src/panel.rs b/crates/collab_ui/src/panel.rs index 8fec29133f..28cb57cf79 100644 --- a/crates/collab_ui/src/panel.rs +++ b/crates/collab_ui/src/panel.rs @@ -1,39 +1,51 @@ -mod contacts; +mod contact_finder; mod panel_settings; -use std::sync::Arc; - use anyhow::Result; -use client::Client; +use call::ActiveCall; +use client::{proto::PeerId, Client, Contact, User, UserStore}; +use contact_finder::{build_contact_finder, ContactFinder}; use context_menu::ContextMenu; use db::kvp::KEY_VALUE_STORE; +use editor::{Cancel, Editor}; +use futures::StreamExt; +use fuzzy::{match_strings, StringMatchCandidate}; use gpui::{ actions, - elements::{ChildView, Flex, Label, ParentElement, Stack}, - serde_json, AppContext, AsyncAppContext, Element, Entity, Task, View, ViewContext, ViewHandle, - WeakViewHandle, + elements::{ + Canvas, ChildView, Empty, Flex, Image, Label, List, ListOffset, ListState, + MouseEventHandler, Orientation, Padding, ParentElement, Stack, Svg, + }, + geometry::{rect::RectF, vector::vec2f}, + platform::{CursorStyle, MouseButton, PromptLevel}, + serde_json, AnyElement, AppContext, AsyncAppContext, Element, Entity, ModelHandle, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, }; -use project::Fs; +use menu::{Confirm, SelectNext, SelectPrev}; +use panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}; +use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::SettingsStore; +use std::{mem, sync::Arc}; +use theme::IconButton; use util::{ResultExt, TryFutureExt}; use workspace::{ dock::{DockPosition, Panel}, Workspace, }; -use self::{ - contacts::Contacts, - panel_settings::{ChannelsPanelDockPosition, ChannelsPanelSettings}, -}; - actions!(collab_panel, [ToggleFocus]); const CHANNELS_PANEL_KEY: &'static str = "ChannelsPanel"; pub fn init(_client: Arc, cx: &mut AppContext) { settings::register::(cx); - contacts::init(cx); + contact_finder::init(cx); + + cx.add_action(CollabPanel::cancel); + cx.add_action(CollabPanel::select_next); + cx.add_action(CollabPanel::select_prev); + cx.add_action(CollabPanel::confirm); } pub struct CollabPanel { @@ -42,7 +54,19 @@ pub struct CollabPanel { has_focus: bool, pending_serialization: Task>, context_menu: ViewHandle, - contacts: ViewHandle, + contact_finder: Option>, + + // from contacts list + filter_editor: ViewHandle, + entries: Vec, + selection: Option, + user_store: ModelHandle, + project: ModelHandle, + match_candidates: Vec, + list_state: ListState, + subscriptions: Vec, + collapsed_sections: Vec
, + workspace: WeakViewHandle, } #[derive(Serialize, Deserialize)] @@ -54,6 +78,40 @@ struct SerializedChannelsPanel { pub enum Event { DockPositionChanged, Focus, + Dismissed, +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] +enum Section { + ActiveCall, + Requests, + Online, + Offline, +} + +#[derive(Clone)] +enum ContactEntry { + Header(Section), + CallParticipant { + user: Arc, + is_pending: bool, + }, + ParticipantProject { + project_id: u64, + worktree_root_names: Vec, + host_user_id: u64, + is_last: bool, + }, + ParticipantScreen { + peer_id: PeerId, + is_last: bool, + }, + IncomingRequest(Arc), + OutgoingRequest(Arc), + Contact { + contact: Arc, + calling: bool, + }, } impl Entity for CollabPanel { @@ -62,35 +120,151 @@ impl Entity for CollabPanel { impl CollabPanel { pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { - cx.add_view(|cx| { + cx.add_view::(|cx| { let view_id = cx.view_id(); - let this = Self { + let filter_editor = cx.add_view(|cx| { + let mut editor = Editor::single_line( + Some(Arc::new(|theme| { + theme.contact_list.user_query_editor.clone() + })), + cx, + ); + editor.set_placeholder_text("Filter 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(cx); + if !query.is_empty() { + this.selection = this + .entries + .iter() + .position(|entry| !matches!(entry, ContactEntry::Header(_))); + } + } + }) + .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] { + ContactEntry::Header(section) => { + let is_collapsed = this.collapsed_sections.contains(section); + Self::render_header( + *section, + &theme.contact_list, + is_selected, + is_collapsed, + cx, + ) + } + ContactEntry::CallParticipant { user, is_pending } => { + Self::render_call_participant( + user, + *is_pending, + is_selected, + &theme.contact_list, + ) + } + ContactEntry::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.contact_list, + cx, + ), + ContactEntry::ParticipantScreen { peer_id, is_last } => { + Self::render_participant_screen( + *peer_id, + *is_last, + is_selected, + &theme.contact_list, + cx, + ) + } + ContactEntry::IncomingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + true, + is_selected, + cx, + ), + ContactEntry::OutgoingRequest(user) => Self::render_contact_request( + user.clone(), + this.user_store.clone(), + &theme.contact_list, + false, + is_selected, + cx, + ), + ContactEntry::Contact { contact, calling } => Self::render_contact( + contact, + *calling, + &this.project, + &theme.contact_list, + is_selected, + cx, + ), + } + }); + + let mut this = Self { width: None, has_focus: false, fs: workspace.app_state().fs.clone(), pending_serialization: Task::ready(None), context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), - contacts: cx.add_view(|cx| { - Contacts::new( - workspace.project().clone(), - workspace.user_store().clone(), - workspace.weak_handle(), - cx, - ) - }), + filter_editor, + contact_finder: None, + entries: Vec::default(), + selection: None, + user_store: workspace.user_store().clone(), + project: workspace.project().clone(), + subscriptions: Vec::default(), + match_candidates: Vec::default(), + collapsed_sections: Vec::default(), + workspace: workspace.weak_handle(), + list_state, }; + this.update_entries(cx); // Update the dock position when the setting changes. let mut old_dock_position = this.position(cx); - cx.observe_global::(move |this: &mut CollabPanel, 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); - } - }) - .detach(); + this.subscriptions + .push( + cx.observe_global::(move |this: &mut CollabPanel, 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); + } + }), + ); + + let active_call = ActiveCall::global(cx); + this.subscriptions + .push(cx.observe(&this.user_store, |this, _, cx| this.update_entries(cx))); + this.subscriptions + .push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); this }) @@ -141,11 +315,1015 @@ impl CollabPanel { .log_err(), ); } + + fn update_entries(&mut self, cx: &mut ViewContext) { + 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); + + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + let mut participant_entries = Vec::new(); + + // 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; + participant_entries.push(ContactEntry::CallParticipant { + user, + is_pending: false, + }); + let mut projects = room.local_participant().projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::ParticipantProject { + project_id: project.id, + worktree_root_names: project.worktree_root_names.clone(), + host_user_id: user_id, + is_last: projects.peek().is_none(), + }); + } + } + } + + // 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]; + participant_entries.push(ContactEntry::CallParticipant { + user: participant.user.clone(), + is_pending: false, + }); + let mut projects = participant.projects.iter().peekable(); + while let Some(project) = projects.next() { + participant_entries.push(ContactEntry::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() { + participant_entries.push(ContactEntry::ParticipantScreen { + peer_id: 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(), + )); + participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { + user: room.pending_participants()[mat.candidate_id].clone(), + is_pending: true, + })); + + if !participant_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::ActiveCall)); + if !self.collapsed_sections.contains(&Section::ActiveCall) { + self.entries.extend(participant_entries); + } + } + } + + let mut request_entries = Vec::new(); + 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| ContactEntry::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| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), + ); + } + + if !request_entries.is_empty() { + self.entries.push(ContactEntry::Header(Section::Requests)); + if !self.collapsed_sections.contains(&Section::Requests) { + 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 (mut online_contacts, offline_contacts) = matches + .iter() + .partition::, _>(|mat| contacts[mat.candidate_id].online); + if let Some(room) = ActiveCall::global(cx).read(cx).room() { + let room = room.read(cx); + online_contacts.retain(|contact| { + let contact = &contacts[contact.candidate_id]; + !room.contains_participant(contact.user.id) + }); + } + + for (matches, section) in [ + (online_contacts, Section::Online), + (offline_contacts, Section::Offline), + ] { + if !matches.is_empty() { + self.entries.push(ContactEntry::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(ContactEntry::Contact { + contact: contact.clone(), + calling: active_call.pending_invites().contains(&contact.user.id), + }); + } + } + } + } + } + + 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; + } + } + } + + let old_scroll_top = self.list_state.logical_scroll_top(); + self.list_state.reset(self.entries.len()); + + // 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, + is_pending: bool, + is_selected: bool, + theme: &theme::ContactList, + ) -> AnyElement { + 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), + ) + .with_children(if is_pending { + Some( + Label::new("Calling", theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + .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::ContactList, + cx: &mut ViewContext, + ) -> AnyElement { + enum JoinProject {} + + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.inactive_state().default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + let project_name = if worktree_root_names.is_empty() { + "untitled".to_string() + } else { + worktree_root_names.join(", ") + }; + + MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { + 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( + Stack::new() + .with_child(Canvas::new(move |scene, bounds, _, _, _| { + let start_x = + bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + })) + .constrained() + .with_width(host_avatar_height), + ) + .with_child( + Label::new(project_name, 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) + }) + .with_cursor_style(if !is_current { + CursorStyle::PointingHand + } else { + CursorStyle::Arrow + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if !is_current { + 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); + } + } + }) + .into_any() + } + + fn render_participant_screen( + peer_id: PeerId, + is_last: bool, + is_selected: bool, + theme: &theme::ContactList, + cx: &mut ViewContext, + ) -> AnyElement { + enum OpenSharedScreen {} + + let font_cache = cx.font_cache(); + let host_avatar_height = theme + .contact_avatar + .width + .or(theme.contact_avatar.height) + .unwrap_or(0.); + let row = &theme.project_row.inactive_state().default; + let tree_branch = theme.tree_branch; + let line_height = row.name.text.line_height(font_cache); + let cap_height = row.name.text.cap_height(font_cache); + let baseline_offset = + row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; + + MouseEventHandler::::new( + peer_id.as_u64() as usize, + cx, + |mouse_state, _| { + 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( + Stack::new() + .with_child(Canvas::new(move |scene, bounds, _, _, _| { + let start_x = bounds.min_x() + (bounds.width() / 2.) + - (tree_branch.width / 2.); + let end_x = bounds.max_x(); + let start_y = bounds.min_y(); + let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); + + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, start_y), + vec2f( + start_x + tree_branch.width, + if is_last { end_y } else { bounds.max_y() }, + ), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + scene.push_quad(gpui::Quad { + bounds: RectF::from_points( + vec2f(start_x, end_y), + vec2f(end_x, end_y + tree_branch.width), + ), + background: Some(tree_branch.color), + border: gpui::Border::default(), + corner_radius: 0., + }); + })) + .constrained() + .with_width(host_avatar_height), + ) + .with_child( + Svg::new("icons/disable_screen_sharing_12.svg") + .with_color(row.icon.color) + .constrained() + .with_width(row.icon.width) + .aligned() + .left() + .contained() + .with_style(row.icon.container), + ) + .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) + }, + ) + .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, cx) + }); + } + }) + .into_any() + } + + fn render_header( + section: Section, + theme: &theme::ContactList, + is_selected: bool, + is_collapsed: bool, + cx: &mut ViewContext, + ) -> AnyElement { + enum Header {} + enum LeaveCallContactList {} + + let header_style = theme + .header_row + .in_state(is_selected) + .style_for(&mut Default::default()); + let text = match section { + Section::ActiveCall => "Collaborators", + Section::Requests => "Contact Requests", + Section::Online => "Online", + Section::Offline => "Offline", + }; + let leave_call = if section == Section::ActiveCall { + Some( + MouseEventHandler::::new(0, cx, |state, _| { + let style = theme.leave_call.style_for(state); + Label::new("Leave Call", style.text.clone()) + .contained() + .with_style(style.container) + }) + .on_click(MouseButton::Left, |_, _, cx| { + ActiveCall::global(cx) + .update(cx, |call, cx| call.hang_up(cx)) + .detach_and_log_err(cx); + }) + .aligned(), + ) + } else { + None + }; + + let icon_size = theme.section_icon_size; + MouseEventHandler::::new(section as usize, cx, |_, _| { + Flex::row() + .with_child( + Svg::new(if is_collapsed { + "icons/chevron_right_8.svg" + } else { + "icons/chevron_down_8.svg" + }) + .with_color(header_style.text.color) + .constrained() + .with_max_width(icon_size) + .with_max_height(icon_size) + .aligned() + .constrained() + .with_width(icon_size), + ) + .with_child( + Label::new(text, header_style.text.clone()) + .aligned() + .left() + .contained() + .with_margin_left(theme.contact_username.container.margin.left) + .flex(1., true), + ) + .with_children(leave_call) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style(header_style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, this, cx| { + this.toggle_expanded(section, cx); + }) + .into_any() + } + + fn render_contact( + contact: &Contact, + calling: bool, + project: &ModelHandle, + theme: &theme::ContactList, + is_selected: bool, + cx: &mut ViewContext, + ) -> AnyElement { + 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 mut event_handler = + MouseEventHandler::::new(contact.user.id as usize, cx, |_, 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 { + theme.contact_status_busy + } else { + theme.contact_status_free + }) + .aligned(), + ) + } else { + None + }; + Stack::new() + .with_child( + Image::from_data(avatar) + .with_style(theme.contact_avatar) + .aligned() + .left(), + ) + .with_children(status_badge) + })) + .with_child( + Label::new( + contact.user.github_login.clone(), + theme.contact_username.text.clone(), + ) + .contained() + .with_style(theme.contact_username.container) + .aligned() + .left() + .flex(1., true), + ) + .with_child( + MouseEventHandler::::new( + contact.user.id as usize, + cx, + |mouse_state, _| { + let button_style = theme.contact_button.style_for(mouse_state); + render_icon_button(button_style, "icons/x_mark_8.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(), + ) + .with_children(if calling { + Some( + Label::new("Calling", theme.calling_indicator.text.clone()) + .contained() + .with_style(theme.calling_indicator.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(theme.row_height) + .contained() + .with_style( + *theme + .contact_row + .in_state(is_selected) + .style_for(&mut Default::default()), + ) + }) + .on_click(MouseButton::Left, move |_, this, cx| { + if online && !busy { + this.call(user_id, Some(initial_project.clone()), cx); + } + }); + + if online { + event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand); + } + + event_handler.into_any() + } + + fn render_contact_request( + user: Arc, + user_store: ModelHandle, + theme: &theme::ContactList, + 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_mark_8.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_8.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_mark_8.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 cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { + if self.contact_finder.take().is_some() { + cx.notify(); + return; + } + + let did_clear = self.filter_editor.update(cx, |editor, cx| { + if editor.buffer().read(cx).len(cx) > 0 { + editor.set_text("", cx); + true + } else { + false + } + }); + + if !did_clear { + cx.emit(Event::Dismissed); + } + } + + fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { + if let Some(ix) = self.selection { + if self.entries.len() > ix + 1 { + self.selection = Some(ix + 1); + } + } else if !self.entries.is_empty() { + self.selection = Some(0); + } + 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) { + if let Some(ix) = self.selection { + if ix > 0 { + self.selection = Some(ix - 1); + } else { + self.selection = None; + } + } + 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 let Some(selection) = self.selection { + if let Some(entry) = self.entries.get(selection) { + match entry { + ContactEntry::Header(section) => { + self.toggle_expanded(*section, cx); + } + ContactEntry::Contact { contact, calling } => { + if contact.online && !contact.busy && !calling { + self.call(contact.user.id, Some(self.project.clone()), cx); + } + } + ContactEntry::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); + } + } + ContactEntry::ParticipantScreen { peer_id, .. } => { + if let Some(workspace) = self.workspace.upgrade(cx) { + workspace.update(cx, |workspace, cx| { + workspace.open_shared_screen(*peer_id, cx) + }); + } + } + _ => {} + } + } + } + } + + fn toggle_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(cx); + } + + fn toggle_contact_finder(&mut self, cx: &mut ViewContext) { + if self.contact_finder.take().is_none() { + let child = cx.add_view(|cx| { + let finder = build_contact_finder(self.user_store.clone(), cx); + finder.set_query(self.filter_editor.read(cx).text(cx), cx); + finder + }); + cx.focus(&child); + // self.subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { + // // PickerEvent::Dismiss => cx.emit(Event::Dismissed), + // })); + self.contact_finder = Some(child); + } + cx.notify(); + } + + 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_id = cx.window_id(); + 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 + { + cx.prompt( + window_id, + PromptLevel::Info, + &format!("Failed to remove contact: {}", e), + &["Ok"], + ); + } + } + }) + .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 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); + } } impl View for CollabPanel { fn ui_name() -> &'static str { - "ChannelsPanel" + "CollabPanel" } fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { @@ -160,28 +1338,58 @@ impl View for CollabPanel { } fn render(&mut self, cx: &mut gpui::ViewContext<'_, '_, Self>) -> gpui::AnyElement { + enum AddContact {} let theme = theme::current(cx).clone(); - enum ChannelsPanelScrollTag {} Stack::new() - .with_child( - // Full panel column + .with_child(if let Some(finder) = &self.contact_finder { + ChildView::new(&finder, cx).into_any() + } else { Flex::column() .with_child( - Flex::column() + Flex::row() .with_child( - Flex::row().with_child( - Label::new( - "Contacts", - theme.editor.invalid_information_diagnostic.message.clone(), - ) - .into_any(), - ), + ChildView::new(&self.filter_editor, cx) + .contained() + .with_style(theme.contact_list.user_query_editor.container) + .flex(1.0, true), ) - .with_child(ChildView::new(&self.contacts, cx)), + .with_child( + MouseEventHandler::::new(0, cx, |_, _| { + render_icon_button( + &theme.contact_list.add_contact_button, + "icons/user_plus_16.svg", + ) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, |_, this, cx| { + this.toggle_contact_finder(cx); + }) + .with_tooltip::( + 0, + "Search for new contact".into(), + None, + theme.tooltip.clone(), + cx, + ) + .constrained() + .with_height(theme.contact_list.user_query_editor_height) + .with_width(theme.contact_list.user_query_editor_height), + ) + .constrained() + .with_width(self.size(cx)), ) - .scrollable::(0, None, cx), - ) + .with_child( + List::new(self.list_state.clone()) + .constrained() + .with_width(self.size(cx)) + .flex(1., true) + .into_any(), + ) + .constrained() + .with_width(self.size(cx)) + .into_any() + }) .with_child(ChildView::new(&self.context_menu, cx)) .into_any_named("channels panel") .into_any() @@ -245,3 +1453,76 @@ impl Panel for CollabPanel { matches!(event, Event::Focus) } } + +impl PartialEq for ContactEntry { + fn eq(&self, other: &Self) -> bool { + match self { + ContactEntry::Header(section_1) => { + if let ContactEntry::Header(section_2) = other { + return section_1 == section_2; + } + } + ContactEntry::CallParticipant { user: user_1, .. } => { + if let ContactEntry::CallParticipant { user: user_2, .. } = other { + return user_1.id == user_2.id; + } + } + ContactEntry::ParticipantProject { + project_id: project_id_1, + .. + } => { + if let ContactEntry::ParticipantProject { + project_id: project_id_2, + .. + } = other + { + return project_id_1 == project_id_2; + } + } + ContactEntry::ParticipantScreen { + peer_id: peer_id_1, .. + } => { + if let ContactEntry::ParticipantScreen { + peer_id: peer_id_2, .. + } = other + { + return peer_id_1 == peer_id_2; + } + } + ContactEntry::IncomingRequest(user_1) => { + if let ContactEntry::IncomingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::OutgoingRequest(user_1) => { + if let ContactEntry::OutgoingRequest(user_2) = other { + return user_1.id == user_2.id; + } + } + ContactEntry::Contact { + contact: contact_1, .. + } => { + if let ContactEntry::Contact { + contact: contact_2, .. + } = other + { + return contact_1.user.id == contact_2.user.id; + } + } + } + false + } +} + +fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { + Svg::new(svg_path) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .contained() + .with_style(style.container) + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) +} diff --git a/crates/collab_ui/src/panel/contacts/contact_finder.rs b/crates/collab_ui/src/panel/contact_finder.rs similarity index 100% rename from crates/collab_ui/src/panel/contacts/contact_finder.rs rename to crates/collab_ui/src/panel/contact_finder.rs diff --git a/crates/collab_ui/src/panel/contacts.rs b/crates/collab_ui/src/panel/contacts.rs deleted file mode 100644 index a1c1061f5e..0000000000 --- a/crates/collab_ui/src/panel/contacts.rs +++ /dev/null @@ -1,140 +0,0 @@ -mod contact_finder; -mod contacts_list; - -use client::UserStore; -use gpui::{ - actions, elements::*, platform::MouseButton, AppContext, Entity, ModelHandle, View, - ViewContext, ViewHandle, WeakViewHandle, -}; -use picker::PickerEvent; -use project::Project; -use workspace::Workspace; - -use self::{contacts_list::ContactList, contact_finder::{ContactFinder, build_contact_finder}}; - -actions!(contacts_popover, [ToggleContactFinder]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(Contacts::toggle_contact_finder); - contact_finder::init(cx); - contacts_list::init(cx); -} - -pub enum Event { - Dismissed, -} - -enum Child { - ContactList(ViewHandle), - ContactFinder(ViewHandle), -} - -pub struct Contacts { - child: Child, - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - _subscription: Option, -} - -impl Contacts { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let mut this = Self { - child: Child::ContactList(cx.add_view(|cx| { - ContactList::new(project.clone(), user_store.clone(), workspace.clone(), cx) - })), - project, - user_store, - workspace, - _subscription: None, - }; - this.show_contact_list(String::new(), cx); - this - } - - fn toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { - match &self.child { - Child::ContactList(list) => self.show_contact_finder(list.read(cx).editor_text(cx), cx), - Child::ContactFinder(finder) => self.show_contact_list(finder.read(cx).query(cx), cx), - } - } - - fn show_contact_finder(&mut self, editor_text: String, cx: &mut ViewContext) { - let child = cx.add_view(|cx| { - let finder = build_contact_finder(self.user_store.clone(), cx); - finder.set_query(editor_text, cx); - finder - }); - cx.focus(&child); - self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { - PickerEvent::Dismiss => cx.emit(Event::Dismissed), - })); - self.child = Child::ContactFinder(child); - cx.notify(); - } - - fn show_contact_list(&mut self, editor_text: String, cx: &mut ViewContext) { - let child = cx.add_view(|cx| { - ContactList::new( - self.project.clone(), - self.user_store.clone(), - self.workspace.clone(), - cx, - ) - .with_editor_text(editor_text, cx) - }); - cx.focus(&child); - self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { - contacts_list::Event::Dismissed => cx.emit(Event::Dismissed), - contacts_list::Event::ToggleContactFinder => { - this.toggle_contact_finder(&Default::default(), cx) - } - })); - self.child = Child::ContactList(child); - cx.notify(); - } -} - -impl Entity for Contacts { - type Event = Event; -} - -impl View for Contacts { - fn ui_name() -> &'static str { - "ContactsPopover" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = theme::current(cx).clone(); - let child = match &self.child { - Child::ContactList(child) => ChildView::new(child, cx), - Child::ContactFinder(child) => ChildView::new(child, cx), - }; - - MouseEventHandler::::new(0, cx, |_, _| { - Flex::column() - .with_child(child) - .contained() - .with_style(theme.contacts_popover.container) - .constrained() - .with_width(theme.contacts_popover.width) - .with_height(theme.contacts_popover.height) - }) - .on_down_out(MouseButton::Left, move |_, _, cx| cx.emit(Event::Dismissed)) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if cx.is_self_focused() { - match &self.child { - Child::ContactList(child) => cx.focus(child), - Child::ContactFinder(child) => cx.focus(child), - } - } - } -} diff --git a/crates/collab_ui/src/panel/contacts/contacts_list.rs b/crates/collab_ui/src/panel/contacts/contacts_list.rs deleted file mode 100644 index f37d64cd05..0000000000 --- a/crates/collab_ui/src/panel/contacts/contacts_list.rs +++ /dev/null @@ -1,1384 +0,0 @@ -use call::ActiveCall; -use client::{proto::PeerId, Contact, User, UserStore}; -use editor::{Cancel, Editor}; -use futures::StreamExt; -use fuzzy::{match_strings, StringMatchCandidate}; -use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - impl_actions, - keymap_matcher::KeymapContext, - platform::{CursorStyle, MouseButton, PromptLevel}, - AppContext, Entity, ModelHandle, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use menu::{Confirm, SelectNext, SelectPrev}; -use project::Project; -use serde::Deserialize; -use std::{mem, sync::Arc}; -use theme::IconButton; -use workspace::Workspace; - -impl_actions!(contact_list, [RemoveContact, RespondToContactRequest]); - -pub fn init(cx: &mut AppContext) { - cx.add_action(ContactList::remove_contact); - cx.add_action(ContactList::respond_to_contact_request); - cx.add_action(ContactList::cancel); - cx.add_action(ContactList::select_next); - cx.add_action(ContactList::select_prev); - cx.add_action(ContactList::confirm); -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug, PartialOrd, Ord)] -enum Section { - ActiveCall, - Requests, - Online, - Offline, -} - -#[derive(Clone)] -enum ContactEntry { - Header(Section), - CallParticipant { - user: Arc, - is_pending: bool, - }, - ParticipantProject { - project_id: u64, - worktree_root_names: Vec, - host_user_id: u64, - is_last: bool, - }, - ParticipantScreen { - peer_id: PeerId, - is_last: bool, - }, - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact { - contact: Arc, - calling: bool, - }, -} - -impl PartialEq for ContactEntry { - fn eq(&self, other: &Self) -> bool { - match self { - ContactEntry::Header(section_1) => { - if let ContactEntry::Header(section_2) = other { - return section_1 == section_2; - } - } - ContactEntry::CallParticipant { user: user_1, .. } => { - if let ContactEntry::CallParticipant { user: user_2, .. } = other { - return user_1.id == user_2.id; - } - } - ContactEntry::ParticipantProject { - project_id: project_id_1, - .. - } => { - if let ContactEntry::ParticipantProject { - project_id: project_id_2, - .. - } = other - { - return project_id_1 == project_id_2; - } - } - ContactEntry::ParticipantScreen { - peer_id: peer_id_1, .. - } => { - if let ContactEntry::ParticipantScreen { - peer_id: peer_id_2, .. - } = other - { - return peer_id_1 == peer_id_2; - } - } - ContactEntry::IncomingRequest(user_1) => { - if let ContactEntry::IncomingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::OutgoingRequest(user_1) => { - if let ContactEntry::OutgoingRequest(user_2) = other { - return user_1.id == user_2.id; - } - } - ContactEntry::Contact { - contact: contact_1, .. - } => { - if let ContactEntry::Contact { - contact: contact_2, .. - } = other - { - return contact_1.user.id == contact_2.user.id; - } - } - } - false - } -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RequestContact(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RemoveContact { - user_id: u64, - github_login: String, -} - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, -} - -pub enum Event { - ToggleContactFinder, - Dismissed, -} - -pub struct ContactList { - entries: Vec, - match_candidates: Vec, - list_state: ListState, - project: ModelHandle, - workspace: WeakViewHandle, - user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _subscriptions: Vec, -} - -impl ContactList { - pub fn new( - project: ModelHandle, - user_store: ModelHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(Arc::new(|theme| { - theme.contact_list.user_query_editor.clone() - })), - cx, - ); - editor.set_placeholder_text("Filter 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(cx); - if !query.is_empty() { - this.selection = this - .entries - .iter() - .position(|entry| !matches!(entry, ContactEntry::Header(_))); - } - } - }) - .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] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contact_list, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.contact_list, - ) - } - ContactEntry::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.contact_list, - cx, - ), - ContactEntry::ParticipantScreen { peer_id, is_last } => { - Self::render_participant_screen( - *peer_id, - *is_last, - is_selected, - &theme.contact_list, - cx, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contact_list, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contact_list, - false, - is_selected, - cx, - ), - ContactEntry::Contact { contact, calling } => Self::render_contact( - contact, - *calling, - &this.project, - &theme.contact_list, - is_selected, - cx, - ), - } - }); - - let active_call = ActiveCall::global(cx); - let mut subscriptions = Vec::new(); - subscriptions.push(cx.observe(&user_store, |this, _, cx| this.update_entries(cx))); - subscriptions.push(cx.observe(&active_call, |this, _, cx| this.update_entries(cx))); - - let mut this = Self { - list_state, - selection: None, - collapsed_sections: Default::default(), - entries: Default::default(), - match_candidates: Default::default(), - filter_editor, - _subscriptions: subscriptions, - project, - workspace, - user_store, - }; - this.update_entries(cx); - this - } - - pub fn editor_text(&self, cx: &AppContext) -> String { - self.filter_editor.read(cx).text(cx) - } - - pub fn with_editor_text(self, editor_text: String, cx: &mut ViewContext) -> Self { - self.filter_editor - .update(cx, |picker, cx| picker.set_text(editor_text, cx)); - self - } - - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - let user_id = request.user_id; - let github_login = &request.github_login; - 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_id = cx.window_id(); - 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 - { - cx.prompt( - window_id, - PromptLevel::Info, - &format!("Failed to remove contact: {}", e), - &["Ok"], - ); - } - } - }) - .detach(); - } - - fn respond_to_contact_request( - &mut self, - action: &RespondToContactRequest, - cx: &mut ViewContext, - ) { - self.user_store - .update(cx, |store, cx| { - store.respond_to_contact_request(action.user_id, action.accept, cx) - }) - .detach(); - } - - fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext) { - let did_clear = self.filter_editor.update(cx, |editor, cx| { - if editor.buffer().read(cx).len(cx) > 0 { - editor.set_text("", cx); - true - } else { - false - } - }); - - if !did_clear { - cx.emit(Event::Dismissed); - } - } - - fn select_next(&mut self, _: &SelectNext, cx: &mut ViewContext) { - if let Some(ix) = self.selection { - if self.entries.len() > ix + 1 { - self.selection = Some(ix + 1); - } - } else if !self.entries.is_empty() { - self.selection = Some(0); - } - 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) { - if let Some(ix) = self.selection { - if ix > 0 { - self.selection = Some(ix - 1); - } else { - self.selection = None; - } - } - 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 let Some(selection) = self.selection { - if let Some(entry) = self.entries.get(selection) { - match entry { - ContactEntry::Header(section) => { - self.toggle_expanded(*section, cx); - } - ContactEntry::Contact { contact, calling } => { - if contact.online && !contact.busy && !calling { - self.call(contact.user.id, Some(self.project.clone()), cx); - } - } - ContactEntry::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); - } - } - ContactEntry::ParticipantScreen { peer_id, .. } => { - if let Some(workspace) = self.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - workspace.open_shared_screen(*peer_id, cx) - }); - } - } - _ => {} - } - } - } - } - - fn toggle_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(cx); - } - - fn update_entries(&mut self, cx: &mut ViewContext) { - 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); - - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - let mut participant_entries = Vec::new(); - - // 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; - participant_entries.push(ContactEntry::CallParticipant { - user, - is_pending: false, - }); - let mut projects = room.local_participant().projects.iter().peekable(); - while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::ParticipantProject { - project_id: project.id, - worktree_root_names: project.worktree_root_names.clone(), - host_user_id: user_id, - is_last: projects.peek().is_none(), - }); - } - } - } - - // 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]; - participant_entries.push(ContactEntry::CallParticipant { - user: participant.user.clone(), - is_pending: false, - }); - let mut projects = participant.projects.iter().peekable(); - while let Some(project) = projects.next() { - participant_entries.push(ContactEntry::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() { - participant_entries.push(ContactEntry::ParticipantScreen { - peer_id: 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(), - )); - participant_entries.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); - - if !participant_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::ActiveCall)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(participant_entries); - } - } - } - - let mut request_entries = Vec::new(); - 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| ContactEntry::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| ContactEntry::OutgoingRequest(outgoing[mat.candidate_id].clone())), - ); - } - - if !request_entries.is_empty() { - self.entries.push(ContactEntry::Header(Section::Requests)); - if !self.collapsed_sections.contains(&Section::Requests) { - 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 (mut online_contacts, offline_contacts) = matches - .iter() - .partition::, _>(|mat| contacts[mat.candidate_id].online); - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - online_contacts.retain(|contact| { - let contact = &contacts[contact.candidate_id]; - !room.contains_participant(contact.user.id) - }); - } - - for (matches, section) in [ - (online_contacts, Section::Online), - (offline_contacts, Section::Offline), - ] { - if !matches.is_empty() { - self.entries.push(ContactEntry::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(ContactEntry::Contact { - contact: contact.clone(), - calling: active_call.pending_invites().contains(&contact.user.id), - }); - } - } - } - } - } - - 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; - } - } - } - - let old_scroll_top = self.list_state.logical_scroll_top(); - self.list_state.reset(self.entries.len()); - - // 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, - is_pending: bool, - is_selected: bool, - theme: &theme::ContactList, - ) -> AnyElement { - 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), - ) - .with_children(if is_pending { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) - .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::ContactList, - cx: &mut ViewContext, - ) -> AnyElement { - enum JoinProject {} - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - let project_name = if worktree_root_names.is_empty() { - "untitled".to_string() - } else { - worktree_root_names.join(", ") - }; - - MouseEventHandler::::new(project_id as usize, cx, |mouse_state, _| { - 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( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = - bounds.min_x() + (bounds.width() / 2.) - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - })) - .constrained() - .with_width(host_avatar_height), - ) - .with_child( - Label::new(project_name, 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) - }) - .with_cursor_style(if !is_current { - CursorStyle::PointingHand - } else { - CursorStyle::Arrow - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if !is_current { - 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); - } - } - }) - .into_any() - } - - fn render_participant_screen( - peer_id: PeerId, - is_last: bool, - is_selected: bool, - theme: &theme::ContactList, - cx: &mut ViewContext, - ) -> AnyElement { - enum OpenSharedScreen {} - - let font_cache = cx.font_cache(); - let host_avatar_height = theme - .contact_avatar - .width - .or(theme.contact_avatar.height) - .unwrap_or(0.); - let row = &theme.project_row.inactive_state().default; - let tree_branch = theme.tree_branch; - let line_height = row.name.text.line_height(font_cache); - let cap_height = row.name.text.cap_height(font_cache); - let baseline_offset = - row.name.text.baseline_offset(font_cache) + (theme.row_height - line_height) / 2.; - - MouseEventHandler::::new( - peer_id.as_u64() as usize, - cx, - |mouse_state, _| { - 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( - Stack::new() - .with_child(Canvas::new(move |scene, bounds, _, _, _| { - let start_x = bounds.min_x() + (bounds.width() / 2.) - - (tree_branch.width / 2.); - let end_x = bounds.max_x(); - let start_y = bounds.min_y(); - let end_y = bounds.min_y() + baseline_offset - (cap_height / 2.); - - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, start_y), - vec2f( - start_x + tree_branch.width, - if is_last { end_y } else { bounds.max_y() }, - ), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - scene.push_quad(gpui::Quad { - bounds: RectF::from_points( - vec2f(start_x, end_y), - vec2f(end_x, end_y + tree_branch.width), - ), - background: Some(tree_branch.color), - border: gpui::Border::default(), - corner_radius: 0., - }); - })) - .constrained() - .with_width(host_avatar_height), - ) - .with_child( - Svg::new("icons/disable_screen_sharing_12.svg") - .with_color(row.icon.color) - .constrained() - .with_width(row.icon.width) - .aligned() - .left() - .contained() - .with_style(row.icon.container), - ) - .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) - }, - ) - .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, cx) - }); - } - }) - .into_any() - } - - fn render_header( - section: Section, - theme: &theme::ContactList, - is_selected: bool, - is_collapsed: bool, - cx: &mut ViewContext, - ) -> AnyElement { - enum Header {} - enum LeaveCallContactList {} - - let header_style = theme - .header_row - .in_state(is_selected) - .style_for(&mut Default::default()); - let text = match section { - Section::ActiveCall => "Collaborators", - Section::Requests => "Contact Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - let leave_call = if section == Section::ActiveCall { - Some( - MouseEventHandler::::new(0, cx, |state, _| { - let style = theme.leave_call.style_for(state); - Label::new("Leave Call", style.text.clone()) - .contained() - .with_style(style.container) - }) - .on_click(MouseButton::Left, |_, _, cx| { - ActiveCall::global(cx) - .update(cx, |call, cx| call.hang_up(cx)) - .detach_and_log_err(cx); - }) - .aligned(), - ) - } else { - None - }; - - let icon_size = theme.section_icon_size; - MouseEventHandler::::new(section as usize, cx, |_, _| { - Flex::row() - .with_child( - Svg::new(if is_collapsed { - "icons/chevron_right_8.svg" - } else { - "icons/chevron_down_8.svg" - }) - .with_color(header_style.text.color) - .constrained() - .with_max_width(icon_size) - .with_max_height(icon_size) - .aligned() - .constrained() - .with_width(icon_size), - ) - .with_child( - Label::new(text, header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true), - ) - .with_children(leave_call) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.toggle_expanded(section, cx); - }) - .into_any() - } - - fn render_contact( - contact: &Contact, - calling: bool, - project: &ModelHandle, - theme: &theme::ContactList, - is_selected: bool, - cx: &mut ViewContext, - ) -> AnyElement { - 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 mut event_handler = - MouseEventHandler::::new(contact.user.id as usize, cx, |_, 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 { - theme.contact_status_busy - } else { - theme.contact_status_free - }) - .aligned(), - ) - } else { - None - }; - Stack::new() - .with_child( - Image::from_data(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left(), - ) - .with_children(status_badge) - })) - .with_child( - Label::new( - contact.user.github_login.clone(), - theme.contact_username.text.clone(), - ) - .contained() - .with_style(theme.contact_username.container) - .aligned() - .left() - .flex(1., true), - ) - .with_child( - MouseEventHandler::::new( - contact.user.id as usize, - cx, - |mouse_state, _| { - let button_style = theme.contact_button.style_for(mouse_state); - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - }, - ) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_contact( - &RemoveContact { - user_id, - github_login: github_login.clone(), - }, - cx, - ); - }) - .flex_float(), - ) - .with_children(if calling { - Some( - Label::new("Calling", theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style( - *theme - .contact_row - .in_state(is_selected) - .style_for(&mut Default::default()), - ) - }) - .on_click(MouseButton::Left, move |_, this, cx| { - if online && !busy { - this.call(user_id, Some(initial_project.clone()), cx); - } - }); - - if online { - event_handler = event_handler.with_cursor_style(CursorStyle::PointingHand); - } - - event_handler.into_any() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactList, - 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_mark_8.svg").aligned() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.respond_to_contact_request( - &RespondToContactRequest { - user_id, - accept: 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_8.svg") - .aligned() - .flex_float() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.respond_to_contact_request( - &RespondToContactRequest { - user_id, - accept: 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_mark_8.svg") - .aligned() - .flex_float() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - this.remove_contact( - &RemoveContact { - user_id, - github_login: github_login.clone(), - }, - 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 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); - } -} - -impl Entity for ContactList { - type Event = Event; -} - -impl View for ContactList { - fn ui_name() -> &'static str { - "ContactList" - } - - fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { - Self::reset_to_default_keymap_context(keymap); - keymap.add_identifier("menu"); - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum AddContact {} - let theme = theme::current(cx).clone(); - - Flex::column() - .with_child( - Flex::row() - // .with_child( - // ChildView::new(&self.filter_editor, cx) - // .contained() - // .with_style(theme.contact_list.user_query_editor.container) - // ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - render_icon_button( - &theme.contact_list.add_contact_button, - "icons/user_plus_16.svg", - ) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, _, cx| { - cx.emit(Event::ToggleContactFinder) - }) - .with_tooltip::( - 0, - "Search for new contact".into(), - None, - theme.tooltip.clone(), - cx, - ), - ) - .constrained() - .with_height(theme.contact_list.user_query_editor_height), - ) - // .with_child(List::new(self.list_state.clone())) - .into_any() - } - - fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.focus(&self.filter_editor); - } - } - - fn focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.emit(Event::Dismissed); - } - } -} - -fn render_icon_button(style: &IconButton, svg_path: &'static str) -> impl Element { - Svg::new(svg_path) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .contained() - .with_style(style.container) - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) -} diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 746238aaa9..533a5de159 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -271,8 +271,16 @@ impl> AnyElementState for ElementState { | ElementState::PostLayout { mut element, .. } | ElementState::PostPaint { mut element, .. } => { let (size, layout) = element.layout(constraint, view, cx); - debug_assert!(size.x().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); - debug_assert!(size.y().is_finite(), "Element for {:?} had infinite x size after layout", element.view_name()); + debug_assert!( + size.x().is_finite(), + "Element for {:?} had infinite x size after layout", + element.view_name() + ); + debug_assert!( + size.y().is_finite(), + "Element for {:?} had infinite y size after layout", + element.view_name() + ); result = size; ElementState::PostLayout { diff --git a/styles/src/style_tree/contacts_popover.ts b/styles/src/style_tree/contacts_popover.ts index 0ce63d088a..7ca258d166 100644 --- a/styles/src/style_tree/contacts_popover.ts +++ b/styles/src/style_tree/contacts_popover.ts @@ -5,10 +5,10 @@ export default function contacts_popover(): any { const theme = useTheme() return { - background: background(theme.middle), - corner_radius: 6, + // background: background(theme.middle), + // corner_radius: 6, padding: { top: 6, bottom: 6 }, - shadow: theme.popover_shadow, + // shadow: theme.popover_shadow, border: border(theme.middle), width: 300, height: 400,