From 6f4edf6df58390a8ba63309951892f780ccfc7a5 Mon Sep 17 00:00:00 2001 From: Antonio Scandurra Date: Mon, 10 Oct 2022 09:56:21 +0200 Subject: [PATCH] Move contact finder into contacts popover --- crates/collab_ui/src/collab_ui.rs | 2 + crates/collab_ui/src/contact_finder.rs | 34 +- crates/collab_ui/src/contacts_popover.rs | 966 +----------------- .../src/incoming_call_notification.rs | 22 +- crates/picker/src/picker.rs | 25 +- crates/theme/src/theme.rs | 33 +- styles/src/styleTree/app.ts | 4 + styles/src/styleTree/contactFinder.ts | 25 +- styles/src/styleTree/contactsPopover.ts | 1 - 9 files changed, 137 insertions(+), 975 deletions(-) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 421258114ef09593d7676b78315e3aea18ac26f0..da2cf775340974b062d90242d9a7fa7975f50886 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -1,6 +1,7 @@ mod active_call_popover; mod collab_titlebar_item; mod contact_finder; +mod contact_list; mod contact_notification; mod contacts_popover; mod incoming_call_notification; @@ -18,6 +19,7 @@ use workspace::{AppState, JoinProject, ToggleFollow, Workspace}; pub fn init(app_state: Arc, cx: &mut MutableAppContext) { collab_titlebar_item::init(cx); contact_notification::init(cx); + contact_list::init(cx); contact_finder::init(cx); contacts_popover::init(cx); incoming_call_notification::init(cx); diff --git a/crates/collab_ui/src/contact_finder.rs b/crates/collab_ui/src/contact_finder.rs index 6814b7479f58b432b9683e1abe8f43d4c1b7b1d8..65ebee2797cd8a0a0c6db468d75f1c84892f9a5e 100644 --- a/crates/collab_ui/src/contact_finder.rs +++ b/crates/collab_ui/src/contact_finder.rs @@ -1,19 +1,15 @@ use client::{ContactRequestStatus, User, UserStore}; use gpui::{ - actions, elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, - RenderContext, Task, View, ViewContext, ViewHandle, + elements::*, AnyViewHandle, Entity, ModelHandle, MouseState, MutableAppContext, RenderContext, + Task, View, ViewContext, ViewHandle, }; use picker::{Picker, PickerDelegate}; use settings::Settings; use std::sync::Arc; use util::TryFutureExt; -use workspace::Workspace; - -actions!(contact_finder, [Toggle]); pub fn init(cx: &mut MutableAppContext) { Picker::::init(cx); - cx.add_action(ContactFinder::toggle); } pub struct ContactFinder { @@ -166,34 +162,16 @@ impl PickerDelegate for ContactFinder { } impl ContactFinder { - fn toggle(workspace: &mut Workspace, _: &Toggle, cx: &mut ViewContext) { - workspace.toggle_modal(cx, |workspace, cx| { - let finder = cx.add_view(|cx| Self::new(workspace.user_store().clone(), cx)); - cx.subscribe(&finder, Self::on_event).detach(); - finder - }); - } - pub fn new(user_store: ModelHandle, cx: &mut ViewContext) -> Self { let this = cx.weak_handle(); Self { - picker: cx.add_view(|cx| Picker::new(this, cx)), + picker: cx.add_view(|cx| { + Picker::new(this, cx) + .with_theme(|cx| &cx.global::().theme.contact_finder.picker) + }), potential_contacts: Arc::from([]), user_store, selected_index: 0, } } - - fn on_event( - workspace: &mut Workspace, - _: ViewHandle, - event: &Event, - cx: &mut ViewContext, - ) { - match event { - Event::Dismissed => { - workspace.dismiss_modal(cx); - } - } - } } diff --git a/crates/collab_ui/src/contacts_popover.rs b/crates/collab_ui/src/contacts_popover.rs index abc8db658b380e60cd67295e9ec112a3b4c9b90f..07ddc487a405412149c57aa78ad005a2732a7844 100644 --- a/crates/collab_ui/src/contacts_popover.rs +++ b/crates/collab_ui/src/contacts_popover.rs @@ -1,120 +1,32 @@ -use std::sync::Arc; - -use crate::contact_finder; -use call::ActiveCall; -use client::{Contact, PeerId, User, UserStore}; -use editor::{Cancel, Editor}; -use fuzzy::{match_strings, StringMatchCandidate}; +use crate::{contact_finder::ContactFinder, contact_list::ContactList}; +use client::UserStore; use gpui::{ - elements::*, impl_actions, impl_internal_actions, keymap, AppContext, ClipboardItem, - CursorStyle, Entity, ModelHandle, MouseButton, MutableAppContext, RenderContext, Subscription, - View, ViewContext, ViewHandle, + actions, elements::*, Entity, ModelHandle, MutableAppContext, RenderContext, View, ViewContext, + ViewHandle, }; -use menu::{Confirm, SelectNext, SelectPrev}; use project::Project; -use serde::Deserialize; use settings::Settings; -use theme::IconButton; -impl_actions!(contacts_popover, [RemoveContact, RespondToContactRequest]); -impl_internal_actions!(contacts_popover, [ToggleExpanded, Call]); +actions!(contacts_popover, [ToggleContactFinder]); pub fn init(cx: &mut MutableAppContext) { - cx.add_action(ContactsPopover::remove_contact); - cx.add_action(ContactsPopover::respond_to_contact_request); - cx.add_action(ContactsPopover::clear_filter); - cx.add_action(ContactsPopover::select_next); - cx.add_action(ContactsPopover::select_prev); - cx.add_action(ContactsPopover::confirm); - cx.add_action(ContactsPopover::toggle_expanded); - cx.add_action(ContactsPopover::call); -} - -#[derive(Clone, PartialEq)] -struct ToggleExpanded(Section); - -#[derive(Clone, PartialEq)] -struct Call { - recipient_user_id: u64, - initial_project: Option>, -} - -#[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 }, - IncomingRequest(Arc), - OutgoingRequest(Arc), - Contact(Arc), -} - -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::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_1) => { - if let ContactEntry::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(pub u64); - -#[derive(Clone, Deserialize, PartialEq)] -pub struct RespondToContactRequest { - pub user_id: u64, - pub accept: bool, + cx.add_action(ContactsPopover::toggle_contact_finder); } pub enum Event { Dismissed, } +enum Child { + ContactList(ViewHandle), + ContactFinder(ViewHandle), +} + pub struct ContactsPopover { - entries: Vec, - match_candidates: Vec, - list_state: ListState, + child: Child, project: ModelHandle, user_store: ModelHandle, - filter_editor: ViewHandle, - collapsed_sections: Vec
, - selection: Option, - _subscriptions: Vec, + _subscription: Option, } impl ContactsPopover { @@ -123,729 +35,44 @@ impl ContactsPopover { user_store: ModelHandle, cx: &mut ViewContext, ) -> Self { - let filter_editor = cx.add_view(|cx| { - let mut editor = Editor::single_line( - Some(|theme| theme.contacts_popover.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., cx, move |this, ix, cx| { - let theme = cx.global::().theme.clone(); - let is_selected = this.selection == Some(ix); - - match &this.entries[ix] { - ContactEntry::Header(section) => { - let is_collapsed = this.collapsed_sections.contains(section); - Self::render_header( - *section, - &theme.contacts_popover, - is_selected, - is_collapsed, - cx, - ) - } - ContactEntry::CallParticipant { user, is_pending } => { - Self::render_call_participant( - user, - *is_pending, - is_selected, - &theme.contacts_popover, - ) - } - ContactEntry::IncomingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_popover, - true, - is_selected, - cx, - ), - ContactEntry::OutgoingRequest(user) => Self::render_contact_request( - user.clone(), - this.user_store.clone(), - &theme.contacts_popover, - false, - is_selected, - cx, - ), - ContactEntry::Contact(contact) => Self::render_contact( - contact, - &this.project, - &theme.contacts_popover, - 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, + child: Child::ContactList( + cx.add_view(|cx| ContactList::new(project.clone(), user_store.clone(), cx)), + ), project, user_store, + _subscription: None, }; - this.update_entries(cx); + this.show_contact_list(cx); this } - fn remove_contact(&mut self, request: &RemoveContact, cx: &mut ViewContext) { - self.user_store - .update(cx, |store, cx| store.remove_contact(request.0, cx)) - .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 clear_filter(&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 toggle_contact_finder(&mut self, _: &ToggleContactFinder, cx: &mut ViewContext) { + match &self.child { + Child::ContactList(_) => self.show_contact_finder(cx), + Child::ContactFinder(_) => self.show_contact_list(cx), } } - 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); - } + fn show_contact_finder(&mut self, cx: &mut ViewContext) { + let child = cx.add_view(|cx| ContactFinder::new(self.user_store.clone(), cx)); + cx.focus(&child); + self._subscription = Some(cx.subscribe(&child, |this, _, event, cx| match event { + crate::contact_finder::Event::Dismissed => this.show_contact_list(cx), + })); + self.child = Child::ContactFinder(child); cx.notify(); - self.list_state.reset(self.entries.len()); } - 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; - } - } + fn show_contact_list(&mut self, cx: &mut ViewContext) { + let child = + cx.add_view(|cx| ContactList::new(self.project.clone(), self.user_store.clone(), cx)); + cx.focus(&child); + self._subscription = Some(cx.subscribe(&child, |_, _, event, cx| match event { + crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed), + })); + self.child = Child::ContactList(child); cx.notify(); - self.list_state.reset(self.entries.len()); - } - - 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) => { - let section = *section; - self.toggle_expanded(&ToggleExpanded(section), cx); - } - ContactEntry::Contact(contact) => { - if contact.online && !contact.busy { - self.call( - &Call { - recipient_user_id: contact.user.id, - initial_project: Some(self.project.clone()), - }, - cx, - ); - } - } - _ => {} - } - } - } - } - - fn toggle_expanded(&mut self, action: &ToggleExpanded, cx: &mut ViewContext) { - let section = action.0; - 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()); - self.entries.clear(); - - if let Some(room) = ActiveCall::global(cx).read(cx).room() { - let room = room.read(cx); - let mut call_participants = 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() { - call_participants.push(ContactEntry::CallParticipant { - user, - is_pending: false, - }); - } - } - - // Populate remote participants. - self.match_candidates.clear(); - self.match_candidates - .extend( - room.remote_participants() - .iter() - .map(|(peer_id, participant)| StringMatchCandidate { - id: peer_id.0 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(), - )); - call_participants.extend(matches.iter().map(|mat| { - ContactEntry::CallParticipant { - user: room.remote_participants()[&PeerId(mat.candidate_id as u32)] - .user - .clone(), - is_pending: false, - } - })); - - // 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(), - )); - call_participants.extend(matches.iter().map(|mat| ContactEntry::CallParticipant { - user: room.pending_participants()[mat.candidate_id].clone(), - is_pending: true, - })); - - if !call_participants.is_empty() { - self.entries.push(ContactEntry::Header(Section::ActiveCall)); - if !self.collapsed_sections.contains(&Section::ActiveCall) { - self.entries.extend(call_participants); - } - } - } - - 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) { - for mat in matches { - let contact = &contacts[mat.candidate_id]; - self.entries.push(ContactEntry::Contact(contact.clone())); - } - } - } - } - } - - 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; - } - } - } - - self.list_state.reset(self.entries.len()); - cx.notify(); - } - - fn render_call_participant( - user: &User, - is_pending: bool, - is_selected: bool, - theme: &theme::ContactsPopover, - ) -> ElementBox { - Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .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) - .boxed(), - ) - .with_children(if is_pending { - Some( - Label::new("Calling".to_string(), theme.calling_indicator.text.clone()) - .contained() - .with_style(theme.calling_indicator.container) - .aligned() - .boxed(), - ) - } else { - None - }) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn render_header( - section: Section, - theme: &theme::ContactsPopover, - is_selected: bool, - is_collapsed: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Header {} - - let header_style = theme.header_row.style_for(Default::default(), is_selected); - let text = match section { - Section::ActiveCall => "Call", - Section::Requests => "Requests", - Section::Online => "Online", - Section::Offline => "Offline", - }; - 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) - .boxed(), - ) - .with_child( - Label::new(text.to_string(), header_style.text.clone()) - .aligned() - .left() - .contained() - .with_margin_left(theme.contact_username.container.margin.left) - .flex(1., true) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(header_style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(ToggleExpanded(section)) - }) - .boxed() - } - - fn render_contact( - contact: &Contact, - project: &ModelHandle, - theme: &theme::ContactsPopover, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - let online = contact.online; - let busy = contact.busy; - let user_id = contact.user.id; - let initial_project = project.clone(); - let mut element = - MouseEventHandler::::new(contact.user.id as usize, 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 contact.busy { - theme.contact_status_busy - } else { - theme.contact_status_free - }) - .aligned() - .boxed(), - ) - } else { - None - }; - Stack::new() - .with_child( - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed(), - ) - .with_children(status_badge) - .boxed() - })) - .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) - .boxed(), - ) - .constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - }) - .on_click(MouseButton::Left, move |_, cx| { - if online && !busy { - cx.dispatch_action(Call { - recipient_user_id: user_id, - initial_project: Some(initial_project.clone()), - }); - } - }); - - if online { - element = element.with_cursor_style(CursorStyle::PointingHand); - } - - element.boxed() - } - - fn render_contact_request( - user: Arc, - user_store: ModelHandle, - theme: &theme::ContactsPopover, - is_incoming: bool, - is_selected: bool, - cx: &mut RenderContext, - ) -> ElementBox { - enum Decline {} - enum Accept {} - enum Cancel {} - - let mut row = Flex::row() - .with_children(user.avatar.clone().map(|avatar| { - Image::new(avatar) - .with_style(theme.contact_avatar) - .aligned() - .left() - .boxed() - })) - .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) - .boxed(), - ); - - let user_id = user.id; - 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_children([ - 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, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: false, - }) - }) - .contained() - .with_margin_right(button_spacing) - .boxed(), - 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, false) - }; - render_icon_button(button_style, "icons/check_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RespondToContactRequest { - user_id, - accept: true, - }) - }) - .boxed(), - ]); - } 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, false) - }; - render_icon_button(button_style, "icons/x_mark_8.svg") - .aligned() - .flex_float() - .boxed() - }) - .with_padding(Padding::uniform(2.)) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.dispatch_action(RemoveContact(user_id)) - }) - .flex_float() - .boxed(), - ); - } - - row.constrained() - .with_height(theme.row_height) - .contained() - .with_style(*theme.contact_row.style_for(Default::default(), is_selected)) - .boxed() - } - - fn call(&mut self, action: &Call, cx: &mut ViewContext) { - let recipient_user_id = action.recipient_user_id; - let initial_project = action.initial_project.clone(); - let window_id = cx.window_id(); - - let active_call = ActiveCall::global(cx); - cx.spawn_weak(|_, mut cx| async move { - active_call - .update(&mut cx, |active_call, cx| { - active_call.invite(recipient_user_id, initial_project.clone(), cx) - }) - .await?; - if cx.update(|cx| cx.window_is_active(window_id)) { - active_call - .update(&mut cx, |call, cx| { - call.set_location(initial_project.as_ref(), cx) - }) - .await?; - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); } } @@ -858,97 +85,14 @@ impl View for ContactsPopover { "ContactsPopover" } - fn keymap_context(&self, _: &AppContext) -> keymap::Context { - let mut cx = Self::default_keymap_context(); - cx.set.insert("menu".into()); - cx - } - fn render(&mut self, cx: &mut RenderContext) -> ElementBox { - enum AddContact {} let theme = cx.global::().theme.clone(); + let child = match &self.child { + Child::ContactList(child) => ChildView::new(child), + Child::ContactFinder(child) => ChildView::new(child), + }; - Flex::column() - .with_child( - Flex::row() - .with_child( - ChildView::new(self.filter_editor.clone()) - .contained() - .with_style(theme.contacts_popover.user_query_editor.container) - .flex(1., true) - .boxed(), - ) - .with_child( - MouseEventHandler::::new(0, cx, |_, _| { - Svg::new("icons/user_plus_16.svg") - .with_color(theme.contacts_popover.add_contact_button.color) - .constrained() - .with_height(16.) - .contained() - .with_style(theme.contacts_popover.add_contact_button.container) - .aligned() - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, cx| { - cx.dispatch_action(contact_finder::Toggle) - }) - .boxed(), - ) - .constrained() - .with_height(theme.contacts_popover.user_query_editor_height) - .boxed(), - ) - .with_child(List::new(self.list_state.clone()).flex(1., false).boxed()) - .with_children( - self.user_store - .read(cx) - .invite_info() - .cloned() - .and_then(|info| { - enum InviteLink {} - - if info.count > 0 { - Some( - MouseEventHandler::::new(0, cx, |state, cx| { - let style = theme - .contacts_popover - .invite_row - .style_for(state, false) - .clone(); - - let copied = cx.read_from_clipboard().map_or(false, |item| { - item.text().as_str() == info.url.as_ref() - }); - - Label::new( - format!( - "{} invite link ({} left)", - if copied { "Copied" } else { "Copy" }, - info.count - ), - style.label.clone(), - ) - .aligned() - .left() - .constrained() - .with_height(theme.contacts_popover.row_height) - .contained() - .with_style(style.container) - .boxed() - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, cx| { - cx.write_to_clipboard(ClipboardItem::new(info.url.to_string())); - cx.notify(); - }) - .boxed(), - ) - } else { - None - } - }), - ) + child .contained() .with_style(theme.contacts_popover.container) .constrained() @@ -958,27 +102,11 @@ impl View for ContactsPopover { } fn on_focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.focus(&self.filter_editor); - } - } - - fn on_focus_out(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { - if !self.filter_editor.is_focused(cx) { - cx.emit(Event::Dismissed); + if cx.is_self_focused() { + match &self.child { + Child::ContactList(child) => cx.focus(child), + Child::ContactFinder(child) => cx.focus(child), + } } } } - -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/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index 0581859ea9ed8da03cce0f5320fe278cbdec943f..47fe8cbbfbdc5976a144d0790603b2f1afb20850 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -82,20 +82,22 @@ impl IncomingCallNotification { } fn render_caller(&self, cx: &mut RenderContext) -> ElementBox { - let theme = &cx.global::().theme.contacts_popover; + let theme = &cx.global::().theme.incoming_call_notification; Flex::row() .with_children( self.call .caller .avatar .clone() - .map(|avatar| Image::new(avatar).with_style(theme.contact_avatar).boxed()), + .map(|avatar| Image::new(avatar).with_style(theme.caller_avatar).boxed()), ) .with_child( Label::new( self.call.caller.github_login.clone(), - theme.contact_username.text.clone(), + theme.caller_username.text.clone(), ) + .contained() + .with_style(theme.caller_username.container) .boxed(), ) .boxed() @@ -108,8 +110,11 @@ impl IncomingCallNotification { Flex::row() .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_popover; - Label::new("Accept".to_string(), theme.contact_username.text.clone()).boxed() + let theme = &cx.global::().theme.incoming_call_notification; + Label::new("Accept".to_string(), theme.accept_button.text.clone()) + .contained() + .with_style(theme.accept_button.container) + .boxed() }) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: true }); @@ -118,8 +123,11 @@ impl IncomingCallNotification { ) .with_child( MouseEventHandler::::new(0, cx, |_, cx| { - let theme = &cx.global::().theme.contacts_popover; - Label::new("Decline".to_string(), theme.contact_username.text.clone()).boxed() + let theme = &cx.global::().theme.incoming_call_notification; + Label::new("Decline".to_string(), theme.decline_button.text.clone()) + .contained() + .with_style(theme.decline_button.container) + .boxed() }) .on_click(MouseButton::Left, |_, cx| { cx.dispatch_action(RespondToCall { accept: false }); diff --git a/crates/picker/src/picker.rs b/crates/picker/src/picker.rs index c2fac6371e3d916c935ebaa158dbe06ae0ffea9c..622dc13309a9b41171d8c45053fe8a835d9270d5 100644 --- a/crates/picker/src/picker.rs +++ b/crates/picker/src/picker.rs @@ -19,6 +19,7 @@ pub struct Picker { query_editor: ViewHandle, list_state: UniformListState, max_size: Vector2F, + theme: Box &theme::Picker>, confirmed: bool, } @@ -51,8 +52,8 @@ impl View for Picker { } fn render(&mut self, cx: &mut RenderContext) -> gpui::ElementBox { - let settings = cx.global::(); - let container_style = settings.theme.picker.container; + let theme = (self.theme)(cx); + let container_style = theme.container; let delegate = self.delegate.clone(); let match_count = if let Some(delegate) = delegate.upgrade(cx.app) { delegate.read(cx).match_count() @@ -64,17 +65,14 @@ impl View for Picker { .with_child( ChildView::new(&self.query_editor) .contained() - .with_style(settings.theme.picker.input_editor.container) + .with_style(theme.input_editor.container) .boxed(), ) .with_child( if match_count == 0 { - Label::new( - "No matches".into(), - settings.theme.picker.empty.label.clone(), - ) - .contained() - .with_style(settings.theme.picker.empty.container) + Label::new("No matches".into(), theme.empty.label.clone()) + .contained() + .with_style(theme.empty.container) } else { UniformList::new( self.list_state.clone(), @@ -147,6 +145,7 @@ impl Picker { list_state: Default::default(), delegate, max_size: vec2f(540., 420.), + theme: Box::new(|cx| &cx.global::().theme.picker), confirmed: false, }; cx.defer(|this, cx| { @@ -163,6 +162,14 @@ impl Picker { self } + pub fn with_theme(mut self, theme: F) -> Self + where + F: 'static + FnMut(&AppContext) -> &theme::Picker, + { + self.theme = Box::new(theme); + self + } + pub fn query(&self, cx: &AppContext) -> String { self.query_editor.read(cx).text(cx) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 96d5b07582463ecdc1d6882c36b58ff43fcce943..268a655c230ace0afaa74b3db70ace7a2c85c8de 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -20,6 +20,7 @@ pub struct Theme { pub context_menu: ContextMenu, pub chat_panel: ChatPanel, pub contacts_popover: ContactsPopover, + pub contact_list: ContactList, pub contact_finder: ContactFinder, pub project_panel: ProjectPanel, pub command_palette: CommandPalette, @@ -31,6 +32,7 @@ pub struct Theme { pub contact_notification: ContactNotification, pub update_notification: UpdateNotification, pub project_shared_notification: ProjectSharedNotification, + pub incoming_call_notification: IncomingCallNotification, pub tooltip: TooltipStyle, pub terminal: TerminalStyle, } @@ -87,6 +89,10 @@ pub struct ContactsPopover { pub container: ContainerStyle, pub height: f32, pub width: f32, +} + +#[derive(Deserialize, Default)] +pub struct ContactList { pub user_query_editor: FieldEditor, pub user_query_editor_height: f32, pub add_contact_button: IconButton, @@ -105,6 +111,16 @@ pub struct ContactsPopover { pub calling_indicator: ContainedText, } +#[derive(Deserialize, Default)] +pub struct ContactFinder { + pub picker: Picker, + pub row_height: f32, + pub contact_avatar: ImageStyle, + pub contact_username: ContainerStyle, + pub contact_button: IconButton, + pub disabled_contact_button: IconButton, +} + #[derive(Clone, Deserialize, Default)] pub struct TabBar { #[serde(flatten)] @@ -353,15 +369,6 @@ pub struct InviteLink { pub icon: Icon, } -#[derive(Deserialize, Default)] -pub struct ContactFinder { - pub row_height: f32, - pub contact_avatar: ImageStyle, - pub contact_username: ContainerStyle, - pub contact_button: IconButton, - pub disabled_contact_button: IconButton, -} - #[derive(Deserialize, Default)] pub struct Icon { #[serde(flatten)] @@ -469,6 +476,14 @@ pub struct ProjectSharedNotification { pub dismiss_button: ContainedText, } +#[derive(Deserialize, Default)] +pub struct IncomingCallNotification { + pub caller_avatar: ImageStyle, + pub caller_username: ContainedText, + pub accept_button: ContainedText, + pub decline_button: ContainedText, +} + #[derive(Clone, Deserialize, Default)] pub struct Editor { pub text_color: Color, diff --git a/styles/src/styleTree/app.ts b/styles/src/styleTree/app.ts index 1b1aa2691657adc290dd2182ffae90e87e83d98a..f540074a70c47daaa0e57dbb56e13ed10596a78c 100644 --- a/styles/src/styleTree/app.ts +++ b/styles/src/styleTree/app.ts @@ -16,6 +16,8 @@ import updateNotification from "./updateNotification"; import projectSharedNotification from "./projectSharedNotification"; import tooltip from "./tooltip"; import terminal from "./terminal"; +import contactList from "./contactList"; +import incomingCallNotification from "./incomingCallNotification"; export const panel = { padding: { top: 12, bottom: 12 }, @@ -36,6 +38,7 @@ export default function app(theme: Theme): Object { projectPanel: projectPanel(theme), chatPanel: chatPanel(theme), contactsPopover: contactsPopover(theme), + contactList: contactList(theme), contactFinder: contactFinder(theme), search: search(theme), breadcrumbs: { @@ -47,6 +50,7 @@ export default function app(theme: Theme): Object { contactNotification: contactNotification(theme), updateNotification: updateNotification(theme), projectSharedNotification: projectSharedNotification(theme), + incomingCallNotification: incomingCallNotification(theme), tooltip: tooltip(theme), terminal: terminal(theme), }; diff --git a/styles/src/styleTree/contactFinder.ts b/styles/src/styleTree/contactFinder.ts index e34fac4b2d734734f6b42d5089a9d7decf852919..bf43a74666da6325676140c9a5c6720b77b0547a 100644 --- a/styles/src/styleTree/contactFinder.ts +++ b/styles/src/styleTree/contactFinder.ts @@ -1,6 +1,6 @@ import Theme from "../themes/common/theme"; import picker from "./picker"; -import { backgroundColor, iconColor } from "./components"; +import { backgroundColor, border, iconColor, player, text } from "./components"; export default function contactFinder(theme: Theme) { const contactButton = { @@ -12,7 +12,28 @@ export default function contactFinder(theme: Theme) { }; return { - ...picker(theme), + picker: { + item: picker(theme).item, + empty: picker(theme).empty, + inputEditor: { + background: backgroundColor(theme, 500), + cornerRadius: 6, + text: text(theme, "mono", "primary"), + placeholderText: text(theme, "mono", "placeholder", { size: "sm" }), + selection: player(theme, 1).selection, + border: border(theme, "secondary"), + padding: { + bottom: 4, + left: 8, + right: 8, + top: 4, + }, + margin: { + left: 12, + right: 12, + } + } + }, rowHeight: 28, contactAvatar: { cornerRadius: 10, diff --git a/styles/src/styleTree/contactsPopover.ts b/styles/src/styleTree/contactsPopover.ts index 8786e81f5c25439c8fdf8e65e5fefe2fefea5231..57af5a6d4d3cdb2b32aa6d73d5f824aeadf24162 100644 --- a/styles/src/styleTree/contactsPopover.ts +++ b/styles/src/styleTree/contactsPopover.ts @@ -19,7 +19,6 @@ export default function contactsPopover(theme: Theme) { padding: { top: 6 }, shadow: popoverShadow(theme), border: border(theme, "primary"), - margin: { top: -5 }, width: 250, height: 300, userQueryEditor: {