diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 62a4dffbfd2ba772ccca5d85a153e044d4692477..eda36f76d2221eeb96c823ab341afcfd85c189fd 100644 --- a/crates/collab_ui2/src/collab_panel.rs +++ b/crates/collab_ui2/src/collab_panel.rs @@ -175,7 +175,7 @@ use gpui::{ Point, PromptLevel, Render, RenderOnce, SharedString, Stateful, Styled, Subscription, Task, View, ViewContext, VisualContext, WeakView, }; -use project::Fs; +use project::{Fs, Project}; use serde_derive::{Deserialize, Serialize}; use settings::{Settings, SettingsStore}; use ui::{ @@ -299,7 +299,7 @@ pub struct CollabPanel { channel_store: Model, user_store: Model, client: Arc, - // project: ModelHandle, + project: Model, match_candidates: Vec, // list_state: ListState, subscriptions: Vec, @@ -582,7 +582,7 @@ impl CollabPanel { selection: None, channel_store: ChannelStore::global(cx), user_store: workspace.user_store().clone(), - // project: workspace.project().clone(), + project: workspace.project().clone(), subscriptions: Vec::default(), match_candidates: Vec::default(), collapsed_sections: vec![Section::Offline], @@ -2280,18 +2280,13 @@ impl CollabPanel { // .detach(); // } - // fn call( - // &mut self, - // recipient_user_id: u64, - // initial_project: Option>, - // cx: &mut ViewContext, - // ) { - // ActiveCall::global(cx) - // .update(cx, |call, cx| { - // call.invite(recipient_user_id, initial_project, cx) - // }) - // .detach_and_log_err(cx); - // } + fn call(&mut self, recipient_user_id: u64, cx: &mut ViewContext) { + ActiveCall::global(cx) + .update(cx, |call, cx| { + call.invite(recipient_user_id, Some(self.project.clone()), cx) + }) + .detach_and_log_err(cx); + } fn join_channel(&self, channel_id: u64, cx: &mut ViewContext) { let Some(handle) = cx.window_handle().downcast::() else { @@ -2473,23 +2468,11 @@ impl CollabPanel { .on_click(cx.listener(|this, _, cx| this.toggle_contact_finder(cx))) .tooltip(|cx| Tooltip::text("Search for new contact", cx)), ), - Section::Channels => { - // todo!() - // if cx - // .global::>() - // .currently_dragged::(cx.window()) - // .is_some() - // && self.drag_target_channel == ChannelDragTarget::Root - // { - // is_dragged_over = true; - // } - - Some( - IconButton::new("add-channel", Icon::Plus) - .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) - .tooltip(|cx| Tooltip::text("Create a channel", cx)), - ) - } + Section::Channels => Some( + IconButton::new("add-channel", Icon::Plus) + .on_click(cx.listener(|this, _, cx| this.new_root_channel(cx))) + .tooltip(|cx| Tooltip::text("Create a channel", cx)), + ), _ => None, }; @@ -2557,113 +2540,57 @@ impl CollabPanel { .w_full() .justify_between() .child(Label::new(github_login.clone())) - .child( - div() - .id("remove_contact") - .invisible() - .group_hover("", |style| style.visible()) - .child( - IconButton::new("remove_contact", Icon::Close) - .color(Color::Muted) - .tooltip(|cx| Tooltip::text("Remove Contact", cx)) - .on_click(cx.listener(move |this, _, cx| { - this.remove_contact(user_id, &github_login, cx); - })), - ), - ), - ); - - if let Some(avatar) = contact.user.avatar.clone() { - item = item.left_avatar(avatar); - } - - div().group("").child(item) - // let event_handler = - // MouseEventHandler::new::(contact.user.id as usize, cx, |state, cx| { - // Flex::row() - // .with_children(contact.user.avatar.clone().map(|avatar| { - // let status_badge = if contact.online { - // Some( - // Empty::new() - // .collapsed() - // .contained() - // .with_style(if busy { - // collab_theme.contact_status_busy - // } else { - // collab_theme.contact_status_free - // }) - // .aligned(), - // ) - // } else { - // None - // }; - // Stack::new() - // .with_child( - // Image::from_data(avatar) - // .with_style(collab_theme.contact_avatar) - // .aligned() - // .left(), - // ) - // .with_children(status_badge) - // })) - - // .with_children(if calling { - // Some( - // Label::new("Calling", collab_theme.calling_indicator.text.clone()) - // .contained() - // .with_style(collab_theme.calling_indicator.container) - // .aligned(), - // ) - // } else { - // None - // }) - // .constrained() - // .with_height(collab_theme.row_height) - // .contained() - // .with_style( - // *collab_theme - // .contact_row - // .in_state(is_selected) - // .style_for(state), - // ) - // }); - - // if online && !busy { - // let room = ActiveCall::global(cx).read(cx).room(); - // let label = if room.is_some() { - // format!("Invite {} to join call", contact.user.github_login) - // } else { - // format!("Call {}", contact.user.github_login) - // }; + .when(calling, |el| { + el.child(Label::new("Calling").color(Color::Muted)) + }) + .when(!calling, |el| { + el.child( + div() + .id("remove_contact") + .invisible() + .group_hover("", |style| style.visible()) + .child( + IconButton::new("remove_contact", Icon::Close) + .color(Color::Muted) + .tooltip(|cx| Tooltip::text("Remove Contact", cx)) + .on_click(cx.listener({ + let github_login = github_login.clone(); + move |this, _, cx| { + this.remove_contact(user_id, &github_login, cx); + } + })), + ), + ) + }), + ) + .left_child( + // todo!() handle contacts with no avatar + Avatar::data(contact.user.avatar.clone().unwrap()) + .availability_indicator(if online { Some(!busy) } else { None }), + ) + .when(online && !busy, |el| { + el.on_click(cx.listener(move |this, _, cx| this.call(user_id, cx))) + }); - // event_handler - // .on_click(MouseButton::Left, move |_, this, cx| { - // this.call(user_id, Some(initial_project.clone()), cx); - // }) - // .with_cursor_style(CursorStyle::PointingHand) - // .with_tooltip::( - // contact.user.id as usize, - // label, - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // } else { - // event_handler - // .with_tooltip::( - // contact.user.id as usize, - // format!( - // "{} is {}", - // contact.user.github_login, - // if busy { "on a call" } else { "offline" } - // ), - // None, - // theme.tooltip.clone(), - // cx, - // ) - // .into_any() - // }; + div() + .id(github_login.clone()) + .group("") + .child(item) + .tooltip(move |cx| { + let text = if !online { + format!(" {} is offline", &github_login) + } else if busy { + format!(" {} is on a call", &github_login) + } else { + let room = ActiveCall::global(cx).read(cx).room(); + if room.is_some() { + format!("Invite {} to join call", &github_login) + } else { + format!("Call {}", &github_login) + } + }; + Tooltip::text(text, cx) + }) } fn render_contact_request( @@ -2831,8 +2758,7 @@ impl CollabPanel { h_stack() .id(channel_id as usize) .child(Label::new(channel.name.clone())) - .children(face_pile.map(|face_pile| face_pile.render(cx))) - .tooltip(|cx| Tooltip::text("Join channel", cx)), + .children(face_pile.map(|face_pile| face_pile.render(cx))), ) .child( h_stack() @@ -2894,6 +2820,7 @@ impl CollabPanel { }, )), ) + .tooltip(|cx| Tooltip::text("Join channel", cx)) // let channel_id = channel.id; // let collab_theme = &theme.collab_panel; diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 7f9d07e20a4097408a96ee4417544b4cfb39a69c..119c9cb8a6c57c7e8faea1cec78e91b7b3aa838a 100644 --- a/crates/gpui2/src/geometry.rs +++ b/crates/gpui2/src/geometry.rs @@ -1034,7 +1034,7 @@ impl sqlez::bindable::Bind for GlobalPixels { } #[derive(Clone, Copy, Default, Add, Sub, Mul, Div, Neg)] -pub struct Rems(f32); +pub struct Rems(pub f32); impl Mul for Rems { type Output = Pixels; diff --git a/crates/ui2/src/components/avatar.rs b/crates/ui2/src/components/avatar.rs index 57aa17ebbaa3c43c559d14803e3ed74a3a2fdc19..4d374d7bb1958aac826f1a64586c0d336aae1af7 100644 --- a/crates/ui2/src/components/avatar.rs +++ b/crates/ui2/src/components/avatar.rs @@ -1,7 +1,7 @@ use std::sync::Arc; use crate::prelude::*; -use gpui::{img, ImageData, ImageSource, Img, IntoElement}; +use gpui::{img, rems, Div, ImageData, ImageSource, IntoElement, Styled}; #[derive(Debug, Default, PartialEq, Clone)] pub enum Shape { @@ -13,13 +13,14 @@ pub enum Shape { #[derive(IntoElement)] pub struct Avatar { src: ImageSource, + is_available: Option, shape: Shape, } impl RenderOnce for Avatar { - type Rendered = Img; + type Rendered = Div; - fn render(self, _: &mut WindowContext) -> Self::Rendered { + fn render(self, cx: &mut WindowContext) -> Self::Rendered { let mut img = img(); if self.shape == Shape::Circle { @@ -28,10 +29,29 @@ impl RenderOnce for Avatar { img = img.rounded_md(); } - img.source(self.src.clone()) - .size_4() - // todo!(Pull the avatar fallback background from the theme.) - .bg(gpui::red()) + let size = rems(1.0); + + div() + .size(size) + .child( + img.source(self.src.clone()) + .size(size) + // todo!(Pull the avatar fallback background from the theme.) + .bg(gpui::red()), + ) + .children(self.is_available.map(|is_free| { + // HACK: non-integer sizes result in oval indicators. + let indicator_size = (size.0 * cx.rem_size() * 0.4).round(); + + div() + .absolute() + .z_index(1) + .bg(if is_free { gpui::green() } else { gpui::red() }) + .size(indicator_size) + .rounded(indicator_size) + .bottom_0() + .right_0() + })) } } @@ -40,12 +60,14 @@ impl Avatar { Self { src: src.into().into(), shape: Shape::Circle, + is_available: None, } } pub fn data(src: Arc) -> Self { Self { src: src.into(), shape: Shape::Circle, + is_available: None, } } @@ -53,10 +75,15 @@ impl Avatar { Self { src, shape: Shape::Circle, + is_available: None, } } pub fn shape(mut self, shape: Shape) -> Self { self.shape = shape; self } + pub fn availability_indicator(mut self, is_available: impl Into>) -> Self { + self.is_available = is_available.into(); + self + } } diff --git a/crates/ui2/src/components/list/list_item.rs b/crates/ui2/src/components/list/list_item.rs index b189e41acd779992746439b23c95db4f56600b5d..f433255bef429cd99bfac38cc5efe10c242c29f7 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -88,7 +88,7 @@ impl ListItem { self } - pub fn left_content(mut self, left_content: impl IntoElement) -> Self { + pub fn left_child(mut self, left_content: impl IntoElement) -> Self { self.left_slot = Some(left_content.into_any_element()); self } diff --git a/crates/ui2/src/components/stories/avatar.rs b/crates/ui2/src/components/stories/avatar.rs index 3e830b8b79df059c4257cfad73691dbda938c6a3..1b5ceec2fd3493aaf44067e120a76a244579e2f4 100644 --- a/crates/ui2/src/components/stories/avatar.rs +++ b/crates/ui2/src/components/stories/avatar.rs @@ -19,5 +19,13 @@ impl Render for AvatarStory { .child(Avatar::uri( "https://avatars.githubusercontent.com/u/326587?v=4", )) + .child( + Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4") + .availability_indicator(true), + ) + .child( + Avatar::uri("https://avatars.githubusercontent.com/u/326587?v=4") + .availability_indicator(false), + ) } }