diff --git a/Cargo.lock b/Cargo.lock index 99d4827966..90cc0460ff 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1193,6 +1193,7 @@ dependencies = [ "fs2", "futures 0.3.28", "gpui2", + "image", "language2", "live_kit_client2", "log", @@ -1204,6 +1205,8 @@ dependencies = [ "serde_derive", "serde_json", "settings2", + "smallvec", + "ui2", "util", "workspace2", ] @@ -11644,6 +11647,7 @@ dependencies = [ "async-recursion 0.3.2", "async-tar", "async-trait", + "audio2", "auto_update2", "backtrace", "call2", diff --git a/crates/call2/Cargo.toml b/crates/call2/Cargo.toml index 43e19b4ccb..8dc37f68dd 100644 --- a/crates/call2/Cargo.toml +++ b/crates/call2/Cargo.toml @@ -31,16 +31,19 @@ media = { path = "../media" } project = { package = "project2", path = "../project2" } settings = { package = "settings2", path = "../settings2" } util = { path = "../util" } +ui = {package = "ui2", path = "../ui2"} workspace = {package = "workspace2", path = "../workspace2"} async-trait.workspace = true anyhow.workspace = true async-broadcast = "0.4" futures.workspace = true +image = "0.23" postage.workspace = true schemars.workspace = true serde.workspace = true serde_json.workspace = true serde_derive.workspace = true +smallvec.workspace = true [dev-dependencies] client = { package = "client2", path = "../client2", features = ["test-support"] } diff --git a/crates/call2/src/call2.rs b/crates/call2/src/call2.rs index 8e553a9b16..9a89ec7167 100644 --- a/crates/call2/src/call2.rs +++ b/crates/call2/src/call2.rs @@ -1,8 +1,9 @@ pub mod call_settings; pub mod participant; pub mod room; +mod shared_screen; -use anyhow::{anyhow, bail, Result}; +use anyhow::{anyhow, Result}; use async_trait::async_trait; use audio::Audio; use call_settings::CallSettings; @@ -13,8 +14,8 @@ use client::{ use collections::HashSet; use futures::{channel::oneshot, future::Shared, Future, FutureExt}; use gpui::{ - AppContext, AsyncAppContext, AsyncWindowContext, Context, EventEmitter, Model, ModelContext, - Subscription, Task, View, ViewContext, WeakModel, WeakView, + AppContext, AsyncAppContext, Context, EventEmitter, Model, ModelContext, Subscription, Task, + View, ViewContext, VisualContext, WeakModel, WeakView, }; pub use participant::ParticipantLocation; use postage::watch; @@ -22,6 +23,7 @@ use project::Project; use room::Event; pub use room::Room; use settings::Settings; +use shared_screen::SharedScreen; use std::sync::Arc; use util::ResultExt; use workspace::{item::ItemHandle, CallHandler, Pane, Workspace}; @@ -549,42 +551,6 @@ impl Call { #[async_trait(?Send)] impl CallHandler for Call { - fn shared_screen_for_peer( - &self, - peer_id: PeerId, - _pane: &View, - cx: &mut ViewContext, - ) -> Option> { - let (call, _) = self.active_call.as_ref()?; - let room = call.read(cx).room()?.read(cx); - let participant = room.remote_participant_for_peer_id(peer_id)?; - let _track = participant.video_tracks.values().next()?.clone(); - let _user = participant.user.clone(); - todo!(); - // for item in pane.read(cx).items_of_type::() { - // if item.read(cx).peer_id == peer_id { - // return Box::new(Some(item)); - // } - // } - - // Some(Box::new(cx.build_view(|cx| { - // SharedScreen::new(&track, peer_id, user.clone(), cx) - // }))) - } - - fn room_id(&self, cx: &AppContext) -> Option { - Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) - } - fn hang_up(&self, mut cx: AsyncWindowContext) -> Result>> { - let Some((call, _)) = self.active_call.as_ref() else { - bail!("Cannot exit a call; not in a call"); - }; - - call.update(&mut cx, |this, cx| this.hang_up(cx)) - } - fn active_project(&self, cx: &AppContext) -> Option> { - ActiveCall::global(cx).read(cx).location().cloned() - } fn peer_state( &mut self, leader_id: PeerId, @@ -618,6 +584,131 @@ impl CallHandler for Call { Some((leader_in_this_project, leader_in_this_app)) } + + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option> { + let (call, _) = self.active_call.as_ref()?; + let room = call.read(cx).room()?.read(cx); + let participant = room.remote_participant_for_peer_id(peer_id)?; + let track = participant.video_tracks.values().next()?.clone(); + let user = participant.user.clone(); + for item in pane.read(cx).items_of_type::() { + if item.read(cx).peer_id == peer_id { + return Some(Box::new(item)); + } + } + + Some(Box::new(cx.build_view(|cx| { + SharedScreen::new(&track, peer_id, user.clone(), cx) + }))) + } + fn room_id(&self, cx: &AppContext) -> Option { + Some(self.active_call.as_ref()?.0.read(cx).room()?.read(cx).id()) + } + fn hang_up(&self, cx: &mut AppContext) -> Task> { + let Some((call, _)) = self.active_call.as_ref() else { + return Task::ready(Err(anyhow!("Cannot exit a call; not in a call"))); + }; + + call.update(cx, |this, cx| this.hang_up(cx)) + } + fn active_project(&self, cx: &AppContext) -> Option> { + ActiveCall::global(cx).read(cx).location().cloned() + } + fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut AppContext, + ) -> Task> { + ActiveCall::global(cx).update(cx, |this, cx| { + this.invite(called_user_id, initial_project, cx) + }) + } + fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { + self.active_call + .as_ref() + .map(|call| { + call.0.read(cx).room().map(|room| { + room.read(cx) + .remote_participants() + .iter() + .map(|participant| { + (participant.1.user.clone(), participant.1.peer_id.clone()) + }) + .collect() + }) + }) + .flatten() + } + fn is_muted(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_muted(cx)) + }) + .flatten() + } + fn toggle_mute(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + this.toggle_mute(cx).log_err(); + }) + }) + }) + }); + } + fn toggle_screen_share(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + if this.is_screen_sharing() { + this.unshare_screen(cx).log_err(); + } else { + let t = this.share_screen(cx); + cx.spawn(move |_, _| async move { + t.await.log_err(); + }) + .detach(); + } + }) + }) + }) + }); + } + fn toggle_deafen(&self, cx: &mut AppContext) { + self.active_call.as_ref().map(|call| { + call.0.update(cx, |this, cx| { + this.room().map(|room| { + room.update(cx, |this, cx| { + this.toggle_deafen(cx).log_err(); + }) + }) + }) + }); + } + fn is_deafened(&self, cx: &AppContext) -> Option { + self.active_call + .as_ref() + .map(|call| { + call.0 + .read(cx) + .room() + .map(|room| room.read(cx).is_deafened()) + }) + .flatten() + .flatten() + } } #[cfg(test)] diff --git a/crates/call2/src/participant.rs b/crates/call2/src/participant.rs index f62d103f17..325a4f812b 100644 --- a/crates/call2/src/participant.rs +++ b/crates/call2/src/participant.rs @@ -4,7 +4,7 @@ use client::{proto, User}; use collections::HashMap; use gpui::WeakModel; pub use live_kit_client::Frame; -use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; +pub(crate) use live_kit_client::{RemoteAudioTrack, RemoteVideoTrack}; use project::Project; use std::sync::Arc; diff --git a/crates/call2/src/room.rs b/crates/call2/src/room.rs index 87118764fd..b55d3348dc 100644 --- a/crates/call2/src/room.rs +++ b/crates/call2/src/room.rs @@ -1,7 +1,4 @@ -use crate::{ - call_settings::CallSettings, - participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}, -}; +use crate::participant::{LocalParticipant, ParticipantLocation, RemoteParticipant}; use anyhow::{anyhow, Result}; use audio::{Audio, Sound}; use client::{ @@ -21,7 +18,6 @@ use live_kit_client::{ }; use postage::{sink::Sink, stream::Stream, watch}; use project::Project; -use settings::Settings; use std::{future::Future, mem, sync::Arc, time::Duration}; use util::{post_inc, ResultExt, TryFutureExt}; @@ -332,8 +328,10 @@ impl Room { } } - pub fn mute_on_join(cx: &AppContext) -> bool { - CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() + pub fn mute_on_join(_cx: &AppContext) -> bool { + // todo!() po: This should be uncommented, though then unmuting does not work + false + //CallSettings::get_global(cx).mute_on_join || client::IMPERSONATE_LOGIN.is_some() } fn from_join_response( diff --git a/crates/call2/src/shared_screen.rs b/crates/call2/src/shared_screen.rs new file mode 100644 index 0000000000..7b7cd7d9df --- /dev/null +++ b/crates/call2/src/shared_screen.rs @@ -0,0 +1,157 @@ +use crate::participant::{Frame, RemoteVideoTrack}; +use anyhow::Result; +use client::{proto::PeerId, User}; +use futures::StreamExt; +use gpui::{ + div, AppContext, Div, Element, EventEmitter, FocusHandle, FocusableView, ParentElement, Render, + SharedString, Task, View, ViewContext, VisualContext, WindowContext, +}; +use std::sync::{Arc, Weak}; +use workspace::{item::Item, ItemNavHistory, WorkspaceId}; + +pub enum Event { + Close, +} + +pub struct SharedScreen { + track: Weak, + frame: Option, + // temporary addition just to render something interactive. + current_frame_id: usize, + pub peer_id: PeerId, + user: Arc, + nav_history: Option, + _maintain_frame: Task>, + focus: FocusHandle, +} + +impl SharedScreen { + pub fn new( + track: &Arc, + peer_id: PeerId, + user: Arc, + cx: &mut ViewContext, + ) -> Self { + cx.focus_handle(); + let mut frames = track.frames(); + Self { + track: Arc::downgrade(track), + frame: None, + peer_id, + user, + nav_history: Default::default(), + _maintain_frame: cx.spawn(|this, mut cx| async move { + while let Some(frame) = frames.next().await { + this.update(&mut cx, |this, cx| { + this.frame = Some(frame); + cx.notify(); + })?; + } + this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; + Ok(()) + }), + focus: cx.focus_handle(), + current_frame_id: 0, + } + } +} + +impl EventEmitter for SharedScreen {} +impl EventEmitter for SharedScreen {} + +impl FocusableView for SharedScreen { + fn focus_handle(&self, _: &AppContext) -> FocusHandle { + self.focus.clone() + } +} +impl Render for SharedScreen { + type Element = Div; + fn render(&mut self, _: &mut ViewContext) -> Self::Element { + let frame = self.frame.clone(); + let frame_id = self.current_frame_id; + self.current_frame_id = self.current_frame_id.wrapping_add(1); + div().children(frame.map(|_| { + ui::Label::new(frame_id.to_string()).color(ui::Color::Error) + // img().data(Arc::new(ImageData::new(image::ImageBuffer::new( + // frame.width() as u32, + // frame.height() as u32, + // )))) + })) + } +} +// impl View for SharedScreen { +// fn ui_name() -> &'static str { +// "SharedScreen" +// } + +// fn render(&mut self, cx: &mut ViewContext) -> AnyElement { +// enum Focus {} + +// let frame = self.frame.clone(); +// MouseEventHandler::new::(0, cx, |_, cx| { +// Canvas::new(move |bounds, _, _, cx| { +// if let Some(frame) = frame.clone() { +// let size = constrain_size_preserving_aspect_ratio( +// bounds.size(), +// vec2f(frame.width() as f32, frame.height() as f32), +// ); +// let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; +// cx.scene().push_surface(gpui::platform::mac::Surface { +// bounds: RectF::new(origin, size), +// image_buffer: frame.image(), +// }); +// } +// }) +// .contained() +// .with_style(theme::current(cx).shared_screen) +// }) +// .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) +// .into_any() +// } +// } + +impl Item for SharedScreen { + fn tab_tooltip_text(&self, _: &AppContext) -> Option { + Some(format!("{}'s screen", self.user.github_login).into()) + } + fn deactivated(&mut self, cx: &mut ViewContext) { + if let Some(nav_history) = self.nav_history.as_mut() { + nav_history.push::<()>(None, cx); + } + } + + fn tab_content(&self, _: Option, _: &WindowContext<'_>) -> gpui::AnyElement { + div().child("Shared screen").into_any() + // Flex::row() + // .with_child( + // Svg::new("icons/desktop.svg") + // .with_color(style.label.text.color) + // .constrained() + // .with_width(style.type_icon_width) + // .aligned() + // .contained() + // .with_margin_right(style.spacing), + // ) + // .with_child( + // Label::new( + // format!("{}'s screen", self.user.github_login), + // style.label.clone(), + // ) + // .aligned(), + // ) + // .into_any() + } + + fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { + self.nav_history = Some(history); + } + + fn clone_on_split( + &self, + _workspace_id: WorkspaceId, + cx: &mut ViewContext, + ) -> Option> { + let track = self.track.upgrade()?; + Some(cx.build_view(|cx| Self::new(&track, self.peer_id, self.user.clone(), cx))) + } +} diff --git a/crates/client2/src/client2.rs b/crates/client2/src/client2.rs index 4ad354f2f9..b31451aa87 100644 --- a/crates/client2/src/client2.rs +++ b/crates/client2/src/client2.rs @@ -551,7 +551,6 @@ impl Client { F: 'static + Future>, { let message_type_id = TypeId::of::(); - let mut state = self.state.write(); state .models_by_message_type diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6af188dfd2..e9c17a6589 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -157,15 +157,17 @@ const COLLABORATION_PANEL_KEY: &'static str = "CollaborationPanel"; use std::sync::Arc; +use client::{Client, Contact, UserStore}; use db::kvp::KEY_VALUE_STORE; use gpui::{ actions, div, serde_json, AppContext, AsyncWindowContext, Div, EventEmitter, FocusHandle, - Focusable, FocusableView, InteractiveElement, ParentElement, Render, View, ViewContext, - VisualContext, WeakView, + Focusable, FocusableView, InteractiveElement, Model, ParentElement, Render, Styled, View, + ViewContext, VisualContext, WeakView, }; use project::Fs; use serde_derive::{Deserialize, Serialize}; use settings::Settings; +use ui::{h_stack, Avatar, Label}; use util::ResultExt; use workspace::{ dock::{DockPosition, Panel, PanelEvent}, @@ -299,8 +301,8 @@ pub struct CollabPanel { // channel_editing_state: Option, // entries: Vec, // selection: Option, - // user_store: ModelHandle, - // client: Arc, + user_store: Model, + _client: Arc, // channel_store: ModelHandle, // project: ModelHandle, // match_candidates: Vec, @@ -595,7 +597,7 @@ impl CollabPanel { // entries: Vec::default(), // channel_editing_state: None, // selection: None, - // user_store: workspace.user_store().clone(), + user_store: workspace.user_store().clone(), // channel_store: ChannelStore::global(cx), // project: workspace.project().clone(), // subscriptions: Vec::default(), @@ -603,7 +605,7 @@ impl CollabPanel { // collapsed_sections: vec![Section::Offline], // collapsed_channels: Vec::default(), _workspace: workspace.weak_handle(), - // client: workspace.app_state().client.clone(), + _client: workspace.app_state().client.clone(), // context_menu_on_selected: true, // drag_target_channel: ChannelDragTarget::None, // list_state, @@ -663,6 +665,9 @@ impl CollabPanel { }) } + fn contacts(&self, cx: &AppContext) -> Option>> { + Some(self.user_store.read(cx).contacts().to_owned()) + } pub async fn load( workspace: WeakView, mut cx: AsyncWindowContext, @@ -3297,11 +3302,38 @@ impl CollabPanel { impl Render for CollabPanel { type Element = Focusable
; - fn render(&mut self, _cx: &mut ViewContext) -> Self::Element { + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let contacts = self.contacts(cx).unwrap_or_default(); + let workspace = self._workspace.clone(); div() .key_context("CollabPanel") .track_focus(&self.focus_handle) - .child("COLLAB PANEL") + .children(contacts.into_iter().map(|contact| { + let id = contact.user.id; + h_stack() + .p_2() + .gap_2() + .children( + contact + .user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child(Label::new(contact.user.github_login.clone())) + .on_mouse_down(gpui::MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state() + .invite(id, None, cx) + .detach_and_log_err(cx) + }) + .log_err(); + } + }) + })) } } diff --git a/crates/collab_ui2/src/collab_titlebar_item.rs b/crates/collab_ui2/src/collab_titlebar_item.rs index 9eb285f0e7..d208eb91f1 100644 --- a/crates/collab_ui2/src/collab_titlebar_item.rs +++ b/crates/collab_ui2/src/collab_titlebar_item.rs @@ -31,15 +31,18 @@ use std::sync::Arc; use call::ActiveCall; use client::{Client, UserStore}; use gpui::{ - div, px, rems, AppContext, Div, InteractiveElement, IntoElement, Model, ParentElement, Render, - Stateful, StatefulInteractiveElement, Styled, Subscription, ViewContext, VisualContext, - WeakView, WindowBounds, + div, px, rems, AppContext, Div, Element, InteractiveElement, IntoElement, Model, MouseButton, + ParentElement, Render, RenderOnce, Stateful, StatefulInteractiveElement, Styled, Subscription, + ViewContext, VisualContext, WeakView, WindowBounds, }; use project::Project; use theme::ActiveTheme; -use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip}; +use ui::{h_stack, Avatar, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip}; +use util::ResultExt; use workspace::Workspace; +use crate::face_pile::FacePile; + // const MAX_PROJECT_NAME_LENGTH: usize = 40; // const MAX_BRANCH_NAME_LENGTH: usize = 40; @@ -85,6 +88,41 @@ impl Render for CollabTitlebarItem { type Element = Stateful
; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + let is_in_room = self + .workspace + .update(cx, |this, cx| this.call_state().is_in_room(cx)) + .unwrap_or_default(); + let is_shared = is_in_room && self.project.read(cx).is_shared(); + let current_user = self.user_store.read(cx).current_user(); + let client = self.client.clone(); + let users = self + .workspace + .update(cx, |this, cx| this.call_state().remote_participants(cx)) + .log_err() + .flatten(); + let mic_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_muted(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::MicMute + } else { + ui::Icon::Mic + }; + let speakers_icon = if self + .workspace + .update(cx, |this, cx| this.call_state().is_deafened(cx)) + .log_err() + .flatten() + .unwrap_or_default() + { + ui::Icon::AudioOff + } else { + ui::Icon::AudioOn + }; + let workspace = self.workspace.clone(); h_stack() .id("titlebar") .justify_between() @@ -155,8 +193,111 @@ impl Render for CollabTitlebarItem { .into() }), ), - ) // self.titlebar_item - .child(h_stack().child(Label::new("Right side titlebar item"))) + ) + .when_some( + users.zip(current_user.clone()), + |this, (remote_participants, current_user)| { + let mut pile = FacePile::default(); + pile.extend( + current_user + .avatar + .clone() + .map(|avatar| { + div().child(Avatar::data(avatar.clone())).into_any_element() + }) + .into_iter() + .chain(remote_participants.into_iter().flat_map(|(user, peer_id)| { + user.avatar.as_ref().map(|avatar| { + div() + .child( + Avatar::data(avatar.clone()).into_element().into_any(), + ) + .on_mouse_down(MouseButton::Left, { + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.open_shared_screen(peer_id, cx); + }) + .log_err(); + } + }) + .into_any_element() + }) + })), + ); + this.child(pile.render(cx)) + }, + ) + .child(div().flex_1()) + .when(is_in_room, |this| { + this.child( + h_stack() + .child( + h_stack() + .child(Button::new(if is_shared { "Unshare" } else { "Share" })) + .child(IconButton::new("leave-call", ui::Icon::Exit).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().hang_up(cx).detach(); + }) + .log_err(); + } + })), + ) + .child( + h_stack() + .child(IconButton::new("mute-microphone", mic_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_mute(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("mute-sound", speakers_icon).on_click({ + let workspace = workspace.clone(); + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_deafen(cx); + }) + .log_err(); + } + })) + .child(IconButton::new("screen-share", ui::Icon::Screen).on_click( + move |_, cx| { + workspace + .update(cx, |this, cx| { + this.call_state().toggle_screen_share(cx); + }) + .log_err(); + }, + )) + .pl_2(), + ), + ) + }) + .map(|this| { + if let Some(user) = current_user { + this.when_some(user.avatar.clone(), |this, avatar| { + this.child(ui::Avatar::data(avatar)) + }) + } else { + this.child(Button::new("Sign in").on_click(move |_, cx| { + let client = client.clone(); + cx.spawn(move |cx| async move { + client.authenticate_and_connect(true, &cx).await?; + Ok::<(), anyhow::Error>(()) + }) + .detach_and_log_err(cx); + })) + } + }) } } diff --git a/crates/collab_ui2/src/collab_ui.rs b/crates/collab_ui2/src/collab_ui.rs index d2e6b28115..57a33c6790 100644 --- a/crates/collab_ui2/src/collab_ui.rs +++ b/crates/collab_ui2/src/collab_ui.rs @@ -7,11 +7,14 @@ pub mod notification_panel; pub mod notifications; mod panel_settings; -use std::sync::Arc; +use std::{rc::Rc, sync::Arc}; pub use collab_panel::CollabPanel; pub use collab_titlebar_item::CollabTitlebarItem; -use gpui::AppContext; +use gpui::{ + point, AppContext, GlobalPixels, Pixels, PlatformDisplay, Size, WindowBounds, WindowKind, + WindowOptions, +}; pub use panel_settings::{ ChatPanelSettings, CollaborationPanelSettings, NotificationPanelSettings, }; @@ -23,7 +26,7 @@ use workspace::AppState; // [ToggleScreenSharing, ToggleMute, ToggleDeafen, LeaveCall] // ); -pub fn init(_app_state: &Arc, cx: &mut AppContext) { +pub fn init(app_state: &Arc, cx: &mut AppContext) { CollaborationPanelSettings::register(cx); ChatPanelSettings::register(cx); NotificationPanelSettings::register(cx); @@ -32,7 +35,7 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { collab_titlebar_item::init(cx); collab_panel::init(cx); // chat_panel::init(cx); - // notifications::init(&app_state, cx); + notifications::init(&app_state, cx); // cx.add_global_action(toggle_screen_sharing); // cx.add_global_action(toggle_mute); @@ -95,31 +98,36 @@ pub fn init(_app_state: &Arc, cx: &mut AppContext) { // } // } -// fn notification_window_options( -// screen: Rc, -// window_size: Vector2F, -// ) -> WindowOptions<'static> { -// const NOTIFICATION_PADDING: f32 = 16.; +fn notification_window_options( + screen: Rc, + window_size: Size, +) -> WindowOptions { + let notification_margin_width = GlobalPixels::from(16.); + let notification_margin_height = GlobalPixels::from(-0.) - GlobalPixels::from(48.); -// 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), -// } -// } + let screen_bounds = screen.bounds(); + let size: Size = window_size.into(); + + // todo!() use content bounds instead of screen.bounds and get rid of magics in point's 2nd argument. + let bounds = gpui::Bounds:: { + origin: screen_bounds.upper_right() + - point( + size.width + notification_margin_width, + notification_margin_height, + ), + size: window_size.into(), + }; + WindowOptions { + bounds: WindowBounds::Fixed(bounds), + titlebar: None, + center: false, + focus: false, + show: true, + kind: WindowKind::PopUp, + is_movable: false, + display_id: Some(screen.id()), + } +} // fn render_avatar( // avatar: Option>, diff --git a/crates/collab_ui2/src/face_pile.rs b/crates/collab_ui2/src/face_pile.rs index 077b813fbd..e235f33ce6 100644 --- a/crates/collab_ui2/src/face_pile.rs +++ b/crates/collab_ui2/src/face_pile.rs @@ -1,54 +1,48 @@ -// use std::ops::Range; +use gpui::{ + div, AnyElement, Div, IntoElement as _, ParentElement as _, RenderOnce, Styled, WindowContext, +}; -// use gpui::{ -// geometry::{ -// rect::RectF, -// vector::{vec2f, Vector2F}, -// }, -// json::ToJson, -// serde_json::{self, json}, -// AnyElement, Axis, Element, View, ViewContext, -// }; +#[derive(Default)] +pub(crate) struct FacePile { + faces: Vec, +} -// pub(crate) struct FacePile { -// overlap: f32, -// faces: Vec>, -// } +impl RenderOnce for FacePile { + type Rendered = Div; -// impl FacePile { -// pub fn new(overlap: f32) -> Self { -// Self { -// overlap, -// faces: Vec::new(), -// } -// } -// } + fn render(self, _: &mut WindowContext) -> Self::Rendered { + let player_count = self.faces.len(); + let player_list = self.faces.into_iter().enumerate().map(|(ix, player)| { + let isnt_last = ix < player_count - 1; -// impl Element for FacePile { -// type LayoutState = (); -// type PaintState = (); + div().when(isnt_last, |div| div.neg_mr_1()).child(player) + }); + div().p_1().flex().items_center().children(player_list) + } +} +// impl Element for FacePile { +// type State = (); // fn layout( // &mut self, -// constraint: gpui::SizeConstraint, -// view: &mut V, -// cx: &mut ViewContext, -// ) -> (Vector2F, Self::LayoutState) { -// debug_assert!(constraint.max_along(Axis::Horizontal) == f32::INFINITY); - +// state: Option, +// cx: &mut WindowContext, +// ) -> (LayoutId, Self::State) { // let mut width = 0.; // let mut max_height = 0.; +// let mut faces = Vec::with_capacity(self.faces.len()); // for face in &mut self.faces { -// let layout = face.layout(constraint, view, cx); +// let layout = face.layout(cx); // width += layout.x(); // max_height = f32::max(max_height, layout.y()); +// faces.push(layout); // } // width -= self.overlap * self.faces.len().saturating_sub(1) as f32; - -// ( -// Vector2F::new(width, max_height.clamp(1., constraint.max.y())), -// (), -// ) +// (cx.request_layout(&Style::default(), faces), ()) +// // ( +// // Vector2F::new(width, max_height.clamp(1., constraint.max.y())), +// // (), +// // )) // } // fn paint( @@ -77,37 +71,10 @@ // () // } - -// fn rect_for_text_range( -// &self, -// _: Range, -// _: RectF, -// _: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &V, -// _: &ViewContext, -// ) -> Option { -// None -// } - -// fn debug( -// &self, -// bounds: RectF, -// _: &Self::LayoutState, -// _: &Self::PaintState, -// _: &V, -// _: &ViewContext, -// ) -> serde_json::Value { -// json!({ -// "type": "FacePile", -// "bounds": bounds.to_json() -// }) -// } // } -// impl Extend> for FacePile { -// fn extend>>(&mut self, children: T) { -// self.faces.extend(children); -// } -// } +impl Extend for FacePile { + fn extend>(&mut self, children: T) { + self.faces.extend(children); + } +} diff --git a/crates/collab_ui2/src/notifications.rs b/crates/collab_ui2/src/notifications.rs index bc5d7ad3bf..b58473476a 100644 --- a/crates/collab_ui2/src/notifications.rs +++ b/crates/collab_ui2/src/notifications.rs @@ -1,11 +1,11 @@ -// use gpui::AppContext; -// use std::sync::Arc; -// use workspace::AppState; +use gpui::AppContext; +use std::sync::Arc; +use workspace::AppState; -// pub mod incoming_call_notification; +pub mod incoming_call_notification; // pub mod project_shared_notification; -// pub fn init(app_state: &Arc, cx: &mut AppContext) { -// incoming_call_notification::init(app_state, cx); -// project_shared_notification::init(app_state, cx); -// } +pub fn init(app_state: &Arc, cx: &mut AppContext) { + incoming_call_notification::init(app_state, cx); + //project_shared_notification::init(app_state, cx); +} diff --git a/crates/collab_ui2/src/notifications/incoming_call_notification.rs b/crates/collab_ui2/src/notifications/incoming_call_notification.rs index c614a814ca..0519b6fc4a 100644 --- a/crates/collab_ui2/src/notifications/incoming_call_notification.rs +++ b/crates/collab_ui2/src/notifications/incoming_call_notification.rs @@ -1,14 +1,12 @@ use crate::notification_window_options; use call::{ActiveCall, IncomingCall}; -use client::proto; use futures::StreamExt; use gpui::{ - elements::*, - geometry::vector::vec2f, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Entity, View, ViewContext, WindowHandle, + div, green, px, red, AppContext, Div, Element, ParentElement, Render, RenderOnce, + StatefulInteractiveElement, Styled, ViewContext, VisualContext as _, WindowHandle, }; use std::sync::{Arc, Weak}; +use ui::{h_stack, v_stack, Avatar, Button, Label}; use util::ResultExt; use workspace::AppState; @@ -19,23 +17,44 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { let mut notification_windows: Vec> = Vec::new(); while let Some(incoming_call) = incoming_call.next().await { for window in notification_windows.drain(..) { - window.remove(&mut cx); + window + .update(&mut cx, |_, cx| { + // todo!() + cx.remove_window(); + }) + .log_err(); } if let Some(incoming_call) = incoming_call { - let window_size = cx.read(|cx| { - let theme = &theme::current(cx).incoming_call_notification; - vec2f(theme.window_width, theme.window_height) - }); + let unique_screens = cx.update(|cx| cx.displays()).unwrap(); + let window_size = gpui::Size { + width: px(380.), + height: px(64.), + }; - for screen in cx.platform().screens() { + for window in unique_screens { + let options = notification_window_options(window, window_size); let window = cx - .add_window(notification_window_options(screen, window_size), |_| { - IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) - }); - + .open_window(options, |cx| { + cx.build_view(|_| { + IncomingCallNotification::new( + incoming_call.clone(), + app_state.clone(), + ) + }) + }) + .unwrap(); notification_windows.push(window); } + + // for screen in cx.platform().screens() { + // let window = cx + // .add_window(notification_window_options(screen, window_size), |_| { + // IncomingCallNotification::new(incoming_call.clone(), app_state.clone()) + // }); + + // notification_windows.push(window); + // } } } }) @@ -47,167 +66,196 @@ struct RespondToCall { accept: bool, } -pub struct IncomingCallNotification { +struct IncomingCallNotificationState { call: IncomingCall, app_state: Weak, } -impl IncomingCallNotification { +pub struct IncomingCallNotification { + state: Arc, +} +impl IncomingCallNotificationState { pub fn new(call: IncomingCall, app_state: Weak) -> Self { Self { call, app_state } } - fn respond(&mut self, accept: bool, cx: &mut ViewContext) { + fn respond(&self, accept: bool, cx: &mut AppContext) { let active_call = ActiveCall::global(cx); if accept { let join = active_call.update(cx, |active_call, cx| active_call.accept_incoming(cx)); - let caller_user_id = self.call.calling_user.id; let initial_project_id = self.call.initial_project.as_ref().map(|project| project.id); let app_state = self.app_state.clone(); - cx.app_context() - .spawn(|mut cx| async move { - join.await?; - if let Some(project_id) = initial_project_id { - cx.update(|cx| { - if let Some(app_state) = app_state.upgrade() { - workspace::join_remote_project( - project_id, - caller_user_id, - app_state, - cx, - ) - .detach_and_log_err(cx); - } - }); - } - anyhow::Ok(()) - }) - .detach_and_log_err(cx); + let cx: &mut AppContext = cx; + cx.spawn(|cx| async move { + join.await?; + if let Some(_project_id) = initial_project_id { + cx.update(|_cx| { + if let Some(_app_state) = app_state.upgrade() { + // workspace::join_remote_project( + // project_id, + // caller_user_id, + // app_state, + // cx, + // ) + // .detach_and_log_err(cx); + } + }) + .log_err(); + } + anyhow::Ok(()) + }) + .detach_and_log_err(cx); } else { active_call.update(cx, |active_call, cx| { active_call.decline_incoming(cx).log_err(); }); } } +} - fn render_caller(&self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx).incoming_call_notification; - let default_project = proto::ParticipantProject::default(); - let initial_project = self - .call - .initial_project - .as_ref() - .unwrap_or(&default_project); - Flex::row() - .with_children(self.call.calling_user.avatar.clone().map(|avatar| { - Image::from_data(avatar) - .with_style(theme.caller_avatar) - .aligned() +impl IncomingCallNotification { + pub fn new(call: IncomingCall, app_state: Weak) -> Self { + Self { + state: Arc::new(IncomingCallNotificationState::new(call, app_state)), + } + } + fn render_caller(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .children( + self.state + .call + .calling_user + .avatar + .as_ref() + .map(|avatar| Avatar::data(avatar.clone())), + ) + .child( + v_stack() + .child(Label::new(format!( + "{} is sharing a project in Zed", + self.state.call.calling_user.github_login + ))) + .child(self.render_buttons(cx)), + ) + // let theme = &theme::current(cx).incoming_call_notification; + // let default_project = proto::ParticipantProject::default(); + // let initial_project = self + // .call + // .initial_project + // .as_ref() + // .unwrap_or(&default_project); + // Flex::row() + // .with_children(self.call.calling_user.avatar.clone().map(|avatar| { + // Image::from_data(avatar) + // .with_style(theme.caller_avatar) + // .aligned() + // })) + // .with_child( + // Flex::column() + // .with_child( + // Label::new( + // self.call.calling_user.github_login.clone(), + // theme.caller_username.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_username.container), + // ) + // .with_child( + // Label::new( + // format!( + // "is sharing a project in Zed{}", + // if initial_project.worktree_root_names.is_empty() { + // "" + // } else { + // ":" + // } + // ), + // theme.caller_message.text.clone(), + // ) + // .contained() + // .with_style(theme.caller_message.container), + // ) + // .with_children(if initial_project.worktree_root_names.is_empty() { + // None + // } else { + // Some( + // Label::new( + // initial_project.worktree_root_names.join(", "), + // theme.worktree_roots.text.clone(), + // ) + // .contained() + // .with_style(theme.worktree_roots.container), + // ) + // }) + // .contained() + // .with_style(theme.caller_metadata) + // .aligned(), + // ) + // .contained() + // .with_style(theme.caller_container) + // .flex(1., true) + // .into_any() + } + + fn render_buttons(&self, cx: &mut ViewContext) -> impl Element { + h_stack() + .child(Button::new("Accept").render(cx).bg(green()).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(true, cx) + })) + .child(Button::new("Decline").render(cx).bg(red()).on_click({ + let state = self.state.clone(); + move |_, cx| state.respond(false, cx) })) - .with_child( - Flex::column() - .with_child( - Label::new( - self.call.calling_user.github_login.clone(), - theme.caller_username.text.clone(), - ) - .contained() - .with_style(theme.caller_username.container), - ) - .with_child( - Label::new( - format!( - "is sharing a project in Zed{}", - if initial_project.worktree_root_names.is_empty() { - "" - } else { - ":" - } - ), - theme.caller_message.text.clone(), - ) - .contained() - .with_style(theme.caller_message.container), - ) - .with_children(if initial_project.worktree_root_names.is_empty() { - None - } else { - Some( - Label::new( - initial_project.worktree_root_names.join(", "), - theme.worktree_roots.text.clone(), - ) - .contained() - .with_style(theme.worktree_roots.container), - ) - }) - .contained() - .with_style(theme.caller_metadata) - .aligned(), - ) - .contained() - .with_style(theme.caller_container) - .flex(1., true) - .into_any() - } - fn render_buttons(&self, cx: &mut ViewContext) -> AnyElement { - enum Accept {} - enum Decline {} + // enum Accept {} + // enum Decline {} - let theme = theme::current(cx); - Flex::column() - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Accept", theme.accept_button.text.clone()) - .aligned() - .contained() - .with_style(theme.accept_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(true, cx); - }) - .flex(1., true), - ) - .with_child( - MouseEventHandler::new::(0, cx, |_, _| { - let theme = &theme.incoming_call_notification; - Label::new("Decline", theme.decline_button.text.clone()) - .aligned() - .contained() - .with_style(theme.decline_button.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, |_, this, cx| { - this.respond(false, cx); - }) - .flex(1., true), - ) - .constrained() - .with_width(theme.incoming_call_notification.button_width) - .into_any() + // let theme = theme::current(cx); + // Flex::column() + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Accept", theme.accept_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.accept_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(true, cx); + // }) + // .flex(1., true), + // ) + // .with_child( + // MouseEventHandler::new::(0, cx, |_, _| { + // let theme = &theme.incoming_call_notification; + // Label::new("Decline", theme.decline_button.text.clone()) + // .aligned() + // .contained() + // .with_style(theme.decline_button.container) + // }) + // .with_cursor_style(CursorStyle::PointingHand) + // .on_click(MouseButton::Left, |_, this, cx| { + // this.respond(false, cx); + // }) + // .flex(1., true), + // ) + // .constrained() + // .with_width(theme.incoming_call_notification.button_width) + // .into_any() } } - -impl Entity for IncomingCallNotification { - type Event = (); -} - -impl View for IncomingCallNotification { - fn ui_name() -> &'static str { - "IncomingCallNotification" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let background = theme::current(cx).incoming_call_notification.background; - Flex::row() - .with_child(self.render_caller(cx)) - .with_child(self.render_buttons(cx)) - .contained() - .with_background_color(background) - .expanded() - .into_any() +impl Render for IncomingCallNotification { + type Element = Div; + fn render(&mut self, cx: &mut ViewContext) -> Self::Element { + div().bg(red()).flex_none().child(self.render_caller(cx)) + // Flex::row() + // .with_child() + // .with_child(self.render_buttons(cx)) + // .contained() + // .with_background_color(background) + // .expanded() + // .into_any() } } diff --git a/crates/gpui2/src/app.rs b/crates/gpui2/src/app.rs index 617c0b5600..e8f2a60a6a 100644 --- a/crates/gpui2/src/app.rs +++ b/crates/gpui2/src/app.rs @@ -580,7 +580,7 @@ impl AppContext { .windows .iter() .filter_map(|(_, window)| { - let window = window.as_ref().unwrap(); + let window = window.as_ref()?; if window.dirty { Some(window.handle.clone()) } else { @@ -1049,7 +1049,9 @@ impl Context for AppContext { let root_view = window.root_view.clone().unwrap(); let result = update(root_view, &mut WindowContext::new(cx, &mut window)); - if !window.removed { + if window.removed { + cx.windows.remove(handle.id); + } else { cx.windows .get_mut(handle.id) .ok_or_else(|| anyhow!("window not found"))? diff --git a/crates/gpui2/src/elements/img.rs b/crates/gpui2/src/elements/img.rs index f7dcd7ab82..2aece17b47 100644 --- a/crates/gpui2/src/elements/img.rs +++ b/crates/gpui2/src/elements/img.rs @@ -1,30 +1,59 @@ +use std::sync::Arc; + use crate::{ - Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, IntoElement, - LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, + Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity, + IntoElement, LayoutId, Pixels, SharedString, StyleRefinement, Styled, WindowContext, }; use futures::FutureExt; use util::ResultExt; +#[derive(Clone, Debug)] +pub enum ImageSource { + /// Image content will be loaded from provided URI at render time. + Uri(SharedString), + Data(Arc), +} + +impl From for ImageSource { + fn from(value: SharedString) -> Self { + Self::Uri(value) + } +} + +impl From> for ImageSource { + fn from(value: Arc) -> Self { + Self::Data(value) + } +} + pub struct Img { interactivity: Interactivity, - uri: Option, + source: Option, grayscale: bool, } pub fn img() -> Img { Img { interactivity: Interactivity::default(), - uri: None, + source: None, grayscale: false, } } impl Img { pub fn uri(mut self, uri: impl Into) -> Self { - self.uri = Some(uri.into()); + self.source = Some(ImageSource::from(uri.into())); + self + } + pub fn data(mut self, data: Arc) -> Self { + self.source = Some(ImageSource::from(data)); self } + pub fn source(mut self, source: impl Into) -> Self { + self.source = Some(source.into()); + self + } pub fn grayscale(mut self, grayscale: bool) -> Self { self.grayscale = grayscale; self @@ -58,28 +87,33 @@ impl Element for Img { |style, _scroll_offset, cx| { let corner_radii = style.corner_radii; - if let Some(uri) = self.uri.clone() { - // eprintln!(">>> image_cache.get({uri}"); - let image_future = cx.image_cache.get(uri.clone()); - // eprintln!("<<< image_cache.get({uri}"); - if let Some(data) = image_future - .clone() - .now_or_never() - .and_then(|result| result.ok()) - { - let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); - cx.with_z_index(1, |cx| { - cx.paint_image(bounds, corner_radii, data, self.grayscale) - .log_err() - }); - } else { - cx.spawn(|mut cx| async move { - if image_future.await.ok().is_some() { - cx.on_next_frame(|cx| cx.notify()); + if let Some(source) = self.source { + let image = match source { + ImageSource::Uri(uri) => { + let image_future = cx.image_cache.get(uri.clone()); + if let Some(data) = image_future + .clone() + .now_or_never() + .and_then(|result| result.ok()) + { + data + } else { + cx.spawn(|mut cx| async move { + if image_future.await.ok().is_some() { + cx.on_next_frame(|cx| cx.notify()); + } + }) + .detach(); + return; } - }) - .detach() - } + } + ImageSource::Data(image) => image, + }; + let corner_radii = corner_radii.to_pixels(bounds.size, cx.rem_size()); + cx.with_z_index(1, |cx| { + cx.paint_image(bounds, corner_radii, image, self.grayscale) + .log_err() + }); } }, ) diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 976243365c..d358b221da 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,5 +1,7 @@ +use std::sync::Arc; + use crate::prelude::*; -use gpui::{img, Img, IntoElement}; +use gpui::{img, ImageData, ImageSource, Img, IntoElement}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { @@ -10,7 +12,7 @@ pub enum Shape { #[derive(IntoElement)] pub struct Avatar { - src: SharedString, + src: ImageSource, shape: Shape, } @@ -26,7 +28,7 @@ impl RenderOnce for Avatar { img = img.rounded_md(); } - img.uri(self.src.clone()) + img.source(self.src.clone()) .size_4() // todo!(Pull the avatar fallback background from the theme.) .bg(gpui::red()) @@ -34,7 +36,13 @@ impl RenderOnce for Avatar { } impl Avatar { - pub fn new(src: impl Into) -> Self { + pub fn uri(src: impl Into) -> Self { + Self { + src: src.into().into(), + shape: Shape::Circle, + } + } + pub fn data(src: Arc) -> Self { Self { src: src.into(), shape: Shape::Circle, diff --git a/crates/ui2/src/components/list.rs b/crates/ui2/src/components/list.rs index a994583666..875ab6d97e 100644 --- a/crates/ui2/src/components/list.rs +++ b/crates/ui2/src/components/list.rs @@ -323,8 +323,8 @@ impl RenderOnce for ListItem { .color(Color::Muted), ), ), - Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::new(src))), - Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::new(src))), + Some(GraphicSlot::Avatar(src)) => Some(h_stack().child(Avatar::uri(src))), + Some(GraphicSlot::PublicActor(src)) => Some(h_stack().child(Avatar::uri(src))), None => None, }; diff --git a/crates/ui2/src/components/stories/avatar.rs b/crates/ui2/src/components/stories/avatar.rs index ad9c3ccb39..505ede4ecc 100644 --- a/crates/ui2/src/components/stories/avatar.rs +++ b/crates/ui2/src/components/stories/avatar.rs @@ -13,10 +13,10 @@ impl Render for AvatarStory { Story::container() .child(Story::title_for::()) .child(Story::label("Default")) - .child(Avatar::new( + .child(Avatar::uri( "https://avatars.githubusercontent.com/u/1714999?v=4", )) - .child(Avatar::new( + .child(Avatar::uri( "https://avatars.githubusercontent.com/u/326587?v=4", )) } diff --git a/crates/util/src/channel.rs b/crates/util/src/channel.rs index 6ae5fbe844..135e49c87f 100644 --- a/crates/util/src/channel.rs +++ b/crates/util/src/channel.rs @@ -19,7 +19,7 @@ lazy_static! { pub struct AppCommitSha(pub String); -#[derive(Copy, Clone, PartialEq, Eq, Default)] +#[derive(Debug, Copy, Clone, PartialEq, Eq, Default)] pub enum ReleaseChannel { #[default] Dev, diff --git a/crates/workspace2/src/shared_screen.rs b/crates/workspace2/src/shared_screen.rs deleted file mode 100644 index b99c5f3ab9..0000000000 --- a/crates/workspace2/src/shared_screen.rs +++ /dev/null @@ -1,151 +0,0 @@ -use crate::{ - item::{Item, ItemEvent}, - ItemNavHistory, WorkspaceId, -}; -use anyhow::Result; -use call::participant::{Frame, RemoteVideoTrack}; -use client::{proto::PeerId, User}; -use futures::StreamExt; -use gpui::{ - elements::*, - geometry::{rect::RectF, vector::vec2f}, - platform::MouseButton, - AppContext, Entity, Task, View, ViewContext, -}; -use smallvec::SmallVec; -use std::{ - borrow::Cow, - sync::{Arc, Weak}, -}; - -pub enum Event { - Close, -} - -pub struct SharedScreen { - track: Weak, - frame: Option, - pub peer_id: PeerId, - user: Arc, - nav_history: Option, - _maintain_frame: Task>, -} - -impl SharedScreen { - pub fn new( - track: &Arc, - peer_id: PeerId, - user: Arc, - cx: &mut ViewContext, - ) -> Self { - let mut frames = track.frames(); - Self { - track: Arc::downgrade(track), - frame: None, - peer_id, - user, - nav_history: Default::default(), - _maintain_frame: cx.spawn(|this, mut cx| async move { - while let Some(frame) = frames.next().await { - this.update(&mut cx, |this, cx| { - this.frame = Some(frame); - cx.notify(); - })?; - } - this.update(&mut cx, |_, cx| cx.emit(Event::Close))?; - Ok(()) - }), - } - } -} - -impl Entity for SharedScreen { - type Event = Event; -} - -impl View for SharedScreen { - fn ui_name() -> &'static str { - "SharedScreen" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - enum Focus {} - - let frame = self.frame.clone(); - MouseEventHandler::new::(0, cx, |_, cx| { - Canvas::new(move |bounds, _, _, cx| { - if let Some(frame) = frame.clone() { - let size = constrain_size_preserving_aspect_ratio( - bounds.size(), - vec2f(frame.width() as f32, frame.height() as f32), - ); - let origin = bounds.origin() + (bounds.size() / 2.) - size / 2.; - cx.scene().push_surface(gpui::platform::mac::Surface { - bounds: RectF::new(origin, size), - image_buffer: frame.image(), - }); - } - }) - .contained() - .with_style(theme::current(cx).shared_screen) - }) - .on_down(MouseButton::Left, |_, _, cx| cx.focus_parent()) - .into_any() - } -} - -impl Item for SharedScreen { - fn tab_tooltip_text(&self, _: &AppContext) -> Option> { - Some(format!("{}'s screen", self.user.github_login).into()) - } - fn deactivated(&mut self, cx: &mut ViewContext) { - if let Some(nav_history) = self.nav_history.as_mut() { - nav_history.push::<()>(None, cx); - } - } - - fn tab_content( - &self, - _: Option, - style: &theme::Tab, - _: &AppContext, - ) -> gpui::AnyElement { - Flex::row() - .with_child( - Svg::new("icons/desktop.svg") - .with_color(style.label.text.color) - .constrained() - .with_width(style.type_icon_width) - .aligned() - .contained() - .with_margin_right(style.spacing), - ) - .with_child( - Label::new( - format!("{}'s screen", self.user.github_login), - style.label.clone(), - ) - .aligned(), - ) - .into_any() - } - - fn set_nav_history(&mut self, history: ItemNavHistory, _: &mut ViewContext) { - self.nav_history = Some(history); - } - - fn clone_on_split( - &self, - _workspace_id: WorkspaceId, - cx: &mut ViewContext, - ) -> Option { - let track = self.track.upgrade()?; - Some(Self::new(&track, self.peer_id, self.user.clone(), cx)) - } - - fn to_item_events(event: &Self::Event) -> SmallVec<[ItemEvent; 2]> { - match event { - Event::Close => smallvec::smallvec!(ItemEvent::CloseItem), - } - } -} diff --git a/crates/workspace2/src/workspace2.rs b/crates/workspace2/src/workspace2.rs index 5480ac4d3c..fbc706c560 100644 --- a/crates/workspace2/src/workspace2.rs +++ b/crates/workspace2/src/workspace2.rs @@ -9,7 +9,6 @@ pub mod pane_group; mod persistence; pub mod searchable; // todo!() -// pub mod shared_screen; mod modal_layer; mod status_bar; mod toolbar; @@ -19,7 +18,7 @@ use anyhow::{anyhow, Context as _, Result}; use async_trait::async_trait; use client2::{ proto::{self, PeerId}, - Client, TypedEnvelope, UserStore, + Client, TypedEnvelope, User, UserStore, }; use collections::{hash_map, HashMap, HashSet}; use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle}; @@ -344,14 +343,42 @@ impl CallHandler for TestCallHandler { None } - fn hang_up(&self, cx: AsyncWindowContext) -> Result>> { - anyhow::bail!("TestCallHandler should not be hanging up") + fn hang_up(&self, cx: &mut AppContext) -> Task> { + Task::ready(Err(anyhow!("TestCallHandler should not be hanging up"))) } fn active_project(&self, cx: &AppContext) -> Option> { None } + + fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut AppContext, + ) -> Task> { + unimplemented!() + } + + fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>> { + None + } + + fn is_muted(&self, cx: &AppContext) -> Option { + None + } + + fn toggle_mute(&self, cx: &mut AppContext) {} + + fn toggle_screen_share(&self, cx: &mut AppContext) {} + + fn toggle_deafen(&self, cx: &mut AppContext) {} + + fn is_deafened(&self, cx: &AppContext) -> Option { + None + } } + impl AppState { #[cfg(any(test, feature = "test-support"))] pub fn test(cx: &mut AppContext) -> Arc { @@ -452,8 +479,20 @@ pub trait CallHandler { fn is_in_room(&self, cx: &mut ViewContext) -> bool { self.room_id(cx).is_some() } - fn hang_up(&self, cx: AsyncWindowContext) -> Result>>; + fn hang_up(&self, cx: &mut AppContext) -> Task>; fn active_project(&self, cx: &AppContext) -> Option>; + fn invite( + &mut self, + called_user_id: u64, + initial_project: Option>, + cx: &mut AppContext, + ) -> Task>; + fn remote_participants(&self, cx: &AppContext) -> Option, PeerId)>>; + fn is_muted(&self, cx: &AppContext) -> Option; + fn is_deafened(&self, cx: &AppContext) -> Option; + fn toggle_mute(&self, cx: &mut AppContext); + fn toggle_deafen(&self, cx: &mut AppContext); + fn toggle_screen_share(&self, cx: &mut AppContext); } pub struct Workspace { @@ -1197,7 +1236,7 @@ impl Workspace { if answer.await.log_err() == Some(1) { return anyhow::Ok(false); } else { - this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx.to_async()))?? + this.update(&mut cx, |this, cx| this.call_handler.hang_up(cx))? .await .log_err(); } @@ -1981,13 +2020,13 @@ impl Workspace { item } - // pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { - // if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { - // self.active_pane.update(cx, |pane, cx| { - // pane.add_item(Box::new(shared_screen), false, true, None, cx) - // }); - // } - // } + pub fn open_shared_screen(&mut self, peer_id: PeerId, cx: &mut ViewContext) { + if let Some(shared_screen) = self.shared_screen_for_peer(peer_id, &self.active_pane, cx) { + self.active_pane.update(cx, |pane, cx| { + pane.add_item(shared_screen, false, true, None, cx) + }); + } + } pub fn activate_item(&mut self, item: &dyn ItemHandle, cx: &mut ViewContext) -> bool { let result = self.panes.iter().find_map(|pane| { @@ -2861,10 +2900,10 @@ impl Workspace { } continue; } - // todo!() - // if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { - // items_to_activate.push((pane.clone(), Box::new(shared_screen))); - // } + + if let Some(shared_screen) = self.shared_screen_for_peer(leader_id, pane, cx) { + items_to_activate.push((pane.clone(), shared_screen)); + } } for (pane, item) in items_to_activate { @@ -2885,27 +2924,27 @@ impl Workspace { None } - // todo!() - // fn shared_screen_for_peer( - // &self, - // peer_id: PeerId, - // pane: &View, - // cx: &mut ViewContext, - // ) -> Option> { - // let call = self.active_call()?; - // let room = call.read(cx).room()?.read(cx); - // let participant = room.remote_participant_for_peer_id(peer_id)?; - // let track = participant.video_tracks.values().next()?.clone(); - // let user = participant.user.clone(); + fn shared_screen_for_peer( + &self, + peer_id: PeerId, + pane: &View, + cx: &mut ViewContext, + ) -> Option> { + self.call_handler.shared_screen_for_peer(peer_id, pane, cx) + // let call = self.active_call()?; + // let room = call.read(cx).room()?.read(cx); + // let participant = room.remote_participant_for_peer_id(peer_id)?; + // let track = participant.video_tracks.values().next()?.clone(); + // let user = participant.user.clone(); - // for item in pane.read(cx).items_of_type::() { - // if item.read(cx).peer_id == peer_id { - // return Some(item); - // } - // } + // for item in pane.read(cx).items_of_type::() { + // if item.read(cx).peer_id == peer_id { + // return Some(item); + // } + // } - // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) - // } + // Some(cx.build_view(|cx| SharedScreen::new(&track, peer_id, user.clone(), cx))) + } pub fn on_window_activation_changed(&mut self, cx: &mut ViewContext) { if cx.is_window_active() { @@ -3421,6 +3460,10 @@ impl Workspace { self.modal_layer .update(cx, |modal_layer, cx| modal_layer.toggle_modal(cx, build)) } + + pub fn call_state(&mut self) -> &mut dyn CallHandler { + &mut *self.call_handler + } } fn window_bounds_env_override(cx: &AsyncAppContext) -> Option { diff --git a/crates/zed2/Cargo.toml b/crates/zed2/Cargo.toml index 24648f87f1..3212b6182b 100644 --- a/crates/zed2/Cargo.toml +++ b/crates/zed2/Cargo.toml @@ -16,7 +16,7 @@ path = "src/main.rs" [dependencies] ai = { package = "ai2", path = "../ai2"} -# audio = { path = "../audio" } +audio = { package = "audio2", path = "../audio2" } # activity_indicator = { path = "../activity_indicator" } auto_update = { package = "auto_update2", path = "../auto_update2" } # breadcrumbs = { path = "../breadcrumbs" } diff --git a/crates/zed2/src/main.rs b/crates/zed2/src/main.rs index bbf7c3ae9c..c9ed26436a 100644 --- a/crates/zed2/src/main.rs +++ b/crates/zed2/src/main.rs @@ -8,7 +8,7 @@ use anyhow::{anyhow, Context as _, Result}; use backtrace::Backtrace; use chrono::Utc; use cli::FORCE_CLI_MODE_ENV_VAR_NAME; -use client::UserStore; +use client::{Client, UserStore}; use db::kvp::KEY_VALUE_STORE; use editor::Editor; use fs::RealFs; @@ -199,7 +199,7 @@ fn main() { }); cx.set_global(Arc::downgrade(&app_state)); - // audio::init(Assets, cx); + audio::init(Assets, cx); auto_update::init(http.clone(), client::ZED_SERVER_URL.clone(), cx); workspace::init(app_state.clone(), cx); @@ -249,7 +249,7 @@ fn main() { } } - let mut _triggered_authentication = false; + let mut triggered_authentication = false; fn open_paths_and_log_errs( paths: &[PathBuf], @@ -328,23 +328,23 @@ fn main() { }) .detach(); - // if !triggered_authentication { - // cx.spawn(|cx| async move { authenticate(client, &cx).await }) - // .detach_and_log_err(cx); - // } + if !triggered_authentication { + cx.spawn(|cx| async move { authenticate(client, &cx).await }) + .detach_and_log_err(cx); + } }); } -// async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { -// if stdout_is_a_pty() { -// if client::IMPERSONATE_LOGIN.is_some() { -// client.authenticate_and_connect(false, &cx).await?; -// } -// } else if client.has_keychain_credentials(&cx) { -// client.authenticate_and_connect(true, &cx).await?; -// } -// Ok::<_, anyhow::Error>(()) -// } +async fn authenticate(client: Arc, cx: &AsyncAppContext) -> Result<()> { + if stdout_is_a_pty() { + if client::IMPERSONATE_LOGIN.is_some() { + client.authenticate_and_connect(false, &cx).await?; + } + } else if client.has_keychain_credentials(&cx).await { + client.authenticate_and_connect(true, &cx).await?; + } + Ok::<_, anyhow::Error>(()) +} async fn installation_id() -> Result<(String, bool)> { let legacy_key_name = "device_id";