Merge branch 'main' into divs
This commit is contained in:
commit
d375f7992d
277 changed files with 19044 additions and 8896 deletions
2524
crates/collab_ui/src/collab_panel.rs
Normal file
2524
crates/collab_ui/src/collab_panel.rs
Normal file
File diff suppressed because it is too large
Load diff
615
crates/collab_ui/src/collab_panel/channel_modal.rs
Normal file
615
crates/collab_ui/src/collab_panel/channel_modal.rs
Normal file
|
@ -0,0 +1,615 @@
|
|||
use client::{proto, ChannelId, ChannelMembership, ChannelStore, User, UserId, UserStore};
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use fuzzy::{match_strings, StringMatchCandidate};
|
||||
use gpui::{
|
||||
actions,
|
||||
elements::*,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
actions!(
|
||||
channel_modal,
|
||||
[
|
||||
SelectNextControl,
|
||||
ToggleMode,
|
||||
ToggleMemberAdmin,
|
||||
RemoveMember
|
||||
]
|
||||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ChannelModalDelegate>::init(cx);
|
||||
cx.add_action(ChannelModal::toggle_mode);
|
||||
cx.add_action(ChannelModal::toggle_member_admin);
|
||||
cx.add_action(ChannelModal::remove_member);
|
||||
cx.add_action(ChannelModal::dismiss);
|
||||
}
|
||||
|
||||
pub struct ChannelModal {
|
||||
picker: ViewHandle<Picker<ChannelModalDelegate>>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
impl ChannelModal {
|
||||
pub fn new(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
mode: Mode,
|
||||
members: Vec<ChannelMembership>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Self {
|
||||
cx.observe(&channel_store, |_, _, cx| cx.notify()).detach();
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ChannelModalDelegate {
|
||||
matching_users: Vec::new(),
|
||||
matching_member_indices: Vec::new(),
|
||||
selected_index: 0,
|
||||
user_store: user_store.clone(),
|
||||
channel_store: channel_store.clone(),
|
||||
channel_id,
|
||||
match_candidates: Vec::new(),
|
||||
members,
|
||||
mode,
|
||||
context_menu: cx.add_view(|cx| {
|
||||
let mut menu = ContextMenu::new(cx.view_id(), cx);
|
||||
menu.set_position_mode(OverlayPositionMode::Local);
|
||||
menu
|
||||
}),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
let has_focus = picker.read(cx).has_focus();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
channel_store,
|
||||
channel_id,
|
||||
has_focus,
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_mode(&mut self, _: &ToggleMode, cx: &mut ViewContext<Self>) {
|
||||
let mode = match self.picker.read(cx).delegate().mode {
|
||||
Mode::ManageMembers => Mode::InviteMembers,
|
||||
Mode::InviteMembers => Mode::ManageMembers,
|
||||
};
|
||||
self.set_mode(mode, cx);
|
||||
}
|
||||
|
||||
fn set_mode(&mut self, mode: Mode, cx: &mut ViewContext<Self>) {
|
||||
let channel_store = self.channel_store.clone();
|
||||
let channel_id = self.channel_id;
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
if mode == Mode::ManageMembers {
|
||||
let members = channel_store
|
||||
.update(&mut cx, |channel_store, cx| {
|
||||
channel_store.get_channel_member_details(channel_id, cx)
|
||||
})
|
||||
.await?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker
|
||||
.update(cx, |picker, _| picker.delegate_mut().members = members);
|
||||
})?;
|
||||
}
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.picker.update(cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.mode = mode;
|
||||
delegate.selected_index = 0;
|
||||
picker.set_query("", cx);
|
||||
picker.update_matches(picker.query(cx), cx);
|
||||
cx.notify()
|
||||
});
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn toggle_member_admin(&mut self, _: &ToggleMemberAdmin, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().toggle_selected_member_admin(cx);
|
||||
})
|
||||
}
|
||||
|
||||
fn remove_member(&mut self, _: &RemoveMember, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.delegate_mut().remove_selected_member(cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ChannelModal {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ChannelModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ChannelModal"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let theme = &theme::current(cx).collab_panel.tabbed_modal;
|
||||
|
||||
let mode = self.picker.read(cx).delegate().mode;
|
||||
let Some(channel) = self
|
||||
.channel_store
|
||||
.read(cx)
|
||||
.channel_for_id(self.channel_id) else {
|
||||
return Empty::new().into_any()
|
||||
};
|
||||
|
||||
enum InviteMembers {}
|
||||
enum ManageMembers {}
|
||||
|
||||
fn render_mode_button<T: 'static>(
|
||||
mode: Mode,
|
||||
text: &'static str,
|
||||
current_mode: Mode,
|
||||
theme: &theme::TabbedModal,
|
||||
cx: &mut ViewContext<ChannelModal>,
|
||||
) -> AnyElement<ChannelModal> {
|
||||
let active = mode == current_mode;
|
||||
MouseEventHandler::new::<T, _>(0, cx, move |state, _| {
|
||||
let contained_text = theme.tab_button.style_for(active, state);
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
})
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
if !active {
|
||||
this.set_mode(mode, cx);
|
||||
}
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new(format!("#{}", channel.name), theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([
|
||||
render_mode_button::<InviteMembers>(
|
||||
Mode::InviteMembers,
|
||||
"Invite members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
render_mode_button::<ManageMembers>(
|
||||
Mode::ManageMembers,
|
||||
"Manage members",
|
||||
mode,
|
||||
theme,
|
||||
cx,
|
||||
),
|
||||
]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ChannelModal {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, PartialEq)]
|
||||
pub enum Mode {
|
||||
ManageMembers,
|
||||
InviteMembers,
|
||||
}
|
||||
|
||||
pub struct ChannelModalDelegate {
|
||||
matching_users: Vec<Arc<User>>,
|
||||
matching_member_indices: Vec<usize>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
channel_store: ModelHandle<ChannelStore>,
|
||||
channel_id: ChannelId,
|
||||
selected_index: usize,
|
||||
mode: Mode,
|
||||
match_candidates: Vec<StringMatchCandidate>,
|
||||
members: Vec<ChannelMembership>,
|
||||
context_menu: ViewHandle<ContextMenu>,
|
||||
}
|
||||
|
||||
impl PickerDelegate for ChannelModalDelegate {
|
||||
fn placeholder_text(&self) -> Arc<str> {
|
||||
"Search collaborator by username...".into()
|
||||
}
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.len(),
|
||||
Mode::InviteMembers => self.matching_users.len(),
|
||||
}
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => {
|
||||
self.match_candidates.clear();
|
||||
self.match_candidates
|
||||
.extend(self.members.iter().enumerate().map(|(id, member)| {
|
||||
StringMatchCandidate {
|
||||
id,
|
||||
string: member.user.github_login.clone(),
|
||||
char_bag: member.user.github_login.chars().collect(),
|
||||
}
|
||||
}));
|
||||
|
||||
let matches = cx.background().block(match_strings(
|
||||
&self.match_candidates,
|
||||
&query,
|
||||
true,
|
||||
usize::MAX,
|
||||
&Default::default(),
|
||||
cx.background().clone(),
|
||||
));
|
||||
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
picker
|
||||
.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_member_indices.clear();
|
||||
delegate
|
||||
.matching_member_indices
|
||||
.extend(matches.into_iter().map(|m| m.candidate_id));
|
||||
cx.notify();
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
}
|
||||
Mode::InviteMembers => {
|
||||
let search_users = self
|
||||
.user_store
|
||||
.update(cx, |store, cx| store.fuzzy_search_users(query, cx));
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
async {
|
||||
let users = search_users.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let delegate = picker.delegate_mut();
|
||||
delegate.matching_users = users;
|
||||
cx.notify();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
}
|
||||
.log_err()
|
||||
.await;
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
if let Some((selected_user, admin)) = self.user_at_index(self.selected_index) {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.show_context_menu(admin.unwrap_or(false), cx),
|
||||
Mode::InviteMembers => match self.member_status(selected_user.id, cx) {
|
||||
Some(proto::channel_member::Kind::Invitee) => {
|
||||
self.remove_selected_member(cx);
|
||||
}
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => {
|
||||
self.invite_member(selected_user, cx)
|
||||
}
|
||||
Some(proto::channel_member::Kind::Member) => {}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
mouse_state: &mut MouseState,
|
||||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.channel_modal;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let (user, admin) = self.user_at_index(ix).unwrap();
|
||||
let request_status = self.member_status(user.id, cx);
|
||||
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
.style_for(mouse_state);
|
||||
|
||||
let in_manage = matches!(self.mode, Mode::ManageMembers);
|
||||
|
||||
let mut result = 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(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
.with_children({
|
||||
(in_manage && request_status == Some(proto::channel_member::Kind::Invitee)).then(
|
||||
|| {
|
||||
Label::new("Invited", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
},
|
||||
)
|
||||
})
|
||||
.with_children(admin.and_then(|admin| {
|
||||
(in_manage && admin).then(|| {
|
||||
Label::new("Admin", theme.member_tag.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.member_tag.container)
|
||||
.aligned()
|
||||
.left()
|
||||
})
|
||||
}))
|
||||
.with_children({
|
||||
let svg = match self.mode {
|
||||
Mode::ManageMembers => Some(
|
||||
Svg::new("icons/ellipsis.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Mode::InviteMembers => match request_status {
|
||||
Some(proto::channel_member::Kind::Member) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.member_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.member_icon.button_width)
|
||||
.with_height(theme.member_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.member_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::Invitee) => Some(
|
||||
Svg::new("icons/check.svg")
|
||||
.with_color(theme.invitee_icon.color)
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(theme.invitee_icon.button_width)
|
||||
.with_height(theme.invitee_icon.button_width)
|
||||
.contained()
|
||||
.with_style(theme.invitee_icon.container),
|
||||
),
|
||||
Some(proto::channel_member::Kind::AncestorMember) | None => None,
|
||||
},
|
||||
};
|
||||
|
||||
svg.map(|svg| svg.aligned().flex_float().into_any())
|
||||
})
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any();
|
||||
|
||||
if selected {
|
||||
result = Stack::new()
|
||||
.with_child(result)
|
||||
.with_child(
|
||||
ChildView::new(&self.context_menu, cx)
|
||||
.aligned()
|
||||
.top()
|
||||
.right(),
|
||||
)
|
||||
.into_any();
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
}
|
||||
|
||||
impl ChannelModalDelegate {
|
||||
fn member_status(
|
||||
&self,
|
||||
user_id: UserId,
|
||||
cx: &AppContext,
|
||||
) -> Option<proto::channel_member::Kind> {
|
||||
self.members
|
||||
.iter()
|
||||
.find_map(|membership| (membership.user.id == user_id).then_some(membership.kind))
|
||||
.or_else(|| {
|
||||
self.channel_store
|
||||
.read(cx)
|
||||
.has_pending_channel_invite(self.channel_id, user_id)
|
||||
.then_some(proto::channel_member::Kind::Invitee)
|
||||
})
|
||||
}
|
||||
|
||||
fn user_at_index(&self, ix: usize) -> Option<(Arc<User>, Option<bool>)> {
|
||||
match self.mode {
|
||||
Mode::ManageMembers => self.matching_member_indices.get(ix).and_then(|ix| {
|
||||
let channel_membership = self.members.get(*ix)?;
|
||||
Some((
|
||||
channel_membership.user.clone(),
|
||||
Some(channel_membership.admin),
|
||||
))
|
||||
}),
|
||||
Mode::InviteMembers => Some((self.matching_users.get(ix).cloned()?, None)),
|
||||
}
|
||||
}
|
||||
|
||||
fn toggle_selected_member_admin(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, admin) = self.user_at_index(self.selected_index)?;
|
||||
let admin = !admin.unwrap_or(false);
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.set_member_admin(self.channel_id, user.id, admin, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(member) = this.members.iter_mut().find(|m| m.user.id == user.id) {
|
||||
member.admin = admin;
|
||||
}
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn remove_selected_member(&mut self, cx: &mut ViewContext<Picker<Self>>) -> Option<()> {
|
||||
let (user, _) = self.user_at_index(self.selected_index)?;
|
||||
let user_id = user.id;
|
||||
let update = self.channel_store.update(cx, |store, cx| {
|
||||
store.remove_member(self.channel_id, user_id, cx)
|
||||
});
|
||||
cx.spawn(|picker, mut cx| async move {
|
||||
update.await?;
|
||||
picker.update(&mut cx, |picker, cx| {
|
||||
let this = picker.delegate_mut();
|
||||
if let Some(ix) = this.members.iter_mut().position(|m| m.user.id == user_id) {
|
||||
this.members.remove(ix);
|
||||
this.matching_member_indices.retain_mut(|member_ix| {
|
||||
if *member_ix == ix {
|
||||
return false;
|
||||
} else if *member_ix > ix {
|
||||
*member_ix -= 1;
|
||||
}
|
||||
true
|
||||
})
|
||||
}
|
||||
|
||||
this.selected_index = this
|
||||
.selected_index
|
||||
.min(this.matching_member_indices.len().saturating_sub(1));
|
||||
|
||||
cx.focus_self();
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
Some(())
|
||||
}
|
||||
|
||||
fn invite_member(&mut self, user: Arc<User>, cx: &mut ViewContext<Picker<Self>>) {
|
||||
let invite_member = self.channel_store.update(cx, |store, cx| {
|
||||
store.invite_member(self.channel_id, user.id, false, cx)
|
||||
});
|
||||
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
invite_member.await?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.delegate_mut().members.push(ChannelMembership {
|
||||
user,
|
||||
kind: proto::channel_member::Kind::Invitee,
|
||||
admin: false,
|
||||
});
|
||||
cx.notify();
|
||||
})
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
fn show_context_menu(&mut self, user_is_admin: bool, cx: &mut ViewContext<Picker<Self>>) {
|
||||
self.context_menu.update(cx, |context_menu, cx| {
|
||||
context_menu.show(
|
||||
Default::default(),
|
||||
AnchorCorner::TopRight,
|
||||
vec![
|
||||
ContextMenuItem::action("Remove", RemoveMember),
|
||||
ContextMenuItem::action(
|
||||
if user_is_admin {
|
||||
"Make non-admin"
|
||||
} else {
|
||||
"Make admin"
|
||||
},
|
||||
ToggleMemberAdmin,
|
||||
),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
|
@ -1,28 +1,132 @@
|
|||
use client::{ContactRequestStatus, User, UserStore};
|
||||
use gpui::{elements::*, AppContext, ModelHandle, MouseState, Task, ViewContext};
|
||||
use gpui::{
|
||||
elements::*, AppContext, Entity, ModelHandle, MouseState, Task, View, ViewContext, ViewHandle,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate, PickerEvent};
|
||||
use std::sync::Arc;
|
||||
use util::TryFutureExt;
|
||||
use workspace::Modal;
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
Picker::<ContactFinderDelegate>::init(cx);
|
||||
cx.add_action(ContactFinder::dismiss)
|
||||
}
|
||||
|
||||
pub type ContactFinder = Picker<ContactFinderDelegate>;
|
||||
pub struct ContactFinder {
|
||||
picker: ViewHandle<Picker<ContactFinderDelegate>>,
|
||||
has_focus: bool,
|
||||
}
|
||||
|
||||
pub fn build_contact_finder(
|
||||
user_store: ModelHandle<UserStore>,
|
||||
cx: &mut ViewContext<ContactFinder>,
|
||||
) -> ContactFinder {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.contact_finder.picker.clone())
|
||||
impl ContactFinder {
|
||||
pub fn new(user_store: ModelHandle<UserStore>, cx: &mut ViewContext<Self>) -> Self {
|
||||
let picker = cx.add_view(|cx| {
|
||||
Picker::new(
|
||||
ContactFinderDelegate {
|
||||
user_store,
|
||||
potential_contacts: Arc::from([]),
|
||||
selected_index: 0,
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.with_theme(|theme| theme.collab_panel.tabbed_modal.picker.clone())
|
||||
});
|
||||
|
||||
cx.subscribe(&picker, |_, _, e, cx| cx.emit(*e)).detach();
|
||||
|
||||
Self {
|
||||
picker,
|
||||
has_focus: false,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_query(&mut self, query: String, cx: &mut ViewContext<Self>) {
|
||||
self.picker.update(cx, |picker, cx| {
|
||||
picker.set_query(query, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
|
||||
cx.emit(PickerEvent::Dismiss);
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactFinder {
|
||||
type Event = PickerEvent;
|
||||
}
|
||||
|
||||
impl View for ContactFinder {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactFinder"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.tabbed_modal;
|
||||
|
||||
fn render_mode_button(
|
||||
text: &'static str,
|
||||
theme: &theme::TabbedModal,
|
||||
_cx: &mut ViewContext<ContactFinder>,
|
||||
) -> AnyElement<ContactFinder> {
|
||||
let contained_text = &theme.tab_button.active_state().default;
|
||||
Label::new(text, contained_text.text.clone())
|
||||
.contained()
|
||||
.with_style(contained_text.container.clone())
|
||||
.into_any()
|
||||
}
|
||||
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Flex::column()
|
||||
.with_child(
|
||||
Label::new("Contacts", theme.title.text.clone())
|
||||
.contained()
|
||||
.with_style(theme.title.container.clone()),
|
||||
)
|
||||
.with_child(Flex::row().with_children([render_mode_button(
|
||||
"Invite new contacts",
|
||||
&theme,
|
||||
cx,
|
||||
)]))
|
||||
.expanded()
|
||||
.contained()
|
||||
.with_style(theme.header),
|
||||
)
|
||||
.with_child(
|
||||
ChildView::new(&self.picker, cx)
|
||||
.contained()
|
||||
.with_style(theme.body),
|
||||
)
|
||||
.constrained()
|
||||
.with_max_height(theme.max_height)
|
||||
.with_max_width(theme.max_width)
|
||||
.contained()
|
||||
.with_style(theme.modal)
|
||||
.into_any()
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
|
||||
self.has_focus = true;
|
||||
if cx.is_self_focused() {
|
||||
cx.focus(&self.picker)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext<Self>) {
|
||||
self.has_focus = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl Modal for ContactFinder {
|
||||
fn has_focus(&self) -> bool {
|
||||
self.has_focus
|
||||
}
|
||||
|
||||
fn dismiss_on_event(event: &Self::Event) -> bool {
|
||||
match event {
|
||||
PickerEvent::Dismiss => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ContactFinderDelegate {
|
||||
|
@ -97,7 +201,9 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
selected: bool,
|
||||
cx: &gpui::AppContext,
|
||||
) -> AnyElement<Picker<Self>> {
|
||||
let theme = &theme::current(cx);
|
||||
let full_theme = &theme::current(cx);
|
||||
let theme = &full_theme.collab_panel.contact_finder;
|
||||
let tabbed_modal = &full_theme.collab_panel.tabbed_modal;
|
||||
let user = &self.potential_contacts[ix];
|
||||
let request_status = self.user_store.read(cx).contact_request_status(user);
|
||||
|
||||
|
@ -109,12 +215,11 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
ContactRequestStatus::RequestAccepted => None,
|
||||
};
|
||||
let button_style = if self.user_store.read(cx).is_contact_request_pending(user) {
|
||||
&theme.contact_finder.disabled_contact_button
|
||||
&theme.disabled_contact_button
|
||||
} else {
|
||||
&theme.contact_finder.contact_button
|
||||
&theme.contact_button
|
||||
};
|
||||
let style = theme
|
||||
.contact_finder
|
||||
let style = tabbed_modal
|
||||
.picker
|
||||
.item
|
||||
.in_state(selected)
|
||||
|
@ -122,14 +227,14 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
Flex::row()
|
||||
.with_children(user.avatar.clone().map(|avatar| {
|
||||
Image::from_data(avatar)
|
||||
.with_style(theme.contact_finder.contact_avatar)
|
||||
.with_style(theme.contact_avatar)
|
||||
.aligned()
|
||||
.left()
|
||||
}))
|
||||
.with_child(
|
||||
Label::new(user.github_login.clone(), style.label.clone())
|
||||
.contained()
|
||||
.with_style(theme.contact_finder.contact_username)
|
||||
.with_style(theme.contact_username)
|
||||
.aligned()
|
||||
.left(),
|
||||
)
|
||||
|
@ -150,7 +255,7 @@ impl PickerDelegate for ContactFinderDelegate {
|
|||
.contained()
|
||||
.with_style(style.container)
|
||||
.constrained()
|
||||
.with_height(theme.contact_finder.row_height)
|
||||
.with_height(tabbed_modal.row_height)
|
||||
.into_any()
|
||||
}
|
||||
}
|
39
crates/collab_ui/src/collab_panel/panel_settings.rs
Normal file
39
crates/collab_ui/src/collab_panel/panel_settings.rs
Normal file
|
@ -0,0 +1,39 @@
|
|||
use anyhow;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::Setting;
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum CollaborationPanelDockPosition {
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CollaborationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: CollaborationPanelDockPosition,
|
||||
pub default_width: f32,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
pub struct CollaborationPanelSettingsContent {
|
||||
pub button: Option<bool>,
|
||||
pub dock: Option<CollaborationPanelDockPosition>,
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
impl Setting for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
|
||||
type FileContent = CollaborationPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
default_value: &Self::FileContent,
|
||||
user_values: &[&Self::FileContent],
|
||||
_: &gpui::AppContext,
|
||||
) -> anyhow::Result<Self> {
|
||||
Self::load_via_json_merge(default_value, user_values)
|
||||
}
|
||||
}
|
|
@ -1,12 +1,10 @@
|
|||
use crate::{
|
||||
contact_notification::ContactNotification, contacts_popover, face_pile::FacePile,
|
||||
toggle_deafen, toggle_mute, toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute,
|
||||
ToggleScreenSharing,
|
||||
contact_notification::ContactNotification, face_pile::FacePile, toggle_deafen, toggle_mute,
|
||||
toggle_screen_sharing, LeaveCall, ToggleDeafen, ToggleMute, ToggleScreenSharing,
|
||||
};
|
||||
use call::{ActiveCall, ParticipantLocation, Room};
|
||||
use client::{proto::PeerId, Client, ContactEventKind, SignIn, SignOut, User, UserStore};
|
||||
use clock::ReplicaId;
|
||||
use contacts_popover::ContactsPopover;
|
||||
use context_menu::{ContextMenu, ContextMenuItem};
|
||||
use gpui::{
|
||||
actions,
|
||||
|
@ -33,7 +31,6 @@ const MAX_BRANCH_NAME_LENGTH: usize = 40;
|
|||
actions!(
|
||||
collab,
|
||||
[
|
||||
ToggleContactsMenu,
|
||||
ToggleUserMenu,
|
||||
ToggleProjectMenu,
|
||||
SwitchBranch,
|
||||
|
@ -43,7 +40,6 @@ actions!(
|
|||
);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(CollabTitlebarItem::toggle_contacts_popover);
|
||||
cx.add_action(CollabTitlebarItem::share_project);
|
||||
cx.add_action(CollabTitlebarItem::unshare_project);
|
||||
cx.add_action(CollabTitlebarItem::toggle_user_menu);
|
||||
|
@ -56,7 +52,6 @@ pub struct CollabTitlebarItem {
|
|||
user_store: ModelHandle<UserStore>,
|
||||
client: Arc<Client>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
contacts_popover: Option<ViewHandle<ContactsPopover>>,
|
||||
branch_popover: Option<ViewHandle<BranchList>>,
|
||||
project_popover: Option<ViewHandle<recent_projects::RecentProjects>>,
|
||||
user_menu: ViewHandle<ContextMenu>,
|
||||
|
@ -95,7 +90,7 @@ impl View for CollabTitlebarItem {
|
|||
right_container
|
||||
.add_children(self.render_in_call_share_unshare_button(&workspace, &theme, cx));
|
||||
right_container.add_child(self.render_leave_call(&theme, cx));
|
||||
let muted = room.read(cx).is_muted();
|
||||
let muted = room.read(cx).is_muted(cx);
|
||||
let speaking = room.read(cx).is_speaking();
|
||||
left_container.add_child(
|
||||
self.render_current_user(&workspace, &theme, &user, peer_id, muted, speaking, cx),
|
||||
|
@ -109,7 +104,6 @@ impl View for CollabTitlebarItem {
|
|||
let status = workspace.read(cx).client().status();
|
||||
let status = &*status.borrow();
|
||||
if matches!(status, client::Status::Connected { .. }) {
|
||||
right_container.add_child(self.render_toggle_contacts_button(&theme, cx));
|
||||
let avatar = user.as_ref().and_then(|user| user.avatar.clone());
|
||||
right_container.add_child(self.render_user_menu_button(&theme, avatar, cx));
|
||||
} else {
|
||||
|
@ -184,7 +178,6 @@ impl CollabTitlebarItem {
|
|||
project,
|
||||
user_store,
|
||||
client,
|
||||
contacts_popover: None,
|
||||
user_menu: cx.add_view(|cx| {
|
||||
let view_id = cx.view_id();
|
||||
let mut menu = ContextMenu::new(view_id, cx);
|
||||
|
@ -226,7 +219,7 @@ impl CollabTitlebarItem {
|
|||
let mut ret = Flex::row().with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleProjectMenu, Self>::new(0, cx, |mouse_state, cx| {
|
||||
MouseEventHandler::new::<ToggleProjectMenu, _>(0, cx, |mouse_state, cx| {
|
||||
let style = project_style
|
||||
.in_state(self.project_popover.is_some())
|
||||
.style_for(mouse_state);
|
||||
|
@ -266,7 +259,7 @@ impl CollabTitlebarItem {
|
|||
.with_child(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleVcsMenu, Self>::new(
|
||||
MouseEventHandler::new::<ToggleVcsMenu, _>(
|
||||
0,
|
||||
cx,
|
||||
|mouse_state, cx| {
|
||||
|
@ -315,9 +308,6 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
fn active_call_changed(&mut self, cx: &mut ViewContext<Self>) {
|
||||
if ActiveCall::global(cx).read(cx).room().is_none() {
|
||||
self.contacts_popover = None;
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
|
@ -337,32 +327,6 @@ impl CollabTitlebarItem {
|
|||
.log_err();
|
||||
}
|
||||
|
||||
pub fn toggle_contacts_popover(&mut self, _: &ToggleContactsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.contacts_popover.take().is_none() {
|
||||
let view = cx.add_view(|cx| {
|
||||
ContactsPopover::new(
|
||||
self.project.clone(),
|
||||
self.user_store.clone(),
|
||||
self.workspace.clone(),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&view, |this, _, event, cx| {
|
||||
match event {
|
||||
contacts_popover::Event::Dismissed => {
|
||||
this.contacts_popover = None;
|
||||
}
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.detach();
|
||||
self.contacts_popover = Some(view);
|
||||
}
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn toggle_user_menu(&mut self, _: &ToggleUserMenu, cx: &mut ViewContext<Self>) {
|
||||
self.user_menu.update(cx, |user_menu, cx| {
|
||||
let items = if let Some(_) = self.user_store.read(cx).current_user() {
|
||||
|
@ -390,6 +354,7 @@ impl CollabTitlebarItem {
|
|||
user_menu.toggle(Default::default(), AnchorCorner::TopRight, items, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_branches_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
|
@ -398,13 +363,13 @@ impl CollabTitlebarItem {
|
|||
self.branch_popover.as_ref().map(|child| {
|
||||
let theme = theme::current(cx).clone();
|
||||
let child = ChildView::new(child, cx);
|
||||
let child = MouseEventHandler::<BranchList, Self>::new(0, cx, |_, _| {
|
||||
let child = MouseEventHandler::new::<BranchList, _>(0, cx, |_, _| {
|
||||
child
|
||||
.flex(1., true)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.with_width(theme.titlebar.menu.width)
|
||||
.with_height(theme.titlebar.menu.height)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, _| {})
|
||||
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -425,6 +390,7 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_project_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
|
@ -433,13 +399,13 @@ impl CollabTitlebarItem {
|
|||
self.project_popover.as_ref().map(|child| {
|
||||
let theme = theme::current(cx).clone();
|
||||
let child = ChildView::new(child, cx);
|
||||
let child = MouseEventHandler::<RecentProjects, Self>::new(0, cx, |_, _| {
|
||||
let child = MouseEventHandler::new::<RecentProjects, _>(0, cx, |_, _| {
|
||||
child
|
||||
.flex(1., true)
|
||||
.contained()
|
||||
.constrained()
|
||||
.with_width(theme.contacts_popover.width)
|
||||
.with_height(theme.contacts_popover.height)
|
||||
.with_width(theme.titlebar.menu.width)
|
||||
.with_height(theme.titlebar.menu.height)
|
||||
})
|
||||
.on_click(MouseButton::Left, |_, _, _| {})
|
||||
.on_down_out(MouseButton::Left, move |_, this, cx| {
|
||||
|
@ -459,6 +425,7 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn toggle_vcs_menu(&mut self, _: &ToggleVcsMenu, cx: &mut ViewContext<Self>) {
|
||||
if self.branch_popover.take().is_none() {
|
||||
if let Some(workspace) = self.workspace.upgrade(cx) {
|
||||
|
@ -519,79 +486,7 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
cx.notify();
|
||||
}
|
||||
fn render_toggle_contacts_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> AnyElement<Self> {
|
||||
let titlebar = &theme.titlebar;
|
||||
|
||||
let badge = if self
|
||||
.user_store
|
||||
.read(cx)
|
||||
.incoming_contact_requests()
|
||||
.is_empty()
|
||||
{
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
Empty::new()
|
||||
.collapsed()
|
||||
.contained()
|
||||
.with_style(titlebar.toggle_contacts_badge)
|
||||
.contained()
|
||||
.with_margin_left(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.with_margin_top(
|
||||
titlebar
|
||||
.toggle_contacts_button
|
||||
.inactive_state()
|
||||
.default
|
||||
.icon_width,
|
||||
)
|
||||
.aligned(),
|
||||
)
|
||||
};
|
||||
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleContactsMenu, Self>::new(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_contacts_button
|
||||
.in_state(self.contacts_popover.is_some())
|
||||
.style_for(state);
|
||||
Svg::new("icons/radix/person.svg")
|
||||
.with_color(style.color)
|
||||
.constrained()
|
||||
.with_width(style.icon_width)
|
||||
.aligned()
|
||||
.constrained()
|
||||
.with_width(style.button_width)
|
||||
.with_height(style.button_width)
|
||||
.contained()
|
||||
.with_style(style.container)
|
||||
})
|
||||
.with_cursor_style(CursorStyle::PointingHand)
|
||||
.on_click(MouseButton::Left, move |_, this, cx| {
|
||||
this.toggle_contacts_popover(&Default::default(), cx)
|
||||
})
|
||||
.with_tooltip::<ToggleContactsMenu>(
|
||||
0,
|
||||
"Show contacts menu",
|
||||
Some(Box::new(ToggleContactsMenu)),
|
||||
theme.tooltip.clone(),
|
||||
cx,
|
||||
),
|
||||
)
|
||||
.with_children(badge)
|
||||
.with_children(self.render_contacts_popover_host(titlebar, cx))
|
||||
.into_any()
|
||||
}
|
||||
fn render_toggle_screen_sharing_button(
|
||||
&self,
|
||||
theme: &Theme,
|
||||
|
@ -610,7 +505,7 @@ impl CollabTitlebarItem {
|
|||
|
||||
let active = room.read(cx).is_screen_sharing();
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleScreenSharing, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<ToggleScreenSharing, _>(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.screen_share_button
|
||||
.in_state(active)
|
||||
|
@ -649,7 +544,7 @@ impl CollabTitlebarItem {
|
|||
) -> AnyElement<Self> {
|
||||
let icon;
|
||||
let tooltip;
|
||||
let is_muted = room.read(cx).is_muted();
|
||||
let is_muted = room.read(cx).is_muted(cx);
|
||||
if is_muted {
|
||||
icon = "icons/radix/mic-mute.svg";
|
||||
tooltip = "Unmute microphone";
|
||||
|
@ -659,7 +554,7 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleMute, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<ToggleMute, _>(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_microphone_button
|
||||
.in_state(is_muted)
|
||||
|
@ -712,7 +607,7 @@ impl CollabTitlebarItem {
|
|||
}
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<ToggleDeafen, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<ToggleDeafen, _>(0, cx, |state, _| {
|
||||
let style = titlebar
|
||||
.toggle_speakers_button
|
||||
.in_state(is_deafened)
|
||||
|
@ -747,7 +642,7 @@ impl CollabTitlebarItem {
|
|||
let tooltip = "Leave call";
|
||||
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<LeaveCall, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<LeaveCall, _>(0, cx, |state, _| {
|
||||
let style = titlebar.leave_call_button.style_for(state);
|
||||
Svg::new(icon)
|
||||
.with_color(style.color)
|
||||
|
@ -801,7 +696,7 @@ impl CollabTitlebarItem {
|
|||
Some(
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ShareUnshare, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<ShareUnshare, _>(0, cx, |state, _| {
|
||||
//TODO: Ensure this button has consistent width for both text variations
|
||||
let style = titlebar.share_button.inactive_state().style_for(state);
|
||||
Label::new(label, style.text.clone())
|
||||
|
@ -847,7 +742,7 @@ impl CollabTitlebarItem {
|
|||
let avatar_style = &user_menu_button_style.avatar;
|
||||
Stack::new()
|
||||
.with_child(
|
||||
MouseEventHandler::<ToggleUserMenu, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<ToggleUserMenu, _>(0, cx, |state, _| {
|
||||
let style = user_menu_button_style
|
||||
.user_menu
|
||||
.inactive_state()
|
||||
|
@ -907,7 +802,7 @@ impl CollabTitlebarItem {
|
|||
|
||||
fn render_sign_in_button(&self, theme: &Theme, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
let titlebar = &theme.titlebar;
|
||||
MouseEventHandler::<SignIn, Self>::new(0, cx, |state, _| {
|
||||
MouseEventHandler::new::<SignIn, _>(0, cx, |state, _| {
|
||||
let style = titlebar.sign_in_button.inactive_state().style_for(state);
|
||||
Label::new("Sign In", style.text.clone())
|
||||
.contained()
|
||||
|
@ -923,23 +818,6 @@ impl CollabTitlebarItem {
|
|||
.into_any()
|
||||
}
|
||||
|
||||
fn render_contacts_popover_host<'a>(
|
||||
&'a self,
|
||||
_theme: &'a theme::Titlebar,
|
||||
cx: &'a ViewContext<Self>,
|
||||
) -> Option<AnyElement<Self>> {
|
||||
self.contacts_popover.as_ref().map(|popover| {
|
||||
Overlay::new(ChildView::new(popover, cx))
|
||||
.with_fit_mode(OverlayFitMode::SwitchAnchor)
|
||||
.with_anchor_corner(AnchorCorner::TopLeft)
|
||||
.with_z_index(999)
|
||||
.aligned()
|
||||
.bottom()
|
||||
.right()
|
||||
.into_any()
|
||||
})
|
||||
}
|
||||
|
||||
fn render_collaborators(
|
||||
&self,
|
||||
workspace: &ViewHandle<Workspace>,
|
||||
|
@ -1142,7 +1020,7 @@ impl CollabTitlebarItem {
|
|||
if let Some(replica_id) = replica_id {
|
||||
enum ToggleFollow {}
|
||||
|
||||
content = MouseEventHandler::<ToggleFollow, Self>::new(
|
||||
content = MouseEventHandler::new::<ToggleFollow, _>(
|
||||
replica_id.into(),
|
||||
cx,
|
||||
move |_, _| content,
|
||||
|
@ -1173,7 +1051,7 @@ impl CollabTitlebarItem {
|
|||
enum JoinProject {}
|
||||
|
||||
let user_id = user.id;
|
||||
content = MouseEventHandler::<JoinProject, Self>::new(
|
||||
content = MouseEventHandler::new::<JoinProject, _>(
|
||||
peer_id.as_u64() as usize,
|
||||
cx,
|
||||
move |_, _| content,
|
||||
|
@ -1261,7 +1139,7 @@ impl CollabTitlebarItem {
|
|||
.into_any(),
|
||||
),
|
||||
client::Status::UpgradeRequired => Some(
|
||||
MouseEventHandler::<ConnectionStatusButton, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<ConnectionStatusButton, _>(0, cx, |_, _| {
|
||||
Label::new(
|
||||
"Please update Zed to collaborate",
|
||||
theme.titlebar.outdated_warning.text.clone(),
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
pub mod collab_panel;
|
||||
mod collab_titlebar_item;
|
||||
mod contact_finder;
|
||||
mod contact_list;
|
||||
mod contact_notification;
|
||||
mod contacts_popover;
|
||||
mod face_pile;
|
||||
mod incoming_call_notification;
|
||||
mod notifications;
|
||||
|
@ -10,9 +8,17 @@ mod project_shared_notification;
|
|||
mod sharing_status_indicator;
|
||||
|
||||
use call::{ActiveCall, Room};
|
||||
pub use collab_titlebar_item::{CollabTitlebarItem, ToggleContactsMenu};
|
||||
use gpui::{actions, AppContext, Task};
|
||||
use std::sync::Arc;
|
||||
pub use collab_titlebar_item::CollabTitlebarItem;
|
||||
use gpui::{
|
||||
actions,
|
||||
geometry::{
|
||||
rect::RectF,
|
||||
vector::{vec2f, Vector2F},
|
||||
},
|
||||
platform::{Screen, WindowBounds, WindowKind, WindowOptions},
|
||||
AppContext, Task,
|
||||
};
|
||||
use std::{rc::Rc, sync::Arc};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
|
@ -24,9 +30,7 @@ actions!(
|
|||
pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
||||
vcs_menu::init(cx);
|
||||
collab_titlebar_item::init(cx);
|
||||
contact_list::init(cx);
|
||||
contact_finder::init(cx);
|
||||
contacts_popover::init(cx);
|
||||
collab_panel::init(app_state.client.clone(), cx);
|
||||
incoming_call_notification::init(&app_state, cx);
|
||||
project_shared_notification::init(&app_state, cx);
|
||||
sharing_status_indicator::init(cx);
|
||||
|
@ -45,6 +49,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
|||
ActiveCall::report_call_event_for_room(
|
||||
"disable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
|
@ -53,6 +58,7 @@ pub fn toggle_screen_sharing(_: &ToggleScreenSharing, cx: &mut AppContext) {
|
|||
ActiveCall::report_call_event_for_room(
|
||||
"enable screen share",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
|
@ -68,12 +74,19 @@ pub fn toggle_mute(_: &ToggleMute, cx: &mut AppContext) {
|
|||
if let Some(room) = call.room().cloned() {
|
||||
let client = call.client();
|
||||
room.update(cx, |room, cx| {
|
||||
if room.is_muted() {
|
||||
ActiveCall::report_call_event_for_room("enable microphone", room.id(), &client, cx);
|
||||
if room.is_muted(cx) {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"enable microphone",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
} else {
|
||||
ActiveCall::report_call_event_for_room(
|
||||
"disable microphone",
|
||||
room.id(),
|
||||
room.channel_id(),
|
||||
&client,
|
||||
cx,
|
||||
);
|
||||
|
@ -92,3 +105,29 @@ pub fn toggle_deafen(_: &ToggleDeafen, cx: &mut AppContext) {
|
|||
.log_err();
|
||||
}
|
||||
}
|
||||
|
||||
fn notification_window_options(
|
||||
screen: Rc<dyn Screen>,
|
||||
window_size: Vector2F,
|
||||
) -> WindowOptions<'static> {
|
||||
const NOTIFICATION_PADDING: f32 = 16.;
|
||||
|
||||
let screen_bounds = screen.content_bounds();
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
screen_bounds.upper_right()
|
||||
+ vec2f(
|
||||
-NOTIFICATION_PADDING - window_size.x(),
|
||||
NOTIFICATION_PADDING,
|
||||
),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
}
|
||||
}
|
||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -1,137 +0,0 @@
|
|||
use crate::{
|
||||
contact_finder::{build_contact_finder, ContactFinder},
|
||||
contact_list::ContactList,
|
||||
};
|
||||
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;
|
||||
|
||||
actions!(contacts_popover, [ToggleContactFinder]);
|
||||
|
||||
pub fn init(cx: &mut AppContext) {
|
||||
cx.add_action(ContactsPopover::toggle_contact_finder);
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
Dismissed,
|
||||
}
|
||||
|
||||
enum Child {
|
||||
ContactList(ViewHandle<ContactList>),
|
||||
ContactFinder(ViewHandle<ContactFinder>),
|
||||
}
|
||||
|
||||
pub struct ContactsPopover {
|
||||
child: Child,
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
_subscription: Option<gpui::Subscription>,
|
||||
}
|
||||
|
||||
impl ContactsPopover {
|
||||
pub fn new(
|
||||
project: ModelHandle<Project>,
|
||||
user_store: ModelHandle<UserStore>,
|
||||
workspace: WeakViewHandle<Workspace>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> 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<Self>) {
|
||||
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<ContactsPopover>) {
|
||||
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<ContactsPopover>) {
|
||||
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 {
|
||||
crate::contact_list::Event::Dismissed => cx.emit(Event::Dismissed),
|
||||
crate::contact_list::Event::ToggleContactFinder => {
|
||||
this.toggle_contact_finder(&Default::default(), cx)
|
||||
}
|
||||
}));
|
||||
self.child = Child::ContactList(child);
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ContactsPopover {
|
||||
type Event = Event;
|
||||
}
|
||||
|
||||
impl View for ContactsPopover {
|
||||
fn ui_name() -> &'static str {
|
||||
"ContactsPopover"
|
||||
}
|
||||
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
|
||||
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::<ContactsPopover, Self>::new(0, cx, |_, _| {
|
||||
Flex::column()
|
||||
.with_child(child.flex(1., true))
|
||||
.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<Self>) {
|
||||
if cx.is_self_focused() {
|
||||
match &self.child {
|
||||
Child::ContactList(child) => cx.focus(child),
|
||||
Child::ContactFinder(child) => cx.focus(child),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,44 +7,48 @@ use gpui::{
|
|||
},
|
||||
json::ToJson,
|
||||
serde_json::{self, json},
|
||||
AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, ViewContext,
|
||||
AnyElement, Axis, Element, LayoutContext, PaintContext, SceneBuilder, View, ViewContext,
|
||||
};
|
||||
|
||||
use crate::CollabTitlebarItem;
|
||||
|
||||
pub(crate) struct FacePile {
|
||||
pub(crate) struct FacePile<V: View> {
|
||||
overlap: f32,
|
||||
faces: Vec<AnyElement<CollabTitlebarItem>>,
|
||||
faces: Vec<AnyElement<V>>,
|
||||
}
|
||||
|
||||
impl FacePile {
|
||||
pub fn new(overlap: f32) -> FacePile {
|
||||
FacePile {
|
||||
impl<V: View> FacePile<V> {
|
||||
pub fn new(overlap: f32) -> Self {
|
||||
Self {
|
||||
overlap,
|
||||
faces: Vec::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Element<CollabTitlebarItem> for FacePile {
|
||||
impl<V: View> Element<V> for FacePile<V> {
|
||||
type LayoutState = ();
|
||||
type PaintState = ();
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
constraint: gpui::SizeConstraint,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut LayoutContext<CollabTitlebarItem>,
|
||||
view: &mut V,
|
||||
cx: &mut LayoutContext<V>,
|
||||
) -> (Vector2F, Self::LayoutState) {
|
||||
debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY);
|
||||
|
||||
let mut width = 0.;
|
||||
let mut max_height = 0.;
|
||||
for face in &mut self.faces {
|
||||
width += face.layout(constraint, view, cx).x();
|
||||
let layout = face.layout(constraint, view, cx);
|
||||
width += layout.x();
|
||||
max_height = f32::max(max_height, layout.y());
|
||||
}
|
||||
width -= self.overlap * self.faces.len().saturating_sub(1) as f32;
|
||||
|
||||
(Vector2F::new(width, constraint.max.y()), ())
|
||||
(
|
||||
Vector2F::new(width, max_height.clamp(1., constraint.max.y())),
|
||||
(),
|
||||
)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
|
@ -53,8 +57,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
bounds: RectF,
|
||||
visible_bounds: RectF,
|
||||
_layout: &mut Self::LayoutState,
|
||||
view: &mut CollabTitlebarItem,
|
||||
cx: &mut PaintContext<CollabTitlebarItem>,
|
||||
view: &mut V,
|
||||
cx: &mut PaintContext<V>,
|
||||
) -> Self::PaintState {
|
||||
let visible_bounds = bounds.intersection(visible_bounds).unwrap_or_default();
|
||||
|
||||
|
@ -64,6 +68,7 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
for face in self.faces.iter_mut().rev() {
|
||||
let size = face.size();
|
||||
origin_x -= size.x();
|
||||
let origin_y = origin_y + (bounds.height() - size.y()) / 2.0;
|
||||
scene.paint_layer(None, |scene| {
|
||||
face.paint(scene, vec2f(origin_x, origin_y), visible_bounds, view, cx);
|
||||
});
|
||||
|
@ -80,8 +85,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
_: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> Option<RectF> {
|
||||
None
|
||||
}
|
||||
|
@ -91,8 +96,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
bounds: RectF,
|
||||
_: &Self::LayoutState,
|
||||
_: &Self::PaintState,
|
||||
_: &CollabTitlebarItem,
|
||||
_: &ViewContext<CollabTitlebarItem>,
|
||||
_: &V,
|
||||
_: &ViewContext<V>,
|
||||
) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "FacePile",
|
||||
|
@ -101,8 +106,8 @@ impl Element<CollabTitlebarItem> for FacePile {
|
|||
}
|
||||
}
|
||||
|
||||
impl Extend<AnyElement<CollabTitlebarItem>> for FacePile {
|
||||
fn extend<T: IntoIterator<Item = AnyElement<CollabTitlebarItem>>>(&mut self, children: T) {
|
||||
impl<V: View> Extend<AnyElement<V>> for FacePile<V> {
|
||||
fn extend<T: IntoIterator<Item = AnyElement<V>>>(&mut self, children: T) {
|
||||
self.faces.extend(children);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,14 @@
|
|||
use std::sync::{Arc, Weak};
|
||||
|
||||
use crate::notification_window_options;
|
||||
use call::{ActiveCall, IncomingCall};
|
||||
use client::proto;
|
||||
use futures::StreamExt;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AnyElement, AppContext, Entity, View, ViewContext, WindowHandle,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
use util::ResultExt;
|
||||
use workspace::AppState;
|
||||
|
||||
|
@ -23,31 +23,16 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
}
|
||||
|
||||
if let Some(incoming_call) = incoming_call {
|
||||
const PADDING: f32 = 16.;
|
||||
let window_size = cx.read(|cx| {
|
||||
let theme = &theme::current(cx).incoming_call_notification;
|
||||
vec2f(theme.window_width, theme.window_height)
|
||||
});
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_bounds = screen.bounds();
|
||||
let window = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
screen_bounds.upper_right()
|
||||
- vec2f(PADDING + window_size.x(), PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
},
|
||||
|_| IncomingCallNotification::new(incoming_call.clone(), app_state.clone()),
|
||||
);
|
||||
let window = cx
|
||||
.add_window(notification_window_options(screen, window_size), |_| {
|
||||
IncomingCallNotification::new(incoming_call.clone(), app_state.clone())
|
||||
});
|
||||
|
||||
notification_windows.push(window);
|
||||
}
|
||||
|
@ -173,7 +158,7 @@ impl IncomingCallNotification {
|
|||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::<Accept, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<Accept, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Accept", theme.accept_button.text.clone())
|
||||
.aligned()
|
||||
|
@ -187,7 +172,7 @@ impl IncomingCallNotification {
|
|||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Decline, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<Decline, _>(0, cx, |_, _| {
|
||||
let theme = &theme.incoming_call_notification;
|
||||
Label::new("Decline", theme.decline_button.text.clone())
|
||||
.aligned()
|
||||
|
|
|
@ -51,7 +51,7 @@ where
|
|||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss, V>::new(user.id as usize, cx, |state, _| {
|
||||
MouseEventHandler::new::<Dismiss, _>(user.id as usize, cx, |state, _| {
|
||||
let style = theme.dismiss_button.style_for(state);
|
||||
Svg::new("icons/x_mark_8.svg")
|
||||
.with_color(style.color)
|
||||
|
@ -91,7 +91,7 @@ where
|
|||
Flex::row()
|
||||
.with_children(buttons.into_iter().enumerate().map(
|
||||
|(ix, (message, handler))| {
|
||||
MouseEventHandler::<Button, V>::new(ix, cx, |state, _| {
|
||||
MouseEventHandler::new::<Button, _>(ix, cx, |state, _| {
|
||||
let button = theme.button.style_for(state);
|
||||
Label::new(message, button.text.clone())
|
||||
.contained()
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
use crate::notification_window_options;
|
||||
use call::{room, ActiveCall};
|
||||
use client::User;
|
||||
use collections::HashMap;
|
||||
use gpui::{
|
||||
elements::*,
|
||||
geometry::{rect::RectF, vector::vec2f},
|
||||
platform::{CursorStyle, MouseButton, WindowBounds, WindowKind, WindowOptions},
|
||||
geometry::vector::vec2f,
|
||||
platform::{CursorStyle, MouseButton},
|
||||
AppContext, Entity, View, ViewContext,
|
||||
};
|
||||
use std::sync::{Arc, Weak};
|
||||
|
@ -20,35 +21,19 @@ pub fn init(app_state: &Arc<AppState>, cx: &mut AppContext) {
|
|||
project_id,
|
||||
worktree_root_names,
|
||||
} => {
|
||||
const PADDING: f32 = 16.;
|
||||
let theme = &theme::current(cx).project_shared_notification;
|
||||
let window_size = vec2f(theme.window_width, theme.window_height);
|
||||
|
||||
for screen in cx.platform().screens() {
|
||||
let screen_bounds = screen.bounds();
|
||||
let window = cx.add_window(
|
||||
WindowOptions {
|
||||
bounds: WindowBounds::Fixed(RectF::new(
|
||||
screen_bounds.upper_right() - vec2f(PADDING + window_size.x(), PADDING),
|
||||
window_size,
|
||||
)),
|
||||
titlebar: None,
|
||||
center: false,
|
||||
focus: false,
|
||||
show: true,
|
||||
kind: WindowKind::PopUp,
|
||||
is_movable: false,
|
||||
screen: Some(screen),
|
||||
},
|
||||
|_| {
|
||||
let window =
|
||||
cx.add_window(notification_window_options(screen, window_size), |_| {
|
||||
ProjectSharedNotification::new(
|
||||
owner.clone(),
|
||||
*project_id,
|
||||
worktree_root_names.clone(),
|
||||
app_state.clone(),
|
||||
)
|
||||
},
|
||||
);
|
||||
});
|
||||
notification_windows
|
||||
.entry(*project_id)
|
||||
.or_insert(Vec::new())
|
||||
|
@ -170,7 +155,7 @@ impl ProjectSharedNotification {
|
|||
let theme = theme::current(cx);
|
||||
Flex::column()
|
||||
.with_child(
|
||||
MouseEventHandler::<Open, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<Open, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Open", theme.open_button.text.clone())
|
||||
.aligned()
|
||||
|
@ -182,7 +167,7 @@ impl ProjectSharedNotification {
|
|||
.flex(1., true),
|
||||
)
|
||||
.with_child(
|
||||
MouseEventHandler::<Dismiss, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<Dismiss, _>(0, cx, |_, _| {
|
||||
let theme = &theme.project_shared_notification;
|
||||
Label::new("Dismiss", theme.dismiss_button.text.clone())
|
||||
.aligned()
|
||||
|
|
|
@ -47,7 +47,7 @@ impl View for SharingStatusIndicator {
|
|||
Appearance::Dark | Appearance::VibrantDark => Color::white(),
|
||||
};
|
||||
|
||||
MouseEventHandler::<Self, Self>::new(0, cx, |_, _| {
|
||||
MouseEventHandler::new::<Self, _>(0, cx, |_, _| {
|
||||
Svg::new("icons/disable_screen_sharing_12.svg")
|
||||
.with_color(color)
|
||||
.constrained()
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue