Add blank pane experience

Mikayla Maki created

Change summary

crates/collab/src/tests.rs                        |  11 -
crates/collab/src/tests/integration_tests.rs      |  30 ---
crates/collab_ui/src/collab_ui.rs                 |   1 
crates/command_palette/src/command_palette.rs     |   4 
crates/diagnostics/src/diagnostics.rs             |  10 -
crates/editor/src/editor_tests.rs                 |  20 +-
crates/editor/src/test/editor_lsp_test_context.rs |  12 -
crates/file_finder/src/file_finder.rs             |  28 --
crates/project_panel/src/project_panel.rs         |  69 +-------
crates/terminal_view/src/terminal_view.rs         |  10 -
crates/theme/src/theme.rs                         |  42 +---
crates/theme/src/ui.rs                            | 119 ++++++++++++++
crates/welcome/src/welcome.rs                     | 142 +++++++++-------
crates/workspace/src/dock.rs                      |  15 +
crates/workspace/src/pane.rs                      |  90 ++++++++--
crates/workspace/src/workspace.rs                 |  71 ++++++--
crates/zed/src/main.rs                            |  18 +
crates/zed/src/zed.rs                             |  44 ----
styles/src/styleTree/contextMenu.ts               |   9 
styles/src/styleTree/welcome.ts                   |  20 +
styles/src/styleTree/workspace.ts                 |  28 +++
21 files changed, 454 insertions(+), 339 deletions(-)

Detailed changes

crates/collab/src/tests.rs πŸ”—

@@ -198,6 +198,7 @@ impl TestServer {
             build_window_options: |_, _, _| Default::default(),
             initialize_workspace: |_, _, _| unimplemented!(),
             dock_default_item_factory: |_, _| unimplemented!(),
+            background_actions: || unimplemented!(),
         });
 
         Project::init(&client);
@@ -434,15 +435,7 @@ impl TestClient {
         cx: &mut TestAppContext,
     ) -> ViewHandle<Workspace> {
         let (_, root_view) = cx.add_window(|_| EmptyView);
-        cx.add_view(&root_view, |cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        })
+        cx.add_view(&root_view, |cx| Workspace::test_new(project.clone(), cx))
     }
 
     fn create_new_root_dir(&mut self) -> PathBuf {

crates/collab/src/tests/integration_tests.rs πŸ”—

@@ -1449,15 +1449,7 @@ async fn test_host_disconnect(
     deterministic.run_until_parked();
     assert!(worktree_a.read_with(cx_a, |tree, _| tree.as_local().unwrap().is_shared()));
 
-    let (_, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "b.txt"), None, true, cx)
@@ -4706,15 +4698,7 @@ async fn test_collaborating_with_code_actions(
 
     // Join the project as client B.
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "main.rs"), None, true, cx)
@@ -4937,15 +4921,7 @@ async fn test_collaborating_with_renames(
         .unwrap();
     let project_b = client_b.build_remote_project(project_id, cx_b).await;
 
-    let (_window_b, workspace_b) = cx_b.add_window(|cx| {
-        Workspace::new(
-            Default::default(),
-            0,
-            project_b.clone(),
-            |_, _| unimplemented!(),
-            cx,
-        )
-    });
+    let (_window_b, workspace_b) = cx_b.add_window(|cx| Workspace::test_new(project_b.clone(), cx));
     let editor_b = workspace_b
         .update(cx_b, |workspace, cx| {
             workspace.open_path((worktree_id, "one.rs"), None, true, cx)

crates/collab_ui/src/collab_ui.rs πŸ”—

@@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc<AppState>, cx: &mut Mutable
                         0,
                         project,
                         app_state.dock_default_item_factory,
+                        app_state.background_actions,
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);

crates/command_palette/src/command_palette.rs πŸ”—

@@ -352,9 +352,7 @@ mod tests {
         });
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let editor = cx.add_view(&workspace, |cx| {
             let mut editor = Editor::single_line(None, cx);
             editor.set_text("abc", cx);

crates/diagnostics/src/diagnostics.rs πŸ”—

@@ -805,15 +805,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/test".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         // Create some diagnostics
         project.update(cx, |project, cx| {

crates/editor/src/editor_tests.rs πŸ”—

@@ -484,7 +484,9 @@ fn test_navigation_history(cx: &mut gpui::MutableAppContext) {
     cx.set_global(Settings::test(cx));
     cx.set_global(DragAndDrop::<Workspace>::default());
     use workspace::item::Item;
-    let (_, pane) = cx.add_window(Default::default(), |cx| Pane::new(None, cx));
+    let (_, pane) = cx.add_window(Default::default(), |cx| {
+        Pane::new(None, || unimplemented!(), cx)
+    });
     let buffer = MultiBuffer::build_simple(&sample_text(300, 5, 'a'), cx);
 
     cx.add_view(&pane, |cx| {
@@ -2354,10 +2356,10 @@ async fn test_clipboard(cx: &mut gpui::TestAppContext) {
         e.handle_input(") ", cx);
     });
     cx.assert_editor_state(indoc! {"
-        ( oneβœ… 
-        three 
-        five ) Λ‡two oneβœ… four three six five ( oneβœ… 
-        three 
+        ( oneβœ…
+        three
+        five ) Λ‡two oneβœ… four three six five ( oneβœ…
+        three
         five ) Λ‡"});
 
     // Cut with three selections, one of which is full-line.
@@ -5562,7 +5564,7 @@ async fn test_following_with_multiple_excerpts(cx: &mut gpui::TestAppContext) {
     Settings::test_async(cx);
     let fs = FakeFs::new(cx.background());
     let project = Project::test(fs, ["/file.rs".as_ref()], cx).await;
-    let (_, pane) = cx.add_window(|cx| Pane::new(None, cx));
+    let (_, pane) = cx.add_window(|cx| Pane::new(None, || unimplemented!(), cx));
 
     let leader = pane.update(cx, |_, cx| {
         let multibuffer = cx.add_model(|_| MultiBuffer::new(0));
@@ -5831,11 +5833,11 @@ async fn go_to_hunk(deterministic: Arc<Deterministic>, cx: &mut gpui::TestAppCon
     cx.assert_editor_state(
         &r#"
         Λ‡use some::modified;
-    
-    
+
+
         fn main() {
             println!("hello there");
-    
+
             println!("around the");
             println!("world");
         }

crates/editor/src/test/editor_lsp_test_context.rs πŸ”—

@@ -65,15 +65,7 @@ impl<'a> EditorLspTestContext<'a> {
             .insert_tree("/root", json!({ "dir": { file_name.clone(): "" }}))
             .await;
 
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         project
             .update(cx, |project, cx| {
                 project.find_or_create_local_worktree("/root", true, cx)
@@ -134,7 +126,7 @@ impl<'a> EditorLspTestContext<'a> {
                     (let_chain)
                     (await_expression)
                 ] @indent
-                
+
                 (_ "[" "]" @end) @indent
                 (_ "<" ">" @end) @indent
                 (_ "{" "}" @end) @indent

crates/file_finder/src/file_finder.rs πŸ”—

@@ -329,9 +329,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         cx.dispatch_action(window_id, Toggle);
 
         let finder = cx.read(|cx| workspace.read(cx).modal::<FileFinder>().unwrap());
@@ -385,9 +383,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
@@ -461,9 +457,7 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder
@@ -487,9 +481,7 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
 
@@ -541,9 +533,7 @@ mod tests {
             cx,
         )
         .await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
@@ -585,9 +575,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // When workspace has an active item, sort items which are closer to that item
         // first when they have the same name. In this case, b.txt is closer to dir2's a.txt
@@ -624,9 +612,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let (_, finder) =
             cx.add_window(|cx| FileFinder::new(workspace.read(cx).project().clone(), None, cx));
         finder

crates/project_panel/src/project_panel.rs πŸ”—

@@ -6,15 +6,14 @@ use gpui::{
     actions,
     anyhow::{anyhow, Result},
     elements::{
-        AnchorCorner, ChildView, ConstrainedBox, Container, ContainerStyle, Empty, Flex,
-        KeystrokeLabel, Label, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg,
-        UniformList, UniformListState,
+        AnchorCorner, ChildView, ConstrainedBox, ContainerStyle, Empty, Flex, Label,
+        MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState,
     },
     geometry::vector::Vector2F,
     impl_internal_actions,
     keymap_matcher::KeymapContext,
     platform::CursorStyle,
-    Action, AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
+    AppContext, ClipboardItem, Element, ElementBox, Entity, ModelHandle, MouseButton,
     MutableAppContext, PromptLevel, RenderContext, Task, View, ViewContext, ViewHandle,
 };
 use menu::{Confirm, SelectNext, SelectPrev};
@@ -28,7 +27,7 @@ use std::{
     path::{Path, PathBuf},
     sync::Arc,
 };
-use theme::{ContainedText, ProjectPanelEntry};
+use theme::ProjectPanelEntry;
 use unicase::UniCase;
 use workspace::Workspace;
 
@@ -1315,7 +1314,6 @@ impl View for ProjectPanel {
                 .with_child(ChildView::new(&self.context_menu, cx).boxed())
                 .boxed()
         } else {
-            let parent_view_id = cx.handle().id();
             Flex::column()
                 .with_child(
                     MouseEventHandler::<Self>::new(2, cx, {
@@ -1327,12 +1325,11 @@ impl View for ProjectPanel {
                             let context_menu_item =
                                 context_menu_item_style.style_for(state, true).clone();
 
-                            keystroke_label(
-                                parent_view_id,
+                            theme::ui::keystroke_label(
                                 "Open a project",
                                 &button_style,
-                                context_menu_item.keystroke,
-                                workspace::Open,
+                                &context_menu_item.keystroke,
+                                Box::new(workspace::Open),
                                 cx,
                             )
                             .boxed()
@@ -1357,38 +1354,6 @@ impl View for ProjectPanel {
     }
 }
 
-fn keystroke_label<A>(
-    view_id: usize,
-    label_text: &'static str,
-    label_style: &ContainedText,
-    keystroke_style: ContainedText,
-    action: A,
-    cx: &mut RenderContext<ProjectPanel>,
-) -> Container
-where
-    A: Action,
-{
-    Flex::row()
-        .with_child(
-            Label::new(label_text, label_style.text.clone())
-                .contained()
-                .boxed(),
-        )
-        .with_child({
-            KeystrokeLabel::new(
-                cx.window_id(),
-                view_id,
-                Box::new(action),
-                keystroke_style.container,
-                keystroke_style.text.clone(),
-            )
-            .flex_float()
-            .boxed()
-        })
-        .contained()
-        .with_style(label_style.container)
-}
-
 impl Entity for ProjectPanel {
     type Event = Event;
 }
@@ -1474,15 +1439,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
         assert_eq!(
             visible_entries_as_strings(&panel, 0..50, cx),
@@ -1574,15 +1531,7 @@ mod tests {
         .await;
 
         let project = Project::test(fs.clone(), ["/root1".as_ref(), "/root2".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let panel = workspace.update(cx, |_, cx| ProjectPanel::new(project, cx));
 
         select_path(&panel, "root1", cx);

crates/terminal_view/src/terminal_view.rs πŸ”—

@@ -970,15 +970,7 @@ mod tests {
         let params = cx.update(AppState::test);
 
         let project = Project::test(params.fs.clone(), [], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         (project, workspace)
     }

crates/theme/src/theme.rs πŸ”—

@@ -9,6 +9,9 @@ use gpui::{
 use serde::{de::DeserializeOwned, Deserialize};
 use serde_json::Value;
 use std::{collections::HashMap, sync::Arc};
+use ui::{CheckboxStyle, IconStyle};
+
+pub mod ui;
 
 pub use theme_registry::*;
 
@@ -50,6 +53,7 @@ pub struct ThemeMeta {
 #[derive(Deserialize, Default)]
 pub struct Workspace {
     pub background: Color,
+    pub blank_pane: BlankPaneStyle,
     pub titlebar: Titlebar,
     pub tab_bar: TabBar,
     pub pane_divider: Border,
@@ -69,6 +73,14 @@ pub struct Workspace {
     pub drop_target_overlay_color: Color,
 }
 
+#[derive(Clone, Deserialize, Default)]
+pub struct BlankPaneStyle {
+    pub logo: IconStyle,
+    pub keyboard_hints: ContainerStyle,
+    pub keyboard_hint: Interactive<ContainedText>,
+    pub keyboard_hint_width: f32,
+}
+
 #[derive(Clone, Deserialize, Default)]
 pub struct Titlebar {
     #[serde(flatten)]
@@ -858,46 +870,18 @@ pub struct WelcomeStyle {
     pub logo: IconStyle,
     pub logo_subheading: ContainedText,
     pub checkbox: CheckboxStyle,
+    pub checkbox_container: ContainerStyle,
     pub button: Interactive<ContainedText>,
     pub button_group: ContainerStyle,
     pub heading_group: ContainerStyle,
     pub checkbox_group: ContainerStyle,
 }
 
-#[derive(Clone, Deserialize, Default)]
-pub struct IconStyle {
-    pub color: Color,
-    pub icon: String,
-    pub dimensions: Dimensions,
-}
-
-#[derive(Clone, Deserialize, Default)]
-pub struct Dimensions {
-    pub width: f32,
-    pub height: f32,
-}
-
-#[derive(Clone, Deserialize, Default)]
-pub struct CheckboxStyle {
-    pub check_icon: String,
-    pub check_icon_color: Color,
-    pub label: ContainedText,
-    pub container: ContainerStyle,
-    pub width: f32,
-    pub height: f32,
-    pub default: ContainerStyle,
-    pub checked: ContainerStyle,
-    pub hovered: ContainerStyle,
-    pub hovered_and_checked: ContainerStyle,
-}
-
 #[derive(Clone, Deserialize, Default)]
 pub struct ColorScheme {
     pub name: String,
     pub is_light: bool,
-
     pub ramps: RampSet,
-
     pub lowest: Layer,
     pub middle: Layer,
     pub highest: Layer,

crates/theme/src/ui.rs πŸ”—

@@ -0,0 +1,119 @@
+use gpui::{
+    color::Color,
+    elements::{
+        ConstrainedBox, Container, ContainerStyle, Empty, Flex, KeystrokeLabel, Label,
+        MouseEventHandler, ParentElement, Svg,
+    },
+    Action, Element, EventContext, RenderContext, View,
+};
+use serde::Deserialize;
+
+use crate::ContainedText;
+
+#[derive(Clone, Deserialize, Default)]
+pub struct CheckboxStyle {
+    pub icon: IconStyle,
+    pub label: ContainedText,
+    pub default: ContainerStyle,
+    pub checked: ContainerStyle,
+    pub hovered: ContainerStyle,
+    pub hovered_and_checked: ContainerStyle,
+}
+
+pub fn checkbox<T: 'static, V: View>(
+    label: &'static str,
+    style: &CheckboxStyle,
+    checked: bool,
+    cx: &mut RenderContext<V>,
+    change: fn(checked: bool, cx: &mut EventContext) -> (),
+) -> MouseEventHandler<T> {
+    MouseEventHandler::<T>::new(0, cx, |state, _| {
+        let indicator = if checked {
+            icon(&style.icon)
+        } else {
+            Empty::new()
+                .constrained()
+                .with_width(style.icon.dimensions.width)
+                .with_height(style.icon.dimensions.height)
+        };
+
+        Flex::row()
+            .with_children([
+                indicator
+                    .contained()
+                    .with_style(if checked {
+                        if state.hovered() {
+                            style.hovered_and_checked
+                        } else {
+                            style.checked
+                        }
+                    } else {
+                        if state.hovered() {
+                            style.hovered
+                        } else {
+                            style.default
+                        }
+                    })
+                    .boxed(),
+                Label::new(label, style.label.text.clone())
+                    .contained()
+                    .with_style(style.label.container)
+                    .boxed(),
+            ])
+            .align_children_center()
+            .boxed()
+    })
+    .on_click(gpui::MouseButton::Left, move |_, cx| change(!checked, cx))
+    .with_cursor_style(gpui::CursorStyle::PointingHand)
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct IconStyle {
+    pub color: Color,
+    pub icon: String,
+    pub dimensions: Dimensions,
+}
+
+#[derive(Clone, Deserialize, Default)]
+pub struct Dimensions {
+    pub width: f32,
+    pub height: f32,
+}
+
+pub fn icon(style: &IconStyle) -> ConstrainedBox {
+    Svg::new(style.icon.clone())
+        .with_color(style.color)
+        .constrained()
+        .with_width(style.dimensions.width)
+        .with_height(style.dimensions.height)
+}
+
+pub fn keystroke_label<V: View>(
+    label_text: &'static str,
+    label_style: &ContainedText,
+    keystroke_style: &ContainedText,
+    action: Box<dyn Action>,
+    cx: &mut RenderContext<V>,
+) -> Container {
+    // FIXME: Put the theme in it's own global so we can
+    // query the keystroke style on our own
+    Flex::row()
+        .with_child(
+            Label::new(label_text, label_style.text.clone())
+                .contained()
+                .boxed(),
+        )
+        .with_child({
+            KeystrokeLabel::new(
+                cx.window_id(),
+                cx.handle().id(),
+                action,
+                keystroke_style.container,
+                keystroke_style.text.clone(),
+            )
+            .flex_float()
+            .boxed()
+        })
+        .contained()
+        .with_style(label_style.container)
+}

crates/welcome/src/welcome.rs πŸ”—

@@ -4,12 +4,12 @@ use std::{borrow::Cow, sync::Arc};
 
 use db::kvp::KEY_VALUE_STORE;
 use gpui::{
-    elements::{Empty, Flex, Label, MouseEventHandler, ParentElement, Svg},
+    elements::{Flex, Label, MouseEventHandler, ParentElement},
     Action, Element, ElementBox, Entity, MouseButton, MutableAppContext, RenderContext,
     Subscription, View, ViewContext,
 };
-use settings::{settings_file::SettingsFile, Settings, SettingsFileContent};
-use theme::CheckboxStyle;
+use settings::{settings_file::SettingsFile, Settings};
+
 use workspace::{
     item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace,
     WorkspaceId,
@@ -77,11 +77,7 @@ impl View for WelcomePage {
                 .with_children([
                     Flex::column()
                         .with_children([
-                            Svg::new(theme.welcome.logo.icon.clone())
-                                .with_color(theme.welcome.logo.color)
-                                .constrained()
-                                .with_width(theme.welcome.logo.dimensions.width)
-                                .with_height(theme.welcome.logo.dimensions.height)
+                            theme::ui::icon(&theme.welcome.logo)
                                 .aligned()
                                 .contained()
                                 .aligned()
@@ -128,20 +124,34 @@ impl View for WelcomePage {
                         .boxed(),
                     Flex::column()
                         .with_children([
-                            self.render_settings_checkbox::<Metrics>(
+                            theme::ui::checkbox::<Metrics, Self>(
                                 "Do you want to send telemetry?",
                                 &theme.welcome.checkbox,
                                 metrics,
                                 cx,
-                                |content, checked| content.telemetry.set_metrics(checked),
-                            ),
-                            self.render_settings_checkbox::<Diagnostics>(
+                                |checked, cx| {
+                                    SettingsFile::update(cx, move |file| {
+                                        file.telemetry.set_metrics(checked)
+                                    })
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container)
+                            .boxed(),
+                            theme::ui::checkbox::<Diagnostics, Self>(
                                 "Send crash reports",
                                 &theme.welcome.checkbox,
                                 diagnostics,
                                 cx,
-                                |content, checked| content.telemetry.set_diagnostics(checked),
-                            ),
+                                |checked, cx| {
+                                    SettingsFile::update(cx, move |file| {
+                                        file.telemetry.set_diagnostics(checked)
+                                    })
+                                },
+                            )
+                            .contained()
+                            .with_style(theme.welcome.checkbox_container)
+                            .boxed(),
                         ])
                         .contained()
                         .with_style(theme.welcome.checkbox_group)
@@ -204,59 +214,59 @@ impl WelcomePage {
         .boxed()
     }
 
-    fn render_settings_checkbox<T: 'static>(
-        &self,
-        label: &'static str,
-        style: &CheckboxStyle,
-        checked: bool,
-        cx: &mut RenderContext<Self>,
-        set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
-    ) -> ElementBox {
-        MouseEventHandler::<T>::new(0, cx, |state, _| {
-            let indicator = if checked {
-                Svg::new(style.check_icon.clone())
-                    .with_color(style.check_icon_color)
-                    .constrained()
-            } else {
-                Empty::new().constrained()
-            };
+    // fn render_settings_checkbox<T: 'static>(
+    //     &self,
+    //     label: &'static str,
+    //     style: &CheckboxStyle,
+    //     checked: bool,
+    //     cx: &mut RenderContext<Self>,
+    //     set_value: fn(&mut SettingsFileContent, checked: bool) -> (),
+    // ) -> ElementBox {
+    //     MouseEventHandler::<T>::new(0, cx, |state, _| {
+    //         let indicator = if checked {
+    //             Svg::new(style.check_icon.clone())
+    //                 .with_color(style.check_icon_color)
+    //                 .constrained()
+    //         } else {
+    //             Empty::new().constrained()
+    //         };
 
-            Flex::row()
-                .with_children([
-                    indicator
-                        .with_width(style.width)
-                        .with_height(style.height)
-                        .contained()
-                        .with_style(if checked {
-                            if state.hovered() {
-                                style.hovered_and_checked
-                            } else {
-                                style.checked
-                            }
-                        } else {
-                            if state.hovered() {
-                                style.hovered
-                            } else {
-                                style.default
-                            }
-                        })
-                        .boxed(),
-                    Label::new(label, style.label.text.clone())
-                        .contained()
-                        .with_style(style.label.container)
-                        .boxed(),
-                ])
-                .align_children_center()
-                .boxed()
-        })
-        .on_click(gpui::MouseButton::Left, move |_, cx| {
-            SettingsFile::update(cx, move |content| set_value(content, !checked))
-        })
-        .with_cursor_style(gpui::CursorStyle::PointingHand)
-        .contained()
-        .with_style(style.container)
-        .boxed()
-    }
+    //         Flex::row()
+    //             .with_children([
+    //                 indicator
+    //                     .with_width(style.width)
+    //                     .with_height(style.height)
+    //                     .contained()
+    //                     .with_style(if checked {
+    //                         if state.hovered() {
+    //                             style.hovered_and_checked
+    //                         } else {
+    //                             style.checked
+    //                         }
+    //                     } else {
+    //                         if state.hovered() {
+    //                             style.hovered
+    //                         } else {
+    //                             style.default
+    //                         }
+    //                     })
+    //                     .boxed(),
+    //                 Label::new(label, style.label.text.clone())
+    //                     .contained()
+    //                     .with_style(style.label.container)
+    //                     .boxed(),
+    //             ])
+    //             .align_children_center()
+    //             .boxed()
+    //     })
+    //     .on_click(gpui::MouseButton::Left, move |_, cx| {
+    //         SettingsFile::update(cx, move |content| set_value(content, !checked))
+    //     })
+    //     .with_cursor_style(gpui::CursorStyle::PointingHand)
+    //     .contained()
+    //     .with_style(style.container)
+    //     .boxed()
+    // }
 }
 
 impl Item for WelcomePage {

crates/workspace/src/dock.rs πŸ”—

@@ -13,7 +13,7 @@ use gpui::{
 use settings::{DockAnchor, Settings};
 use theme::Theme;
 
-use crate::{sidebar::SidebarSide, ItemHandle, Pane, Workspace};
+use crate::{sidebar::SidebarSide, BackgroundActions, ItemHandle, Pane, Workspace};
 pub use toggle_dock_button::ToggleDockButton;
 
 #[derive(PartialEq, Clone, Deserialize)]
@@ -182,11 +182,12 @@ pub struct Dock {
 impl Dock {
     pub fn new(
         default_item_factory: DockDefaultItemFactory,
+        background_actions: BackgroundActions,
         cx: &mut ViewContext<Workspace>,
     ) -> Self {
         let position = DockPosition::Hidden(cx.global::<Settings>().default_dock_anchor);
 
-        let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), cx));
+        let pane = cx.add_view(|cx| Pane::new(Some(position.anchor()), background_actions, cx));
         pane.update(cx, |pane, cx| {
             pane.set_active(false, cx);
         });
@@ -492,6 +493,7 @@ mod tests {
                 0,
                 project.clone(),
                 default_item_factory,
+                || unimplemented!(),
                 cx,
             )
         });
@@ -620,7 +622,14 @@ mod tests {
             cx.update(|cx| init(cx));
             let project = Project::test(fs, [], cx).await;
             let (window_id, workspace) = cx.add_window(|cx| {
-                Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+                Workspace::new(
+                    Default::default(),
+                    0,
+                    project,
+                    default_item_factory,
+                    || unimplemented!(),
+                    cx,
+                )
             });
 
             workspace.update(cx, |workspace, cx| {

crates/workspace/src/pane.rs πŸ”—

@@ -110,6 +110,8 @@ impl_internal_actions!(
 
 const MAX_NAVIGATION_HISTORY_LEN: usize = 1024;
 
+pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)];
+
 pub fn init(cx: &mut MutableAppContext) {
     cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| {
         pane.activate_item(action.0, true, true, cx);
@@ -215,6 +217,7 @@ pub struct Pane {
     toolbar: ViewHandle<Toolbar>,
     tab_bar_context_menu: ViewHandle<ContextMenu>,
     docked: Option<DockAnchor>,
+    background_actions: BackgroundActions,
 }
 
 pub struct ItemNavHistory {
@@ -271,7 +274,11 @@ enum ItemType {
 }
 
 impl Pane {
-    pub fn new(docked: Option<DockAnchor>, cx: &mut ViewContext<Self>) -> Self {
+    pub fn new(
+        docked: Option<DockAnchor>,
+        background_actions: BackgroundActions,
+        cx: &mut ViewContext<Self>,
+    ) -> Self {
         let handle = cx.weak_handle();
         let context_menu = cx.add_view(ContextMenu::new);
         Self {
@@ -292,6 +299,7 @@ impl Pane {
             toolbar: cx.add_view(|_| Toolbar::new(handle)),
             tab_bar_context_menu: context_menu,
             docked,
+            background_actions,
         }
     }
 
@@ -1415,6 +1423,64 @@ impl Pane {
             .flex(1., false)
             .boxed()
     }
+
+    fn render_blank_pane(&mut self, theme: &Theme, cx: &mut RenderContext<Self>) -> ElementBox {
+        let background = theme.workspace.background;
+        let keystroke_style = &theme.context_menu.item;
+        let theme = &theme.workspace.blank_pane;
+        Stack::new()
+            .with_children([
+                Empty::new()
+                    .contained()
+                    .with_background_color(background)
+                    .boxed(),
+                Flex::column()
+                    .align_children_center()
+                    .with_children([
+                        theme::ui::icon(&theme.logo).aligned().boxed(),
+                        Flex::column()
+                            .with_children({
+                                enum KeyboardHint {}
+                                let keyboard_hint = &theme.keyboard_hint;
+                                (self.background_actions)().into_iter().enumerate().map(
+                                    move |(idx, (text, action))| {
+                                        let hint_action = action.boxed_clone();
+                                        MouseEventHandler::<KeyboardHint>::new(
+                                            idx,
+                                            cx,
+                                            move |state, cx| {
+                                                theme::ui::keystroke_label(
+                                                    text,
+                                                    &keyboard_hint.style_for(state, false),
+                                                    &keystroke_style
+                                                        .style_for(state, false)
+                                                        .keystroke,
+                                                    hint_action,
+                                                    cx,
+                                                )
+                                                .boxed()
+                                            },
+                                        )
+                                        .on_click(MouseButton::Left, move |_, cx| {
+                                            cx.dispatch_any_action(action.boxed_clone())
+                                        })
+                                        .with_cursor_style(CursorStyle::PointingHand)
+                                        .boxed()
+                                    },
+                                )
+                            })
+                            .contained()
+                            .with_style(theme.keyboard_hints)
+                            .constrained()
+                            .with_max_width(theme.keyboard_hint_width)
+                            .aligned()
+                            .boxed(),
+                    ])
+                    .aligned()
+                    .boxed(),
+            ])
+            .boxed()
+    }
 }
 
 impl Entity for Pane {
@@ -1508,11 +1574,8 @@ impl View for Pane {
                         enum EmptyPane {}
                         let theme = cx.global::<Settings>().theme.clone();
 
-                        dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, _| {
-                            Empty::new()
-                                .contained()
-                                .with_background_color(theme.workspace.background)
-                                .boxed()
+                        dragged_item_receiver::<EmptyPane, _>(0, 0, false, None, cx, |_, cx| {
+                            self.render_blank_pane(&theme, cx)
                         })
                         .on_down(MouseButton::Left, |_, cx| {
                             cx.focus_parent_view();
@@ -1809,9 +1872,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1899,9 +1960,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // 1. Add with a destination index
@@ -1977,9 +2036,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         // singleton view
@@ -2088,8 +2145,7 @@ mod tests {
         let fs = FakeFs::new(cx.background());
 
         let project = Project::test(fs, None, cx).await;
-        let (_, workspace) =
-            cx.add_window(|cx| Workspace::new(None, 0, project, |_, _| unimplemented!(), cx));
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         add_labled_item(&workspace, &pane, "A", cx);

crates/workspace/src/workspace.rs πŸ”—

@@ -432,6 +432,7 @@ pub struct AppState {
         fn(Option<WindowBounds>, Option<uuid::Uuid>, &dyn Platform) -> WindowOptions<'static>,
     pub initialize_workspace: fn(&mut Workspace, &Arc<AppState>, &mut ViewContext<Workspace>),
     pub dock_default_item_factory: DockDefaultItemFactory,
+    pub background_actions: BackgroundActions,
 }
 
 impl AppState {
@@ -455,6 +456,7 @@ impl AppState {
             initialize_workspace: |_, _, _| {},
             build_window_options: |_, _, _| Default::default(),
             dock_default_item_factory: |_, _| unimplemented!(),
+            background_actions: || unimplemented!(),
         })
     }
 }
@@ -542,6 +544,7 @@ pub struct Workspace {
     active_call: Option<(ModelHandle<ActiveCall>, Vec<gpui::Subscription>)>,
     leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>,
     database_id: WorkspaceId,
+    background_actions: BackgroundActions,
     _window_subscriptions: [Subscription; 3],
     _apply_leader_updates: Task<Result<()>>,
     _observe_current_user: Task<()>,
@@ -572,6 +575,7 @@ impl Workspace {
         workspace_id: WorkspaceId,
         project: ModelHandle<Project>,
         dock_default_factory: DockDefaultItemFactory,
+        background_actions: BackgroundActions,
         cx: &mut ViewContext<Self>,
     ) -> Self {
         cx.observe(&project, |_, _, cx| cx.notify()).detach();
@@ -602,7 +606,7 @@ impl Workspace {
         })
         .detach();
 
-        let center_pane = cx.add_view(|cx| Pane::new(None, cx));
+        let center_pane = cx.add_view(|cx| Pane::new(None, background_actions, cx));
         let pane_id = center_pane.id();
         cx.subscribe(&center_pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -610,7 +614,7 @@ impl Workspace {
         .detach();
         cx.focus(&center_pane);
         cx.emit(Event::PaneAdded(center_pane.clone()));
-        let dock = Dock::new(dock_default_factory, cx);
+        let dock = Dock::new(dock_default_factory, background_actions, cx);
         let dock_pane = dock.pane().clone();
 
         let fs = project.read(cx).fs().clone();
@@ -730,6 +734,7 @@ impl Workspace {
             window_edited: false,
             active_call,
             database_id: workspace_id,
+            background_actions,
             _observe_current_user,
             _apply_leader_updates,
             leader_updates_tx,
@@ -818,6 +823,7 @@ impl Workspace {
                         workspace_id,
                         project_handle,
                         app_state.dock_default_item_factory,
+                        app_state.background_actions,
                         cx,
                     );
                     (app_state.initialize_workspace)(&mut workspace, &app_state, cx);
@@ -1432,7 +1438,7 @@ impl Workspace {
     }
 
     fn add_pane(&mut self, cx: &mut ViewContext<Self>) -> ViewHandle<Pane> {
-        let pane = cx.add_view(|cx| Pane::new(None, cx));
+        let pane = cx.add_view(|cx| Pane::new(None, self.background_actions, cx));
         let pane_id = pane.id();
         cx.subscribe(&pane, move |this, _, event, cx| {
             this.handle_pane_event(pane_id, event, cx)
@@ -2648,6 +2654,11 @@ impl Workspace {
         })
         .detach();
     }
+
+    #[cfg(any(test, feature = "test-support"))]
+    pub fn test_new(project: ModelHandle<Project>, cx: &mut ViewContext<Self>) -> Self {
+        Self::new(None, 0, project, |_, _| None, || &[], cx)
+    }
 }
 
 fn notify_if_database_failed(workspace: &ViewHandle<Workspace>, cx: &mut AsyncAppContext) {
@@ -2988,17 +2999,10 @@ mod tests {
 
     use super::*;
     use fs::FakeFs;
-    use gpui::{executor::Deterministic, TestAppContext, ViewContext};
+    use gpui::{executor::Deterministic, TestAppContext};
     use project::{Project, ProjectEntryId};
     use serde_json::json;
 
-    pub fn default_item_factory(
-        _workspace: &mut Workspace,
-        _cx: &mut ViewContext<Workspace>,
-    ) -> Option<Box<dyn ItemHandle>> {
-        unimplemented!()
-    }
-
     #[gpui::test]
     async fn test_tab_disambiguation(cx: &mut TestAppContext) {
         cx.foreground().forbid_parking();
@@ -3011,7 +3015,8 @@ mod tests {
                 Default::default(),
                 0,
                 project.clone(),
-                default_item_factory,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
                 cx,
             )
         });
@@ -3083,7 +3088,8 @@ mod tests {
                 Default::default(),
                 0,
                 project.clone(),
-                default_item_factory,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
                 cx,
             )
         });
@@ -3183,7 +3189,8 @@ mod tests {
                 Default::default(),
                 0,
                 project.clone(),
-                default_item_factory,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
                 cx,
             )
         });
@@ -3222,7 +3229,14 @@ mod tests {
 
         let project = Project::test(fs, None, cx).await;
         let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+            Workspace::new(
+                Default::default(),
+                0,
+                project,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
+                cx,
+            )
         });
 
         let item1 = cx.add_view(&workspace, |cx| {
@@ -3331,7 +3345,14 @@ mod tests {
 
         let project = Project::test(fs, [], cx).await;
         let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+            Workspace::new(
+                Default::default(),
+                0,
+                project,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
+                cx,
+            )
         });
 
         // Create several workspace items with single project entries, and two
@@ -3440,7 +3461,14 @@ mod tests {
 
         let project = Project::test(fs, [], cx).await;
         let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+            Workspace::new(
+                Default::default(),
+                0,
+                project,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
+                cx,
+            )
         });
 
         let item = cx.add_view(&workspace, |cx| {
@@ -3559,7 +3587,14 @@ mod tests {
 
         let project = Project::test(fs, [], cx).await;
         let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, default_item_factory, cx)
+            Workspace::new(
+                Default::default(),
+                0,
+                project,
+                |_, _| unimplemented!(),
+                || unimplemented!(),
+                cx,
+            )
         });
 
         let item = cx.add_view(&workspace, |cx| {

crates/zed/src/main.rs πŸ”—

@@ -18,7 +18,7 @@ use futures::{
     channel::{mpsc, oneshot},
     FutureExt, SinkExt, StreamExt,
 };
-use gpui::{App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
+use gpui::{Action, App, AssetSource, AsyncAppContext, MutableAppContext, Task, ViewContext};
 use isahc::{config::Configurable, Request};
 use language::LanguageRegistry;
 use log::LevelFilter;
@@ -45,9 +45,10 @@ use theme::ThemeRegistry;
 use util::StaffMode;
 use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt};
 use workspace::{
-    self, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile, OpenPaths, Workspace,
+    self, dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, NewFile,
+    OpenPaths, Workspace,
 };
-use zed::{self, build_window_options, initialize_workspace, languages, menus};
+use zed::{self, build_window_options, initialize_workspace, languages, menus, OpenSettings};
 
 fn main() {
     let http = http::client();
@@ -186,6 +187,7 @@ fn main() {
             build_window_options,
             initialize_workspace,
             dock_default_item_factory,
+            background_actions,
         });
         auto_update::init(http, client::ZED_SERVER_URL.clone(), cx);
 
@@ -703,3 +705,13 @@ pub fn dock_default_item_factory(
 
     Some(Box::new(terminal_view))
 }
+
+pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] {
+    &[
+        ("Go to file", &file_finder::Toggle),
+        ("Open the command palette", &command_palette::Toggle),
+        ("Focus the dock", &FocusDock),
+        ("Open recent projects", &recent_projects::OpenRecent),
+        ("Change your settings", &OpenSettings),
+    ]
+}

crates/zed/src/zed.rs πŸ”—

@@ -889,9 +889,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1010,9 +1008,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/dir1".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1171,9 +1167,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Open a file within an existing worktree.
         cx.update(|cx| {
@@ -1215,9 +1209,7 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let worktree = cx.read(|cx| workspace.read(cx).worktrees(cx).next().unwrap());
 
         // Create a new untitled buffer
@@ -1306,9 +1298,7 @@ mod tests {
 
         let project = Project::test(app_state.fs.clone(), [], cx).await;
         project.update(cx, |project, _| project.languages().add(rust_lang()));
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         // Create a new untitled buffer
         cx.dispatch_action(window_id, NewFile);
@@ -1361,9 +1351,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (window_id, workspace) = cx.add_window(|cx| {
-            Workspace::new(Default::default(), 0, project, |_, _| unimplemented!(), cx)
-        });
+        let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1437,15 +1425,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx));
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));
         let file1 = entries[0].clone();
@@ -1709,15 +1689,7 @@ mod tests {
             .await;
 
         let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await;
-        let (_, workspace) = cx.add_window(|cx| {
-            Workspace::new(
-                Default::default(),
-                0,
-                project.clone(),
-                |_, _| unimplemented!(),
-                cx,
-            )
-        });
+        let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx));
         let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
 
         let entries = cx.read(|cx| workspace.file_project_paths(cx));

styles/src/styleTree/contextMenu.ts πŸ”—

@@ -26,14 +26,19 @@ export default function contextMenu(colorScheme: ColorScheme) {
             hover: {
                 background: background(layer, "hovered"),
                 label: text(layer, "sans", "hovered", { size: "sm" }),
+                keystroke: {
+                    ...text(layer, "sans", "hovered", {
+                        size: "sm",
+                        weight: "bold",
+                    }),
+                    padding: { left: 3, right: 3 },
+                },
             },
             active: {
                 background: background(layer, "active"),
-                label: text(layer, "sans", "active", { size: "sm" }),
             },
             activeHover: {
                 background: background(layer, "active"),
-                label: text(layer, "sans", "active", { size: "sm" }),
             },
         },
         separator: {

styles/src/styleTree/welcome.ts πŸ”—

@@ -86,20 +86,24 @@ export default function welcome(colorScheme: ColorScheme) {
                 border: border(layer, "active"),
             },
         },
+        checkboxContainer: {
+            margin: {
+                top: 4,
+            },
+        },
         checkbox: {
             label: {
                 ...text(layer, "sans", interactive_text_size),
                 // Also supports margin, container, border, etc.
             },
-            container: {
-                margin: {
-                    top: 4,
-                },
+            icon: {
+                color: foreground(layer, "on"),
+                icon: "icons/check_12.svg",
+                dimensions: {
+                    width: 12,
+                    height: 12,
+                }
             },
-            width: 12,
-            height: 12,
-            checkIcon: "icons/check_12.svg",
-            checkIconColor: foreground(layer, "on"),
             default: {
                 ...checkboxBase,
                 background: background(layer, "default"),

styles/src/styleTree/workspace.ts πŸ”—

@@ -41,6 +41,34 @@ export default function workspace(colorScheme: ColorScheme) {
 
     return {
         background: background(layer),
+        blankPane: {
+            logo: {
+                color: background(layer, "on"),
+                icon: "icons/logo_96.svg",
+                dimensions: {
+                    width: 240,
+                    height: 240,
+                }
+            },
+            keyboardHints: {
+                margin: {
+                    top: 32
+                },
+                padding: {
+                    bottom: -8.
+                }
+            },
+            keyboardHint: {
+                ...text(colorScheme.lowest, "sans", "variant", { size: "sm" }),
+                margin: {
+                    bottom: 8
+                },
+                hover: {
+                    ...text(colorScheme.lowest, "sans", "hovered", { size: "sm" }),
+                }
+            },
+            keyboardHintWidth: 240,
+        },
         joiningProjectAvatar: {
             cornerRadius: 40,
             width: 80,