@@ -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<ChannelStore>,
user_store: Model<UserStore>,
client: Arc<Client>,
- // project: ModelHandle<Project>,
+ project: Model<Project>,
match_candidates: Vec<StringMatchCandidate>,
// list_state: ListState<Self>,
subscriptions: Vec<Subscription>,
@@ -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<ModelHandle<Project>>,
- // cx: &mut ViewContext<Self>,
- // ) {
- // 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<Self>) {
+ 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<Self>) {
let Some(handle) = cx.window_handle().downcast::<Workspace>() 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::<DragAndDrop<Workspace>>()
- // .currently_dragged::<Channel>(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::<DraggedChannelView>(|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);
- })),
- ),
- ),
- );
-
- if let Some(avatar) = contact.user.avatar.clone() {
- item = item.left_avatar(avatar);
- }
-
- div().group("").child(item)
- // let event_handler =
- // MouseEventHandler::new::<Contact, _>(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)
+ .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)))
+ });
- // 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::<ContactTooltip>(
- // contact.user.id as usize,
- // label,
- // None,
- // theme.tooltip.clone(),
- // cx,
- // )
- // .into_any()
- // } else {
- // event_handler
- // .with_tooltip::<ContactTooltip>(
- // 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<Div>;
+ type Element = Focusable<Stateful<Div>>;
fn render(&mut self, cx: &mut ViewContext<Self>) -> 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| {
@@ -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<bool>,
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<ImageData>) -> 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<Option<bool>>) -> Self {
+ self.is_available = is_available.into();
+ self
+ }
}
@@ -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<GraphicSlot>,
+ left_slot: Option<AnyElement>,
toggle: Option<bool>,
inset: bool,
on_click: Option<Rc<dyn Fn(&ClickEvent, &mut WindowContext) + 'static>>,
@@ -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<ImageSource>) -> 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),
)
}