Add basic call/user UI in top-right corner.

Piotr Osiewicz created

Allow ui::Avatar to take custom data instead of always relying on URI resolution

Change summary

crates/collab_ui2/src/collab_titlebar_item.rs | 45 ++++++++++
crates/gpui2/src/elements/img.rs              | 86 ++++++++++++++------
crates/ui2/src/components/avatar.rs           |  8 
crates/ui2/src/components/stories/avatar.rs   |  4 
crates/util/src/channel.rs                    |  2 
crates/workspace2/src/workspace2.rs           |  4 
6 files changed, 113 insertions(+), 36 deletions(-)

Detailed changes

crates/collab_ui2/src/collab_titlebar_item.rs 🔗

@@ -37,7 +37,7 @@ use gpui::{
 };
 use project::Project;
 use theme::ActiveTheme;
-use ui::{h_stack, Button, ButtonVariant, Color, KeyBinding, Label, Tooltip};
+use ui::{h_stack, Button, ButtonVariant, Color, IconButton, KeyBinding, Tooltip};
 use workspace::Workspace;
 
 // const MAX_PROJECT_NAME_LENGTH: usize = 40;
@@ -85,6 +85,13 @@ impl Render for CollabTitlebarItem {
     type Element = Stateful<Div>;
 
     fn render(&mut self, cx: &mut ViewContext<Self>) -> 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();
         h_stack()
             .id("titlebar")
             .justify_between()
@@ -149,8 +156,40 @@ impl Render for CollabTitlebarItem {
                                 .into()
                             }),
                     ),
-            ) // self.titlebar_item
-            .child(h_stack().child(Label::new("Right side titlebar item")))
+            )
+            .map(|this| {
+                if let Some(user) = current_user {
+                    this.when_some(user.avatar.clone(), |this, avatar| {
+                        this.child(ui::Avatar::new(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);
+                    }))
+                }
+            }) // that's obviously wrong as we should check for current call,not current user
+            .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)),
+                        )
+                        .child(
+                            h_stack()
+                                .child(IconButton::new("mute-microphone", ui::Icon::Mic))
+                                .child(IconButton::new("mute-sound", ui::Icon::AudioOn))
+                                .child(IconButton::new("screen-share", ui::Icon::Screen))
+                                .pl_2(),
+                        ),
+                )
+            })
     }
 }
 

crates/gpui2/src/elements/img.rs 🔗

@@ -1,30 +1,59 @@
+use std::sync::Arc;
+
 use crate::{
-    Bounds, Element, InteractiveElement, InteractiveElementState, Interactivity, LayoutId, Pixels,
-    RenderOnce, SharedString, StyleRefinement, Styled, WindowContext,
+    Bounds, Element, ImageData, InteractiveElement, InteractiveElementState, Interactivity,
+    LayoutId, Pixels, RenderOnce, 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<ImageData>),
+}
+
+impl From<SharedString> for ImageSource {
+    fn from(value: SharedString) -> Self {
+        Self::Uri(value)
+    }
+}
+
+impl From<Arc<ImageData>> for ImageSource {
+    fn from(value: Arc<ImageData>) -> Self {
+        Self::Data(value)
+    }
+}
+
 pub struct Img {
     interactivity: Interactivity,
-    uri: Option<SharedString>,
+    source: Option<ImageSource>,
     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<SharedString>) -> Self {
-        self.uri = Some(uri.into());
+        self.source = Some(ImageSource::from(uri.into()));
+        self
+    }
+    pub fn data(mut self, data: Arc<ImageData>) -> Self {
+        self.source = Some(ImageSource::from(data));
         self
     }
 
+    pub fn source(mut self, source: impl Into<ImageSource>) -> 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()
+                    });
                 }
             },
         )

crates/ui2/src/components/avatar.rs 🔗

@@ -1,5 +1,5 @@
 use crate::prelude::*;
-use gpui::{img, Img, RenderOnce};
+use gpui::{img, ImageSource, Img, RenderOnce};
 
 #[derive(Debug, Default, PartialEq, Clone)]
 pub enum Shape {
@@ -10,7 +10,7 @@ pub enum Shape {
 
 #[derive(RenderOnce)]
 pub struct Avatar {
-    src: SharedString,
+    src: ImageSource,
     shape: Shape,
 }
 
@@ -26,7 +26,7 @@ impl Component 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 +34,7 @@ impl Component for Avatar {
 }
 
 impl Avatar {
-    pub fn new(src: impl Into<SharedString>) -> Self {
+    pub fn new(src: impl Into<ImageSource>) -> Self {
         Self {
             src: src.into(),
             shape: Shape::Circle,

crates/ui2/src/components/stories/avatar.rs 🔗

@@ -14,10 +14,10 @@ impl Render for AvatarStory {
             .child(Story::title_for::<Avatar>())
             .child(Story::label("Default"))
             .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/1714999?v=4",
+                "https://avatars.githubusercontent.com/u/1714999?v=4".into(),
             ))
             .child(Avatar::new(
-                "https://avatars.githubusercontent.com/u/326587?v=4",
+                "https://avatars.githubusercontent.com/u/326587?v=4".into(),
             ))
     }
 }

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,

crates/workspace2/src/workspace2.rs 🔗

@@ -3408,6 +3408,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<WindowBounds> {