diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index e9ffecf246..8949b60993 100644 --- a/crates/collab/src/tests.rs +++ b/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 { 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 { diff --git a/crates/collab/src/tests/integration_tests.rs b/crates/collab/src/tests/integration_tests.rs index 1ab78ac310..902242df01 100644 --- a/crates/collab/src/tests/integration_tests.rs +++ b/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) diff --git a/crates/collab_ui/src/collab_ui.rs b/crates/collab_ui/src/collab_ui.rs index 6abfec21f7..2dd2b0e6b4 100644 --- a/crates/collab_ui/src/collab_ui.rs +++ b/crates/collab_ui/src/collab_ui.rs @@ -86,6 +86,7 @@ fn join_project(action: &JoinProject, app_state: Arc, 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); diff --git a/crates/command_palette/src/command_palette.rs b/crates/command_palette/src/command_palette.rs index 55522966fa..52a0e1cdc0 100644 --- a/crates/command_palette/src/command_palette.rs +++ b/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); diff --git a/crates/diagnostics/src/diagnostics.rs b/crates/diagnostics/src/diagnostics.rs index e447a26c6b..2232555442 100644 --- a/crates/diagnostics/src/diagnostics.rs +++ b/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| { diff --git a/crates/editor/src/editor_tests.rs b/crates/editor/src/editor_tests.rs index 269390f72f..21cc9f8895 100644 --- a/crates/editor/src/editor_tests.rs +++ b/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::::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, cx: &mut gpui::TestAppCon cx.assert_editor_state( &r#" ˇuse some::modified; - - + + fn main() { println!("hello there"); - + println!("around the"); println!("world"); } diff --git a/crates/editor/src/test/editor_lsp_test_context.rs b/crates/editor/src/test/editor_lsp_test_context.rs index 1b6d846e71..fe9a7909b8 100644 --- a/crates/editor/src/test/editor_lsp_test_context.rs +++ b/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 diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 273440dce2..51d4df45f5 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/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::().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 diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 08a83fbc43..079de79b11 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/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::::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( - view_id: usize, - label_text: &'static str, - label_style: &ContainedText, - keystroke_style: ContainedText, - action: A, - cx: &mut RenderContext, -) -> 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); diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 3821185ec0..110815e870 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/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) } diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index 13546b40cd..524b765626 100644 --- a/crates/theme/src/theme.rs +++ b/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, + 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, 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, diff --git a/crates/theme/src/ui.rs b/crates/theme/src/ui.rs new file mode 100644 index 0000000000..ca71db3723 --- /dev/null +++ b/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( + label: &'static str, + style: &CheckboxStyle, + checked: bool, + cx: &mut RenderContext, + change: fn(checked: bool, cx: &mut EventContext) -> (), +) -> MouseEventHandler { + MouseEventHandler::::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( + label_text: &'static str, + label_style: &ContainedText, + keystroke_style: &ContainedText, + action: Box, + cx: &mut RenderContext, +) -> 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) +} diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index a2386b8d28..89f161a283 100644 --- a/crates/welcome/src/welcome.rs +++ b/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::( + theme::ui::checkbox::( "Do you want to send telemetry?", &theme.welcome.checkbox, metrics, cx, - |content, checked| content.telemetry.set_metrics(checked), - ), - self.render_settings_checkbox::( + |checked, cx| { + SettingsFile::update(cx, move |file| { + file.telemetry.set_metrics(checked) + }) + }, + ) + .contained() + .with_style(theme.welcome.checkbox_container) + .boxed(), + theme::ui::checkbox::( "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( - &self, - label: &'static str, - style: &CheckboxStyle, - checked: bool, - cx: &mut RenderContext, - set_value: fn(&mut SettingsFileContent, checked: bool) -> (), - ) -> ElementBox { - MouseEventHandler::::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( + // &self, + // label: &'static str, + // style: &CheckboxStyle, + // checked: bool, + // cx: &mut RenderContext, + // set_value: fn(&mut SettingsFileContent, checked: bool) -> (), + // ) -> ElementBox { + // MouseEventHandler::::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 { diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index 4281c04649..f86a9db71a 100644 --- a/crates/workspace/src/dock.rs +++ b/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, ) -> Self { let position = DockPosition::Hidden(cx.global::().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| { diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index fe33996961..7e0e6bbe01 100644 --- a/crates/workspace/src/pane.rs +++ b/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, tab_bar_context_menu: ViewHandle, docked: Option, + background_actions: BackgroundActions, } pub struct ItemNavHistory { @@ -271,7 +274,11 @@ enum ItemType { } impl Pane { - pub fn new(docked: Option, cx: &mut ViewContext) -> Self { + pub fn new( + docked: Option, + background_actions: BackgroundActions, + cx: &mut ViewContext, + ) -> 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) -> 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::::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::().theme.clone(); - dragged_item_receiver::(0, 0, false, None, cx, |_, _| { - Empty::new() - .contained() - .with_background_color(theme.workspace.background) - .boxed() + dragged_item_receiver::(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); diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 7c439522e8..65335d8671 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -432,6 +432,7 @@ pub struct AppState { fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), 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, Vec)>, leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, + background_actions: BackgroundActions, _window_subscriptions: [Subscription; 3], _apply_leader_updates: Task>, _observe_current_user: Task<()>, @@ -572,6 +575,7 @@ impl Workspace { workspace_id: WorkspaceId, project: ModelHandle, dock_default_factory: DockDefaultItemFactory, + background_actions: BackgroundActions, cx: &mut ViewContext, ) -> 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(¢er_pane, move |this, _, event, cx| { this.handle_pane_event(pane_id, event, cx) @@ -610,7 +614,7 @@ impl Workspace { .detach(); cx.focus(¢er_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) -> ViewHandle { - 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, cx: &mut ViewContext) -> Self { + Self::new(None, 0, project, |_, _| None, || &[], cx) + } } fn notify_if_database_failed(workspace: &ViewHandle, 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, - ) -> Option> { - 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| { diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 9982b4114a..2e2026309f 100644 --- a/crates/zed/src/main.rs +++ b/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), + ] +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 3c093836f2..ae9fcc9b31 100644 --- a/crates/zed/src/zed.rs +++ b/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)); diff --git a/styles/src/styleTree/contextMenu.ts b/styles/src/styleTree/contextMenu.ts index 30f44a6314..b4a21deba4 100644 --- a/styles/src/styleTree/contextMenu.ts +++ b/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: { diff --git a/styles/src/styleTree/welcome.ts b/styles/src/styleTree/welcome.ts index 137c6df111..7c437f110e 100644 --- a/styles/src/styleTree/welcome.ts +++ b/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"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index c758e0227e..55b5c61341 100644 --- a/styles/src/styleTree/workspace.ts +++ b/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,