Render an offline icon in titlebar when connection is lost

Nathan Sobo and Max Brunsfeld created

Co-Authored-By: Max Brunsfeld <maxbrunsfeld@gmail.com>

Change summary

gpui/src/elements/container.rs  | 12 ++++----
gpui/src/views/select.rs        |  4 +-
zed/assets/icons/offline-14.svg |  1 
zed/assets/themes/_base.toml    |  8 ++++-
zed/src/chat_panel.rs           | 16 +++++-----
zed/src/file_finder.rs          |  8 ++--
zed/src/theme.rs                | 10 ++++++
zed/src/theme_selector.rs       |  8 ++--
zed/src/workspace.rs            | 52 ++++++++++++++++++++++++++++++----
zed/src/workspace/pane.rs       | 17 ++++++++--
zed/src/workspace/sidebar.rs    |  4 +-
11 files changed, 100 insertions(+), 40 deletions(-)

Detailed changes

gpui/src/elements/container.rs 🔗

@@ -13,7 +13,7 @@ use crate::{
     Element, ElementBox, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
 };
 
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize)]
 pub struct ContainerStyle {
     #[serde(default)]
     pub margin: Margin,
@@ -42,8 +42,8 @@ impl Container {
         }
     }
 
-    pub fn with_style(mut self, style: &ContainerStyle) -> Self {
-        self.style = style.clone();
+    pub fn with_style(mut self, style: ContainerStyle) -> Self {
+        self.style = style;
         self
     }
 
@@ -242,7 +242,7 @@ impl ToJson for ContainerStyle {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct Margin {
     pub top: f32,
     pub left: f32,
@@ -269,7 +269,7 @@ impl ToJson for Margin {
     }
 }
 
-#[derive(Clone, Debug, Default)]
+#[derive(Clone, Copy, Debug, Default)]
 pub struct Padding {
     pub top: f32,
     pub left: f32,
@@ -367,7 +367,7 @@ impl ToJson for Padding {
     }
 }
 
-#[derive(Clone, Debug, Default, Deserialize)]
+#[derive(Clone, Copy, Debug, Default, Deserialize)]
 pub struct Shadow {
     #[serde(default, deserialize_with = "deserialize_vec2f")]
     offset: Vector2F,

gpui/src/views/select.rs 🔗

@@ -111,7 +111,7 @@ impl View for Select {
                     mouse_state.hovered,
                     cx,
                 ))
-                .with_style(&style.header)
+                .with_style(style.header)
                 .boxed()
             })
             .on_click(move |cx| cx.dispatch_action(ToggleSelect))
@@ -158,7 +158,7 @@ impl View for Select {
                         .with_max_height(200.)
                         .boxed(),
                     )
-                    .with_style(&style.menu)
+                    .with_style(style.menu)
                     .boxed(),
                 )
                 .boxed(),

zed/assets/themes/_base.toml 🔗

@@ -9,9 +9,13 @@ pane_divider = { width = 1, color = "$border.0" }
 border = { width = 1, bottom = true, color = "$border.0" }
 title = "$text.0"
 avatar_width = 20
-icon_signed_out = "$text.2.color"
+icon_color = "$text.2.color"
 avatar = { corner_radius = 10, border = { width = 1, color = "#00000088" } }
 
+[workspace.titlebar.offline_icon]
+padding = { right = 4 }
+width = 16
+
 [workspace.tab]
 text = "$text.2"
 padding = { left = 10, right = 10 }
@@ -29,7 +33,7 @@ background = "$surface.1"
 text = "$text.0"
 
 [workspace.sidebar]
-width = 38
+width = 32
 border = { right = true, width = 1, color = "$border.0" }
 
 [workspace.sidebar.resize_handle]

zed/src/chat_panel.rs 🔗

@@ -209,7 +209,7 @@ impl ChatPanel {
         Flex::column()
             .with_child(
                 Container::new(ChildView::new(self.channel_select.id()).boxed())
-                    .with_style(&theme.chat_panel.channel_select.container)
+                    .with_style(theme.chat_panel.channel_select.container)
                     .boxed(),
             )
             .with_child(self.render_active_channel_messages())
@@ -243,7 +243,7 @@ impl ChatPanel {
                                 )
                                 .boxed(),
                             )
-                            .with_style(&theme.sender.container)
+                            .with_style(theme.sender.container)
                             .boxed(),
                         )
                         .with_child(
@@ -254,7 +254,7 @@ impl ChatPanel {
                                 )
                                 .boxed(),
                             )
-                            .with_style(&theme.timestamp.container)
+                            .with_style(theme.timestamp.container)
                             .boxed(),
                         )
                         .boxed(),
@@ -262,14 +262,14 @@ impl ChatPanel {
                 .with_child(Text::new(message.body.clone(), theme.body.clone()).boxed())
                 .boxed(),
         )
-        .with_style(&theme.container)
+        .with_style(theme.container)
         .boxed()
     }
 
     fn render_input_box(&self) -> ElementBox {
         let theme = &self.settings.borrow().theme;
         Container::new(ChildView::new(self.input_editor.id()).boxed())
-            .with_style(&theme.chat_panel.input_editor.container)
+            .with_style(theme.chat_panel.input_editor.container)
             .boxed()
     }
 
@@ -293,13 +293,13 @@ impl ChatPanel {
             Flex::row()
                 .with_child(
                     Container::new(Label::new("#".to_string(), theme.hash.text.clone()).boxed())
-                        .with_style(&theme.hash.container)
+                        .with_style(theme.hash.container)
                         .boxed(),
                 )
                 .with_child(Label::new(channel.name.clone(), theme.name.clone()).boxed())
                 .boxed(),
         )
-        .with_style(&theme.container)
+        .with_style(theme.container)
         .boxed()
     }
 
@@ -387,7 +387,7 @@ impl View for ChatPanel {
         };
         ConstrainedBox::new(
             Container::new(element)
-                .with_style(&theme.chat_panel.container)
+                .with_style(theme.chat_panel.container)
                 .boxed(),
         )
         .with_min_width(150.)

zed/src/file_finder.rs 🔗

@@ -88,13 +88,13 @@ impl View for FileFinder {
                     Flex::new(Axis::Vertical)
                         .with_child(
                             Container::new(ChildView::new(self.query_editor.id()).boxed())
-                                .with_style(&settings.theme.selector.input_editor.container)
+                                .with_style(settings.theme.selector.input_editor.container)
                                 .boxed(),
                         )
                         .with_child(Flexible::new(1.0, self.render_matches()).boxed())
                         .boxed(),
                 )
-                .with_style(&settings.theme.selector.container)
+                .with_style(settings.theme.selector.container)
                 .boxed(),
             )
             .with_max_width(500.0)
@@ -127,7 +127,7 @@ impl FileFinder {
                 )
                 .boxed(),
             )
-            .with_style(&settings.theme.selector.empty.container)
+            .with_style(settings.theme.selector.empty.container)
             .named("empty matches");
         }
 
@@ -200,7 +200,7 @@ impl FileFinder {
                 )
                 .boxed(),
         )
-        .with_style(&style.container);
+        .with_style(style.container);
 
         let action = Select(Entry {
             worktree_id: path_match.tree_id,

zed/src/theme.rs 🔗

@@ -48,10 +48,18 @@ pub struct Titlebar {
     pub container: ContainerStyle,
     pub title: TextStyle,
     pub avatar_width: f32,
-    pub icon_signed_out: Color,
+    pub offline_icon: OfflineIcon,
+    pub icon_color: Color,
     pub avatar: ImageStyle,
 }
 
+#[derive(Clone, Deserialize)]
+pub struct OfflineIcon {
+    #[serde(flatten)]
+    pub container: ContainerStyle,
+    pub width: f32,
+}
+
 #[derive(Clone, Deserialize)]
 pub struct Tab {
     #[serde(flatten)]

zed/src/theme_selector.rs 🔗

@@ -214,7 +214,7 @@ impl ThemeSelector {
                 )
                 .boxed(),
             )
-            .with_style(&settings.theme.selector.empty.container)
+            .with_style(settings.theme.selector.empty.container)
             .named("empty matches");
         }
 
@@ -259,9 +259,9 @@ impl ThemeSelector {
             .boxed(),
         )
         .with_style(if index == self.selected_index {
-            &theme.selector.active_item.container
+            theme.selector.active_item.container
         } else {
-            &theme.selector.item.container
+            theme.selector.item.container
         });
 
         container.boxed()
@@ -288,7 +288,7 @@ impl View for ThemeSelector {
                         .with_child(Flexible::new(1.0, self.render_matches(cx)).boxed())
                         .boxed(),
                 )
-                .with_style(&settings.theme.selector.container)
+                .with_style(settings.theme.selector.container)
                 .boxed(),
             )
             .with_max_width(600.0)

zed/src/workspace.rs 🔗

@@ -31,7 +31,6 @@ pub use pane::*;
 pub use pane_group::*;
 use postage::{prelude::Stream, watch};
 use sidebar::{Side, Sidebar, ToggleSidebarItem};
-use smol::prelude::*;
 use std::{
     collections::{hash_map::Entry, HashMap, HashSet},
     future::Future,
@@ -391,9 +390,14 @@ impl Workspace {
         right_sidebar.add_item("icons/user-16.svg", cx.add_view(|_| ProjectBrowser).into());
 
         let mut current_user = app_state.user_store.current_user().clone();
+        let mut connection_status = app_state.rpc.status().clone();
         let _observe_current_user = cx.spawn_weak(|this, mut cx| async move {
             current_user.recv().await;
-            while current_user.recv().await.is_some() {
+            connection_status.recv().await;
+            let mut stream =
+                Stream::map(current_user, drop).merge(Stream::map(connection_status, drop));
+
+            while stream.recv().await.is_some() {
                 cx.update(|cx| {
                     if let Some(this) = this.upgrade(&cx) {
                         this.update(cx, |_, cx| cx.notify());
@@ -642,7 +646,7 @@ impl Workspace {
                 if let Some(load_result) = watch.borrow().as_ref() {
                     break load_result.clone();
                 }
-                watch.next().await;
+                watch.recv().await;
             };
 
             this.update(&mut cx, |this, cx| {
@@ -954,7 +958,34 @@ impl Workspace {
         &self.active_pane
     }
 
-    fn render_current_user(&self, cx: &mut RenderContext<Self>) -> ElementBox {
+    fn render_connection_status(&self) -> Option<ElementBox> {
+        let theme = &self.settings.borrow().theme;
+        match dbg!(&*self.rpc.status().borrow()) {
+            rpc::Status::ConnectionError
+            | rpc::Status::ConnectionLost
+            | rpc::Status::Reauthenticating
+            | rpc::Status::Reconnecting { .. }
+            | rpc::Status::ReconnectionError { .. } => Some(
+                Container::new(
+                    Align::new(
+                        ConstrainedBox::new(
+                            Svg::new("icons/offline-14.svg")
+                                .with_color(theme.workspace.titlebar.icon_color)
+                                .boxed(),
+                        )
+                        .with_width(theme.workspace.titlebar.offline_icon.width)
+                        .boxed(),
+                    )
+                    .boxed(),
+                )
+                .with_style(theme.workspace.titlebar.offline_icon.container)
+                .boxed(),
+            ),
+            _ => None,
+        }
+    }
+
+    fn render_avatar(&self, cx: &mut RenderContext<Self>) -> ElementBox {
         let theme = &self.settings.borrow().theme;
         let avatar = if let Some(avatar) = self
             .user_store
@@ -969,7 +1000,7 @@ impl Workspace {
         } else {
             MouseEventHandler::new::<Authenticate, _, _, _>(0, cx, |_, _| {
                 Svg::new("icons/signed-out-12.svg")
-                    .with_color(theme.workspace.titlebar.icon_signed_out)
+                    .with_color(theme.workspace.titlebar.icon_color)
                     .boxed()
             })
             .on_click(|cx| cx.dispatch_action(Authenticate))
@@ -1019,11 +1050,18 @@ impl View for Workspace {
                                     .boxed(),
                                 )
                                 .with_child(
-                                    Align::new(self.render_current_user(cx)).right().boxed(),
+                                    Align::new(
+                                        Flex::row()
+                                            .with_children(self.render_connection_status())
+                                            .with_child(self.render_avatar(cx))
+                                            .boxed(),
+                                    )
+                                    .right()
+                                    .boxed(),
                                 )
                                 .boxed(),
                         )
-                        .with_style(&theme.workspace.titlebar.container)
+                        .with_style(theme.workspace.titlebar.container)
                         .boxed(),
                     )
                     .with_height(32.)

zed/src/workspace/pane.rs 🔗

@@ -1,6 +1,14 @@
 use super::{ItemViewHandle, SplitDirection};
 use crate::settings::Settings;
-use gpui::{Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle, action, color::Color, elements::*, geometry::{rect::RectF, vector::vec2f}, keymap::Binding, platform::CursorStyle};
+use gpui::{
+    action,
+    color::Color,
+    elements::*,
+    geometry::{rect::RectF, vector::vec2f},
+    keymap::Binding,
+    platform::CursorStyle,
+    Border, Entity, MutableAppContext, Quad, RenderContext, View, ViewContext, ViewHandle,
+};
 use postage::watch;
 use std::{cmp, path::Path, sync::Arc};
 
@@ -256,7 +264,7 @@ impl Pane {
                                         )
                                         .boxed(),
                                     )
-                                    .with_style(&ContainerStyle {
+                                    .with_style(ContainerStyle {
                                         margin: Margin {
                                             left: style.spacing,
                                             right: style.spacing,
@@ -283,7 +291,8 @@ impl Pane {
                                                         icon.with_color(style.icon_close).boxed()
                                                     }
                                                 },
-                                            ).with_cursor_style(CursorStyle::PointingHand)
+                                            )
+                                            .with_cursor_style(CursorStyle::PointingHand)
                                             .on_click(move |cx| {
                                                 cx.dispatch_action(CloseItem(item_id))
                                             })
@@ -298,7 +307,7 @@ impl Pane {
                                 )
                                 .boxed(),
                         )
-                        .with_style(&style.container)
+                        .with_style(style.container)
                         .boxed(),
                     )
                     .on_mouse_down(move |cx| {

zed/src/workspace/sidebar.rs 🔗

@@ -113,7 +113,7 @@ impl Sidebar {
                     }))
                     .boxed(),
             )
-            .with_style(&theme.container)
+            .with_style(theme.container)
             .boxed(),
         )
         .with_width(theme.width)
@@ -165,7 +165,7 @@ impl Sidebar {
         let side = self.side;
         MouseEventHandler::new::<Self, _, _, _>(self.side.id(), &mut cx, |_, _| {
             Container::new(Empty::new().boxed())
-                .with_style(&self.theme(settings).resize_handle)
+                .with_style(self.theme(settings).resize_handle)
                 .boxed()
         })
         .with_padding(Padding {