diff --git a/crates/collab_ui2/src/collab_panel.rs b/crates/collab_ui2/src/collab_panel.rs index 6b51d30026..3c3e125108 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::prelude::*; @@ -300,7 +300,7 @@ pub struct CollabPanel { channel_store: Model, user_store: Model, client: Arc, - // project: ModelHandle, + project: Model, match_candidates: Vec, // list_state: ListState, subscriptions: Vec, @@ -583,7 +583,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], @@ -2281,18 +2281,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 { @@ -2476,23 +2471,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, }; @@ -2504,18 +2487,26 @@ impl CollabPanel { | Section::Offline => true, }; - let header = ListHeader::new(text) - .when_some(button, |el, button| el.right_button(button)) - .selected(is_selected) - .when(can_collapse, |el| { - el.toggle(is_collapsed).on_toggle( - cx.listener(move |this, _, cx| this.toggle_section_expanded(section, cx)), - ) - }); - h_stack() .w_full() - .child(header) + .map(|el| { + if can_collapse { + el.child( + ListItem::new(text.clone()) + .child(div().w_full().child(Label::new(text))) + .toggle(Some(!is_collapsed)) + .on_click(cx.listener(move |this, _, cx| { + this.toggle_section_expanded(section, cx) + })), + ) + } else { + el.child( + ListHeader::new(text) + .when_some(button, |el, button| el.right_button(button)) + .selected(is_selected), + ) + } + }) .when(section == Section::Channels, |el| { el.drag_over::(|style| { style.bg(cx.theme().colors().ghost_element_hover) @@ -2560,113 +2551,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) - .icon_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); - })), - ), - ), - ); + .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) + .icon_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))) + }); - 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) - // }; - - // 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( @@ -2834,8 +2769,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() @@ -2897,6 +2831,7 @@ impl CollabPanel { }, )), ) + .tooltip(|cx| Tooltip::text("Join channel", cx)) // let channel_id = channel.id; // let collab_theme = &theme.collab_panel; @@ -3279,12 +3214,15 @@ impl CollabPanel { // } impl Render for CollabPanel { - type Element = Focusable
; + type Element = Focusable>; fn render(&mut self, cx: &mut ViewContext) -> Self::Element { div() + .id("collab-panel") .key_context("CollabPanel") .track_focus(&self.focus_handle) + .size_full() + .overflow_scroll() .on_action(cx.listener(Self::confirm)) .on_action(cx.listener(Self::insert_space)) .map(|el| { diff --git a/crates/gpui2/src/geometry.rs b/crates/gpui2/src/geometry.rs index 7f9d07e20a..119c9cb8a6 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 57aa17ebba..4d374d7bb1 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 34a9844eaf..7ad1d5fb72 100644 --- a/crates/ui2/src/components/list/list_item.rs +++ b/crates/ui2/src/components/list/list_item.rs @@ -6,7 +6,7 @@ use gpui::{ use smallvec::SmallVec; use crate::prelude::*; -use crate::{Avatar, Disclosure, GraphicSlot, Icon, IconElement, IconSize}; +use crate::{Avatar, Disclosure, Icon, IconElement, IconSize}; #[derive(IntoElement)] pub struct ListItem { @@ -16,7 +16,7 @@ pub struct ListItem { // disclosure_control_style: DisclosureControlVisibility, indent_level: usize, indent_step_size: Pixels, - left_slot: Option, + left_slot: Option, toggle: Option, inset: bool, on_click: Option>, @@ -88,18 +88,23 @@ impl ListItem { self } - pub fn left_content(mut self, left_content: GraphicSlot) -> Self { - self.left_slot = Some(left_content); + pub fn left_child(mut self, left_content: impl IntoElement) -> Self { + self.left_slot = Some(left_content.into_any_element()); self } pub fn left_icon(mut self, left_icon: Icon) -> Self { - self.left_slot = Some(GraphicSlot::Icon(left_icon)); + self.left_slot = Some( + IconElement::new(left_icon) + .size(IconSize::Small) + .color(Color::Muted) + .into_any_element(), + ); self } pub fn left_avatar(mut self, left_avatar: impl Into) -> Self { - self.left_slot = Some(GraphicSlot::Avatar(left_avatar.into())); + self.left_slot = Some(Avatar::source(left_avatar.into()).into_any_element()); self } } @@ -154,16 +159,7 @@ impl RenderOnce for ListItem { self.toggle .map(|is_open| Disclosure::new(is_open).on_toggle(self.on_toggle)), ) - .map(|this| match self.left_slot { - Some(GraphicSlot::Icon(i)) => this.child( - IconElement::new(i) - .size(IconSize::Small) - .color(Color::Muted), - ), - Some(GraphicSlot::Avatar(src)) => this.child(Avatar::source(src)), - Some(GraphicSlot::PublicActor(src)) => this.child(Avatar::uri(src)), - None => this, - }) + .children(self.left_slot) .children(self.children), ) } diff --git a/crates/ui2/src/components/stories/avatar.rs b/crates/ui2/src/components/stories/avatar.rs index 3e830b8b79..1b5ceec2fd 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), + ) } }