WIP: Add toast when users attempt to use shift-escape for the first time

Mikayla Maki created

Change summary

crates/util/src/channel.rs            |   2 
crates/workspace/src/notifications.rs | 166 +++++++++++++++-------------
crates/workspace/src/pane.rs          |  10 +
crates/workspace/src/workspace.rs     |  60 +++++++++
crates/zed/src/main.rs                |   1 
5 files changed, 157 insertions(+), 82 deletions(-)

Detailed changes

crates/util/src/channel.rs 🔗

@@ -2,6 +2,8 @@ use std::env;
 
 use lazy_static::lazy_static;
 
+pub struct ZedVersion(pub &'static str);
+
 lazy_static! {
     pub static ref RELEASE_CHANNEL_NAME: String = if cfg!(debug_assertions) {
         env::var("ZED_RELEASE_CHANNEL")

crates/workspace/src/notifications.rs 🔗

@@ -1,5 +1,5 @@
 use crate::{Toast, Workspace};
-use collections::HashSet;
+use collections::HashMap;
 use gpui::{AnyViewHandle, AppContext, Entity, View, ViewContext, ViewHandle};
 use std::{any::TypeId, ops::DerefMut};
 
@@ -34,11 +34,11 @@ impl From<&dyn NotificationHandle> for AnyViewHandle {
 }
 
 struct NotificationTracker {
-    notifications_sent: HashSet<TypeId>,
+    notifications_sent: HashMap<TypeId, Vec<usize>>,
 }
 
 impl std::ops::Deref for NotificationTracker {
-    type Target = HashSet<TypeId>;
+    type Target = HashMap<TypeId, Vec<usize>>;
 
     fn deref(&self) -> &Self::Target {
         &self.notifications_sent
@@ -54,24 +54,35 @@ impl DerefMut for NotificationTracker {
 impl NotificationTracker {
     fn new() -> Self {
         Self {
-            notifications_sent: HashSet::default(),
+            notifications_sent: Default::default(),
         }
     }
 }
 
 impl Workspace {
+    pub fn has_shown_notification_once<V: Notification>(
+        &self,
+        id: usize,
+        cx: &ViewContext<Self>,
+    ) -> bool {
+        cx
+            .global::<NotificationTracker>()
+            .get(&TypeId::of::<V>())
+            .map(|ids| ids.contains(&id))
+            .unwrap_or(false)
+    }
+
     pub fn show_notification_once<V: Notification>(
         &mut self,
         id: usize,
         cx: &mut ViewContext<Self>,
         build_notification: impl FnOnce(&mut ViewContext<Self>) -> ViewHandle<V>,
     ) {
-        if !cx
-            .global::<NotificationTracker>()
-            .contains(&TypeId::of::<V>())
+        if !self.has_shown_notification_once::<V>(id, cx)
         {
             cx.update_global::<NotificationTracker, _, _>(|tracker, _| {
-                tracker.insert(TypeId::of::<V>())
+                let entry = tracker.entry(TypeId::of::<V>()).or_default();
+                entry.push(id);
             });
 
             self.show_notification::<V>(id, cx, build_notification)
@@ -247,80 +258,81 @@ pub mod simple_message_notification {
             let on_click = self.on_click.clone();
             let has_click_action = on_click.is_some();
 
-            MouseEventHandler::<MessageNotificationTag, _>::new(0, cx, |state, cx| {
-                Flex::column()
-                    .with_child(
-                        Flex::row()
-                            .with_child(
-                                Text::new(message, theme.message.text.clone())
-                                    .contained()
-                                    .with_style(theme.message.container)
-                                    .aligned()
-                                    .top()
-                                    .left()
-                                    .flex(1., true),
-                            )
-                            .with_child(
-                                MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
-                                    let style = theme.dismiss_button.style_for(state, false);
-                                    Svg::new("icons/x_mark_8.svg")
-                                        .with_color(style.color)
-                                        .constrained()
-                                        .with_width(style.icon_width)
-                                        .aligned()
-                                        .contained()
-                                        .with_style(style.container)
-                                        .constrained()
-                                        .with_width(style.button_width)
-                                        .with_height(style.button_width)
-                                })
-                                .with_padding(Padding::uniform(5.))
-                                .on_click(MouseButton::Left, move |_, this, cx| {
-                                    this.dismiss(&Default::default(), cx);
-                                })
-                                .with_cursor_style(CursorStyle::PointingHand)
-                                .aligned()
-                                .constrained()
-                                .with_height(
-                                    cx.font_cache().line_height(theme.message.text.font_size),
-                                )
+            Flex::column()
+                .with_child(
+                    Flex::row()
+                        .with_child(
+                            Text::new(message, theme.message.text.clone())
+                                .contained()
+                                .with_style(theme.message.container)
                                 .aligned()
                                 .top()
-                                .flex_float(),
-                            ),
-                    )
-                    .with_children({
-                        let style = theme.action_message.style_for(state, false);
-                        if let Some(click_message) = click_message {
-                            Some(
-                                Flex::row().with_child(
-                                    Text::new(click_message, style.text.clone())
+                                .left()
+                                .flex(1., true),
+                        )
+                        .with_child(
+                            MouseEventHandler::<Cancel, _>::new(0, cx, |state, _| {
+                                let style = theme.dismiss_button.style_for(state, false);
+                                Svg::new("icons/x_mark_8.svg")
+                                    .with_color(style.color)
+                                    .constrained()
+                                    .with_width(style.icon_width)
+                                    .aligned()
+                                    .contained()
+                                    .with_style(style.container)
+                                    .constrained()
+                                    .with_width(style.button_width)
+                                    .with_height(style.button_width)
+                            })
+                            .with_padding(Padding::uniform(5.))
+                            .on_click(MouseButton::Left, move |_, this, cx| {
+                                this.dismiss(&Default::default(), cx);
+                            })
+                            .with_cursor_style(CursorStyle::PointingHand)
+                            .aligned()
+                            .constrained()
+                            .with_height(cx.font_cache().line_height(theme.message.text.font_size))
+                            .aligned()
+                            .top()
+                            .flex_float(),
+                        ),
+                )
+                .with_children({
+                    click_message
+                        .map(|click_message| {
+                            MouseEventHandler::<MessageNotificationTag, _>::new(
+                                0,
+                                cx,
+                                |state, _| {
+                                    let style = theme.action_message.style_for(state, false);
+
+                                    Flex::row()
+                                        .with_child(
+                                            Text::new(click_message, style.text.clone())
+                                                .contained()
+                                                .with_style(style.container),
+                                        )
                                         .contained()
-                                        .with_style(style.container),
-                                ),
+                                },
                             )
-                        } else {
-                            None
-                        }
+                            .on_click(MouseButton::Left, move |_, this, cx| {
+                                if let Some(on_click) = on_click.as_ref() {
+                                    on_click(cx);
+                                    this.dismiss(&Default::default(), cx);
+                                }
+                            })
+                            // Since we're not using a proper overlay, we have to capture these extra events
+                            .on_down(MouseButton::Left, |_, _, _| {})
+                            .on_up(MouseButton::Left, |_, _, _| {})
+                            .with_cursor_style(if has_click_action {
+                                CursorStyle::PointingHand
+                            } else {
+                                CursorStyle::Arrow
+                            })
+                        })
                         .into_iter()
-                    })
-                    .contained()
-            })
-            // Since we're not using a proper overlay, we have to capture these extra events
-            .on_down(MouseButton::Left, |_, _, _| {})
-            .on_up(MouseButton::Left, |_, _, _| {})
-            .on_click(MouseButton::Left, move |_, this, cx| {
-                if let Some(on_click) = on_click.as_ref() {
-                    on_click(cx);
-                    this.dismiss(&Default::default(), cx);
-                }
-            })
-            .with_cursor_style(if has_click_action {
-                CursorStyle::PointingHand
-            } else {
-                CursorStyle::Arrow
-            })
-            .into_any()
+                })
+                .into_any()
         }
     }
 

crates/workspace/src/pane.rs 🔗

@@ -2,8 +2,8 @@ mod dragged_item_receiver;
 
 use super::{ItemHandle, SplitDirection};
 use crate::{
-    item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewCenterTerminal, NewFile,
-    NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
+    item::WeakItemHandle, notify_of_new_dock, toolbar::Toolbar, AutosaveSetting, Item,
+    NewCenterTerminal, NewFile, NewSearch, ToggleZoom, Workspace, WorkspaceSettings,
 };
 use anyhow::Result;
 use collections::{HashMap, HashSet, VecDeque};
@@ -536,6 +536,12 @@ impl Pane {
     }
 
     pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext<Self>) {
+        // Potentially warn the user of the new keybinding
+        let workspace_handle = self.workspace().clone();
+        cx.spawn(|_, mut cx| async move { notify_of_new_dock(&workspace_handle, &mut cx) })
+            .detach();
+
+
         if self.zoomed {
             cx.emit(Event::ZoomOut);
         } else if !self.items.is_empty() {

crates/workspace/src/workspace.rs 🔗

@@ -19,7 +19,7 @@ use assets::Assets;
 use call::ActiveCall;
 use client::{
     proto::{self, PeerId},
-    Client, TypedEnvelope, UserStore,
+    Client, TypedEnvelope, UserStore, ZED_APP_VERSION,
 };
 use collections::{hash_map, HashMap, HashSet};
 use drag_and_drop::DragAndDrop;
@@ -83,7 +83,7 @@ use status_bar::StatusBar;
 pub use status_bar::StatusItemView;
 use theme::Theme;
 pub use toolbar::{ToolbarItemLocation, ToolbarItemView};
-use util::{async_iife, paths, ResultExt};
+use util::{async_iife, channel::ZedVersion, paths, ResultExt};
 pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings};
 
 lazy_static! {
@@ -3190,6 +3190,60 @@ async fn open_items(
     opened_items
 }
 
+fn notify_of_new_dock(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
+    const NEW_PANEL_BLOG_POST: &str = "https://zed.dev/blog/new-panel-system";
+    const NEW_DOCK_HINT_KEY: &str = "show_new_dock_key";
+
+    if workspace
+        .read_with(cx, |workspace, cx| {
+            let version = cx.global::<ZedVersion>().0;
+            if !version.contains("0.88")
+                && !version.contains("0.89")
+                && !version.contains("0.90")
+                && !version.contains("0.91")
+                && !version.contains("0.92")
+            {
+                return true;
+            }
+            workspace.has_shown_notification_once::<MessageNotification>(2, cx)
+        })
+        .unwrap_or(false)
+    {
+        return;
+    }
+
+    if db::kvp::KEY_VALUE_STORE
+        .read_kvp(NEW_DOCK_HINT_KEY)
+        .ok()
+        .flatten()
+        .is_some()
+    {
+        return;
+    }
+
+    cx.spawn(|_| async move {
+        db::kvp::KEY_VALUE_STORE
+            .write_kvp(NEW_DOCK_HINT_KEY.to_string(), "seen".to_string())
+            .await
+            .ok();
+    })
+    .detach();
+
+    workspace
+        .update(cx, |workspace, cx| {
+            workspace.show_notification_once(2, cx, |cx| {
+                cx.add_view(|_| {
+                    MessageNotification::new(
+                        "Looking for the dock? Try 'ctrl-`'!\n'shift-escape' now zooms your pane",
+                    )
+                    .with_click_message("Click to read more about the new panel system")
+                    .on_click(|cx| cx.platform().open_url(NEW_PANEL_BLOG_POST))
+                })
+            })
+        })
+        .ok();
+}
+
 fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut AsyncAppContext) {
     const REPORT_ISSUE_URL: &str ="https://github.com/zed-industries/community/issues/new?assignees=&labels=defect%2Ctriage&template=2_bug_report.yml";
 
@@ -3206,7 +3260,7 @@ fn notify_if_database_failed(workspace: &WeakViewHandle<Workspace>, cx: &mut Asy
             } else {
                 let backup_path = (*db::BACKUP_DB_PATH).read();
                 if let Some(backup_path) = backup_path.clone() {
-                    workspace.show_notification_once(0, cx, move |cx| {
+                    workspace.show_notification_once(1, cx, move |cx| {
                         cx.add_view(move |_| {
                             MessageNotification::new(format!(
                                 "Database file was corrupted. Old database backed up to {}",

crates/zed/src/main.rs 🔗

@@ -119,6 +119,7 @@ fn main() {
 
     app.run(move |cx| {
         cx.set_global(*RELEASE_CHANNEL);
+        cx.set_global(util::channel::ZedVersion(env!("CARGO_PKG_VERSION")));
 
         #[cfg(debug_assertions)]
         cx.set_global(StaffMode(true));