diff --git a/Cargo.lock b/Cargo.lock index 981005f3f2b0e0de0364a67378903f41e5383b2b..35ea2eefd8b41c1208df3014aa3ec389c4e5c7a3 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4889,6 +4889,7 @@ dependencies = [ "anyhow", "client", "context_menu", + "db", "drag_and_drop", "editor", "futures 0.3.28", diff --git a/assets/keymaps/atom.json b/assets/keymaps/atom.json index 99aae2638fb206e3caf648fc55c6075cb12154a6..634aed322abde3333cef56f352a752d03e68005f 100644 --- a/assets/keymaps/atom.json +++ b/assets/keymaps/atom.json @@ -39,8 +39,8 @@ { "context": "Workspace", "bindings": { - "cmd-\\": "workspace::ToggleLeftSidebar", - "cmd-k cmd-b": "workspace::ToggleLeftSidebar", + "cmd-\\": "workspace::ToggleLeftDock", + "cmd-k cmd-b": "workspace::ToggleLeftDock", "cmd-t": "file_finder::Toggle", "cmd-shift-r": "project_symbols::Toggle" } @@ -62,9 +62,5 @@ "ctrl-f": "project_panel::ExpandSelectedEntry", "ctrl-shift-c": "project_panel::CopyPath" } - }, - { - "context": "Dock", - "bindings": {} } ] diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index cab4333f74c30842e2f23927ece00c4c03a398f1..987c6cf1054fd58c3cb40086669df8b2d3fd1e50 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -39,7 +39,8 @@ "cmd-shift-n": "workspace::NewWindow", "cmd-o": "workspace::Open", "alt-cmd-o": "projects::OpenRecent", - "ctrl-`": "workspace::NewTerminal" + "ctrl-~": "workspace::NewTerminal", + "ctrl-`": "terminal_panel::ToggleFocus" } }, { @@ -223,7 +224,8 @@ "cmd-shift-g": "search::SelectPrevMatch", "alt-cmd-c": "search::ToggleCaseSensitive", "alt-cmd-w": "search::ToggleWholeWord", - "alt-cmd-r": "search::ToggleRegex" + "alt-cmd-r": "search::ToggleRegex", + "shift-escape": "workspace::ToggleZoom" } }, // Bindings from VS Code @@ -365,7 +367,7 @@ "workspace::ActivatePane", 8 ], - "cmd-b": "workspace::ToggleLeftSidebar", + "cmd-b": "workspace::ToggleLeftDock", "cmd-shift-f": "workspace::NewSearch", "cmd-k cmd-t": "theme_selector::Toggle", "cmd-k cmd-s": "zed::OpenKeymap", @@ -459,32 +461,6 @@ "cmd-enter": "project_search::SearchInNew" } }, - { - "context": "Workspace", - "bindings": { - "shift-escape": "dock::FocusDock" - } - }, - { - "bindings": { - "cmd-shift-k cmd-shift-right": "dock::AnchorDockRight", - "cmd-shift-k cmd-shift-down": "dock::AnchorDockBottom", - "cmd-shift-k cmd-shift-up": "dock::ExpandDock" - } - }, - { - "context": "Pane", - "bindings": { - "cmd-escape": "dock::AddTabToDock" - } - }, - { - "context": "Pane && docked", - "bindings": { - "shift-escape": "dock::HideDock", - "cmd-escape": "dock::RemoveTabFromDock" - } - }, { "context": "ProjectPanel", "bindings": { diff --git a/assets/keymaps/jetbrains.json b/assets/keymaps/jetbrains.json index 383de0790420cd446ed8823f204a28e44752f1d3..4825d3e8b5fa2163e5a40b446c390f8d18ef96e8 100644 --- a/assets/keymaps/jetbrains.json +++ b/assets/keymaps/jetbrains.json @@ -68,15 +68,8 @@ "cmd-shift-o": "file_finder::Toggle", "cmd-shift-a": "command_palette::Toggle", "cmd-alt-o": "project_symbols::Toggle", - "cmd-1": "workspace::ToggleLeftSidebar", - "cmd-6": "diagnostics::Deploy", - "alt-f12": "dock::FocusDock" - } - }, - { - "context": "Dock", - "bindings": { - "alt-f12": "dock::HideDock" + "cmd-1": "workspace::ToggleLeftDock", + "cmd-6": "diagnostics::Deploy" } } ] diff --git a/assets/keymaps/sublime_text.json b/assets/keymaps/sublime_text.json index 373f3f4d3a7c0dd978b34ad535dff30b14671fbe..2d32b77d58a91b0a508a8da03754ce5eada9d64b 100644 --- a/assets/keymaps/sublime_text.json +++ b/assets/keymaps/sublime_text.json @@ -45,18 +45,11 @@ { "context": "Workspace", "bindings": { - "ctrl-`": "dock::FocusDock", - "cmd-k cmd-b": "workspace::ToggleLeftSidebar", + "cmd-k cmd-b": "workspace::ToggleLeftDock", "cmd-t": "file_finder::Toggle", "shift-cmd-r": "project_symbols::Toggle", // Currently busted: https://github.com/zed-industries/feedback/issues/898 "ctrl-0": "project_panel::ToggleFocus" } - }, - { - "context": "Dock", - "bindings": { - "ctrl-`": "dock::HideDock" - } } ] diff --git a/assets/keymaps/textmate.json b/assets/keymaps/textmate.json index 1abcaa376cdd62fe9d02f064000dc23127c873f2..06be72742950a320ae483df70c37b2db8a4bcf02 100644 --- a/assets/keymaps/textmate.json +++ b/assets/keymaps/textmate.json @@ -68,7 +68,7 @@ { "context": "Workspace", "bindings": { - "cmd-alt-ctrl-d": "workspace::ToggleLeftSidebar", + "cmd-alt-ctrl-d": "workspace::ToggleLeftDock", "cmd-t": "file_finder::Toggle", "cmd-shift-t": "project_symbols::Toggle" } @@ -83,9 +83,5 @@ { "context": "ProjectPanel", "bindings": {} - }, - { - "context": "Dock", - "bindings": {} } ] diff --git a/assets/settings/default.json b/assets/settings/default.json index 38d0ce820ba3eaf06281bb9992e5ed0c3a0b56ae..246e28cc8e20c57c60179f0bfb2ce572d6c27c42 100644 --- a/assets/settings/default.json +++ b/assets/settings/default.json @@ -72,7 +72,11 @@ }, "project_panel": { // Whether to show the git status in the project panel. - "git_status": true + "git_status": true, + // Where to dock project panel. Can be 'left' or 'right'. + "dock": "left", + // Default width of the project panel. + "default_width": 240 }, // Whether the screen sharing icon is shown in the os status bar. "show_call_status_icon": true, @@ -90,16 +94,6 @@ // 4. Save when idle for a certain amount of time: // "autosave": { "after_delay": {"milliseconds": 500} }, "autosave": "off", - // Where to place the dock by default. This setting can take three - // values: - // - // 1. Position the dock attached to the bottom of the workspace - // "default_dock_anchor": "bottom" - // 2. Position the dock to the right of the workspace like a side panel - // "default_dock_anchor": "right" - // 3. Position the dock full screen over the entire workspace" - // "default_dock_anchor": "expanded" - "default_dock_anchor": "bottom", // Whether or not to remove any trailing whitespace from lines of a buffer // before saving it. "remove_trailing_whitespace_on_save": true, @@ -190,6 +184,12 @@ // } // } "shell": "system", + // Where to dock terminals panel. Can be 'left', 'right', 'bottom'. + "dock": "bottom", + // Default width when the terminal is docked to the left or right. + "default_width": 640, + // Default height when the terminal is docked to the bottom. + "default_height": 320, // What working directory to use when launching the terminal. // May take 4 values: // 1. Use the current file's project directory. Will Fallback to the diff --git a/crates/collab/src/tests.rs b/crates/collab/src/tests.rs index 3c571327eb2c7ab4482779de9e3ec5c7936304ef..b51c5240a833661152f4f6f9c72ae5f522e51344 100644 --- a/crates/collab/src/tests.rs +++ b/crates/collab/src/tests.rs @@ -192,8 +192,7 @@ impl TestServer { languages: Arc::new(LanguageRegistry::test()), fs: fs.clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _| unimplemented!(), - dock_default_item_factory: |_, _| None, + initialize_workspace: |_, _, _, _| unimplemented!(), background_actions: || &[], }); diff --git a/crates/collab_ui/src/incoming_call_notification.rs b/crates/collab_ui/src/incoming_call_notification.rs index ed3e6485552eeb8f78173b47dbd58c1dcafd856e..12fad467e34c7f0db7fc707a4059a37990aff8e9 100644 --- a/crates/collab_ui/src/incoming_call_notification.rs +++ b/crates/collab_ui/src/incoming_call_notification.rs @@ -41,6 +41,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { titlebar: None, center: false, focus: false, + show: true, kind: WindowKind::PopUp, is_movable: false, screen: Some(screen), diff --git a/crates/collab_ui/src/project_shared_notification.rs b/crates/collab_ui/src/project_shared_notification.rs index 155209f47079a2587c2bf10814638445031e7ee1..fea6118bdf4055d2b4bb862d1cde876b9dd8c60f 100644 --- a/crates/collab_ui/src/project_shared_notification.rs +++ b/crates/collab_ui/src/project_shared_notification.rs @@ -35,6 +35,7 @@ pub fn init(app_state: &Arc, cx: &mut AppContext) { titlebar: None, center: false, focus: false, + show: true, kind: WindowKind::PopUp, is_movable: false, screen: Some(screen), diff --git a/crates/copilot/src/sign_in.rs b/crates/copilot/src/sign_in.rs index 764a0e4df18c3fd1c67db4a4f3ff2ca330026b02..0993a33e6c886323586c21620520440ca4bcd68c 100644 --- a/crates/copilot/src/sign_in.rs +++ b/crates/copilot/src/sign_in.rs @@ -73,6 +73,7 @@ fn create_copilot_auth_window( titlebar: None, center: true, focus: true, + show: true, kind: WindowKind::Normal, is_movable: true, screen: None, diff --git a/crates/copilot_button/src/copilot_button.rs b/crates/copilot_button/src/copilot_button.rs index fdd0da19700a408cc992f9f0d911112c036ebbbd..17d27ca41f66caf08117518d29b7f6f627febe4c 100644 --- a/crates/copilot_button/src/copilot_button.rs +++ b/crates/copilot_button/src/copilot_button.rs @@ -66,8 +66,8 @@ impl View for CopilotButton { let style = theme .workspace .status_bar - .sidebar_buttons - .item + .panel_buttons + .button .style_for(state, active); Flex::row() diff --git a/crates/editor/src/items.rs b/crates/editor/src/items.rs index 988f263337e16f78af76fad7d3fd4150057cc75d..483fd56cc5f3c3934aeb4619525ee7746567cdae 100644 --- a/crates/editor/src/items.rs +++ b/crates/editor/src/items.rs @@ -1231,27 +1231,27 @@ mod tests { } fn as_local(&self) -> Option<&dyn language::LocalFile> { - todo!() + unimplemented!() } fn mtime(&self) -> SystemTime { - todo!() + unimplemented!() } fn file_name<'a>(&'a self, _: &'a gpui::AppContext) -> &'a std::ffi::OsStr { - todo!() + unimplemented!() } fn is_deleted(&self) -> bool { - todo!() + unimplemented!() } fn as_any(&self) -> &dyn std::any::Any { - todo!() + unimplemented!() } fn to_proto(&self) -> rpc::proto::File { - todo!() + unimplemented!() } } } diff --git a/crates/feedback/src/deploy_feedback_button.rs b/crates/feedback/src/deploy_feedback_button.rs index 9133174475145ee3cc159695f14af99e562de43a..d32a3e5b4c863be333ca3077d0988bdbd34dce9c 100644 --- a/crates/feedback/src/deploy_feedback_button.rs +++ b/crates/feedback/src/deploy_feedback_button.rs @@ -39,8 +39,8 @@ impl View for DeployFeedbackButton { let style = &theme .workspace .status_bar - .sidebar_buttons - .item + .panel_buttons + .button .style_for(state, active); Svg::new("icons/feedback_16.svg") diff --git a/crates/file_finder/src/file_finder.rs b/crates/file_finder/src/file_finder.rs index 5270b694b233fc0885312cd4a5e56f6833722867..6d2ba115b7f7eadaf4c2aa51903f0fdb95adae38 100644 --- a/crates/file_finder/src/file_finder.rs +++ b/crates/file_finder/src/file_finder.rs @@ -380,7 +380,7 @@ mod tests { use gpui::{TestAppContext, ViewHandle}; use menu::{Confirm, SelectNext}; use serde_json::json; - use workspace::{AppState, Pane, Workspace}; + use workspace::{AppState, Workspace}; #[ctor::ctor] fn init_logger() { @@ -1161,9 +1161,13 @@ mod tests { assert!(insertion_result.is_none(), "Pane id {pane_id} collision"); } }); - workspace.update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &workspace::CloseActiveItem, cx); - }); + active_pane + .update(cx, |pane, cx| { + pane.close_active_item(&workspace::CloseActiveItem, cx) + .unwrap() + }) + .await + .unwrap(); deterministic.run_until_parked(); cx.read(|cx| { for pane in workspace.read(cx).panes() { diff --git a/crates/gpui/src/app.rs b/crates/gpui/src/app.rs index 5def0bed9d37ec24360cb2fe2f5cba3c6ed2bbcf..0fafe7694204b2500b203db5edc6489ed094bf40 100644 --- a/crates/gpui/src/app.rs +++ b/crates/gpui/src/app.rs @@ -1460,27 +1460,13 @@ impl AppContext { self.views_metadata.remove(&(window_id, view_id)); let mut view = self.views.remove(&(window_id, view_id)).unwrap(); view.release(self); - let change_focus_to = self.windows.get_mut(&window_id).and_then(|window| { + if let Some(window) = self.windows.get_mut(&window_id) { window.parents.remove(&view_id); window .invalidation .get_or_insert_with(Default::default) .removed .push(view_id); - if window.focused_view_id == Some(view_id) { - Some(window.root_view().id()) - } else { - None - } - }); - - if let Some(view_id) = change_focus_to { - self.pending_effects - .push_back(Effect::Focus(FocusEffect::View { - window_id, - view_id: Some(view_id), - is_forced: false, - })); } self.pending_effects @@ -1717,8 +1703,69 @@ impl AppContext { if let Some(invalidation) = invalidation { let appearance = cx.window.platform_window.appearance(); cx.invalidate(invalidation, appearance); - if cx.layout(refreshing).log_err().is_some() { + if let Some(old_parents) = cx.layout(refreshing).log_err() { updated_windows.insert(window_id); + + if let Some(focused_view_id) = cx.focused_view_id() { + let old_ancestors = std::iter::successors( + Some(focused_view_id), + |&view_id| old_parents.get(&view_id).copied(), + ) + .collect::>(); + let new_ancestors = + cx.ancestors(focused_view_id).collect::>(); + + // Notify the old ancestors of the focused view when they don't contain it anymore. + for old_ancestor in old_ancestors.iter().copied() { + if !new_ancestors.contains(&old_ancestor) { + if let Some(mut view) = + cx.views.remove(&(window_id, old_ancestor)) + { + view.focus_out( + focused_view_id, + cx, + old_ancestor, + ); + cx.views + .insert((window_id, old_ancestor), view); + } + } + } + + // Notify the new ancestors of the focused view if they contain it now. + for new_ancestor in new_ancestors.iter().copied() { + if !old_ancestors.contains(&new_ancestor) { + if let Some(mut view) = + cx.views.remove(&(window_id, new_ancestor)) + { + view.focus_in( + focused_view_id, + cx, + new_ancestor, + ); + cx.views + .insert((window_id, new_ancestor), view); + } + } + } + + // When the previously-focused view has been dropped and + // there isn't any pending focus, focus the root view. + let root_view_id = cx.window.root_view().id(); + if focused_view_id != root_view_id + && !cx.views.contains_key(&(window_id, focused_view_id)) + && !focus_effects.contains_key(&window_id) + { + focus_effects.insert( + window_id, + FocusEffect::View { + window_id, + view_id: Some(root_view_id), + is_forced: false, + }, + ); + } + } } } }); @@ -1895,9 +1942,27 @@ impl AppContext { fn handle_focus_effect(&mut self, effect: FocusEffect) { let window_id = effect.window_id(); self.update_window(window_id, |cx| { + // Ensure the newly-focused view still exists, otherwise focus + // the root view instead. let focused_id = match effect { - FocusEffect::View { view_id, .. } => view_id, - FocusEffect::ViewParent { view_id, .. } => cx.ancestors(view_id).skip(1).next(), + FocusEffect::View { view_id, .. } => { + if let Some(view_id) = view_id { + if cx.views.contains_key(&(window_id, view_id)) { + Some(view_id) + } else { + Some(cx.root_view().id()) + } + } else { + None + } + } + FocusEffect::ViewParent { view_id, .. } => Some( + cx.window + .parents + .get(&view_id) + .copied() + .unwrap_or(cx.root_view().id()), + ), }; let focus_changed = cx.window.focused_view_id != focused_id; @@ -3802,6 +3867,12 @@ impl PartialEq for ViewHandle { } } +impl PartialEq for ViewHandle { + fn eq(&self, other: &AnyViewHandle) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl PartialEq> for ViewHandle { fn eq(&self, other: &WeakViewHandle) -> bool { self.window_id == other.window_id && self.view_id == other.view_id @@ -3952,6 +4023,12 @@ impl Clone for AnyViewHandle { } } +impl PartialEq for AnyViewHandle { + fn eq(&self, other: &Self) -> bool { + self.window_id == other.window_id && self.view_id == other.view_id + } +} + impl PartialEq> for AnyViewHandle { fn eq(&self, other: &ViewHandle) -> bool { self.window_id == other.window_id && self.view_id == other.view_id @@ -4198,7 +4275,7 @@ impl Hash for WeakViewHandle { } } -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Eq, PartialEq)] pub struct AnyWeakViewHandle { window_id: usize, view_id: usize, diff --git a/crates/gpui/src/app/window.rs b/crates/gpui/src/app/window.rs index bd9bd6d2db0842a3cf40a630f7f83779856ee143..24a36865635b895908bd9f647be9f3880e5ab2a0 100644 --- a/crates/gpui/src/app/window.rs +++ b/crates/gpui/src/app/window.rs @@ -29,6 +29,7 @@ use sqlez::{ }; use std::{ any::TypeId, + mem, ops::{Deref, DerefMut, Range}, }; use util::ResultExt; @@ -890,7 +891,7 @@ impl<'a> WindowContext<'a> { Ok(element) } - pub(crate) fn layout(&mut self, refreshing: bool) -> Result<()> { + pub(crate) fn layout(&mut self, refreshing: bool) -> Result> { let window_size = self.window.platform_window.content_size(); let root_view_id = self.window.root_view().id(); let mut rendered_root = self.window.rendered_views.remove(&root_view_id).unwrap(); @@ -923,11 +924,11 @@ impl<'a> WindowContext<'a> { } } - self.window.parents = new_parents; + let old_parents = mem::replace(&mut self.window.parents, new_parents); self.window .rendered_views .insert(root_view_id, rendered_root); - Ok(()) + Ok(old_parents) } pub(crate) fn paint(&mut self) -> Result { diff --git a/crates/gpui/src/elements.rs b/crates/gpui/src/elements.rs index 27b01a8db2da0308282e244d62e66a72e4c8166e..779f4b6ec3d3d7581e26ed1c1ffa8c21382ce9f8 100644 --- a/crates/gpui/src/elements.rs +++ b/crates/gpui/src/elements.rs @@ -187,25 +187,23 @@ pub trait Element: 'static { Tooltip::new::(id, text, action, style, self.into_any(), cx) } - fn with_resize_handle( + fn resizable( self, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), ) -> Resizable where Self: 'static + Sized, { - Resizable::new::( - self.into_any(), - element_id, - side, - handle_size, - initial_size, - cx, - ) + Resizable::new(self.into_any(), side, size, on_resize) + } + + fn mouse(self, region_id: usize) -> MouseEventHandler + where + Self: Sized, + { + MouseEventHandler::for_child(self.into_any(), region_id) } } diff --git a/crates/gpui/src/elements/list.rs b/crates/gpui/src/elements/list.rs index ca73196c8bb04a1e3dba879c23e667ecba1b7e22..1cf8cc986f08afe5325b3fe8f756192969487e99 100644 --- a/crates/gpui/src/elements/list.rs +++ b/crates/gpui/src/elements/list.rs @@ -990,7 +990,7 @@ mod tests { _: &mut V, _: &mut ViewContext, ) { - todo!() + unimplemented!() } fn rect_for_text_range( @@ -1003,7 +1003,7 @@ mod tests { _: &V, _: &ViewContext, ) -> Option { - todo!() + unimplemented!() } fn debug(&self, _: RectF, _: &(), _: &(), _: &V, _: &ViewContext) -> serde_json::Value { diff --git a/crates/gpui/src/elements/mouse_event_handler.rs b/crates/gpui/src/elements/mouse_event_handler.rs index ed624922d57362d0040a1d7e28a54ae9b3f28e9c..6f2762db66144f1099bd05cee01be43f399b0141 100644 --- a/crates/gpui/src/elements/mouse_event_handler.rs +++ b/crates/gpui/src/elements/mouse_event_handler.rs @@ -32,10 +32,25 @@ pub struct MouseEventHandler { /// Element which provides a render_child callback with a MouseState and paints a mouse /// region under (or above) it for easy mouse event handling. impl MouseEventHandler { - pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self + pub fn for_child(child: impl Element, region_id: usize) -> Self { + Self { + child: child.into_any(), + region_id, + cursor_style: None, + handlers: Default::default(), + notify_on_hover: false, + notify_on_click: false, + hoverable: false, + above: false, + padding: Default::default(), + _tag: PhantomData, + } + } + + pub fn new(region_id: usize, cx: &mut ViewContext, render_child: F) -> Self where - D: Element, - F: FnOnce(&mut MouseState, &mut ViewContext) -> D, + E: Element, + F: FnOnce(&mut MouseState, &mut ViewContext) -> E, { let mut mouse_state = cx.mouse_state::(region_id); let child = render_child(&mut mouse_state, cx).into_any(); diff --git a/crates/gpui/src/elements/resizable.rs b/crates/gpui/src/elements/resizable.rs index 0e78cc07fbce76fa73f6c5106ea218b774042281..da4b3473b3069ea343d7acdd0cc85c262e7a76cf 100644 --- a/crates/gpui/src/elements/resizable.rs +++ b/crates/gpui/src/elements/resizable.rs @@ -1,4 +1,4 @@ -use std::{cell::Cell, rc::Rc}; +use std::{cell::RefCell, rc::Rc}; use pathfinder_geometry::vector::{vec2f, Vector2F}; use serde_json::json; @@ -7,25 +7,23 @@ use crate::{ geometry::rect::RectF, platform::{CursorStyle, MouseButton}, scene::MouseDrag, - AnyElement, Axis, Element, ElementStateHandle, LayoutContext, MouseRegion, SceneBuilder, View, + AnyElement, Axis, Element, LayoutContext, MouseRegion, SceneBuilder, SizeConstraint, View, ViewContext, }; -use super::{ConstrainedBox, Hook}; - #[derive(Copy, Clone, Debug)] -pub enum Side { +pub enum HandleSide { Top, Bottom, Left, Right, } -impl Side { +impl HandleSide { fn axis(&self) -> Axis { match self { - Side::Left | Side::Right => Axis::Horizontal, - Side::Top | Side::Bottom => Axis::Vertical, + HandleSide::Left | HandleSide::Right => Axis::Horizontal, + HandleSide::Top | HandleSide::Bottom => Axis::Vertical, } } @@ -33,8 +31,8 @@ impl Side { /// then top-to-bottom fn before_content(self) -> bool { match self { - Side::Left | Side::Top => true, - Side::Right | Side::Bottom => false, + HandleSide::Left | HandleSide::Top => true, + HandleSide::Right | HandleSide::Bottom => false, } } @@ -55,14 +53,14 @@ impl Side { fn of_rect(&self, bounds: RectF, handle_size: f32) -> RectF { match self { - Side::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), - Side::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())), - Side::Bottom => { + HandleSide::Top => RectF::new(bounds.origin(), vec2f(bounds.width(), handle_size)), + HandleSide::Left => RectF::new(bounds.origin(), vec2f(handle_size, bounds.height())), + HandleSide::Bottom => { let mut origin = bounds.lower_left(); origin.set_y(origin.y() - handle_size); RectF::new(origin, vec2f(bounds.width(), handle_size)) } - Side::Right => { + HandleSide::Right => { let mut origin = bounds.upper_right(); origin.set_x(origin.x() - handle_size); RectF::new(origin, vec2f(handle_size, bounds.height())) @@ -71,69 +69,44 @@ impl Side { } } -struct ResizeHandleState { - actual_dimension: Cell, - custom_dimension: Cell, -} - pub struct Resizable { - side: Side, - handle_size: f32, child: AnyElement, - state: Rc, - _state_handle: ElementStateHandle>, + handle_side: HandleSide, + handle_size: f32, + on_resize: Rc)>>, } +const DEFAULT_HANDLE_SIZE: f32 = 4.0; + impl Resizable { - pub fn new( + pub fn new( child: AnyElement, - element_id: usize, - side: Side, - handle_size: f32, - initial_size: f32, - cx: &mut ViewContext, + handle_side: HandleSide, + size: f32, + on_resize: impl 'static + FnMut(&mut V, f32, &mut ViewContext), ) -> Self { - let state_handle = cx.element_state::>( - element_id, - Rc::new(ResizeHandleState { - actual_dimension: Cell::new(initial_size), - custom_dimension: Cell::new(initial_size), - }), - ); - - let state = state_handle.read(cx).clone(); - - let child = Hook::new({ - let constrained = ConstrainedBox::new(child); - match side.axis() { - Axis::Horizontal => constrained.with_max_width(state.custom_dimension.get()), - Axis::Vertical => constrained.with_max_height(state.custom_dimension.get()), - } - }) - .on_after_layout({ - let state = state.clone(); - move |size, _| { - state.actual_dimension.set(side.relevant_component(size)); - } - }) + let child = match handle_side.axis() { + Axis::Horizontal => child.constrained().with_max_width(size), + Axis::Vertical => child.constrained().with_max_height(size), + } .into_any(); Self { - side, child, - handle_size, - state, - _state_handle: state_handle, + handle_side, + handle_size: DEFAULT_HANDLE_SIZE, + on_resize: Rc::new(RefCell::new(on_resize)), } } - pub fn current_size(&self) -> f32 { - self.state.actual_dimension.get() + pub fn with_handle_size(mut self, handle_size: f32) -> Self { + self.handle_size = handle_size; + self } } impl Element for Resizable { - type LayoutState = (); + type LayoutState = SizeConstraint; type PaintState = (); fn layout( @@ -142,7 +115,7 @@ impl Element for Resizable { view: &mut V, cx: &mut LayoutContext, ) -> (Vector2F, Self::LayoutState) { - (self.child.layout(constraint, view, cx), ()) + (self.child.layout(constraint, view, cx), constraint) } fn paint( @@ -150,34 +123,44 @@ impl Element for Resizable { scene: &mut SceneBuilder, bounds: pathfinder_geometry::rect::RectF, visible_bounds: pathfinder_geometry::rect::RectF, - _child_size: &mut Self::LayoutState, + constraint: &mut SizeConstraint, view: &mut V, cx: &mut ViewContext, ) -> Self::PaintState { scene.push_stacking_context(None, None); - let handle_region = self.side.of_rect(bounds, self.handle_size); + let handle_region = self.handle_side.of_rect(bounds, self.handle_size); enum ResizeHandle {} scene.push_mouse_region( - MouseRegion::new::(cx.view_id(), self.side as usize, handle_region) - .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere - .on_drag(MouseButton::Left, { - let state = self.state.clone(); - let side = self.side; - move |e, _: &mut V, cx| { - let prev_width = state.actual_dimension.get(); - state - .custom_dimension - .set(0f32.max(prev_width + side.compute_delta(e)).round()); - cx.notify(); + MouseRegion::new::( + cx.view_id(), + self.handle_side as usize, + handle_region, + ) + .on_down(MouseButton::Left, |_, _: &mut V, _| {}) // This prevents the mouse down event from being propagated elsewhere + .on_drag(MouseButton::Left, { + let bounds = bounds.clone(); + let side = self.handle_side; + let prev_size = side.relevant_component(bounds.size()); + let min_size = side.relevant_component(constraint.min); + let max_size = side.relevant_component(constraint.max); + let on_resize = self.on_resize.clone(); + move |event, view: &mut V, cx| { + let new_size = min_size + .max(prev_size + side.compute_delta(event)) + .min(max_size) + .round(); + if new_size != prev_size { + on_resize.borrow_mut()(view, new_size, cx); } - }), + } + }), ); scene.push_cursor_region(crate::CursorRegion { bounds: handle_region, - style: match self.side.axis() { + style: match self.handle_side.axis() { Axis::Horizontal => CursorStyle::ResizeLeftRight, Axis::Vertical => CursorStyle::ResizeUpDown, }, diff --git a/crates/gpui/src/platform.rs b/crates/gpui/src/platform.rs index ec301939f77ffae8b615d8f96f9ada8590545c8d..9b4fd7ca510a49a12b439da29a6df8002f9dada0 100644 --- a/crates/gpui/src/platform.rs +++ b/crates/gpui/src/platform.rs @@ -173,6 +173,7 @@ pub struct WindowOptions<'a> { pub titlebar: Option>, pub center: bool, pub focus: bool, + pub show: bool, pub kind: WindowKind, pub is_movable: bool, pub screen: Option>, @@ -222,21 +223,21 @@ impl Bind for WindowBounds { fn bind(&self, statement: &Statement, start_index: i32) -> Result { let (region, next_index) = match self { WindowBounds::Fullscreen => { - let next_index = statement.bind("Fullscreen", start_index)?; + let next_index = statement.bind(&"Fullscreen", start_index)?; (None, next_index) } WindowBounds::Maximized => { - let next_index = statement.bind("Maximized", start_index)?; + let next_index = statement.bind(&"Maximized", start_index)?; (None, next_index) } WindowBounds::Fixed(region) => { - let next_index = statement.bind("Fixed", start_index)?; + let next_index = statement.bind(&"Fixed", start_index)?; (Some(*region), next_index) } }; statement.bind( - region.map(|region| { + ®ion.map(|region| { ( region.min_x(), region.min_y(), @@ -376,6 +377,7 @@ impl<'a> Default for WindowOptions<'a> { }), center: false, focus: true, + show: true, kind: WindowKind::Normal, is_movable: true, screen: None, diff --git a/crates/gpui/src/platform/mac/window.rs b/crates/gpui/src/platform/mac/window.rs index 50fcec52ec94d5f74d93c16e0d43398d7a6c2c83..3c825386114332870500d0eed93171064134c699 100644 --- a/crates/gpui/src/platform/mac/window.rs +++ b/crates/gpui/src/platform/mac/window.rs @@ -614,7 +614,7 @@ impl Window { } if options.focus { native_window.makeKeyAndOrderFront_(nil); - } else { + } else if options.show { native_window.orderFront_(nil); } diff --git a/crates/project_panel/Cargo.toml b/crates/project_panel/Cargo.toml index 01d1be53b9eaeb572a69de15c655a750812f7df3..55efc09deb179437d837e7f50e1acbdb6613a2eb 100644 --- a/crates/project_panel/Cargo.toml +++ b/crates/project_panel/Cargo.toml @@ -10,6 +10,7 @@ doctest = false [dependencies] context_menu = { path = "../context_menu" } +db = { path = "../db" } drag_and_drop = { path = "../drag_and_drop" } editor = { path = "../editor" } gpui = { path = "../gpui" } @@ -26,7 +27,6 @@ serde_derive.workspace = true serde_json.workspace = true anyhow.workspace = true schemars.workspace = true - unicase = "2.6" [dev-dependencies] diff --git a/crates/project_panel/src/project_panel.rs b/crates/project_panel/src/project_panel.rs index 69d8b8be4ddeca6f63c59368be4dda153c80965c..32a8148cbb7562d36f4a3bb2c7a530f72b728363 100644 --- a/crates/project_panel/src/project_panel.rs +++ b/crates/project_panel/src/project_panel.rs @@ -1,12 +1,13 @@ mod project_panel_settings; use context_menu::{ContextMenu, ContextMenuItem}; +use db::kvp::KEY_VALUE_STORE; use drag_and_drop::{DragAndDrop, Draggable}; use editor::{Cancel, Editor}; use futures::stream::StreamExt; use gpui::{ actions, - anyhow::{anyhow, Result}, + anyhow::{self, anyhow, Result}, elements::{ AnchorCorner, ChildView, ContainerStyle, Empty, Flex, Label, MouseEventHandler, ParentElement, ScrollTarget, Stack, Svg, UniformList, UniformListState, @@ -14,15 +15,17 @@ use gpui::{ geometry::vector::Vector2F, keymap_matcher::KeymapContext, platform::{CursorStyle, MouseButton, PromptLevel}, - AnyElement, AppContext, ClipboardItem, Element, Entity, ModelHandle, Task, View, ViewContext, - ViewHandle, WeakViewHandle, + AnyElement, AppContext, AsyncAppContext, ClipboardItem, Element, Entity, ModelHandle, Task, + View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, }; use menu::{Confirm, SelectNext, SelectPrev}; use project::{ - repository::GitFileStatus, Entry, EntryKind, Project, ProjectEntryId, ProjectPath, Worktree, - WorktreeId, + repository::GitFileStatus, Entry, EntryKind, Fs, Project, ProjectEntryId, ProjectPath, + Worktree, WorktreeId, }; -use project_panel_settings::ProjectPanelSettings; +use project_panel_settings::{ProjectPanelDockPosition, ProjectPanelSettings}; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; use std::{ cmp::Ordering, collections::{hash_map, HashMap}, @@ -33,12 +36,18 @@ use std::{ }; use theme::ProjectPanelEntry; use unicase::UniCase; -use workspace::Workspace; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + Workspace, +}; +const PROJECT_PANEL_KEY: &'static str = "ProjectPanel"; const NEW_ENTRY_ID: ProjectEntryId = ProjectEntryId::MAX; pub struct ProjectPanel { project: ModelHandle, + fs: Arc, list: UniformListState, visible_entries: Vec<(WorktreeId, Vec)>, last_worktree_root_id: Option, @@ -50,6 +59,9 @@ pub struct ProjectPanel { context_menu: ViewHandle, dragged_entry_destination: Option>, workspace: WeakViewHandle, + has_focus: bool, + width: Option, + pending_serialization: Task>, } #[derive(Copy, Clone)] @@ -146,10 +158,17 @@ pub enum Event { entry_id: ProjectEntryId, focus_opened_item: bool, }, + DockPositionChanged, + Focus, +} + +#[derive(Serialize, Deserialize)] +struct SerializedProjectPanel { + width: Option, } impl ProjectPanel { - pub fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { + fn new(workspace: &mut Workspace, cx: &mut ViewContext) -> ViewHandle { let project = workspace.project().clone(); let project_panel = cx.add_view(|cx: &mut ViewContext| { cx.observe(&project, |this, _, cx| { @@ -210,6 +229,7 @@ impl ProjectPanel { let view_id = cx.view_id(); let mut this = Self { project: project.clone(), + fs: workspace.app_state().fs.clone(), list: Default::default(), visible_entries: Default::default(), last_worktree_root_id: Default::default(), @@ -221,8 +241,23 @@ impl ProjectPanel { context_menu: cx.add_view(|cx| ContextMenu::new(view_id, cx)), dragged_entry_destination: None, workspace: workspace.weak_handle(), + has_focus: false, + width: None, + pending_serialization: Task::ready(None), }; this.update_visible_entries(None, cx); + + // Update the dock position when the setting changes. + let mut old_dock_position = this.position(cx); + cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(Event::DockPositionChanged); + } + }) + .detach(); + this }); @@ -254,6 +289,7 @@ impl ProjectPanel { } } } + _ => {} } }) .detach(); @@ -261,6 +297,51 @@ impl ProjectPanel { project_panel } + pub fn load( + workspace: WeakViewHandle, + cx: AsyncAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let serialized_panel = if let Some(panel) = cx + .background() + .spawn(async move { KEY_VALUE_STORE.read_kvp(PROJECT_PANEL_KEY) }) + .await + .log_err() + .flatten() + { + Some(serde_json::from_str::(&panel)?) + } else { + None + }; + workspace.update(&mut cx, |workspace, cx| { + let panel = ProjectPanel::new(workspace, cx); + if let Some(serialized_panel) = serialized_panel { + panel.update(cx, |panel, cx| { + panel.width = serialized_panel.width; + cx.notify(); + }); + } + panel + }) + }) + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let width = self.width; + self.pending_serialization = cx.background().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + PROJECT_PANEL_KEY.into(), + serde_json::to_string(&SerializedProjectPanel { width })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } + fn deploy_context_menu( &mut self, position: Vector2F, @@ -1352,16 +1433,103 @@ impl View for ProjectPanel { Self::reset_to_default_keymap_context(keymap); keymap.add_identifier("menu"); } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + } + + fn focus_out(&mut self, _: gpui::AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; + } } impl Entity for ProjectPanel { type Event = Event; } -impl workspace::sidebar::SidebarItem for ProjectPanel { - fn should_show_badge(&self, _: &AppContext) -> bool { +impl workspace::dock::Panel for ProjectPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(cx).dock { + ProjectPanelDockPosition::Left => DockPosition::Left, + ProjectPanelDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, position: DockPosition) -> bool { + matches!(position, DockPosition::Left | DockPosition::Right) + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::( + self.fs.clone(), + cx, + move |settings| { + let dock = match position { + DockPosition::Left | DockPosition::Bottom => ProjectPanelDockPosition::Left, + DockPosition::Right => ProjectPanelDockPosition::Right, + }; + settings.dock = Some(dock); + }, + ); + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.width + .unwrap_or_else(|| settings::get::(cx).default_width) + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + self.width = Some(size); + self.serialize(cx); + cx.notify(); + } + + fn should_zoom_in_on_event(_: &Self::Event) -> bool { false } + + fn should_zoom_out_on_event(_: &Self::Event) -> bool { + false + } + + fn is_zoomed(&self, _: &WindowContext) -> bool { + false + } + + fn set_zoomed(&mut self, _: bool, _: &mut ViewContext) {} + + fn set_active(&mut self, _: bool, _: &mut ViewContext) {} + + fn icon_path(&self) -> &'static str { + "icons/folder_tree_16.svg" + } + + fn icon_tooltip(&self) -> String { + "Project Panel".into() + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, Event::DockPositionChanged) + } + + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + + fn should_close_on_event(_: &Self::Event) -> bool { + false + } + + fn has_focus(&self, _: &WindowContext) -> bool { + self.has_focus + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, Event::Focus) + } } impl ClipboardEntry { @@ -2059,6 +2227,7 @@ mod tests { theme::init((), cx); language::init(cx); editor::init_settings(cx); + crate::init(cx); workspace::init_settings(cx); }); } @@ -2072,6 +2241,7 @@ mod tests { language::init(cx); editor::init(cx); pane::init(cx); + crate::init(cx); workspace::init(app_state.clone(), cx); }); } diff --git a/crates/project_panel/src/project_panel_settings.rs b/crates/project_panel/src/project_panel_settings.rs index 7786f1b4380b10956c8b3ea327f98b349c0e6ad3..1d6c590710b5b783b6acbfe27e89903de5ad3f69 100644 --- a/crates/project_panel/src/project_panel_settings.rs +++ b/crates/project_panel/src/project_panel_settings.rs @@ -3,14 +3,25 @@ use schemars::JsonSchema; use serde_derive::{Deserialize, Serialize}; use settings::Setting; +#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)] +#[serde(rename_all = "snake_case")] +pub enum ProjectPanelDockPosition { + Left, + Right, +} + #[derive(Deserialize, Debug)] pub struct ProjectPanelSettings { pub git_status: bool, + pub dock: ProjectPanelDockPosition, + pub default_width: f32, } #[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)] pub struct ProjectPanelSettingsContent { pub git_status: Option, + pub dock: Option, + pub default_width: Option, } impl Setting for ProjectPanelSettings { diff --git a/crates/sqlez/src/bindable.rs b/crates/sqlez/src/bindable.rs index 86d69afe5f8203224f5db5f7fd319b75ecb993c8..4c874c4585ebb6cc852704581485cff17d52c047 100644 --- a/crates/sqlez/src/bindable.rs +++ b/crates/sqlez/src/bindable.rs @@ -27,7 +27,7 @@ impl StaticColumnCount for bool {} impl Bind for bool { fn bind(&self, statement: &Statement, start_index: i32) -> Result { statement - .bind(self.then_some(1).unwrap_or(0), start_index) + .bind(&self.then_some(1).unwrap_or(0), start_index) .with_context(|| format!("Failed to bind bool at index {start_index}")) } } diff --git a/crates/sqlez/src/statement.rs b/crates/sqlez/src/statement.rs index 69d5685ba02ceadeeb5bec364366c76121aa12a9..de0ad626a535cca8f18c6455435664403bb170f5 100644 --- a/crates/sqlez/src/statement.rs +++ b/crates/sqlez/src/statement.rs @@ -236,7 +236,7 @@ impl<'a> Statement<'a> { Ok(str::from_utf8(slice)?) } - pub fn bind(&self, value: T, index: i32) -> Result { + pub fn bind(&self, value: &T, index: i32) -> Result { debug_assert!(index > 0); Ok(value.bind(self, index)?) } @@ -258,7 +258,7 @@ impl<'a> Statement<'a> { } } - pub fn with_bindings(&mut self, bindings: impl Bind) -> Result<&mut Self> { + pub fn with_bindings(&mut self, bindings: &impl Bind) -> Result<&mut Self> { self.bind(bindings, 1)?; Ok(self) } @@ -464,7 +464,7 @@ mod test { connection .exec(indoc! {" CREATE TABLE texts ( - text TEXT + text TEXT )"}) .unwrap()() .unwrap(); diff --git a/crates/sqlez/src/typed_statements.rs b/crates/sqlez/src/typed_statements.rs index 488ee27c0c155f4de4c34d259e0d07faa6f43411..d7f25cde5174b65a1c97acfd615314c3fe3b3382 100644 --- a/crates/sqlez/src/typed_statements.rs +++ b/crates/sqlez/src/typed_statements.rs @@ -29,7 +29,7 @@ impl Connection { query: &str, ) -> Result Result<()>> { let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(bindings)?.exec()) + Ok(move |bindings| statement.with_bindings(&bindings)?.exec()) } /// Prepare a statement which has no bindings and returns a `Vec`. @@ -55,7 +55,7 @@ impl Connection { query: &str, ) -> Result Result>> { let mut statement = Statement::prepare(self, query)?; - Ok(move |bindings| statement.with_bindings(bindings)?.rows::()) + Ok(move |bindings| statement.with_bindings(&bindings)?.rows::()) } /// Prepare a statement that selects a single row from the database. @@ -87,7 +87,7 @@ impl Connection { let mut statement = Statement::prepare(self, query)?; Ok(move |bindings| { statement - .with_bindings(bindings) + .with_bindings(&bindings) .context("Bindings failed")? .maybe_row::() .context("Maybe row failed") diff --git a/crates/terminal/src/terminal.rs b/crates/terminal/src/terminal.rs index 98d85d00e1c51922347f690a6700a64819b51c87..576719526da0104cb92b6e6c05b2f768c042da84 100644 --- a/crates/terminal/src/terminal.rs +++ b/crates/terminal/src/terminal.rs @@ -119,6 +119,14 @@ pub fn init(cx: &mut AppContext) { settings::register::(cx); } +#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq)] +#[serde(rename_all = "snake_case")] +pub enum TerminalDockPosition { + Left, + Bottom, + Right, +} + #[derive(Deserialize)] pub struct TerminalSettings { pub shell: Shell, @@ -132,6 +140,9 @@ pub struct TerminalSettings { pub alternate_scroll: AlternateScroll, pub option_as_meta: bool, pub copy_on_select: bool, + pub dock: TerminalDockPosition, + pub default_width: f32, + pub default_height: f32, } #[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] @@ -147,6 +158,9 @@ pub struct TerminalSettingsContent { pub alternate_scroll: Option, pub option_as_meta: Option, pub copy_on_select: Option, + pub dock: Option, + pub default_width: Option, + pub default_height: Option, } impl TerminalSettings { diff --git a/crates/terminal_view/src/terminal_button.rs b/crates/terminal_view/src/terminal_button.rs deleted file mode 100644 index fcb5e7feb3ae8d5eaa1db8c87229b8c5c31c2e0e..0000000000000000000000000000000000000000 --- a/crates/terminal_view/src/terminal_button.rs +++ /dev/null @@ -1,173 +0,0 @@ -use crate::TerminalView; -use context_menu::{ContextMenu, ContextMenuItem}; -use gpui::{ - elements::*, - platform::{CursorStyle, MouseButton}, - AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle, -}; -use std::any::TypeId; -use workspace::{ - dock::{Dock, FocusDock}, - item::ItemHandle, - NewTerminal, StatusItemView, Workspace, -}; - -pub struct TerminalButton { - workspace: WeakViewHandle, - popup_menu: ViewHandle, -} - -impl Entity for TerminalButton { - type Event = (); -} - -impl View for TerminalButton { - fn ui_name() -> &'static str { - "TerminalButton" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let workspace = self.workspace.upgrade(cx); - let project = match workspace { - Some(workspace) => workspace.read(cx).project().read(cx), - None => return Empty::new().into_any(), - }; - - let focused_view = cx.focused_view_id(); - let active = focused_view - .map(|view_id| { - cx.view_type_id(cx.window_id(), view_id) == Some(TypeId::of::()) - }) - .unwrap_or(false); - - let has_terminals = !project.local_terminal_handles().is_empty(); - let terminal_count = project.local_terminal_handles().len() as i32; - let theme = theme::current(cx).clone(); - - Stack::new() - .with_child( - MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _cx| { - let style = theme - .workspace - .status_bar - .sidebar_buttons - .item - .style_for(state, active); - - Flex::row() - .with_child( - Svg::new("icons/terminal_12.svg") - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .aligned() - .into_any_named("terminals-icon"), - ) - .with_children(has_terminals.then(|| { - Label::new(terminal_count.to_string(), style.label.text.clone()) - .contained() - .with_style(style.label.container) - .aligned() - })) - .constrained() - .with_height(style.icon_size) - .contained() - .with_style(style.container) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, this, cx| { - if has_terminals { - this.deploy_terminal_menu(cx); - } else { - if !active { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - Dock::focus_dock(workspace, &Default::default(), cx) - }) - } - } - }; - }) - .with_tooltip::( - 0, - "Show Terminal".into(), - Some(Box::new(FocusDock)), - theme.tooltip.clone(), - cx, - ), - ) - .with_child(ChildView::new(&self.popup_menu, cx).aligned().top().right()) - .into_any_named("terminal button") - } -} - -impl TerminalButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { - let button_view_id = cx.view_id(); - cx.observe(&workspace, |_, _, cx| cx.notify()).detach(); - Self { - workspace: workspace.downgrade(), - popup_menu: cx.add_view(|cx| { - let mut menu = ContextMenu::new(button_view_id, cx); - menu.set_position_mode(OverlayPositionMode::Local); - menu - }), - } - } - - pub fn deploy_terminal_menu(&mut self, cx: &mut ViewContext) { - let mut menu_options = vec![ContextMenuItem::action("New Terminal", NewTerminal)]; - - if let Some(workspace) = self.workspace.upgrade(cx) { - let project = workspace.read(cx).project().read(cx); - let local_terminal_handles = project.local_terminal_handles(); - - if !local_terminal_handles.is_empty() { - menu_options.push(ContextMenuItem::Separator) - } - - for local_terminal_handle in local_terminal_handles { - if let Some(terminal) = local_terminal_handle.upgrade(cx) { - let workspace = self.workspace.clone(); - let local_terminal_handle = local_terminal_handle.clone(); - menu_options.push(ContextMenuItem::handler( - terminal.read(cx).title(), - move |cx| { - if let Some(workspace) = workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - let terminal = workspace - .items_of_type::(cx) - .find(|terminal| { - terminal.read(cx).model().downgrade() - == local_terminal_handle - }); - if let Some(terminal) = terminal { - workspace.activate_item(&terminal, cx); - } - }); - } - }, - )) - } - } - } - - self.popup_menu.update(cx, |menu, cx| { - menu.show( - Default::default(), - AnchorCorner::BottomRight, - menu_options, - cx, - ); - }); - } -} - -impl StatusItemView for TerminalButton { - fn set_active_pane_item(&mut self, _: Option<&dyn ItemHandle>, cx: &mut ViewContext) { - cx.notify(); - } -} diff --git a/crates/terminal_view/src/terminal_panel.rs b/crates/terminal_view/src/terminal_panel.rs new file mode 100644 index 0000000000000000000000000000000000000000..9c8073eada819f26ca944140025394c93023be3f --- /dev/null +++ b/crates/terminal_view/src/terminal_panel.rs @@ -0,0 +1,389 @@ +use std::sync::Arc; + +use crate::TerminalView; +use db::kvp::KEY_VALUE_STORE; +use gpui::{ + actions, anyhow::Result, elements::*, serde_json, AppContext, AsyncAppContext, Entity, + Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, +}; +use project::Fs; +use serde::{Deserialize, Serialize}; +use settings::SettingsStore; +use terminal::{TerminalDockPosition, TerminalSettings}; +use util::{ResultExt, TryFutureExt}; +use workspace::{ + dock::{DockPosition, Panel}, + item::Item, + pane, DraggedItem, Pane, Workspace, +}; + +const TERMINAL_PANEL_KEY: &'static str = "TerminalPanel"; + +actions!(terminal_panel, [ToggleFocus]); + +pub fn init(cx: &mut AppContext) { + cx.add_action(TerminalPanel::add_terminal); +} + +pub enum Event { + Close, + DockPositionChanged, + ZoomIn, + ZoomOut, + Focus, +} + +pub struct TerminalPanel { + pane: ViewHandle, + fs: Arc, + workspace: WeakViewHandle, + width: Option, + height: Option, + pending_serialization: Task>, + _subscriptions: Vec, +} + +impl TerminalPanel { + fn new(workspace: &Workspace, cx: &mut ViewContext) -> Self { + let weak_self = cx.weak_handle(); + let pane = cx.add_view(|cx| { + let window_id = cx.window_id(); + let mut pane = Pane::new( + workspace.weak_handle(), + workspace.app_state().background_actions, + Default::default(), + cx, + ); + pane.set_can_split(false, cx); + pane.on_can_drop(move |drag_and_drop, cx| { + drag_and_drop + .currently_dragged::(window_id) + .map_or(false, |(_, item)| { + item.handle.act_as::(cx).is_some() + }) + }); + pane.set_render_tab_bar_buttons(cx, move |_, cx| { + let this = weak_self.clone(); + Pane::render_tab_bar_button( + 0, + "icons/plus_12.svg", + cx, + move |_, cx| { + let this = this.clone(); + cx.window_context().defer(move |cx| { + if let Some(this) = this.upgrade(cx) { + this.update(cx, |this, cx| { + this.add_terminal(&Default::default(), cx); + }); + } + }) + }, + None, + ) + }); + pane + }); + let subscriptions = vec![ + cx.observe(&pane, |_, _, cx| cx.notify()), + cx.subscribe(&pane, Self::handle_pane_event), + ]; + let this = Self { + pane, + fs: workspace.app_state().fs.clone(), + workspace: workspace.weak_handle(), + pending_serialization: Task::ready(None), + width: None, + height: None, + _subscriptions: subscriptions, + }; + let mut old_dock_position = this.position(cx); + cx.observe_global::(move |this, cx| { + let new_dock_position = this.position(cx); + if new_dock_position != old_dock_position { + old_dock_position = new_dock_position; + cx.emit(Event::DockPositionChanged); + } + }) + .detach(); + this + } + + pub fn load( + workspace: WeakViewHandle, + cx: AsyncAppContext, + ) -> Task>> { + cx.spawn(|mut cx| async move { + let serialized_panel = if let Some(panel) = cx + .background() + .spawn(async move { KEY_VALUE_STORE.read_kvp(TERMINAL_PANEL_KEY) }) + .await + .log_err() + .flatten() + { + Some(serde_json::from_str::(&panel)?) + } else { + None + }; + let (panel, pane, items) = workspace.update(&mut cx, |workspace, cx| { + let panel = cx.add_view(|cx| TerminalPanel::new(workspace, cx)); + let items = if let Some(serialized_panel) = serialized_panel.as_ref() { + panel.update(cx, |panel, cx| { + cx.notify(); + panel.height = serialized_panel.height; + panel.width = serialized_panel.width; + panel.pane.update(cx, |_, cx| { + serialized_panel + .items + .iter() + .map(|item_id| { + TerminalView::deserialize( + workspace.project().clone(), + workspace.weak_handle(), + workspace.database_id(), + *item_id, + cx, + ) + }) + .collect::>() + }) + }) + } else { + Default::default() + }; + let pane = panel.read(cx).pane.clone(); + (panel, pane, items) + })?; + + let items = futures::future::join_all(items).await; + workspace.update(&mut cx, |workspace, cx| { + let active_item_id = serialized_panel + .as_ref() + .and_then(|panel| panel.active_item_id); + let mut active_ix = None; + for item in items { + if let Some(item) = item.log_err() { + let item_id = item.id(); + Pane::add_item(workspace, &pane, Box::new(item), false, false, None, cx); + if Some(item_id) == active_item_id { + active_ix = Some(pane.read(cx).items_len() - 1); + } + } + } + + if let Some(active_ix) = active_ix { + pane.update(cx, |pane, cx| { + pane.activate_item(active_ix, false, false, cx) + }); + } + })?; + + Ok(panel) + }) + } + + fn handle_pane_event( + &mut self, + _pane: ViewHandle, + event: &pane::Event, + cx: &mut ViewContext, + ) { + match event { + pane::Event::ActivateItem { .. } => self.serialize(cx), + pane::Event::RemoveItem { .. } => self.serialize(cx), + pane::Event::Remove => cx.emit(Event::Close), + pane::Event::ZoomIn => cx.emit(Event::ZoomIn), + pane::Event::ZoomOut => cx.emit(Event::ZoomOut), + pane::Event::Focus => cx.emit(Event::Focus), + _ => {} + } + } + + fn add_terminal(&mut self, _: &workspace::NewTerminal, cx: &mut ViewContext) { + let workspace = self.workspace.clone(); + cx.spawn(|this, mut cx| async move { + let pane = this.read_with(&cx, |this, _| this.pane.clone())?; + workspace.update(&mut cx, |workspace, cx| { + let working_directory_strategy = settings::get::(cx) + .working_directory + .clone(); + let working_directory = + crate::get_working_directory(workspace, cx, working_directory_strategy); + let window_id = cx.window_id(); + if let Some(terminal) = workspace.project().update(cx, |project, cx| { + project + .create_terminal(working_directory, window_id, cx) + .log_err() + }) { + let terminal = + Box::new(cx.add_view(|cx| { + TerminalView::new(terminal, workspace.database_id(), cx) + })); + Pane::add_item(workspace, &pane, terminal, true, true, None, cx); + } + })?; + this.update(&mut cx, |this, cx| this.serialize(cx))?; + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn serialize(&mut self, cx: &mut ViewContext) { + let items = self + .pane + .read(cx) + .items() + .map(|item| item.id()) + .collect::>(); + let active_item_id = self.pane.read(cx).active_item().map(|item| item.id()); + let height = self.height; + let width = self.width; + self.pending_serialization = cx.background().spawn( + async move { + KEY_VALUE_STORE + .write_kvp( + TERMINAL_PANEL_KEY.into(), + serde_json::to_string(&SerializedTerminalPanel { + items, + active_item_id, + height, + width, + })?, + ) + .await?; + anyhow::Ok(()) + } + .log_err(), + ); + } +} + +impl Entity for TerminalPanel { + type Event = Event; +} + +impl View for TerminalPanel { + fn ui_name() -> &'static str { + "TerminalPanel" + } + + fn render(&mut self, cx: &mut ViewContext) -> gpui::AnyElement { + ChildView::new(&self.pane, cx).into_any() + } + + fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext) { + if cx.is_self_focused() { + cx.focus(&self.pane); + } + } +} + +impl Panel for TerminalPanel { + fn position(&self, cx: &WindowContext) -> DockPosition { + match settings::get::(cx).dock { + TerminalDockPosition::Left => DockPosition::Left, + TerminalDockPosition::Bottom => DockPosition::Bottom, + TerminalDockPosition::Right => DockPosition::Right, + } + } + + fn position_is_valid(&self, _: DockPosition) -> bool { + true + } + + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + settings::update_settings_file::(self.fs.clone(), cx, move |settings| { + let dock = match position { + DockPosition::Left => TerminalDockPosition::Left, + DockPosition::Bottom => TerminalDockPosition::Bottom, + DockPosition::Right => TerminalDockPosition::Right, + }; + settings.dock = Some(dock); + }); + } + + fn size(&self, cx: &WindowContext) -> f32 { + let settings = settings::get::(cx); + match self.position(cx) { + DockPosition::Left | DockPosition::Right => { + self.width.unwrap_or_else(|| settings.default_width) + } + DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height), + } + } + + fn set_size(&mut self, size: f32, cx: &mut ViewContext) { + match self.position(cx) { + DockPosition::Left | DockPosition::Right => self.width = Some(size), + DockPosition::Bottom => self.height = Some(size), + } + self.serialize(cx); + cx.notify(); + } + + fn should_zoom_in_on_event(event: &Event) -> bool { + matches!(event, Event::ZoomIn) + } + + fn should_zoom_out_on_event(event: &Event) -> bool { + matches!(event, Event::ZoomOut) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).is_zoomed() + } + + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx)); + } + + fn set_active(&mut self, active: bool, cx: &mut ViewContext) { + if active && self.pane.read(cx).items_len() == 0 { + self.add_terminal(&Default::default(), cx) + } + } + + fn icon_path(&self) -> &'static str { + "icons/terminal_12.svg" + } + + fn icon_tooltip(&self) -> String { + "Terminals".to_string() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + let count = self.pane.read(cx).items_len(); + if count == 0 { + None + } else { + Some(count.to_string()) + } + } + + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, Event::DockPositionChanged) + } + + fn should_activate_on_event(_: &Self::Event) -> bool { + false + } + + fn should_close_on_event(event: &Event) -> bool { + matches!(event, Event::Close) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.pane.read(cx).has_focus() + } + + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, Event::Focus) + } +} + +#[derive(Serialize, Deserialize)] +struct SerializedTerminalPanel { + items: Vec, + active_item_id: Option, + width: Option, + height: Option, +} diff --git a/crates/terminal_view/src/terminal_view.rs b/crates/terminal_view/src/terminal_view.rs index 0a7a69bf7392aa5dfc28e6175d8a51ae1c0590ee..767e3bf4dbb3847064c7a32d217596840457c080 100644 --- a/crates/terminal_view/src/terminal_view.rs +++ b/crates/terminal_view/src/terminal_view.rs @@ -1,6 +1,6 @@ mod persistence; -pub mod terminal_button; pub mod terminal_element; +pub mod terminal_panel; use crate::{persistence::TERMINAL_DB, terminal_element::TerminalElement}; use context_menu::{ContextMenu, ContextMenuItem}; @@ -63,6 +63,7 @@ actions!( impl_actions!(terminal, [SendText, SendKeystroke]); pub fn init(cx: &mut AppContext) { + terminal_panel::init(cx); terminal::init(cx); cx.add_action(TerminalView::deploy); diff --git a/crates/theme/src/theme.rs b/crates/theme/src/theme.rs index cd2bf90b7eb85fb51068e11c31fa7be6b0ef84ce..b1c9e9c215324bbd6dcdf363d2aeb272ec86f057 100644 --- a/crates/theme/src/theme.rs +++ b/crates/theme/src/theme.rs @@ -82,19 +82,20 @@ pub struct Workspace { pub pane_divider: Border, pub leader_border_opacity: f32, pub leader_border_width: f32, - pub sidebar: Sidebar, + pub dock: Dock, pub status_bar: StatusBar, pub toolbar: Toolbar, pub breadcrumb_height: f32, pub breadcrumbs: Interactive, pub disconnected_overlay: ContainedText, pub modal: ContainerStyle, + pub zoomed_foreground: ContainerStyle, + pub zoomed_background: ContainerStyle, pub notification: ContainerStyle, pub notifications: Notifications, pub joining_project_avatar: ImageStyle, pub joining_project_message: ContainedText, pub external_location_message: ContainedText, - pub dock: Dock, pub drop_target_overlay_color: Color, } @@ -317,15 +318,6 @@ pub struct Toolbar { pub nav_button: Interactive, } -#[derive(Clone, Deserialize, Default)] -pub struct Dock { - pub initial_size_right: f32, - pub initial_size_bottom: f32, - pub wash_color: Color, - pub panel: ContainerStyle, - pub maximized: ContainerStyle, -} - #[derive(Clone, Deserialize, Default)] pub struct Notifications { #[serde(flatten)] @@ -369,17 +361,17 @@ pub struct StatusBar { pub auto_update_progress_message: TextStyle, pub auto_update_done_message: TextStyle, pub lsp_status: Interactive, - pub sidebar_buttons: StatusBarSidebarButtons, + pub panel_buttons: StatusBarPanelButtons, pub diagnostic_summary: Interactive, pub diagnostic_message: Interactive, } #[derive(Deserialize, Default)] -pub struct StatusBarSidebarButtons { +pub struct StatusBarPanelButtons { pub group_left: ContainerStyle, + pub group_bottom: ContainerStyle, pub group_right: ContainerStyle, - pub item: Interactive, - pub badge: ContainerStyle, + pub button: Interactive, } #[derive(Deserialize, Default)] @@ -409,14 +401,14 @@ pub struct StatusBarLspStatus { } #[derive(Deserialize, Default)] -pub struct Sidebar { - pub initial_size: f32, - #[serde(flatten)] - pub container: ContainerStyle, +pub struct Dock { + pub left: ContainerStyle, + pub bottom: ContainerStyle, + pub right: ContainerStyle, } #[derive(Clone, Deserialize, Default)] -pub struct SidebarItem { +pub struct PanelButton { #[serde(flatten)] pub container: ContainerStyle, pub icon_color: Color, diff --git a/crates/welcome/src/welcome.rs b/crates/welcome/src/welcome.rs index 000c2fd8d97fbddadeda4ca7cb884f697a083e0b..b7460c4c461f6a707bf77ee193145770418eeb50 100644 --- a/crates/welcome/src/welcome.rs +++ b/crates/welcome/src/welcome.rs @@ -11,7 +11,7 @@ use gpui::{ use settings::{update_settings_file, SettingsStore}; use std::{borrow::Cow, sync::Arc}; use workspace::{ - item::Item, open_new, sidebar::SidebarSide, AppState, PaneBackdrop, Welcome, Workspace, + dock::DockPosition, item::Item, open_new, AppState, PaneBackdrop, Welcome, Workspace, WorkspaceId, }; @@ -32,7 +32,7 @@ pub fn init(cx: &mut AppContext) { pub fn show_welcome_experience(app_state: &Arc, cx: &mut AppContext) { open_new(&app_state, cx, |workspace, cx| { - workspace.toggle_sidebar(SidebarSide::Left, cx); + workspace.toggle_dock(DockPosition::Left, cx); let welcome_page = cx.add_view(|cx| WelcomePage::new(workspace, cx)); workspace.add_item_to_center(Box::new(welcome_page.clone()), cx); cx.focus(&welcome_page); diff --git a/crates/workspace/src/dock.rs b/crates/workspace/src/dock.rs index beec6a051525b573c78872ee3c3c212047f1cda5..6ca78cd935b0c35aa3303ba683364fd07449c4d1 100644 --- a/crates/workspace/src/dock.rs +++ b/crates/workspace/src/dock.rs @@ -1,826 +1,707 @@ -mod toggle_dock_button; - -use crate::{ - sidebar::SidebarSide, BackgroundActions, DockAnchor, ItemHandle, Pane, Workspace, - WorkspaceSettings, -}; -use collections::HashMap; +use crate::{StatusItemView, Workspace}; +use context_menu::{ContextMenu, ContextMenuItem}; use gpui::{ - actions, - elements::{ChildView, Empty, MouseEventHandler, ParentElement, Side, Stack}, - geometry::vector::Vector2F, - platform::{CursorStyle, MouseButton}, - AnyElement, AppContext, Border, Element, SizeConstraint, ViewContext, ViewHandle, + elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle, + AppContext, Axis, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, + WindowContext, }; -use std::sync::{atomic::AtomicUsize, Arc}; -use theme::Theme; -pub use toggle_dock_button::ToggleDockButton; - -actions!( - dock, - [ - FocusDock, - HideDock, - AnchorDockRight, - AnchorDockBottom, - ExpandDock, - AddTabToDock, - RemoveTabFromDock, - ] -); - -pub fn init(cx: &mut AppContext) { - cx.add_action(Dock::focus_dock); - cx.add_action(Dock::hide_dock); - cx.add_action( - |workspace: &mut Workspace, _: &AnchorDockRight, cx: &mut ViewContext| { - Dock::move_dock(workspace, DockAnchor::Right, true, cx); - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &AnchorDockBottom, cx: &mut ViewContext| { - Dock::move_dock(workspace, DockAnchor::Bottom, true, cx) - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &ExpandDock, cx: &mut ViewContext| { - Dock::move_dock(workspace, DockAnchor::Expanded, true, cx) - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &AddTabToDock, cx: &mut ViewContext| { - if let Some(active_item) = workspace.active_item(cx) { - let item_id = active_item.id(); - - let from = workspace.active_pane(); - let to = workspace.dock_pane(); - if from.id() == to.id() { - return; - } +use serde::Deserialize; +use std::rc::Rc; +use theme::ThemeSettings; + +pub trait Panel: View { + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition) -> bool; + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&mut self, size: f32, cx: &mut ViewContext); + fn icon_path(&self) -> &'static str; + fn icon_tooltip(&self) -> String; + fn icon_label(&self, _: &WindowContext) -> Option { + None + } + fn should_change_position_on_event(_: &Self::Event) -> bool; + fn should_zoom_in_on_event(_: &Self::Event) -> bool; + fn should_zoom_out_on_event(_: &Self::Event) -> bool; + fn is_zoomed(&self, cx: &WindowContext) -> bool; + fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext); + fn set_active(&mut self, active: bool, cx: &mut ViewContext); + fn should_activate_on_event(_: &Self::Event) -> bool; + fn should_close_on_event(_: &Self::Event) -> bool; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn is_focus_event(_: &Self::Event) -> bool; +} - let destination_index = to.read(cx).items_len() + 1; +pub trait PanelHandle { + fn id(&self) -> usize; + fn position(&self, cx: &WindowContext) -> DockPosition; + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool; + fn set_position(&self, position: DockPosition, cx: &mut WindowContext); + fn is_zoomed(&self, cx: &WindowContext) -> bool; + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext); + fn set_active(&self, active: bool, cx: &mut WindowContext); + fn size(&self, cx: &WindowContext) -> f32; + fn set_size(&self, size: f32, cx: &mut WindowContext); + fn icon_path(&self, cx: &WindowContext) -> &'static str; + fn icon_tooltip(&self, cx: &WindowContext) -> String; + fn icon_label(&self, cx: &WindowContext) -> Option; + fn has_focus(&self, cx: &WindowContext) -> bool; + fn as_any(&self) -> &AnyViewHandle; +} - Pane::move_item( - workspace, - from.clone(), - to.clone(), - item_id, - destination_index, - cx, - ); - } - }, - ); - cx.add_action( - |workspace: &mut Workspace, _: &RemoveTabFromDock, cx: &mut ViewContext| { - if let Some(active_item) = workspace.active_item(cx) { - let item_id = active_item.id(); - - let from = workspace.dock_pane(); - let to = workspace - .last_active_center_pane - .as_ref() - .and_then(|pane| pane.upgrade(cx)) - .unwrap_or_else(|| { - workspace - .panes - .first() - .expect("There must be a pane") - .clone() - }); - - if from.id() == to.id() { - return; - } +impl PanelHandle for ViewHandle +where + T: Panel, +{ + fn id(&self) -> usize { + self.id() + } - let destination_index = to.read(cx).items_len() + 1; + fn position(&self, cx: &WindowContext) -> DockPosition { + self.read(cx).position(cx) + } - Pane::move_item( - workspace, - from.clone(), - to.clone(), - item_id, - destination_index, - cx, - ); - } - }, - ); -} + fn position_is_valid(&self, position: DockPosition, cx: &WindowContext) -> bool { + self.read(cx).position_is_valid(position) + } -#[derive(Copy, Clone, PartialEq, Eq, Debug)] -pub enum DockPosition { - Shown(DockAnchor), - Hidden(DockAnchor), -} + fn set_position(&self, position: DockPosition, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_position(position, cx)) + } + + fn size(&self, cx: &WindowContext) -> f32 { + self.read(cx).size(cx) + } + + fn set_size(&self, size: f32, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_size(size, cx)) + } + + fn is_zoomed(&self, cx: &WindowContext) -> bool { + self.read(cx).is_zoomed(cx) + } -impl Default for DockPosition { - fn default() -> Self { - DockPosition::Hidden(Default::default()) + fn set_zoomed(&self, zoomed: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_zoomed(zoomed, cx)) + } + + fn set_active(&self, active: bool, cx: &mut WindowContext) { + self.update(cx, |this, cx| this.set_active(active, cx)) + } + + fn icon_path(&self, cx: &WindowContext) -> &'static str { + self.read(cx).icon_path() + } + + fn icon_tooltip(&self, cx: &WindowContext) -> String { + self.read(cx).icon_tooltip() + } + + fn icon_label(&self, cx: &WindowContext) -> Option { + self.read(cx).icon_label(cx) + } + + fn has_focus(&self, cx: &WindowContext) -> bool { + self.read(cx).has_focus(cx) + } + + fn as_any(&self) -> &AnyViewHandle { + self } } -pub fn icon_for_dock_anchor(anchor: DockAnchor) -> &'static str { - match anchor { - DockAnchor::Right => "icons/dock_right_12.svg", - DockAnchor::Bottom => "icons/dock_bottom_12.svg", - DockAnchor::Expanded => "icons/dock_modal_12.svg", +impl From<&dyn PanelHandle> for AnyViewHandle { + fn from(val: &dyn PanelHandle) -> Self { + val.as_any().clone() } } +pub struct Dock { + position: DockPosition, + panel_entries: Vec, + is_open: bool, + active_panel_index: usize, +} + +#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] +pub enum DockPosition { + Left, + Bottom, + Right, +} + impl DockPosition { - pub fn is_visible(&self) -> bool { + fn to_label(&self) -> &'static str { match self { - DockPosition::Shown(_) => true, - DockPosition::Hidden(_) => false, + Self::Left => "left", + Self::Bottom => "bottom", + Self::Right => "right", } } - pub fn anchor(&self) -> DockAnchor { + fn to_resize_handle_side(self) -> HandleSide { match self { - DockPosition::Shown(anchor) | DockPosition::Hidden(anchor) => *anchor, + Self::Left => HandleSide::Right, + Self::Bottom => HandleSide::Top, + Self::Right => HandleSide::Left, } } - fn hide(self) -> Self { + pub fn axis(&self) -> Axis { match self { - DockPosition::Shown(anchor) => DockPosition::Hidden(anchor), - DockPosition::Hidden(_) => self, + Self::Left | Self::Right => Axis::Horizontal, + Self::Bottom => Axis::Vertical, } } +} - fn show(self) -> Self { - match self { - DockPosition::Hidden(anchor) => DockPosition::Shown(anchor), - DockPosition::Shown(_) => self, - } - } +struct PanelEntry { + panel: Rc, + context_menu: ViewHandle, + _subscriptions: [Subscription; 2], } -pub type DockDefaultItemFactory = - fn(workspace: &mut Workspace, cx: &mut ViewContext) -> Option>; +pub struct PanelButtons { + dock: ViewHandle, + workspace: WeakViewHandle, +} -pub struct Dock { - position: DockPosition, - panel_sizes: HashMap, - pane: ViewHandle, - default_item_factory: DockDefaultItemFactory, +#[derive(Clone, Debug, Deserialize, PartialEq)] +pub struct TogglePanel { + pub dock_position: DockPosition, + pub panel_index: usize, } -impl Dock { - pub fn new( - default_item_factory: DockDefaultItemFactory, - background_actions: BackgroundActions, - pane_history_timestamp: Arc, - cx: &mut ViewContext, - ) -> Self { - let position = - DockPosition::Hidden(settings::get::(cx).default_dock_anchor); - let workspace = cx.weak_handle(); - let pane = cx.add_view(|cx| { - Pane::new( - workspace, - Some(position.anchor()), - background_actions, - pane_history_timestamp, - cx, - ) - }); - pane.update(cx, |pane, cx| { - pane.set_active(false, cx); - }); - cx.subscribe(&pane, Workspace::handle_pane_event).detach(); +impl_actions!(workspace, [TogglePanel]); +impl Dock { + pub fn new(position: DockPosition) -> Self { Self { - pane, - panel_sizes: Default::default(), position, - default_item_factory, + panel_entries: Default::default(), + active_panel_index: 0, + is_open: false, } } - pub fn pane(&self) -> &ViewHandle { - &self.pane + pub fn is_open(&self) -> bool { + self.is_open } - pub fn visible_pane(&self) -> Option<&ViewHandle> { - self.position.is_visible().then(|| self.pane()) + pub fn has_focus(&self, cx: &WindowContext) -> bool { + self.active_panel() + .map_or(false, |panel| panel.has_focus(cx)) } - pub fn is_anchored_at(&self, anchor: DockAnchor) -> bool { - self.position.is_visible() && self.position.anchor() == anchor + pub fn panel_index_for_type(&self) -> Option { + self.panel_entries + .iter() + .position(|entry| entry.panel.as_any().is::()) } - pub(crate) fn set_dock_position( - workspace: &mut Workspace, - new_position: DockPosition, - focus: bool, - cx: &mut ViewContext, - ) { - workspace.dock.position = new_position; - // Tell the pane about the new anchor position - workspace.dock.pane.update(cx, |pane, cx| { - pane.set_docked(Some(new_position.anchor()), cx) - }); + pub fn panel_index_for_ui_name(&self, ui_name: &str, cx: &AppContext) -> Option { + self.panel_entries.iter().position(|entry| { + let panel = entry.panel.as_any(); + cx.view_ui_name(panel.window_id(), panel.id()) == Some(ui_name) + }) + } - if workspace.dock.position.is_visible() { - // Close the right sidebar if the dock is on the right side and the right sidebar is open - if workspace.dock.position.anchor() == DockAnchor::Right { - if workspace.right_sidebar().read(cx).is_open() { - workspace.toggle_sidebar(SidebarSide::Right, cx); - } + pub fn active_panel_index(&self) -> usize { + self.active_panel_index + } + + pub fn set_open(&mut self, open: bool, cx: &mut ViewContext) { + if open != self.is_open { + self.is_open = open; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(open, cx); } - // Ensure that the pane has at least one item or construct a default item to put in it - let pane = workspace.dock.pane.clone(); - if pane.read(cx).items().next().is_none() { - if let Some(item_to_add) = (workspace.dock.default_item_factory)(workspace, cx) { - Pane::add_item(workspace, &pane, item_to_add, focus, focus, None, cx); - } else { - workspace.dock.position = workspace.dock.position.hide(); - } - } else { - if focus { - cx.focus(&pane); + cx.notify(); + } + } + + pub fn toggle_open(&mut self, cx: &mut ViewContext) { + self.set_open(!self.is_open, cx); + cx.notify(); + } + + pub fn set_panel_zoomed( + &mut self, + panel: &AnyViewHandle, + zoomed: bool, + cx: &mut ViewContext, + ) { + for entry in &mut self.panel_entries { + if entry.panel.as_any() == panel { + if zoomed != entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(zoomed, cx); } - } - } else if let Some(last_active_center_pane) = workspace - .last_active_center_pane - .as_ref() - .and_then(|pane| pane.upgrade(cx)) - { - if focus { - cx.focus(&last_active_center_pane); + } else if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); } } - cx.emit(crate::Event::DockAnchorChanged); - workspace.serialize_workspace(cx); + cx.notify(); } - pub fn hide(workspace: &mut Workspace, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); + pub fn zoom_out(&mut self, cx: &mut ViewContext) { + for entry in &mut self.panel_entries { + if entry.panel.is_zoomed(cx) { + entry.panel.set_zoomed(false, cx); + } + } } - pub fn show(workspace: &mut Workspace, focus: bool, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), focus, cx); + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + let subscriptions = [ + cx.observe(&panel, |_, _, cx| cx.notify()), + cx.subscribe(&panel, |this, panel, event, cx| { + if T::should_activate_on_event(event) { + if let Some(ix) = this + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) + { + this.set_open(true, cx); + this.activate_panel(ix, cx); + cx.focus(&panel); + } + } else if T::should_close_on_event(event) + && this.active_panel().map_or(false, |p| p.id() == panel.id()) + { + this.set_open(false, cx); + } + }), + ]; + + let dock_view_id = cx.view_id(); + self.panel_entries.push(PanelEntry { + panel: Rc::new(panel), + context_menu: cx.add_view(|cx| { + let mut menu = ContextMenu::new(dock_view_id, cx); + menu.set_position_mode(OverlayPositionMode::Local); + menu + }), + _subscriptions: subscriptions, + }); + cx.notify() } - pub fn hide_on_sidebar_shown( - workspace: &mut Workspace, - sidebar_side: SidebarSide, - cx: &mut ViewContext, - ) { - if (sidebar_side == SidebarSide::Right && workspace.dock.is_anchored_at(DockAnchor::Right)) - || workspace.dock.is_anchored_at(DockAnchor::Expanded) + pub fn remove_panel(&mut self, panel: &ViewHandle, cx: &mut ViewContext) { + if let Some(panel_ix) = self + .panel_entries + .iter() + .position(|entry| entry.panel.id() == panel.id()) { - Self::hide(workspace, cx); + if panel_ix == self.active_panel_index { + self.active_panel_index = 0; + self.set_open(false, cx); + } else if panel_ix < self.active_panel_index { + self.active_panel_index -= 1; + } + self.panel_entries.remove(panel_ix); + cx.notify(); } } - pub fn focus_dock(workspace: &mut Workspace, _: &FocusDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.show(), true, cx); + pub fn panels_len(&self) -> usize { + self.panel_entries.len() } - pub fn hide_dock(workspace: &mut Workspace, _: &HideDock, cx: &mut ViewContext) { - Self::set_dock_position(workspace, workspace.dock.position.hide(), true, cx); + pub fn activate_panel(&mut self, panel_ix: usize, cx: &mut ViewContext) { + if panel_ix != self.active_panel_index { + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(false, cx); + } + + self.active_panel_index = panel_ix; + if let Some(active_panel) = self.panel_entries.get(self.active_panel_index) { + active_panel.panel.set_active(true, cx); + } + + cx.notify(); + } } - pub fn move_dock( - workspace: &mut Workspace, - new_anchor: DockAnchor, - focus: bool, - cx: &mut ViewContext, - ) { - Self::set_dock_position(workspace, DockPosition::Shown(new_anchor), focus, cx); - } - - pub fn render( - &self, - theme: &Theme, - anchor: DockAnchor, - cx: &mut ViewContext, - ) -> Option> { - let style = &theme.workspace.dock; - - self.position - .is_visible() - .then(|| self.position.anchor()) - .filter(|current_anchor| *current_anchor == anchor) - .map(|anchor| match anchor { - DockAnchor::Bottom | DockAnchor::Right => { - let mut panel_style = style.panel.clone(); - let (resize_side, initial_size) = if anchor == DockAnchor::Bottom { - panel_style.border = Border { - top: true, - bottom: false, - left: false, - right: false, - ..panel_style.border - }; + pub fn active_panel(&self) -> Option<&Rc> { + let entry = self.active_entry()?; + Some(&entry.panel) + } - (Side::Top, style.initial_size_bottom) - } else { - panel_style.border = Border { - top: false, - bottom: false, - left: true, - right: false, - ..panel_style.border - }; - (Side::Left, style.initial_size_right) - }; - - enum DockResizeHandle {} - - let resizable = ChildView::new(&self.pane, cx) - .contained() - .with_style(panel_style) - .with_resize_handle::( - resize_side as usize, - resize_side, - 4., - self.panel_sizes - .get(&anchor) - .copied() - .unwrap_or(initial_size), - cx, - ); - - let size = resizable.current_size(); - cx.defer(move |workspace, _| { - workspace.dock.panel_sizes.insert(anchor, size); - }); - - if anchor == DockAnchor::Right { - resizable.constrained().dynamically(|constraint, _, cx| { - SizeConstraint::new( - Vector2F::new(20., constraint.min.y()), - Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()), - ) - }) - } else { - resizable.constrained().dynamically(|constraint, _, cx| { - SizeConstraint::new( - Vector2F::new(constraint.min.x(), 50.), - Vector2F::new(constraint.max.x(), cx.window_size().y() * 0.8), - ) - }) - } - .into_any() - } - DockAnchor::Expanded => { - enum ExpandedDockWash {} - enum ExpandedDockPane {} - Stack::new() - .with_child( - // Render wash under the dock which when clicked hides it - MouseEventHandler::::new(0, cx, |_, _| { - Empty::new() - .contained() - .with_background_color(style.wash_color) - }) - .capture_all() - .on_down(MouseButton::Left, |_, workspace, cx| { - Dock::hide_dock(workspace, &Default::default(), cx) - }) - .with_cursor_style(CursorStyle::Arrow), - ) - .with_child( - MouseEventHandler::::new(0, cx, |_state, cx| { - ChildView::new(&self.pane, cx) - }) - // Make sure all events directly under the dock pane - // are captured - .capture_all() - .contained() - .with_style(style.maximized), - ) - .into_any() - } - }) + fn active_entry(&self) -> Option<&PanelEntry> { + if self.is_open { + self.panel_entries.get(self.active_panel_index) + } else { + None + } } - pub fn position(&self) -> DockPosition { - self.position + pub fn zoomed_panel(&self, cx: &WindowContext) -> Option> { + let entry = self.active_entry()?; + if entry.panel.is_zoomed(cx) { + Some(entry.panel.clone()) + } else { + None + } + } + + pub fn panel_size(&self, panel: &dyn PanelHandle, cx: &WindowContext) -> Option { + self.panel_entries + .iter() + .find(|entry| entry.panel.id() == panel.id()) + .map(|entry| entry.panel.size(cx)) + } + + pub fn active_panel_size(&self, cx: &WindowContext) -> Option { + if self.is_open { + self.panel_entries + .get(self.active_panel_index) + .map(|entry| entry.panel.size(cx)) + } else { + None + } + } + + pub fn resize_active_panel(&mut self, size: f32, cx: &mut ViewContext) { + if let Some(entry) = self.panel_entries.get_mut(self.active_panel_index) { + entry.panel.set_size(size, cx); + cx.notify(); + } + } + + pub fn render_placeholder(&self, cx: &WindowContext) -> AnyElement { + if let Some(active_entry) = self.active_entry() { + Empty::new() + .into_any() + .contained() + .with_style(self.style(cx)) + .resizable( + self.position.to_resize_handle_side(), + active_entry.panel.size(cx), + |_, _, _| {}, + ) + .into_any() + } else { + Empty::new().into_any() + } + } + + fn style(&self, cx: &WindowContext) -> ContainerStyle { + let theme = &settings::get::(cx).theme; + let style = match self.position { + DockPosition::Left => theme.workspace.dock.left, + DockPosition::Bottom => theme.workspace.dock.bottom, + DockPosition::Right => theme.workspace.dock.right, + }; + style } } -#[cfg(test)] -mod tests { - use std::{ - ops::{Deref, DerefMut}, - path::PathBuf, - sync::Arc, - }; +impl Entity for Dock { + type Event = (); +} - use gpui::{AppContext, BorrowWindowContext, TestAppContext, ViewContext, WindowContext}; - use project::{FakeFs, Project}; +impl View for Dock { + fn ui_name() -> &'static str { + "Dock" + } - use super::*; - use crate::{ - dock, - item::{self, test::TestItem}, - persistence::model::{ - SerializedItem, SerializedPane, SerializedPaneGroup, SerializedWorkspace, - }, - register_deserializable_item, - sidebar::Sidebar, - tests::init_test, - AppState, ItemHandle, Workspace, - }; - - pub fn default_item_factory( - _workspace: &mut Workspace, - cx: &mut ViewContext, - ) -> Option> { - Some(Box::new(cx.add_view(|_| TestItem::new()))) - } - - #[gpui::test] - async fn test_dock_workspace_infinite_loop(cx: &mut TestAppContext) { - init_test(cx); - - cx.update(|cx| { - register_deserializable_item::(cx); - }); + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + if let Some(active_entry) = self.active_entry() { + let style = self.style(cx); + ChildView::new(active_entry.panel.as_any(), cx) + .contained() + .with_style(style) + .resizable( + self.position.to_resize_handle_side(), + active_entry.panel.size(cx), + |dock: &mut Self, size, cx| dock.resize_active_panel(size, cx), + ) + .into_any() + } else { + Empty::new().into_any() + } + } +} - let serialized_workspace = SerializedWorkspace { - id: 0, - location: Vec::::new().into(), - dock_position: dock::DockPosition::Shown(DockAnchor::Expanded), - center_group: SerializedPaneGroup::Pane(SerializedPane { - active: false, - children: vec![], - }), - dock_pane: SerializedPane { - active: true, - children: vec![SerializedItem { - active: true, - item_id: 0, - kind: "TestItem".into(), - }], - }, - left_sidebar_open: false, - bounds: Default::default(), - display: Default::default(), +impl PanelButtons { + pub fn new( + dock: ViewHandle, + workspace: WeakViewHandle, + cx: &mut ViewContext, + ) -> Self { + cx.observe(&dock, |_, _, cx| cx.notify()).detach(); + Self { dock, workspace } + } +} + +impl Entity for PanelButtons { + type Event = (); +} + +impl View for PanelButtons { + fn ui_name() -> &'static str { + "PanelButtons" + } + + fn render(&mut self, cx: &mut ViewContext) -> AnyElement { + let theme = &settings::get::(cx).theme; + let tooltip_style = theme.tooltip.clone(); + let theme = &theme.workspace.status_bar.panel_buttons; + let button_style = theme.button.clone(); + let dock = self.dock.read(cx); + let active_ix = dock.active_panel_index; + let is_open = dock.is_open; + let dock_position = dock.position; + let group_style = match dock_position { + DockPosition::Left => theme.group_left, + DockPosition::Bottom => theme.group_bottom, + DockPosition::Right => theme.group_right, + }; + let menu_corner = match dock_position { + DockPosition::Left => AnchorCorner::BottomLeft, + DockPosition::Bottom | DockPosition::Right => AnchorCorner::BottomRight, }; - let fs = FakeFs::new(cx.background()); - let project = Project::test(fs, [], cx).await; - - let (_, _workspace) = cx.add_window(|cx| { - Workspace::new( - 0, - project.clone(), - Arc::new(AppState { - languages: project.read(cx).languages().clone(), - client: project.read(cx).client(), - user_store: project.read(cx).user_store(), - fs: project.read(cx).fs().clone(), - build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _| {}, - dock_default_item_factory: default_item_factory, - background_actions: || &[], - }), - cx, - ) - }); + let panels = dock + .panel_entries + .iter() + .map(|item| (item.panel.clone(), item.context_menu.clone())) + .collect::>(); + Flex::row() + .with_children( + panels + .into_iter() + .enumerate() + .map(|(ix, (view, context_menu))| { + let action = TogglePanel { + dock_position, + panel_index: ix, + }; - cx.update(|cx| { - Workspace::load_workspace(_workspace.downgrade(), serialized_workspace, Vec::new(), cx) - }) - .await; - - cx.foreground().run_until_parked(); - //Should terminate - } - - #[gpui::test] - async fn test_dock_hides_when_pane_empty(cx: &mut TestAppContext) { - let mut cx = DockTestContext::new(cx).await; - - // Closing the last item in the dock hides the dock - cx.move_dock(DockAnchor::Right); - let old_items = cx.dock_items(); - assert!(!old_items.is_empty()); - cx.close_dock_items().await; - cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right)); - - // Reopening the dock adds a new item - cx.move_dock(DockAnchor::Right); - let new_items = cx.dock_items(); - assert!(!new_items.is_empty()); - assert!(new_items - .into_iter() - .all(|new_item| !old_items.contains(&new_item))); - } - - #[gpui::test] - async fn test_dock_panel_collisions(cx: &mut TestAppContext) { - let mut cx = DockTestContext::new(cx).await; - - // Dock closes when expanded for either panel - cx.move_dock(DockAnchor::Expanded); - cx.open_sidebar(SidebarSide::Left); - cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded)); - cx.close_sidebar(SidebarSide::Left); - cx.move_dock(DockAnchor::Expanded); - cx.open_sidebar(SidebarSide::Right); - cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded)); - - // Dock closes in the right position if the right sidebar is opened - cx.move_dock(DockAnchor::Right); - cx.open_sidebar(SidebarSide::Left); - cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right)); - cx.open_sidebar(SidebarSide::Right); - cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Right)); - cx.close_sidebar(SidebarSide::Right); - - // Dock in bottom position ignores sidebars - cx.move_dock(DockAnchor::Bottom); - cx.open_sidebar(SidebarSide::Left); - cx.open_sidebar(SidebarSide::Right); - cx.assert_dock_position(DockPosition::Shown(DockAnchor::Bottom)); - - // Opening the dock in the right position closes the right sidebar - cx.move_dock(DockAnchor::Right); - cx.assert_sidebar_closed(SidebarSide::Right); - } - - #[gpui::test] - async fn test_focusing_panes_shows_and_hides_dock(cx: &mut TestAppContext) { - let mut cx = DockTestContext::new(cx).await; - - // Focusing an item not in the dock when expanded hides the dock - let center_item = cx.add_item_to_center_pane(); - cx.move_dock(DockAnchor::Expanded); - let dock_item = cx - .dock_items() - .get(0) - .cloned() - .expect("Dock should have an item at this point"); - center_item.update(&mut cx, |_, cx| cx.focus_self()); - cx.assert_dock_position(DockPosition::Hidden(DockAnchor::Expanded)); - - // Focusing an item not in the dock when not expanded, leaves the dock open but inactive - cx.move_dock(DockAnchor::Right); - center_item.update(&mut cx, |_, cx| cx.focus_self()); - cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right)); - cx.assert_dock_pane_inactive(); - cx.assert_workspace_pane_active(); - - // Focusing an item in the dock activates it's pane - dock_item.update(&mut cx, |_, cx| cx.focus_self()); - cx.assert_dock_position(DockPosition::Shown(DockAnchor::Right)); - cx.assert_dock_pane_active(); - cx.assert_workspace_pane_inactive(); - } - - #[gpui::test] - async fn test_toggle_dock_focus(cx: &mut TestAppContext) { - let mut cx = DockTestContext::new(cx).await; - - cx.move_dock(DockAnchor::Right); - cx.assert_dock_pane_active(); - cx.hide_dock(); - cx.move_dock(DockAnchor::Right); - cx.assert_dock_pane_active(); - } - - #[gpui::test] - async fn test_activate_next_and_prev_pane(cx: &mut TestAppContext) { - let mut cx = DockTestContext::new(cx).await; - - cx.move_dock(DockAnchor::Right); - cx.assert_dock_pane_active(); - - cx.update_workspace(|workspace, cx| workspace.activate_next_pane(cx)); - cx.assert_dock_pane_active(); - - cx.update_workspace(|workspace, cx| workspace.activate_previous_pane(cx)); - cx.assert_dock_pane_active(); - } - - struct DockTestContext<'a> { - pub cx: &'a mut TestAppContext, - pub window_id: usize, - pub workspace: ViewHandle, - } - - impl<'a> DockTestContext<'a> { - pub async fn new(cx: &'a mut TestAppContext) -> DockTestContext<'a> { - init_test(cx); - let fs = FakeFs::new(cx.background()); - - cx.update(|cx| init(cx)); - let project = Project::test(fs, [], cx).await; - let (window_id, workspace) = cx.add_window(|cx| { - Workspace::new( - 0, - project.clone(), - Arc::new(AppState { - languages: project.read(cx).languages().clone(), - client: project.read(cx).client(), - user_store: project.read(cx).user_store(), - fs: project.read(cx).fs().clone(), - build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _| {}, - dock_default_item_factory: default_item_factory, - background_actions: || &[], + Stack::new() + .with_child( + MouseEventHandler::::new(ix, cx, |state, cx| { + let is_active = is_open && ix == active_ix; + let style = button_style.style_for(state, is_active); + Flex::row() + .with_child( + Svg::new(view.icon_path(cx)) + .with_color(style.icon_color) + .constrained() + .with_width(style.icon_size) + .aligned(), + ) + .with_children(if let Some(label) = view.icon_label(cx) { + Some( + Label::new(label, style.label.text.clone()) + .contained() + .with_style(style.label.container) + .aligned(), + ) + } else { + None + }) + .constrained() + .with_height(style.icon_size) + .contained() + .with_style(style.container) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, { + let action = action.clone(); + move |_, this, cx| { + if let Some(workspace) = this.workspace.upgrade(cx) { + let action = action.clone(); + cx.window_context().defer(move |cx| { + workspace.update(cx, |workspace, cx| { + workspace.toggle_panel(&action, cx) + }); + }); + } + } + }) + .on_click(MouseButton::Right, { + let view = view.clone(); + let menu = context_menu.clone(); + move |_, _, cx| { + const POSITIONS: [DockPosition; 3] = [ + DockPosition::Left, + DockPosition::Right, + DockPosition::Bottom, + ]; + + menu.update(cx, |menu, cx| { + let items = POSITIONS + .into_iter() + .filter(|position| { + *position != dock_position + && view.position_is_valid(*position, cx) + }) + .map(|position| { + let view = view.clone(); + ContextMenuItem::handler( + format!("Dock {}", position.to_label()), + move |cx| view.set_position(position, cx), + ) + }) + .collect(); + menu.show(Default::default(), menu_corner, items, cx); + }) + } + }) + .with_tooltip::( + ix, + view.icon_tooltip(cx), + Some(Box::new(action)), + tooltip_style.clone(), + cx, + ), + ) + .with_child(ChildView::new(&context_menu, cx)) }), - cx, - ) - }); - - workspace.update(cx, |workspace, cx| { - let left_panel = cx.add_view(|_| TestItem::new()); - workspace.left_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/folder_tree_16.svg", - "Left Test Panel".to_string(), - left_panel.clone(), - cx, - ); - }); - - let right_panel = cx.add_view(|_| TestItem::new()); - workspace.right_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/folder_tree_16.svg", - "Right Test Panel".to_string(), - right_panel.clone(), - cx, - ); - }); - }); + ) + .contained() + .with_style(group_style) + .into_any() + } +} + +impl StatusItemView for PanelButtons { + fn set_active_pane_item( + &mut self, + _: Option<&dyn crate::ItemHandle>, + _: &mut ViewContext, + ) { + } +} + +#[cfg(test)] +pub(crate) mod test { + use super::*; + use gpui::{ViewContext, WindowContext}; + + pub enum TestPanelEvent { + PositionChanged, + Activated, + Closed, + ZoomIn, + ZoomOut, + Focus, + } + + pub struct TestPanel { + pub position: DockPosition, + pub zoomed: bool, + pub active: bool, + pub has_focus: bool, + pub size: f32, + } + impl TestPanel { + pub fn new(position: DockPosition) -> Self { Self { - cx, - window_id, - workspace, + position, + zoomed: false, + active: false, + has_focus: false, + size: 300., } } + } - pub fn workspace(&self, read: F) -> T - where - F: FnOnce(&Workspace, &ViewContext) -> T, - { - self.workspace.read_with(self.cx, read) + impl Entity for TestPanel { + type Event = TestPanelEvent; + } + + impl View for TestPanel { + fn ui_name() -> &'static str { + "TestPanel" } - pub fn update_workspace(&mut self, update: F) -> T - where - F: FnOnce(&mut Workspace, &mut ViewContext) -> T, - { - self.workspace.update(self.cx, update) + fn render(&mut self, _: &mut ViewContext<'_, '_, Self>) -> AnyElement { + Empty::new().into_any() } - pub fn sidebar(&self, sidebar_side: SidebarSide, read: F) -> T - where - F: FnOnce(&Sidebar, &AppContext) -> T, - { - self.workspace(|workspace, cx| { - let sidebar = match sidebar_side { - SidebarSide::Left => workspace.left_sidebar(), - SidebarSide::Right => workspace.right_sidebar(), - } - .read(cx); - - read(sidebar, cx) - }) - } - - pub fn center_pane_handle(&self) -> ViewHandle { - self.workspace(|workspace, cx| { - workspace - .last_active_center_pane - .clone() - .and_then(|pane| pane.upgrade(cx)) - .unwrap_or_else(|| workspace.center.panes()[0].clone()) - }) - } - - pub fn add_item_to_center_pane(&mut self) -> ViewHandle { - self.update_workspace(|workspace, cx| { - let item = cx.add_view(|_| TestItem::new()); - let pane = workspace - .last_active_center_pane - .clone() - .and_then(|pane| pane.upgrade(cx)) - .unwrap_or_else(|| workspace.center.panes()[0].clone()); - Pane::add_item( - workspace, - &pane, - Box::new(item.clone()), - true, - true, - None, - cx, - ); - item - }) - } - - pub fn dock_pane(&self, read: F) -> T - where - F: FnOnce(&Pane, &AppContext) -> T, - { - self.workspace(|workspace, cx| { - let dock_pane = workspace.dock_pane().read(cx); - read(dock_pane, cx) - }) + fn focus_in(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { + self.has_focus = true; + cx.emit(TestPanelEvent::Focus); } - pub fn move_dock(&mut self, anchor: DockAnchor) { - self.update_workspace(|workspace, cx| Dock::move_dock(workspace, anchor, true, cx)); + fn focus_out(&mut self, _: AnyViewHandle, _: &mut ViewContext) { + self.has_focus = false; } + } - pub fn hide_dock(&mut self) { - self.cx.dispatch_action(self.window_id, HideDock); + impl Panel for TestPanel { + fn position(&self, _: &gpui::WindowContext) -> super::DockPosition { + self.position } - pub fn open_sidebar(&mut self, sidebar_side: SidebarSide) { - if !self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) { - self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx)); - } + fn position_is_valid(&self, _: super::DockPosition) -> bool { + true } - pub fn close_sidebar(&mut self, sidebar_side: SidebarSide) { - if self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open()) { - self.update_workspace(|workspace, cx| workspace.toggle_sidebar(sidebar_side, cx)); - } + fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext) { + self.position = position; + cx.emit(TestPanelEvent::PositionChanged); } - pub fn dock_items(&self) -> Vec> { - self.dock_pane(|pane, cx| { - pane.items() - .map(|item| { - item.act_as::(cx) - .expect("Dock Test Context uses TestItems in the dock") - }) - .collect() - }) + fn is_zoomed(&self, _: &WindowContext) -> bool { + self.zoomed } - pub async fn close_dock_items(&mut self) { - self.update_workspace(|workspace, cx| { - Pane::close_items(workspace, workspace.dock_pane().clone(), cx, |_| true) - }) - .await - .expect("Could not close dock items") + fn set_zoomed(&mut self, zoomed: bool, _cx: &mut ViewContext) { + self.zoomed = zoomed; } - pub fn assert_dock_position(&self, expected_position: DockPosition) { - self.workspace(|workspace, _| assert_eq!(workspace.dock.position, expected_position)); + fn set_active(&mut self, active: bool, _cx: &mut ViewContext) { + self.active = active; } - pub fn assert_sidebar_closed(&self, sidebar_side: SidebarSide) { - assert!(!self.sidebar(sidebar_side, |sidebar, _| sidebar.is_open())); + fn size(&self, _: &WindowContext) -> f32 { + self.size } - pub fn assert_workspace_pane_active(&self) { - assert!(self - .center_pane_handle() - .read_with(self.cx, |pane, _| pane.is_active())); + fn set_size(&mut self, size: f32, _: &mut ViewContext) { + self.size = size; } - pub fn assert_workspace_pane_inactive(&self) { - assert!(!self - .center_pane_handle() - .read_with(self.cx, |pane, _| pane.is_active())); + fn icon_path(&self) -> &'static str { + "icons/test_panel.svg" } - pub fn assert_dock_pane_active(&self) { - assert!(self.dock_pane(|pane, _| pane.is_active())) + fn icon_tooltip(&self) -> String { + "Test Panel".into() } - pub fn assert_dock_pane_inactive(&self) { - assert!(!self.dock_pane(|pane, _| pane.is_active())) + fn should_change_position_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::PositionChanged) } - } - impl<'a> Deref for DockTestContext<'a> { - type Target = gpui::TestAppContext; + fn should_zoom_in_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomIn) + } - fn deref(&self) -> &Self::Target { - self.cx + fn should_zoom_out_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::ZoomOut) } - } - impl<'a> DerefMut for DockTestContext<'a> { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.cx + fn should_activate_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Activated) + } + + fn should_close_on_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Closed) } - } - impl BorrowWindowContext for DockTestContext<'_> { - fn read_with T>(&self, window_id: usize, f: F) -> T { - BorrowWindowContext::read_with(self.cx, window_id, f) + fn has_focus(&self, _cx: &WindowContext) -> bool { + self.has_focus } - fn update T>(&mut self, window_id: usize, f: F) -> T { - BorrowWindowContext::update(self.cx, window_id, f) + fn is_focus_event(event: &Self::Event) -> bool { + matches!(event, TestPanelEvent::Focus) } } } diff --git a/crates/workspace/src/dock/toggle_dock_button.rs b/crates/workspace/src/dock/toggle_dock_button.rs deleted file mode 100644 index 9ab7a8996bc18868c1f1f0f06847aebc06183f56..0000000000000000000000000000000000000000 --- a/crates/workspace/src/dock/toggle_dock_button.rs +++ /dev/null @@ -1,125 +0,0 @@ -use super::{icon_for_dock_anchor, Dock, FocusDock, HideDock}; -use crate::{handle_dropped_item, StatusItemView, Workspace}; -use gpui::{ - elements::{Empty, MouseEventHandler, Svg}, - platform::CursorStyle, - platform::MouseButton, - AnyElement, Element, Entity, View, ViewContext, ViewHandle, WeakViewHandle, -}; - -pub struct ToggleDockButton { - workspace: WeakViewHandle, -} - -impl ToggleDockButton { - pub fn new(workspace: ViewHandle, cx: &mut ViewContext) -> Self { - // When dock moves, redraw so that the icon and toggle status matches. - cx.subscribe(&workspace, |_, _, _, cx| cx.notify()).detach(); - - Self { - workspace: workspace.downgrade(), - } - } -} - -impl Entity for ToggleDockButton { - type Event = (); -} - -impl View for ToggleDockButton { - fn ui_name() -> &'static str { - "Dock Toggle" - } - - fn render(&mut self, cx: &mut gpui::ViewContext) -> AnyElement { - let workspace = self.workspace.upgrade(cx); - - if workspace.is_none() { - return Empty::new().into_any(); - } - - let workspace = workspace.unwrap(); - let dock_position = workspace.read(cx).dock.position; - let dock_pane = workspace.read(cx).dock_pane().clone(); - - let theme = theme::current(cx).clone(); - - let button = MouseEventHandler::::new(0, cx, { - let theme = theme.clone(); - move |state, _| { - let style = theme - .workspace - .status_bar - .sidebar_buttons - .item - .style_for(state, dock_position.is_visible()); - - Svg::new(icon_for_dock_anchor(dock_position.anchor())) - .with_color(style.icon_color) - .constrained() - .with_width(style.icon_size) - .with_height(style.icon_size) - .contained() - .with_style(style.container) - } - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_up(MouseButton::Left, move |event, this, cx| { - let drop_index = dock_pane.read(cx).items_len() + 1; - handle_dropped_item( - event, - this.workspace.clone(), - &dock_pane.downgrade(), - drop_index, - false, - None, - cx, - ); - }); - - if dock_position.is_visible() { - button - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - Dock::hide_dock(workspace, &Default::default(), cx) - }) - } - }) - .with_tooltip::( - 0, - "Hide Dock".into(), - Some(Box::new(HideDock)), - theme.tooltip.clone(), - cx, - ) - } else { - button - .on_click(MouseButton::Left, |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - Dock::focus_dock(workspace, &Default::default(), cx) - }) - } - }) - .with_tooltip::( - 0, - "Focus Dock".into(), - Some(Box::new(FocusDock)), - theme.tooltip.clone(), - cx, - ) - } - .into_any() - } -} - -impl StatusItemView for ToggleDockButton { - fn set_active_pane_item( - &mut self, - _active_pane_item: Option<&dyn crate::ItemHandle>, - _cx: &mut ViewContext, - ) { - //Not applicable - } -} diff --git a/crates/workspace/src/item.rs b/crates/workspace/src/item.rs index 16905849a9d5e923f3574b476cb949c4bc0312e1..c9470780153608e25bf879f94109e61626673cc4 100644 --- a/crates/workspace/src/item.rs +++ b/crates/workspace/src/item.rs @@ -437,7 +437,7 @@ impl ItemHandle for ViewHandle { for item_event in T::to_item_events(event).into_iter() { match item_event { ItemEvent::CloseItem => { - Pane::close_item_by_id(workspace, pane, item.id(), cx) + pane.update(cx, |pane, cx| pane.close_item_by_id(item.id(), cx)) .detach_and_log_err(cx); return; } @@ -769,7 +769,7 @@ impl FollowableItemHandle for ViewHandle { #[cfg(test)] pub(crate) mod test { use super::{Item, ItemEvent}; - use crate::{sidebar::SidebarItem, ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; + use crate::{ItemId, ItemNavHistory, Pane, Workspace, WorkspaceId}; use gpui::{ elements::Empty, AnyElement, AppContext, Element, Entity, ModelHandle, Task, View, ViewContext, ViewHandle, WeakViewHandle, @@ -1062,6 +1062,4 @@ pub(crate) mod test { Task::Ready(Some(anyhow::Ok(view))) } } - - impl SidebarItem for TestItem {} } diff --git a/crates/workspace/src/pane.rs b/crates/workspace/src/pane.rs index a196133f3ad8ce210d9fb1f31621c6b8d80693d6..e1079f201b676286590d8b272cc4658d9c4777df 100644 --- a/crates/workspace/src/pane.rs +++ b/crates/workspace/src/pane.rs @@ -2,17 +2,14 @@ mod dragged_item_receiver; use super::{ItemHandle, SplitDirection}; use crate::{ - dock::{icon_for_dock_anchor, AnchorDockBottom, AnchorDockRight, Dock, ExpandDock}, - item::WeakItemHandle, - toolbar::Toolbar, - AutosaveSetting, DockAnchor, Item, NewFile, NewSearch, NewTerminal, Workspace, - WorkspaceSettings, + item::WeakItemHandle, toolbar::Toolbar, AutosaveSetting, Item, NewFile, NewSearch, NewTerminal, + ToggleZoom, Workspace, WorkspaceSettings, }; use anyhow::{anyhow, Result}; use collections::{HashMap, HashSet, VecDeque}; use context_menu::{ContextMenu, ContextMenuItem}; -use drag_and_drop::Draggable; -pub use dragged_item_receiver::{dragged_item_receiver, handle_dropped_item}; +use drag_and_drop::{DragAndDrop, Draggable}; +use dragged_item_receiver::dragged_item_receiver; use futures::StreamExt; use gpui::{ actions, @@ -41,7 +38,7 @@ use std::{ Arc, }, }; -use theme::Theme; +use theme::{Theme, ThemeSettings}; use util::ResultExt; #[derive(Clone, Deserialize, PartialEq)] @@ -104,6 +101,7 @@ const MAX_NAVIGATION_HISTORY_LEN: usize = 1024; pub type BackgroundActions = fn() -> &'static [(&'static str, &'static dyn Action)]; pub fn init(cx: &mut AppContext) { + cx.add_action(Pane::toggle_zoom); cx.add_action(|pane: &mut Pane, action: &ActivateItem, cx| { pane.activate_item(action.0, true, true, cx); }); @@ -145,12 +143,15 @@ pub enum Event { Split(SplitDirection), ChangeItemTitle, Focus, + ZoomIn, + ZoomOut, } pub struct Pane { items: Vec>, activation_history: Vec, is_active: bool, + zoomed: bool, active_item_index: usize, last_focused_view_by_item: HashMap, autoscroll: bool, @@ -158,10 +159,12 @@ pub struct Pane { toolbar: ViewHandle, tab_bar_context_menu: TabBarContextMenu, tab_context_menu: ViewHandle, - docked: Option, _background_actions: BackgroundActions, workspace: WeakViewHandle, has_focus: bool, + can_drop: Rc, &WindowContext) -> bool>, + can_split: bool, + render_tab_bar_buttons: Rc) -> AnyElement>, } pub struct ItemNavHistory { @@ -203,9 +206,9 @@ pub struct NavigationEntry { pub timestamp: usize, } -struct DraggedItem { - item: Box, - pane: WeakViewHandle, +pub struct DraggedItem { + pub handle: Box, + pub pane: WeakViewHandle, } pub enum ReorderBehavior { @@ -218,7 +221,6 @@ pub enum ReorderBehavior { enum TabBarContextMenuKind { New, Split, - Dock, } struct TabBarContextMenu { @@ -238,7 +240,6 @@ impl TabBarContextMenu { impl Pane { pub fn new( workspace: WeakViewHandle, - docked: Option, background_actions: BackgroundActions, next_timestamp: Arc, cx: &mut ViewContext, @@ -254,6 +255,7 @@ impl Pane { items: Vec::new(), activation_history: Vec::new(), is_active: true, + zoomed: false, active_item_index: 0, last_focused_view_by_item: Default::default(), autoscroll: false, @@ -272,10 +274,32 @@ impl Pane { handle: context_menu, }, tab_context_menu: cx.add_view(|cx| ContextMenu::new(pane_view_id, cx)), - docked, _background_actions: background_actions, workspace, has_focus: false, + can_drop: Rc::new(|_, _| true), + can_split: true, + render_tab_bar_buttons: Rc::new(|pane, cx| { + Flex::row() + // New menu + .with_child(Self::render_tab_bar_button( + 0, + "icons/plus_12.svg", + cx, + |pane, cx| pane.deploy_new_menu(cx), + pane.tab_bar_context_menu + .handle_if_kind(TabBarContextMenuKind::New), + )) + .with_child(Self::render_tab_bar_button( + 2, + "icons/split_12.svg", + cx, + |pane, cx| pane.deploy_split_menu(cx), + pane.tab_bar_context_menu + .handle_if_kind(TabBarContextMenuKind::Split), + )) + .into_any() + }), } } @@ -296,8 +320,23 @@ impl Pane { self.has_focus } - pub fn set_docked(&mut self, docked: Option, cx: &mut ViewContext) { - self.docked = docked; + pub fn on_can_drop(&mut self, can_drop: F) + where + F: 'static + Fn(&DragAndDrop, &WindowContext) -> bool, + { + self.can_drop = Rc::new(can_drop); + } + + pub fn set_can_split(&mut self, can_split: bool, cx: &mut ViewContext) { + self.can_split = can_split; + cx.notify(); + } + + pub fn set_render_tab_bar_buttons(&mut self, cx: &mut ViewContext, render: F) + where + F: 'static + Fn(&mut Pane, &mut ViewContext) -> AnyElement, + { + self.render_tab_bar_buttons = Rc::new(render); cx.notify(); } @@ -515,7 +554,7 @@ impl Pane { } } - pub(crate) fn add_item( + pub fn add_item( workspace: &mut Workspace, pane: &ViewHandle, item: Box, @@ -641,6 +680,14 @@ impl Pane { self.items.iter().position(|i| i.id() == item.id()) } + pub fn toggle_zoom(&mut self, _: &ToggleZoom, cx: &mut ViewContext) { + if self.zoomed { + cx.emit(Event::ZoomOut); + } else { + cx.emit(Event::ZoomIn); + } + } + pub fn activate_item( &mut self, index: usize, @@ -704,185 +751,118 @@ impl Pane { } pub fn close_active_item( - workspace: &mut Workspace, + &mut self, _: &CloseActiveItem, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - - let active_item_id = pane.items.get(pane.active_item_index)?.id(); - - let task = Self::close_item_by_id(workspace, pane_handle, active_item_id, cx); - - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_item_by_id(active_item_id, cx)) } pub fn close_item_by_id( - workspace: &mut Workspace, - pane: ViewHandle, + &mut self, item_id_to_close: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - Self::close_items(workspace, pane, cx, move |view_id| { - view_id == item_id_to_close - }) + self.close_items(cx, move |view_id| view_id == item_id_to_close) } pub fn close_inactive_items( - workspace: &mut Workspace, + &mut self, _: &CloseInactiveItems, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - - let active_item_id = pane.items.get(pane.active_item_index)?.id(); - - let task = Self::close_items(workspace, pane_handle, cx, move |item_id| { - item_id != active_item_id - }); + if self.items.is_empty() { + return None; + } - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items(cx, move |item_id| item_id != active_item_id)) } pub fn close_clean_items( - workspace: &mut Workspace, + &mut self, _: &CloseCleanItems, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - - let item_ids: Vec<_> = pane + let item_ids: Vec<_> = self .items() .filter(|item| !item.is_dirty(cx)) .map(|item| item.id()) .collect(); - - let task = Self::close_items(workspace, pane_handle, cx, move |item_id| { - item_ids.contains(&item_id) - }); - - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + Some(self.close_items(cx, move |item_id| item_ids.contains(&item_id))) } pub fn close_items_to_the_left( - workspace: &mut Workspace, + &mut self, _: &CloseItemsToTheLeft, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - let active_item_id = pane.items.get(pane.active_item_index)?.id(); - - let task = Self::close_items_to_the_left_by_id(workspace, pane_handle, active_item_id, cx); - - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items_to_the_left_by_id(active_item_id, cx)) } pub fn close_items_to_the_left_by_id( - workspace: &mut Workspace, - pane: ViewHandle, + &mut self, item_id: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - let item_ids: Vec<_> = pane - .read(cx) + let item_ids: Vec<_> = self .items() .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - - let task = Self::close_items(workspace, pane, cx, move |item_id| { - item_ids.contains(&item_id) - }); - - cx.foreground().spawn(async move { - task.await?; - Ok(()) - }) + self.close_items(cx, move |item_id| item_ids.contains(&item_id)) } pub fn close_items_to_the_right( - workspace: &mut Workspace, + &mut self, _: &CloseItemsToTheRight, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - let pane = pane_handle.read(cx); - let active_item_id = pane.items.get(pane.active_item_index)?.id(); - - let task = Self::close_items_to_the_right_by_id(workspace, pane_handle, active_item_id, cx); - - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + if self.items.is_empty() { + return None; + } + let active_item_id = self.items[self.active_item_index].id(); + Some(self.close_items_to_the_right_by_id(active_item_id, cx)) } pub fn close_items_to_the_right_by_id( - workspace: &mut Workspace, - pane: ViewHandle, + &mut self, item_id: usize, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Task> { - let item_ids: Vec<_> = pane - .read(cx) + let item_ids: Vec<_> = self .items() .rev() .take_while(|item| item.id() != item_id) .map(|item| item.id()) .collect(); - - let task = Self::close_items(workspace, pane, cx, move |item_id| { - item_ids.contains(&item_id) - }); - - cx.foreground().spawn(async move { - task.await?; - Ok(()) - }) + self.close_items(cx, move |item_id| item_ids.contains(&item_id)) } pub fn close_all_items( - workspace: &mut Workspace, + &mut self, _: &CloseAllItems, - cx: &mut ViewContext, + cx: &mut ViewContext, ) -> Option>> { - let pane_handle = workspace.active_pane().clone(); - - let task = Self::close_items(workspace, pane_handle, cx, move |_| true); - - Some(cx.foreground().spawn(async move { - task.await?; - Ok(()) - })) + Some(self.close_items(cx, move |_| true)) } pub fn close_items( - workspace: &mut Workspace, - pane: ViewHandle, - cx: &mut ViewContext, + &mut self, + cx: &mut ViewContext, should_close: impl 'static + Fn(usize) -> bool, ) -> Task> { - let project = workspace.project().clone(); - // Find the items to close. let mut items_to_close = Vec::new(); - for item in &pane.read(cx).items { + for item in &self.items { if should_close(item.id()) { items_to_close.push(item.boxed_clone()); } @@ -894,8 +874,8 @@ impl Pane { // of what content they would be saving. items_to_close.sort_by_key(|item| !item.is_singleton(cx)); - let pane = pane.downgrade(); - cx.spawn(|workspace, mut cx| async move { + let workspace = self.workspace.clone(); + cx.spawn(|pane, mut cx| async move { let mut saved_project_items_ids = HashSet::default(); for item in items_to_close.clone() { // Find the item's current index and its set of project item models. Avoid @@ -913,7 +893,7 @@ impl Pane { // Check if this view has any project items that are not open anywhere else // in the workspace, AND that the user has not already been prompted to save. // If there are any such project entries, prompt the user to save this item. - workspace.read_with(&cx, |workspace, cx| { + let project = workspace.read_with(&cx, |workspace, cx| { for item in workspace.items(cx) { if !items_to_close .iter() @@ -923,6 +903,7 @@ impl Pane { project_item_ids.retain(|id| !other_project_item_ids.contains(id)); } } + workspace.project().clone() })?; let should_save = project_item_ids .iter() @@ -965,7 +946,8 @@ impl Pane { // to activating the item to the left .unwrap_or_else(|| item_index.min(self.items.len()).saturating_sub(1)); - self.activate_item(index_to_activate, activate_pane, activate_pane, cx); + let should_activate = activate_pane || self.has_focus; + self.activate_item(index_to_activate, should_activate, should_activate, cx); } let item = self.items.remove(item_index); @@ -1175,23 +1157,6 @@ impl Pane { self.tab_bar_context_menu.kind = TabBarContextMenuKind::Split; } - fn deploy_dock_menu(&mut self, cx: &mut ViewContext) { - self.tab_bar_context_menu.handle.update(cx, |menu, cx| { - menu.show( - Default::default(), - AnchorCorner::TopRight, - vec![ - ContextMenuItem::action("Anchor Dock Right", AnchorDockRight), - ContextMenuItem::action("Anchor Dock Bottom", AnchorDockBottom), - ContextMenuItem::action("Expand Dock", ExpandDock), - ], - cx, - ); - }); - - self.tab_bar_context_menu.kind = TabBarContextMenuKind::Dock; - } - fn deploy_new_menu(&mut self, cx: &mut ViewContext) { self.tab_bar_context_menu.handle.update(cx, |menu, cx| { menu.show( @@ -1238,14 +1203,11 @@ impl Pane { // In the case of the user right clicking on a non-active tab, for some item-closing commands, we need to provide the id of the tab, for the others, we can reuse the existing command. vec![ ContextMenuItem::handler("Close Inactive Item", { - let workspace = self.workspace.clone(); let pane = target_pane.clone(); move |cx| { - if let Some((workspace, pane)) = - workspace.upgrade(cx).zip(pane.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Self::close_item_by_id(workspace, pane, target_item_id, cx) + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_item_by_id(target_item_id, cx) .detach_and_log_err(cx); }) } @@ -1254,39 +1216,23 @@ impl Pane { ContextMenuItem::action("Close Inactive Items", CloseInactiveItems), ContextMenuItem::action("Close Clean Items", CloseCleanItems), ContextMenuItem::handler("Close Items To The Left", { - let workspace = self.workspace.clone(); let pane = target_pane.clone(); move |cx| { - if let Some((workspace, pane)) = - workspace.upgrade(cx).zip(pane.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Self::close_items_to_the_left_by_id( - workspace, - pane, - target_item_id, - cx, - ) - .detach_and_log_err(cx); + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left_by_id(target_item_id, cx) + .detach_and_log_err(cx); }) } } }), ContextMenuItem::handler("Close Items To The Right", { - let workspace = self.workspace.clone(); let pane = target_pane.clone(); move |cx| { - if let Some((workspace, pane)) = - workspace.upgrade(cx).zip(pane.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Self::close_items_to_the_right_by_id( - workspace, - pane, - target_item_id, - cx, - ) - .detach_and_log_err(cx); + if let Some(pane) = pane.upgrade(cx) { + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right_by_id(target_item_id, cx) + .detach_and_log_err(cx); }) } } @@ -1359,7 +1305,7 @@ impl Pane { row.add_child({ enum TabDragReceiver {} let mut receiver = - dragged_item_receiver::(ix, ix, true, None, cx, { + dragged_item_receiver::(self, ix, ix, true, None, cx, { let item = item.clone(); let pane = pane.clone(); let detail = detail.clone(); @@ -1393,20 +1339,7 @@ impl Pane { .on_click(MouseButton::Middle, { let item_id = item.id(); move |_, pane, cx| { - let workspace = pane.workspace.clone(); - let pane = cx.weak_handle(); - cx.window_context().defer(move |cx| { - if let Some((workspace, pane)) = - workspace.upgrade(cx).zip(pane.upgrade(cx)) - { - workspace.update(cx, |workspace, cx| { - Self::close_item_by_id( - workspace, pane, item_id, cx, - ) - .detach_and_log_err(cx); - }); - } - }); + pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); } }) .on_down( @@ -1438,7 +1371,7 @@ impl Pane { receiver.as_draggable( DraggedItem { - item, + handle: item, pane: pane.clone(), }, { @@ -1448,7 +1381,7 @@ impl Pane { move |dragged_item: &DraggedItem, cx: &mut ViewContext| { let tab_style = &theme.workspace.tab_bar.dragged_tab; Self::render_dragged_tab( - &dragged_item.item, + &dragged_item.handle, dragged_item.pane.clone(), false, detail, @@ -1468,7 +1401,7 @@ impl Pane { let filler_style = theme.workspace.tab_bar.tab_style(pane_active, false); enum Filler {} row.add_child( - dragged_item_receiver::(0, filler_index, true, None, cx, |_, _| { + dragged_item_receiver::(self, 0, filler_index, true, None, cx, |_, _| { Empty::new() .contained() .with_style(filler_style.container) @@ -1613,12 +1546,9 @@ impl Pane { let pane = pane.clone(); cx.window_context().defer(move |cx| { if let Some(pane) = pane.upgrade(cx) { - if let Some(workspace) = pane.read(cx).workspace.upgrade(cx) { - workspace.update(cx, |workspace, cx| { - Self::close_item_by_id(workspace, pane, item_id, cx) - .detach_and_log_err(cx); - }); - } + pane.update(cx, |pane, cx| { + pane.close_item_by_id(item_id, cx).detach_and_log_err(cx); + }); } }); } @@ -1638,83 +1568,55 @@ impl Pane { .into_any() } - fn render_tab_bar_buttons( - &mut self, - theme: &Theme, - cx: &mut ViewContext, - ) -> AnyElement { - Flex::row() - // New menu - .with_child(render_tab_bar_button( - 0, - "icons/plus_12.svg", - cx, - |pane, cx| pane.deploy_new_menu(cx), - self.tab_bar_context_menu - .handle_if_kind(TabBarContextMenuKind::New), - )) + pub fn render_tab_bar_button)>( + index: usize, + icon: &'static str, + cx: &mut ViewContext, + on_click: F, + context_menu: Option>, + ) -> AnyElement { + enum TabBarButton {} + + Stack::new() .with_child( - self.docked - .map(|anchor| { - // Add the dock menu button if this pane is a dock - let dock_icon = icon_for_dock_anchor(anchor); - - render_tab_bar_button( - 1, - dock_icon, - cx, - |pane, cx| pane.deploy_dock_menu(cx), - self.tab_bar_context_menu - .handle_if_kind(TabBarContextMenuKind::Dock), - ) - }) - .unwrap_or_else(|| { - // Add the split menu if this pane is not a dock - render_tab_bar_button( - 2, - "icons/split_12.svg", - cx, - |pane, cx| pane.deploy_split_menu(cx), - self.tab_bar_context_menu - .handle_if_kind(TabBarContextMenuKind::Split), - ) - }), + MouseEventHandler::::new(index, cx, |mouse_state, cx| { + let theme = &settings::get::(cx).theme.workspace.tab_bar; + let style = theme.pane_button.style_for(mouse_state, false); + Svg::new(icon) + .with_color(style.color) + .constrained() + .with_width(style.icon_width) + .aligned() + .constrained() + .with_width(style.button_width) + .with_height(style.button_width) + }) + .with_cursor_style(CursorStyle::PointingHand) + .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)), + ) + .with_children( + context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()), ) - // Add the close dock button if this pane is a dock - .with_children(self.docked.map(|_| { - render_tab_bar_button( - 3, - "icons/x_mark_8.svg", - cx, - |this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - Dock::hide_dock(workspace, &Default::default(), cx) - }) - }); - } - }, - None, - ) - })) - .contained() - .with_style(theme.workspace.tab_bar.pane_button_container) .flex(1., false) - .into_any() + .into_any_named("tab bar button") } - fn render_blank_pane( - &mut self, - theme: &Theme, - _cx: &mut ViewContext, - ) -> AnyElement { + fn render_blank_pane(&self, theme: &Theme, _cx: &mut ViewContext) -> AnyElement { let background = theme.workspace.background; Empty::new() .contained() .with_background_color(background) .into_any() } + + pub fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext) { + self.zoomed = zoomed; + cx.notify(); + } + + pub fn is_zoomed(&self) -> bool { + self.zoomed + } } impl Entity for Pane { @@ -1758,7 +1660,14 @@ impl View for Pane { .with_child(self.render_tabs(cx).flex(1., true).into_any_named("tabs")); if self.is_active { - tab_row.add_child(self.render_tab_bar_buttons(&theme, cx)) + let render_tab_bar_buttons = self.render_tab_bar_buttons.clone(); + tab_row.add_child( + (render_tab_bar_buttons)(self, cx) + .contained() + .with_style(theme.workspace.tab_bar.pane_button_container) + .flex(1., false) + .into_any(), + ) } stack.add_child(tab_row); @@ -1771,14 +1680,11 @@ impl View for Pane { .with_child({ enum PaneContentTabDropTarget {} dragged_item_receiver::( + self, 0, self.active_item_index + 1, - false, - if self.docked.is_some() { - None - } else { - Some(100.) - }, + !self.can_split, + if self.can_split { Some(100.) } else { None }, cx, { let toolbar = self.toolbar.clone(); @@ -1803,7 +1709,7 @@ impl View for Pane { enum EmptyPane {} let theme = theme::current(cx).clone(); - dragged_item_receiver::(0, 0, false, None, cx, |_, cx| { + dragged_item_receiver::(self, 0, 0, false, None, cx, |_, cx| { self.render_blank_pane(&theme, cx) }) .on_down(MouseButton::Left, |_, _, cx| { @@ -1841,7 +1747,11 @@ impl View for Pane { } fn focus_in(&mut self, focused: AnyViewHandle, cx: &mut ViewContext) { - self.has_focus = true; + if !self.has_focus { + self.has_focus = true; + cx.emit(Event::Focus); + } + self.toolbar.update(cx, |toolbar, cx| { toolbar.pane_focus_update(true, cx); }); @@ -1867,8 +1777,6 @@ impl View for Pane { .insert(active_item.id(), focused.downgrade()); } } - - cx.emit(Event::Focus); } fn focus_out(&mut self, _: AnyViewHandle, cx: &mut ViewContext) { @@ -1880,45 +1788,9 @@ impl View for Pane { fn update_keymap_context(&self, keymap: &mut KeymapContext, _: &AppContext) { Self::reset_to_default_keymap_context(keymap); - if self.docked.is_some() { - keymap.add_identifier("docked"); - } } } -fn render_tab_bar_button)>( - index: usize, - icon: &'static str, - cx: &mut ViewContext, - on_click: F, - context_menu: Option>, -) -> AnyElement { - enum TabBarButton {} - - Stack::new() - .with_child( - MouseEventHandler::::new(index, cx, |mouse_state, cx| { - let theme = &theme::current(cx).workspace.tab_bar; - let style = theme.pane_button.style_for(mouse_state, false); - Svg::new(icon) - .with_color(style.color) - .constrained() - .with_width(style.icon_width) - .aligned() - .constrained() - .with_width(style.button_width) - .with_height(style.button_width) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, move |_, pane, cx| on_click(pane, cx)), - ) - .with_children( - context_menu.map(|menu| ChildView::new(&menu, cx).aligned().bottom().right()), - ) - .flex(1., false) - .into_any_named("tab bar button") -} - impl ItemNavHistory { pub fn push(&self, data: Option, cx: &mut WindowContext) { self.history.borrow_mut().push(data, self.item.clone(), cx); @@ -2156,11 +2028,9 @@ impl Element for PaneBackdrop { #[cfg(test)] mod tests { - use std::sync::Arc; - use super::*; use crate::item::test::{TestItem, TestProjectItem}; - use gpui::{executor::Deterministic, TestAppContext}; + use gpui::TestAppContext; use project::FakeFs; use settings::SettingsStore; @@ -2171,9 +2041,10 @@ mod tests { let project = Project::test(fs, None, cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); - workspace.update(cx, |workspace, cx| { - assert!(Pane::close_active_item(workspace, &CloseActiveItem, cx).is_none()) + pane.update(cx, |pane, cx| { + assert!(pane.close_active_item(&CloseActiveItem, cx).is_none()) }); } @@ -2452,7 +2323,7 @@ mod tests { } #[gpui::test] - async fn test_remove_item_ordering(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_remove_item_ordering(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2470,36 +2341,36 @@ mod tests { add_labeled_item(&workspace, &pane, "1", false, cx); assert_item_labels(&pane, ["A", "B", "1*", "C", "D"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &CloseActiveItem, cx); - }); - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B*", "C", "D"], cx); pane.update(cx, |pane, cx| pane.activate_item(3, false, false, cx)); assert_item_labels(&pane, ["A", "B", "C", "D*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &CloseActiveItem, cx); - }); - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B*", "C"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &CloseActiveItem, cx); - }); - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "C*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &CloseActiveItem, cx); - }); - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_active_item(&CloseActiveItem, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A*"], cx); } #[gpui::test] - async fn test_close_inactive_items(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_close_inactive_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2509,16 +2380,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_inactive_items(workspace, &CloseInactiveItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_inactive_items(&CloseInactiveItems, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*"], cx); } #[gpui::test] - async fn test_close_clean_items(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_close_clean_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2533,19 +2405,15 @@ mod tests { add_labeled_item(&workspace, &pane, "E", false, cx); assert_item_labels(&pane, ["A^", "B", "C^", "D", "E*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_clean_items(workspace, &CloseCleanItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_clean_items(&CloseCleanItems, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A^", "C*^"], cx); } #[gpui::test] - async fn test_close_items_to_the_left( - deterministic: Arc, - cx: &mut TestAppContext, - ) { + async fn test_close_items_to_the_left(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2555,19 +2423,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_items_to_the_left(workspace, &CloseItemsToTheLeft, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_left(&CloseItemsToTheLeft, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["C*", "D", "E"], cx); } #[gpui::test] - async fn test_close_items_to_the_right( - deterministic: Arc, - cx: &mut TestAppContext, - ) { + async fn test_close_items_to_the_right(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2577,16 +2443,17 @@ mod tests { set_labeled_items(&workspace, &pane, ["A", "B", "C*", "D", "E"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_items_to_the_right(workspace, &CloseItemsToTheRight, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| { + pane.close_items_to_the_right(&CloseItemsToTheRight, cx) + }) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, ["A", "B", "C*"], cx); } #[gpui::test] - async fn test_close_all_items(deterministic: Arc, cx: &mut TestAppContext) { + async fn test_close_all_items(cx: &mut TestAppContext) { init_test(cx); let fs = FakeFs::new(cx.background()); @@ -2599,11 +2466,10 @@ mod tests { add_labeled_item(&workspace, &pane, "C", false, cx); assert_item_labels(&pane, ["A", "B", "C*"], cx); - workspace.update(cx, |workspace, cx| { - Pane::close_all_items(workspace, &CloseAllItems, cx); - }); - - deterministic.run_until_parked(); + pane.update(cx, |pane, cx| pane.close_all_items(&CloseAllItems, cx)) + .unwrap() + .await + .unwrap(); assert_item_labels(&pane, [], cx); } diff --git a/crates/workspace/src/pane/dragged_item_receiver.rs b/crates/workspace/src/pane/dragged_item_receiver.rs index 532e6bff5c03e6e9565e86f333603c1eacd5d299..bb5d3a2464e89310c2e487df94eb19aaa7246ab3 100644 --- a/crates/workspace/src/pane/dragged_item_receiver.rs +++ b/crates/workspace/src/pane/dragged_item_receiver.rs @@ -12,6 +12,7 @@ use gpui::{ use project::ProjectEntryId; pub fn dragged_item_receiver( + pane: &Pane, region_id: usize, drop_index: usize, allow_same_pane: bool, @@ -24,22 +25,24 @@ where D: Element, F: FnOnce(&mut MouseState, &mut ViewContext) -> D, { - MouseEventHandler::::above(region_id, cx, |state, cx| { + let drag_and_drop = cx.global::>(); + let drag_position = if (pane.can_drop)(drag_and_drop, cx) { + drag_and_drop + .currently_dragged::(cx.window_id()) + .map(|(drag_position, _)| drag_position) + .or_else(|| { + drag_and_drop + .currently_dragged::(cx.window_id()) + .map(|(drag_position, _)| drag_position) + }) + } else { + None + }; + + let mut handler = MouseEventHandler::::above(region_id, cx, |state, cx| { // Observing hovered will cause a render when the mouse enters regardless // of if mouse position was accessed before - let drag_position = if state.hovered() { - cx.global::>() - .currently_dragged::(cx.window_id()) - .map(|(drag_position, _)| drag_position) - .or_else(|| { - cx.global::>() - .currently_dragged::(cx.window_id()) - .map(|(drag_position, _)| drag_position) - }) - } else { - None - }; - + let drag_position = if state.hovered() { drag_position } else { None }; Stack::new() .with_child(render_child(state, cx)) .with_children(drag_position.map(|drag_position| { @@ -64,38 +67,44 @@ where } }) })) - }) - .on_up(MouseButton::Left, { - move |event, pane, cx| { - let workspace = pane.workspace.clone(); - let pane = cx.weak_handle(); - handle_dropped_item( - event, - workspace, - &pane, - drop_index, - allow_same_pane, - split_margin, - cx, - ); - cx.notify(); - } - }) - .on_move(|_, _, cx| { - let drag_and_drop = cx.global::>(); + }); - if drag_and_drop - .currently_dragged::(cx.window_id()) - .is_some() - || drag_and_drop - .currently_dragged::(cx.window_id()) - .is_some() - { - cx.notify(); - } else { - cx.propagate_event(); - } - }) + if drag_position.is_some() { + handler = handler + .on_up(MouseButton::Left, { + move |event, pane, cx| { + let workspace = pane.workspace.clone(); + let pane = cx.weak_handle(); + handle_dropped_item( + event, + workspace, + &pane, + drop_index, + allow_same_pane, + split_margin, + cx, + ); + cx.notify(); + } + }) + .on_move(|_, _, cx| { + let drag_and_drop = cx.global::>(); + + if drag_and_drop + .currently_dragged::(cx.window_id()) + .is_some() + || drag_and_drop + .currently_dragged::(cx.window_id()) + .is_some() + { + cx.notify(); + } else { + cx.propagate_event(); + } + }) + } + + handler } pub fn handle_dropped_item( @@ -115,7 +124,7 @@ pub fn handle_dropped_item( let action = if let Some((_, dragged_item)) = drag_and_drop.currently_dragged::(cx.window_id()) { - Action::Move(dragged_item.pane.clone(), dragged_item.item.id()) + Action::Move(dragged_item.pane.clone(), dragged_item.handle.id()) } else if let Some((_, project_entry)) = drag_and_drop.currently_dragged::(cx.window_id()) { diff --git a/crates/workspace/src/pane_group.rs b/crates/workspace/src/pane_group.rs index 6e7580a103f3bf7a27217cdb0b246b789caffc44..5e5a5a98baaf07482c8b3854b08a3301fd75baa7 100644 --- a/crates/workspace/src/pane_group.rs +++ b/crates/workspace/src/pane_group.rs @@ -7,7 +7,7 @@ use gpui::{ elements::*, geometry::{rect::RectF, vector::Vector2F}, platform::{CursorStyle, MouseButton}, - Axis, Border, ModelHandle, ViewContext, ViewHandle, + AnyViewHandle, Axis, Border, ModelHandle, ViewContext, ViewHandle, }; use project::Project; use serde::Deserialize; @@ -71,6 +71,7 @@ impl PaneGroup { follower_states: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -80,6 +81,7 @@ impl PaneGroup { follower_states, active_call, active_pane, + zoomed, app_state, cx, ) @@ -134,6 +136,7 @@ impl Member { follower_states: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -141,6 +144,12 @@ impl Member { match self { Member::Pane(pane) => { + let pane_element = if Some(&**pane) == zoomed { + Empty::new().into_any() + } else { + ChildView::new(pane, cx).into_any() + }; + let leader = follower_states .iter() .find_map(|(leader_id, follower_states)| { @@ -257,7 +266,7 @@ impl Member { }; Stack::new() - .with_child(ChildView::new(pane, cx).contained().with_border(border)) + .with_child(pane_element.contained().with_border(border)) .with_children(leader_status_box) .into_any() } @@ -267,6 +276,7 @@ impl Member { follower_states, active_call, active_pane, + zoomed, app_state, cx, ), @@ -371,6 +381,7 @@ impl PaneAxis { follower_state: &FollowerStatesByLeader, active_call: Option<&ModelHandle>, active_pane: &ViewHandle, + zoomed: Option<&AnyViewHandle>, app_state: &Arc, cx: &mut ViewContext, ) -> AnyElement { @@ -388,6 +399,7 @@ impl PaneAxis { follower_state, active_call, active_pane, + zoomed, app_state, cx, ); diff --git a/crates/workspace/src/persistence.rs b/crates/workspace/src/persistence.rs index 4ffae0d7e3cacaefbd244ff1af8c80439f44b6f8..d27818d2028eec866d17cc687bb393f37e9cb6e5 100644 --- a/crates/workspace/src/persistence.rs +++ b/crates/workspace/src/persistence.rs @@ -11,7 +11,6 @@ use gpui::{platform::WindowBounds, Axis}; use util::{unzip_option, ResultExt}; use uuid::Uuid; -use crate::dock::DockPosition; use crate::WorkspaceId; use model::{ @@ -19,15 +18,17 @@ use model::{ WorkspaceLocation, }; +use self::model::DockStructure; + define_connection! { // Current schema shape using pseudo-rust syntax: // // workspaces( // workspace_id: usize, // Primary key for workspaces // workspace_location: Bincode>, - // dock_visible: bool, - // dock_anchor: DockAnchor, // 'Bottom' / 'Right' / 'Expanded' - // dock_pane: Option, // PaneId + // dock_visible: bool, // Deprecated + // dock_anchor: DockAnchor, // Deprecated + // dock_pane: Option, // Deprecated // left_sidebar_open: boolean, // timestamp: String, // UTC YYYY-MM-DD HH:MM:SS // window_state: String, // WindowBounds Discriminant @@ -71,10 +72,10 @@ define_connection! { CREATE TABLE workspaces( workspace_id INTEGER PRIMARY KEY, workspace_location BLOB UNIQUE, - dock_visible INTEGER, // Boolean - dock_anchor TEXT, // Enum: 'Bottom' / 'Right' / 'Expanded' - dock_pane INTEGER, // NULL indicates that we don't have a dock pane yet - left_sidebar_open INTEGER, //Boolean + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, FOREIGN KEY(dock_pane) REFERENCES panes(pane_id) ) STRICT; @@ -131,6 +132,36 @@ define_connection! { ALTER TABLE workspaces ADD COLUMN window_width REAL; ALTER TABLE workspaces ADD COLUMN window_height REAL; ALTER TABLE workspaces ADD COLUMN display BLOB; + ), + // Drop foreign key constraint from workspaces.dock_pane to panes table. + sql!( + CREATE TABLE workspaces_2( + workspace_id INTEGER PRIMARY KEY, + workspace_location BLOB UNIQUE, + dock_visible INTEGER, // Deprecated. Preserving so users can downgrade Zed. + dock_anchor TEXT, // Deprecated. Preserving so users can downgrade Zed. + dock_pane INTEGER, // Deprecated. Preserving so users can downgrade Zed. + left_sidebar_open INTEGER, // Boolean + timestamp TEXT DEFAULT CURRENT_TIMESTAMP NOT NULL, + window_state TEXT, + window_x REAL, + window_y REAL, + window_width REAL, + window_height REAL, + display BLOB + ) STRICT; + INSERT INTO workspaces_2 SELECT * FROM workspaces; + DROP TABLE workspaces; + ALTER TABLE workspaces_2 RENAME TO workspaces; + ), + // Add panels related information + sql!( + ALTER TABLE workspaces ADD COLUMN left_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN left_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN right_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN right_dock_active_panel TEXT; + ALTER TABLE workspaces ADD COLUMN bottom_dock_visible INTEGER; //bool + ALTER TABLE workspaces ADD COLUMN bottom_dock_active_panel TEXT; )]; } @@ -146,27 +177,29 @@ impl WorkspaceDb { // Note that we re-assign the workspace_id here in case it's empty // and we've grabbed the most recent workspace - let (workspace_id, workspace_location, left_sidebar_open, dock_position, bounds, display): ( + let (workspace_id, workspace_location, bounds, display, docks): ( WorkspaceId, WorkspaceLocation, - bool, - DockPosition, Option, Option, + DockStructure, ) = self .select_row_bound(sql! { SELECT workspace_id, workspace_location, - left_sidebar_open, - dock_visible, - dock_anchor, window_state, window_x, window_y, window_width, window_height, - display + display, + left_dock_visible, + left_dock_active_panel, + right_dock_visible, + right_dock_active_panel, + bottom_dock_visible, + bottom_dock_active_panel FROM workspaces WHERE workspace_location = ? }) @@ -178,18 +211,13 @@ impl WorkspaceDb { Some(SerializedWorkspace { id: workspace_id, location: workspace_location.clone(), - dock_pane: self - .get_dock_pane(workspace_id) - .context("Getting dock pane") - .log_err()?, center_group: self .get_center_pane_group(workspace_id) .context("Getting center group") .log_err()?, - dock_position, - left_sidebar_open, bounds, display, + docks, }) } @@ -200,7 +228,6 @@ impl WorkspaceDb { conn.with_savepoint("update_worktrees", || { // Clear out panes and pane_groups conn.exec_bound(sql!( - UPDATE workspaces SET dock_pane = NULL WHERE workspace_id = ?1; DELETE FROM pane_groups WHERE workspace_id = ?1; DELETE FROM panes WHERE workspace_id = ?1;))?(workspace.id) .expect("Clearing old panes"); @@ -215,42 +242,32 @@ impl WorkspaceDb { INSERT INTO workspaces( workspace_id, workspace_location, - left_sidebar_open, - dock_visible, - dock_anchor, + left_dock_visible, + left_dock_active_panel, + right_dock_visible, + right_dock_active_panel, + bottom_dock_visible, + bottom_dock_active_panel, timestamp ) - VALUES (?1, ?2, ?3, ?4, ?5, CURRENT_TIMESTAMP) + VALUES (?1, ?2, ?3, ?4, ?5, ?6, ?7, ?8, CURRENT_TIMESTAMP) ON CONFLICT DO UPDATE SET workspace_location = ?2, - left_sidebar_open = ?3, - dock_visible = ?4, - dock_anchor = ?5, + left_dock_visible = ?3, + left_dock_active_panel = ?4, + right_dock_visible = ?5, + right_dock_active_panel = ?6, + bottom_dock_visible = ?7, + bottom_dock_active_panel = ?8, timestamp = CURRENT_TIMESTAMP - ))?(( - workspace.id, - &workspace.location, - workspace.left_sidebar_open, - workspace.dock_position, - )) + ))?((workspace.id, &workspace.location, workspace.docks)) .context("Updating workspace")?; - // Save center pane group and dock pane + // Save center pane group Self::save_pane_group(conn, workspace.id, &workspace.center_group, None) .context("save pane group in save workspace")?; - let dock_id = Self::save_pane(conn, workspace.id, &workspace.dock_pane, None, true) - .context("save pane in save workspace")?; - - // Complete workspace initialization - conn.exec_bound(sql!( - UPDATE workspaces - SET dock_pane = ? - WHERE workspace_id = ? - ))?((dock_id, workspace.id)) - .context("Finishing initialization with dock pane")?; - Ok(()) }) .log_err(); @@ -402,32 +419,17 @@ impl WorkspaceDb { Ok(()) } SerializedPaneGroup::Pane(pane) => { - Self::save_pane(conn, workspace_id, &pane, parent, false)?; + Self::save_pane(conn, workspace_id, &pane, parent)?; Ok(()) } } } - fn get_dock_pane(&self, workspace_id: WorkspaceId) -> Result { - let (pane_id, active) = self.select_row_bound(sql!( - SELECT pane_id, active - FROM panes - WHERE pane_id = (SELECT dock_pane FROM workspaces WHERE workspace_id = ?) - ))?(workspace_id)? - .context("No dock pane for workspace")?; - - Ok(SerializedPane::new( - self.get_items(pane_id).context("Reading items")?, - active, - )) - } - fn save_pane( conn: &Connection, workspace_id: WorkspaceId, pane: &SerializedPane, - parent: Option<(GroupId, usize)>, // None indicates BOTH dock pane AND center_pane - dock: bool, + parent: Option<(GroupId, usize)>, ) -> Result { let pane_id = conn.select_row_bound::<_, i64>(sql!( INSERT INTO panes(workspace_id, active) @@ -436,13 +438,11 @@ impl WorkspaceDb { ))?((workspace_id, pane.active))? .ok_or_else(|| anyhow!("Could not retrieve inserted pane_id"))?; - if !dock { - let (parent_id, order) = unzip_option(parent); - conn.exec_bound(sql!( - INSERT INTO center_panes(pane_id, parent_group_id, position) - VALUES (?, ?, ?) - ))?((pane_id, parent_id, order))?; - } + let (parent_id, order) = unzip_option(parent); + conn.exec_bound(sql!( + INSERT INTO center_panes(pane_id, parent_group_id, position) + VALUES (?, ?, ?) + ))?((pane_id, parent_id, order))?; Self::save_items(conn, workspace_id, pane_id, &pane.children).context("Saving items")?; @@ -498,9 +498,7 @@ impl WorkspaceDb { #[cfg(test)] mod tests { use super::*; - use crate::DockAnchor; use db::open_test_db; - use std::sync::Arc; #[gpui::test] async fn test_next_id_stability() { @@ -575,23 +573,19 @@ mod tests { let mut workspace_1 = SerializedWorkspace { id: 1, location: (["/tmp", "/tmp2"]).into(), - dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom), center_group: Default::default(), - dock_pane: Default::default(), - left_sidebar_open: true, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; - let mut workspace_2 = SerializedWorkspace { + let workspace_2 = SerializedWorkspace { id: 2, location: (["/tmp"]).into(), - dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded), center_group: Default::default(), - dock_pane: Default::default(), - left_sidebar_open: false, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -615,12 +609,6 @@ mod tests { workspace_1.location = (["/tmp", "/tmp3"]).into(); db.save_workspace(workspace_1.clone()).await; db.save_workspace(workspace_1).await; - - workspace_2.dock_pane.children.push(SerializedItem { - kind: Arc::from("Test"), - item_id: 10, - active: true, - }); db.save_workspace(workspace_2).await; let test_text_2 = db @@ -644,16 +632,6 @@ mod tests { let db = WorkspaceDb(open_test_db("test_full_workspace_serialization").await); - let dock_pane = crate::persistence::model::SerializedPane { - children: vec![ - SerializedItem::new("Terminal", 1, false), - SerializedItem::new("Terminal", 2, false), - SerializedItem::new("Terminal", 3, true), - SerializedItem::new("Terminal", 4, false), - ], - active: false, - }; - // ----------------- // | 1,2 | 5,6 | // | - - - | | @@ -694,12 +672,10 @@ mod tests { let workspace = SerializedWorkspace { id: 5, location: (["/tmp", "/tmp2"]).into(), - dock_position: DockPosition::Shown(DockAnchor::Bottom), center_group, - dock_pane, - left_sidebar_open: true, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; db.save_workspace(workspace.clone()).await; @@ -724,23 +700,19 @@ mod tests { let workspace_1 = SerializedWorkspace { id: 1, location: (["/tmp", "/tmp2"]).into(), - dock_position: crate::dock::DockPosition::Shown(DockAnchor::Bottom), center_group: Default::default(), - dock_pane: Default::default(), - left_sidebar_open: true, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; let mut workspace_2 = SerializedWorkspace { id: 2, location: (["/tmp"]).into(), - dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Expanded), center_group: Default::default(), - dock_pane: Default::default(), - left_sidebar_open: false, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; db.save_workspace(workspace_1.clone()).await; @@ -773,12 +745,10 @@ mod tests { let mut workspace_3 = SerializedWorkspace { id: 3, location: (&["/tmp", "/tmp2"]).into(), - dock_position: DockPosition::Shown(DockAnchor::Right), center_group: Default::default(), - dock_pane: Default::default(), - left_sidebar_open: false, bounds: Default::default(), display: Default::default(), + docks: Default::default(), }; db.save_workspace(workspace_3.clone()).await; @@ -798,52 +768,23 @@ mod tests { ); } - use crate::dock::DockPosition; use crate::persistence::model::SerializedWorkspace; use crate::persistence::model::{SerializedItem, SerializedPane, SerializedPaneGroup}; fn default_workspace>( workspace_id: &[P], - dock_pane: SerializedPane, center_group: &SerializedPaneGroup, ) -> SerializedWorkspace { SerializedWorkspace { id: 4, location: workspace_id.into(), - dock_position: crate::dock::DockPosition::Hidden(DockAnchor::Right), center_group: center_group.clone(), - dock_pane, - left_sidebar_open: true, bounds: Default::default(), display: Default::default(), + docks: Default::default(), } } - #[gpui::test] - async fn test_basic_dock_pane() { - env_logger::try_init().ok(); - - let db = WorkspaceDb(open_test_db("basic_dock_pane").await); - - let dock_pane = crate::persistence::model::SerializedPane::new( - vec![ - SerializedItem::new("Terminal", 1, false), - SerializedItem::new("Terminal", 4, false), - SerializedItem::new("Terminal", 2, false), - SerializedItem::new("Terminal", 3, true), - ], - false, - ); - - let workspace = default_workspace(&["/tmp"], dock_pane, &Default::default()); - - db.save_workspace(workspace.clone()).await; - - let new_workspace = db.workspace_for_roots(&["/tmp"]).unwrap(); - - assert_eq!(workspace.dock_pane, new_workspace.dock_pane); - } - #[gpui::test] async fn test_simple_split() { env_logger::try_init().ok(); @@ -887,7 +828,7 @@ mod tests { ], }; - let workspace = default_workspace(&["/tmp"], Default::default(), ¢er_pane); + let workspace = default_workspace(&["/tmp"], ¢er_pane); db.save_workspace(workspace.clone()).await; @@ -936,7 +877,7 @@ mod tests { let id = &["/tmp"]; - let mut workspace = default_workspace(id, Default::default(), ¢er_pane); + let mut workspace = default_workspace(id, ¢er_pane); db.save_workspace(workspace.clone()).await; diff --git a/crates/workspace/src/persistence/model.rs b/crates/workspace/src/persistence/model.rs index b73dfa495d4fbb2d3c60f09bede945227d506da6..fe7735753f75a1db9bac49777d4452647c1f2d3f 100644 --- a/crates/workspace/src/persistence/model.rs +++ b/crates/workspace/src/persistence/model.rs @@ -1,7 +1,4 @@ -use crate::{ - dock::DockPosition, item::ItemHandle, DockAnchor, ItemDeserializers, Member, Pane, PaneAxis, - Workspace, WorkspaceId, -}; +use crate::{item::ItemHandle, ItemDeserializers, Member, Pane, PaneAxis, Workspace, WorkspaceId}; use anyhow::{anyhow, Context, Result}; use async_recursion::async_recursion; use db::sqlez::{ @@ -62,12 +59,68 @@ impl Column for WorkspaceLocation { pub struct SerializedWorkspace { pub id: WorkspaceId, pub location: WorkspaceLocation, - pub dock_position: DockPosition, pub center_group: SerializedPaneGroup, - pub dock_pane: SerializedPane, - pub left_sidebar_open: bool, pub bounds: Option, pub display: Option, + pub docks: DockStructure, +} + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct DockStructure { + pub(crate) left: DockData, + pub(crate) right: DockData, + pub(crate) bottom: DockData, +} + +impl Column for DockStructure { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (left, next_index) = DockData::column(statement, start_index)?; + let (right, next_index) = DockData::column(statement, next_index)?; + let (bottom, next_index) = DockData::column(statement, next_index)?; + Ok(( + DockStructure { + left, + right, + bottom, + }, + next_index, + )) + } +} + +impl Bind for DockStructure { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.left, start_index)?; + let next_index = statement.bind(&self.right, next_index)?; + statement.bind(&self.bottom, next_index) + } +} + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct DockData { + pub(crate) visible: bool, + pub(crate) active_panel: Option, +} + +impl Column for DockData { + fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { + let (visible, next_index) = Option::::column(statement, start_index)?; + let (active_panel, next_index) = Option::::column(statement, next_index)?; + Ok(( + DockData { + visible: visible.unwrap_or(false), + active_panel, + }, + next_index, + )) + } +} + +impl Bind for DockData { + fn bind(&self, statement: &Statement, start_index: i32) -> Result { + let next_index = statement.bind(&self.visible, start_index)?; + statement.bind(&self.active_panel, next_index) + } } #[derive(Debug, PartialEq, Eq, Clone)] @@ -266,9 +319,9 @@ impl StaticColumnCount for SerializedItem { } impl Bind for &SerializedItem { fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(self.kind.clone(), start_index)?; - let next_index = statement.bind(self.item_id, next_index)?; - statement.bind(self.active, next_index) + let next_index = statement.bind(&self.kind, start_index)?; + let next_index = statement.bind(&self.item_id, next_index)?; + statement.bind(&self.active, next_index) } } @@ -287,64 +340,3 @@ impl Column for SerializedItem { )) } } - -impl StaticColumnCount for DockPosition { - fn column_count() -> usize { - 2 - } -} -impl Bind for DockPosition { - fn bind(&self, statement: &Statement, start_index: i32) -> Result { - let next_index = statement.bind(self.is_visible(), start_index)?; - statement.bind(self.anchor(), next_index) - } -} - -impl Column for DockPosition { - fn column(statement: &mut Statement, start_index: i32) -> Result<(Self, i32)> { - let (visible, next_index) = bool::column(statement, start_index)?; - let (dock_anchor, next_index) = DockAnchor::column(statement, next_index)?; - let position = if visible { - DockPosition::Shown(dock_anchor) - } else { - DockPosition::Hidden(dock_anchor) - }; - Ok((position, next_index)) - } -} - -#[cfg(test)] -mod tests { - use super::WorkspaceLocation; - use crate::DockAnchor; - use db::sqlez::connection::Connection; - - #[test] - fn test_workspace_round_trips() { - let db = Connection::open_memory(Some("workspace_id_round_trips")); - - db.exec(indoc::indoc! {" - CREATE TABLE workspace_id_test( - workspace_id INTEGER, - dock_anchor TEXT - );"}) - .unwrap()() - .unwrap(); - - let workspace_id: WorkspaceLocation = WorkspaceLocation::from(&["\test2", "\test1"]); - - db.exec_bound("INSERT INTO workspace_id_test(workspace_id, dock_anchor) VALUES (?,?)") - .unwrap()((&workspace_id, DockAnchor::Bottom)) - .unwrap(); - - assert_eq!( - db.select_row("SELECT workspace_id, dock_anchor FROM workspace_id_test LIMIT 1") - .unwrap()() - .unwrap(), - Some(( - WorkspaceLocation::from(&["\test1", "\test2"]), - DockAnchor::Bottom - )) - ); - } -} diff --git a/crates/workspace/src/sidebar.rs b/crates/workspace/src/sidebar.rs deleted file mode 100644 index 50148fa211dad3a415e59a7381c44f55573c6b18..0000000000000000000000000000000000000000 --- a/crates/workspace/src/sidebar.rs +++ /dev/null @@ -1,321 +0,0 @@ -use crate::{StatusItemView, Workspace}; -use gpui::{ - elements::*, impl_actions, platform::CursorStyle, platform::MouseButton, AnyViewHandle, - AppContext, Entity, Subscription, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext, -}; -use serde::Deserialize; -use std::rc::Rc; - -pub trait SidebarItem: View { - fn should_activate_item_on_event(&self, _: &Self::Event, _: &AppContext) -> bool { - false - } - fn should_show_badge(&self, _: &AppContext) -> bool { - false - } - fn contains_focused_view(&self, _: &AppContext) -> bool { - false - } -} - -pub trait SidebarItemHandle { - fn id(&self) -> usize; - fn should_show_badge(&self, cx: &WindowContext) -> bool; - fn is_focused(&self, cx: &WindowContext) -> bool; - fn as_any(&self) -> &AnyViewHandle; -} - -impl SidebarItemHandle for ViewHandle -where - T: SidebarItem, -{ - fn id(&self) -> usize { - self.id() - } - - fn should_show_badge(&self, cx: &WindowContext) -> bool { - self.read(cx).should_show_badge(cx) - } - - fn is_focused(&self, cx: &WindowContext) -> bool { - ViewHandle::is_focused(self, cx) || self.read(cx).contains_focused_view(cx) - } - - fn as_any(&self) -> &AnyViewHandle { - self - } -} - -impl From<&dyn SidebarItemHandle> for AnyViewHandle { - fn from(val: &dyn SidebarItemHandle) -> Self { - val.as_any().clone() - } -} - -pub struct Sidebar { - sidebar_side: SidebarSide, - items: Vec, - is_open: bool, - active_item_ix: usize, -} - -#[derive(Clone, Copy, Debug, Deserialize, PartialEq)] -pub enum SidebarSide { - Left, - Right, -} - -impl SidebarSide { - fn to_resizable_side(self) -> Side { - match self { - Self::Left => Side::Right, - Self::Right => Side::Left, - } - } -} - -struct Item { - icon_path: &'static str, - tooltip: String, - view: Rc, - _subscriptions: [Subscription; 2], -} - -pub struct SidebarButtons { - sidebar: ViewHandle, - workspace: WeakViewHandle, -} - -#[derive(Clone, Debug, Deserialize, PartialEq)] -pub struct ToggleSidebarItem { - pub sidebar_side: SidebarSide, - pub item_index: usize, -} - -impl_actions!(workspace, [ToggleSidebarItem]); - -impl Sidebar { - pub fn new(sidebar_side: SidebarSide) -> Self { - Self { - sidebar_side, - items: Default::default(), - active_item_ix: 0, - is_open: false, - } - } - - pub fn is_open(&self) -> bool { - self.is_open - } - - pub fn active_item_ix(&self) -> usize { - self.active_item_ix - } - - pub fn set_open(&mut self, open: bool, cx: &mut ViewContext) { - if open != self.is_open { - self.is_open = open; - cx.notify(); - } - } - - pub fn toggle_open(&mut self, cx: &mut ViewContext) { - if self.is_open {} - self.is_open = !self.is_open; - cx.notify(); - } - - pub fn add_item( - &mut self, - icon_path: &'static str, - tooltip: String, - view: ViewHandle, - cx: &mut ViewContext, - ) { - let subscriptions = [ - cx.observe(&view, |_, _, cx| cx.notify()), - cx.subscribe(&view, |this, view, event, cx| { - if view.read(cx).should_activate_item_on_event(event, cx) { - if let Some(ix) = this - .items - .iter() - .position(|item| item.view.id() == view.id()) - { - this.activate_item(ix, cx); - } - } - }), - ]; - - self.items.push(Item { - icon_path, - tooltip, - view: Rc::new(view), - _subscriptions: subscriptions, - }); - cx.notify() - } - - pub fn activate_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - self.active_item_ix = item_ix; - cx.notify(); - } - - pub fn toggle_item(&mut self, item_ix: usize, cx: &mut ViewContext) { - if self.active_item_ix == item_ix { - self.is_open = false; - } else { - self.active_item_ix = item_ix; - } - cx.notify(); - } - - pub fn active_item(&self) -> Option<&Rc> { - if self.is_open { - self.items.get(self.active_item_ix).map(|item| &item.view) - } else { - None - } - } -} - -impl Entity for Sidebar { - type Event = (); -} - -impl View for Sidebar { - fn ui_name() -> &'static str { - "Sidebar" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - if let Some(active_item) = self.active_item() { - enum ResizeHandleTag {} - let style = &theme::current(cx).workspace.sidebar; - ChildView::new(active_item.as_any(), cx) - .contained() - .with_style(style.container) - .with_resize_handle::( - self.sidebar_side as usize, - self.sidebar_side.to_resizable_side(), - 4., - style.initial_size, - cx, - ) - .into_any() - } else { - Empty::new().into_any() - } - } -} - -impl SidebarButtons { - pub fn new( - sidebar: ViewHandle, - workspace: WeakViewHandle, - cx: &mut ViewContext, - ) -> Self { - cx.observe(&sidebar, |_, _, cx| cx.notify()).detach(); - Self { sidebar, workspace } - } -} - -impl Entity for SidebarButtons { - type Event = (); -} - -impl View for SidebarButtons { - fn ui_name() -> &'static str { - "SidebarToggleButton" - } - - fn render(&mut self, cx: &mut ViewContext) -> AnyElement { - let theme = &theme::current(cx); - let tooltip_style = theme.tooltip.clone(); - let theme = &theme.workspace.status_bar.sidebar_buttons; - let sidebar = self.sidebar.read(cx); - let item_style = theme.item.clone(); - let badge_style = theme.badge; - let active_ix = sidebar.active_item_ix; - let is_open = sidebar.is_open; - let sidebar_side = sidebar.sidebar_side; - let group_style = match sidebar_side { - SidebarSide::Left => theme.group_left, - SidebarSide::Right => theme.group_right, - }; - - #[allow(clippy::needless_collect)] - let items = sidebar - .items - .iter() - .map(|item| (item.icon_path, item.tooltip.clone(), item.view.clone())) - .collect::>(); - - Flex::row() - .with_children(items.into_iter().enumerate().map( - |(ix, (icon_path, tooltip, item_view))| { - let action = ToggleSidebarItem { - sidebar_side, - item_index: ix, - }; - MouseEventHandler::::new(ix, cx, |state, cx| { - let is_active = is_open && ix == active_ix; - let style = item_style.style_for(state, is_active); - Stack::new() - .with_child(Svg::new(icon_path).with_color(style.icon_color)) - .with_children(if !is_active && item_view.should_show_badge(cx) { - Some( - Empty::new() - .collapsed() - .contained() - .with_style(badge_style) - .aligned() - .bottom() - .right(), - ) - } else { - None - }) - .constrained() - .with_width(style.icon_size) - .with_height(style.icon_size) - .contained() - .with_style(style.container) - }) - .with_cursor_style(CursorStyle::PointingHand) - .on_click(MouseButton::Left, { - let action = action.clone(); - move |_, this, cx| { - if let Some(workspace) = this.workspace.upgrade(cx) { - let action = action.clone(); - cx.window_context().defer(move |cx| { - workspace.update(cx, |workspace, cx| { - workspace.toggle_sidebar_item(&action, cx) - }); - }); - } - } - }) - .with_tooltip::( - ix, - tooltip, - Some(Box::new(action)), - tooltip_style.clone(), - cx, - ) - }, - )) - .contained() - .with_style(group_style) - .into_any() - } -} - -impl StatusItemView for SidebarButtons { - fn set_active_pane_item( - &mut self, - _: Option<&dyn crate::ItemHandle>, - _: &mut ViewContext, - ) { - } -} diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 8a5ce7a105d69ef7d5feb3e8a2cd9f9d39dbd28a..495989c1bed17e13084b73189dc7bac562cae2dd 100644 --- a/crates/workspace/src/workspace.rs +++ b/crates/workspace/src/workspace.rs @@ -1,8 +1,8 @@ +pub mod dock; /// NOTE: Focus only 'takes' after an update has flushed_effects. /// /// This may cause issues when you're trying to write tests that use workspace focus to add items at /// specific locations. -pub mod dock; pub mod item; pub mod notifications; pub mod pane; @@ -10,7 +10,6 @@ pub mod pane_group; mod persistence; pub mod searchable; pub mod shared_screen; -pub mod sidebar; mod status_bar; mod toolbar; mod workspace_settings; @@ -23,7 +22,6 @@ use client::{ Client, TypedEnvelope, UserStore, }; use collections::{hash_map, HashMap, HashSet}; -use dock::{Dock, DockDefaultItemFactory, ToggleDockButton}; use drag_and_drop::DragAndDrop; use futures::{ channel::{mpsc, oneshot}, @@ -62,10 +60,12 @@ use std::{ use crate::{ notifications::simple_message_notification::MessageNotification, - persistence::model::{SerializedPane, SerializedPaneGroup, SerializedWorkspace}, + persistence::model::{ + DockData, DockStructure, SerializedPane, SerializedPaneGroup, SerializedWorkspace, + }, }; +use dock::{Dock, DockPosition, Panel, PanelButtons, PanelHandle, TogglePanel}; use lazy_static::lazy_static; -use log::warn; use notifications::{NotificationHandle, NotifyResultExt}; pub use pane::*; pub use pane_group::*; @@ -78,13 +78,12 @@ use postage::prelude::Stream; use project::{Project, ProjectEntryId, ProjectPath, Worktree, WorktreeId}; use serde::Deserialize; use shared_screen::SharedScreen; -use sidebar::{Sidebar, SidebarButtons, SidebarSide, ToggleSidebarItem}; use status_bar::StatusBar; pub use status_bar::StatusItemView; use theme::Theme; pub use toolbar::{ToolbarItemLocation, ToolbarItemView}; use util::{async_iife, paths, ResultExt}; -pub use workspace_settings::{AutosaveSetting, DockAnchor, GitGutterSetting, WorkspaceSettings}; +pub use workspace_settings::{AutosaveSetting, GitGutterSetting, WorkspaceSettings}; lazy_static! { static ref ZED_WINDOW_SIZE: Option = env::var("ZED_WINDOW_SIZE") @@ -119,12 +118,14 @@ actions!( ActivatePreviousPane, ActivateNextPane, FollowNextCollaborator, - ToggleLeftSidebar, + ToggleLeftDock, NewTerminal, + ToggleTerminalFocus, NewSearch, Feedback, Restart, - Welcome + Welcome, + ToggleZoom, ] ); @@ -192,7 +193,6 @@ pub fn init_settings(cx: &mut AppContext) { pub fn init(app_state: Arc, cx: &mut AppContext) { init_settings(cx); pane::init(cx); - dock::init(cx); notifications::init(cx); cx.add_global_action({ @@ -240,15 +240,15 @@ pub fn init(app_state: Arc, cx: &mut AppContext) { workspace.save_active_item(true, cx).detach_and_log_err(cx); }, ); - cx.add_action(Workspace::toggle_sidebar_item); + cx.add_action(Workspace::toggle_panel); cx.add_action(|workspace: &mut Workspace, _: &ActivatePreviousPane, cx| { workspace.activate_previous_pane(cx) }); cx.add_action(|workspace: &mut Workspace, _: &ActivateNextPane, cx| { workspace.activate_next_pane(cx) }); - cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftSidebar, cx| { - workspace.toggle_sidebar(SidebarSide::Left, cx); + cx.add_action(|workspace: &mut Workspace, _: &ToggleLeftDock, cx| { + workspace.toggle_dock(DockPosition::Left, cx); }); cx.add_action(Workspace::activate_pane_at_index); @@ -366,8 +366,8 @@ pub struct AppState { pub fs: Arc, pub build_window_options: fn(Option, Option, &dyn Platform) -> WindowOptions<'static>, - pub initialize_workspace: fn(&mut Workspace, &Arc, &mut ViewContext), - pub dock_default_item_factory: DockDefaultItemFactory, + pub initialize_workspace: + fn(WeakViewHandle, bool, Arc, AsyncAppContext) -> Task>, pub background_actions: BackgroundActions, } @@ -395,9 +395,8 @@ impl AppState { fs, languages, user_store, - initialize_workspace: |_, _, _| {}, + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), build_window_options: |_, _, _| Default::default(), - dock_default_item_factory: |_, _| None, background_actions: || &[], }) } @@ -450,7 +449,6 @@ impl DelayedDebouncedEditAction { } pub enum Event { - DockAnchorChanged, PaneAdded(ViewHandle), ContactRequestedJoin(u64), } @@ -460,15 +458,15 @@ pub struct Workspace { remote_entity_subscription: Option, modal: Option, center: PaneGroup, - left_sidebar: ViewHandle, - right_sidebar: ViewHandle, + left_dock: ViewHandle, + bottom_dock: ViewHandle, + right_dock: ViewHandle, panes: Vec>, panes_by_item: HashMap>, active_pane: ViewHandle, last_active_center_pane: Option>, status_bar: ViewHandle, titlebar_item: Option, - dock: Dock, notifications: Vec<(TypeId, usize, Box)>, project: ModelHandle, leader_state: LeaderState, @@ -479,7 +477,7 @@ pub struct Workspace { leader_updates_tx: mpsc::UnboundedSender<(PeerId, proto::UpdateFollowers)>, database_id: WorkspaceId, app_state: Arc, - _window_subscriptions: [Subscription; 3], + subscriptions: Vec, _apply_leader_updates: Task>, _observe_current_user: Task>, pane_history_timestamp: Arc, @@ -557,7 +555,6 @@ impl Workspace { let center_pane = cx.add_view(|cx| { Pane::new( weak_handle.clone(), - None, app_state.background_actions, pane_history_timestamp.clone(), cx, @@ -566,13 +563,6 @@ impl Workspace { cx.subscribe(¢er_pane, Self::handle_pane_event).detach(); cx.focus(¢er_pane); cx.emit(Event::PaneAdded(center_pane.clone())); - let dock = Dock::new( - app_state.dock_default_item_factory, - app_state.background_actions, - pane_history_timestamp.clone(), - cx, - ); - let dock_pane = dock.pane().clone(); let mut current_user = app_state.user_store.read(cx).watch_current_user(); let mut connection_status = app_state.client.status(); @@ -587,7 +577,6 @@ impl Workspace { } anyhow::Ok(()) }); - let handle = cx.handle(); // All leader updates are enqueued and then processed in a single task, so // that each asynchronous operation can be run in order. @@ -605,18 +594,20 @@ impl Workspace { cx.emit_global(WorkspaceCreated(weak_handle.clone())); - let left_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Left)); - let right_sidebar = cx.add_view(|_| Sidebar::new(SidebarSide::Right)); - let left_sidebar_buttons = - cx.add_view(|cx| SidebarButtons::new(left_sidebar.clone(), weak_handle.clone(), cx)); - let toggle_dock = cx.add_view(|cx| ToggleDockButton::new(handle, cx)); - let right_sidebar_buttons = - cx.add_view(|cx| SidebarButtons::new(right_sidebar.clone(), weak_handle.clone(), cx)); + let left_dock = cx.add_view(|_| Dock::new(DockPosition::Left)); + let bottom_dock = cx.add_view(|_| Dock::new(DockPosition::Bottom)); + let right_dock = cx.add_view(|_| Dock::new(DockPosition::Right)); + let left_dock_buttons = + cx.add_view(|cx| PanelButtons::new(left_dock.clone(), weak_handle.clone(), cx)); + let bottom_dock_buttons = + cx.add_view(|cx| PanelButtons::new(bottom_dock.clone(), weak_handle.clone(), cx)); + let right_dock_buttons = + cx.add_view(|cx| PanelButtons::new(right_dock.clone(), weak_handle.clone(), cx)); let status_bar = cx.add_view(|cx| { let mut status_bar = StatusBar::new(¢er_pane.clone(), cx); - status_bar.add_left_item(left_sidebar_buttons, cx); - status_bar.add_right_item(right_sidebar_buttons, cx); - status_bar.add_right_item(toggle_dock, cx); + status_bar.add_left_item(left_dock_buttons, cx); + status_bar.add_right_item(right_dock_buttons, cx); + status_bar.add_right_item(bottom_dock_buttons, cx); status_bar }); @@ -632,7 +623,7 @@ impl Workspace { active_call = Some((call, subscriptions)); } - let subscriptions = [ + let subscriptions = vec![ cx.observe_fullscreen(|_, _, cx| cx.notify()), cx.observe_window_activation(Self::on_window_activation_changed), cx.observe_window_bounds(move |_, mut bounds, display, cx| { @@ -652,17 +643,25 @@ impl Workspace { .spawn(DB.set_window_bounds(workspace_id, bounds, display)) .detach_and_log_err(cx); }), + cx.observe(&left_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), + cx.observe(&bottom_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), + cx.observe(&right_dock, |this, _, cx| { + this.serialize_workspace(cx); + cx.notify(); + }), ]; let mut this = Workspace { - modal: None, weak_self: weak_handle.clone(), + modal: None, center: PaneGroup::new(center_pane.clone()), - dock, - // When removing an item, the last element remaining in this array - // is used to find where focus should fallback to. As such, the order - // of these two variables is important. - panes: vec![dock_pane.clone(), center_pane.clone()], + panes: vec![center_pane.clone()], panes_by_item: Default::default(), active_pane: center_pane.clone(), last_active_center_pane: Some(center_pane.downgrade()), @@ -670,8 +669,9 @@ impl Workspace { titlebar_item: None, notifications: Default::default(), remote_entity_subscription: None, - left_sidebar, - right_sidebar, + left_dock, + bottom_dock, + right_dock, project: project.clone(), leader_state: Default::default(), follower_states_by_leader: Default::default(), @@ -683,12 +683,11 @@ impl Workspace { _observe_current_user, _apply_leader_updates, leader_updates_tx, - _window_subscriptions: subscriptions, + subscriptions, pane_history_timestamp, }; this.project_remote_id_changed(project.read(cx).remote_id(), cx); cx.defer(|this, cx| this.update_window_title(cx)); - this } @@ -750,11 +749,7 @@ impl Workspace { }); let build_workspace = |cx: &mut ViewContext| { - let mut workspace = - Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - - workspace + Workspace::new(workspace_id, project_handle.clone(), app_state.clone(), cx) }; let workspace = requesting_window_id @@ -802,6 +797,17 @@ impl Workspace { .1 }); + (app_state.initialize_workspace)( + workspace.downgrade(), + serialized_workspace.is_some(), + app_state.clone(), + cx.clone(), + ) + .await + .log_err(); + + cx.update_window(workspace.window_id(), |cx| cx.activate_window()); + let workspace = workspace.downgrade(); notify_if_database_failed(&workspace, &mut cx); let opened_items = open_items( @@ -821,12 +827,66 @@ impl Workspace { self.weak_self.clone() } - pub fn left_sidebar(&self) -> &ViewHandle { - &self.left_sidebar + pub fn left_dock(&self) -> &ViewHandle { + &self.left_dock } - pub fn right_sidebar(&self) -> &ViewHandle { - &self.right_sidebar + pub fn bottom_dock(&self) -> &ViewHandle { + &self.bottom_dock + } + + pub fn right_dock(&self) -> &ViewHandle { + &self.right_dock + } + + pub fn add_panel(&mut self, panel: ViewHandle, cx: &mut ViewContext) { + let dock = match panel.position(cx) { + DockPosition::Left => &self.left_dock, + DockPosition::Bottom => &self.bottom_dock, + DockPosition::Right => &self.right_dock, + }; + + self.subscriptions.push(cx.subscribe(&panel, { + let mut dock = dock.clone(); + let mut prev_position = panel.position(cx); + move |this, panel, event, cx| { + if T::should_change_position_on_event(event) { + let new_position = panel.read(cx).position(cx); + let mut was_visible = false; + dock.update(cx, |dock, cx| { + prev_position = new_position; + + was_visible = dock.is_open() + && dock + .active_panel() + .map_or(false, |active_panel| active_panel.id() == panel.id()); + dock.remove_panel(&panel, cx); + }); + dock = match panel.read(cx).position(cx) { + DockPosition::Left => &this.left_dock, + DockPosition::Bottom => &this.bottom_dock, + DockPosition::Right => &this.right_dock, + } + .clone(); + dock.update(cx, |dock, cx| { + dock.add_panel(panel.clone(), cx); + if was_visible { + dock.set_open(true, cx); + dock.activate_panel(dock.panels_len() - 1, cx); + } + }); + } else if T::should_zoom_in_on_event(event) { + this.zoom_out(cx); + dock.update(cx, |dock, cx| dock.set_panel_zoomed(&panel, true, cx)); + } else if T::should_zoom_out_on_event(event) { + this.zoom_out(cx); + } else if T::is_focus_event(event) { + cx.notify(); + } + } + })); + + dock.update(cx, |dock, cx| dock.add_panel(panel, cx)); } pub fn status_bar(&self) -> &ViewHandle { @@ -1272,6 +1332,44 @@ impl Workspace { } } + fn zoomed(&self, cx: &WindowContext) -> Option { + self.zoomed_panel_for_dock(DockPosition::Left, cx) + .or_else(|| self.zoomed_panel_for_dock(DockPosition::Bottom, cx)) + .or_else(|| self.zoomed_panel_for_dock(DockPosition::Right, cx)) + .or_else(|| self.zoomed_pane(cx)) + } + + fn zoomed_panel_for_dock( + &self, + position: DockPosition, + cx: &WindowContext, + ) -> Option { + let (dock, other_docks) = match position { + DockPosition::Left => (&self.left_dock, [&self.bottom_dock, &self.right_dock]), + DockPosition::Bottom => (&self.bottom_dock, [&self.left_dock, &self.right_dock]), + DockPosition::Right => (&self.right_dock, [&self.left_dock, &self.bottom_dock]), + }; + + let zoomed_panel = dock.read(&cx).zoomed_panel(cx)?; + if other_docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) + && !self.active_pane.read(cx).has_focus() + { + Some(zoomed_panel.as_any().clone()) + } else { + None + } + } + + fn zoomed_pane(&self, cx: &WindowContext) -> Option { + let active_pane = self.active_pane.read(cx); + let docks = [&self.left_dock, &self.bottom_dock, &self.right_dock]; + if active_pane.is_zoomed() && docks.iter().all(|dock| !dock.read(cx).has_focus(cx)) { + Some(self.active_pane.clone().into_any()) + } else { + None + } + } + pub fn items<'a>( &'a self, cx: &'a AppContext, @@ -1349,47 +1447,42 @@ impl Workspace { } } - pub fn toggle_sidebar(&mut self, sidebar_side: SidebarSide, cx: &mut ViewContext) { - let sidebar = match sidebar_side { - SidebarSide::Left => &mut self.left_sidebar, - SidebarSide::Right => &mut self.right_sidebar, + pub fn toggle_dock(&mut self, dock_side: DockPosition, cx: &mut ViewContext) { + let dock = match dock_side { + DockPosition::Left => &mut self.left_dock, + DockPosition::Bottom => &mut self.bottom_dock, + DockPosition::Right => &mut self.right_dock, }; - let open = sidebar.update(cx, |sidebar, cx| { - let open = !sidebar.is_open(); - sidebar.set_open(open, cx); - open + dock.update(cx, |dock, cx| { + let open = !dock.is_open(); + dock.set_open(open, cx); }); - if open { - Dock::hide_on_sidebar_shown(self, sidebar_side, cx); - } - self.serialize_workspace(cx); cx.focus_self(); cx.notify(); } - pub fn toggle_sidebar_item(&mut self, action: &ToggleSidebarItem, cx: &mut ViewContext) { - let sidebar = match action.sidebar_side { - SidebarSide::Left => &mut self.left_sidebar, - SidebarSide::Right => &mut self.right_sidebar, + pub fn toggle_panel(&mut self, action: &TogglePanel, cx: &mut ViewContext) { + let dock = match action.dock_position { + DockPosition::Left => &mut self.left_dock, + DockPosition::Bottom => &mut self.bottom_dock, + DockPosition::Right => &mut self.right_dock, }; - let active_item = sidebar.update(cx, move |sidebar, cx| { - if sidebar.is_open() && sidebar.active_item_ix() == action.item_index { - sidebar.set_open(false, cx); + let active_item = dock.update(cx, move |dock, cx| { + if dock.is_open() && dock.active_panel_index() == action.panel_index { + dock.set_open(false, cx); None } else { - sidebar.set_open(true, cx); - sidebar.activate_item(action.item_index, cx); - sidebar.active_item().cloned() + dock.set_open(true, cx); + dock.activate_panel(action.panel_index, cx); + dock.active_panel().cloned() } }); if let Some(active_item) = active_item { - Dock::hide_on_sidebar_shown(self, action.sidebar_side, cx); - - if active_item.is_focused(cx) { + if active_item.has_focus(cx) { cx.focus_self(); } else { cx.focus(active_item.as_any()); @@ -1403,32 +1496,37 @@ impl Workspace { cx.notify(); } - pub fn toggle_sidebar_item_focus( - &mut self, - sidebar_side: SidebarSide, - item_index: usize, - cx: &mut ViewContext, - ) { - let sidebar = match sidebar_side { - SidebarSide::Left => &mut self.left_sidebar, - SidebarSide::Right => &mut self.right_sidebar, - }; - let active_item = sidebar.update(cx, |sidebar, cx| { - sidebar.set_open(true, cx); - sidebar.activate_item(item_index, cx); - sidebar.active_item().cloned() - }); - if let Some(active_item) = active_item { - Dock::hide_on_sidebar_shown(self, sidebar_side, cx); + pub fn toggle_panel_focus(&mut self, cx: &mut ViewContext) { + for dock in [&self.left_dock, &self.bottom_dock, &self.right_dock] { + if let Some(panel_index) = dock.read(cx).panel_index_for_type::() { + let active_item = dock.update(cx, |dock, cx| { + dock.set_open(true, cx); + dock.activate_panel(panel_index, cx); + dock.active_panel().cloned() + }); + if let Some(active_item) = active_item { + if active_item.has_focus(cx) { + cx.focus_self(); + } else { + cx.focus(active_item.as_any()); + } + } - if active_item.is_focused(cx) { - cx.focus_self(); - } else { - cx.focus(active_item.as_any()); + self.serialize_workspace(cx); + cx.notify(); + break; } } + } - self.serialize_workspace(cx); + fn zoom_out(&mut self, cx: &mut ViewContext) { + for pane in &self.panes { + pane.update(cx, |pane, cx| pane.set_zoomed(false, cx)); + } + + self.left_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + self.bottom_dock.update(cx, |dock, cx| dock.zoom_out(cx)); + self.right_dock.update(cx, |dock, cx| dock.zoom_out(cx)); cx.notify(); } @@ -1437,7 +1535,6 @@ impl Workspace { let pane = cx.add_view(|cx| { Pane::new( self.weak_handle(), - None, self.app_state.background_actions, self.pane_history_timestamp.clone(), cx, @@ -1480,16 +1577,12 @@ impl Workspace { cx: &mut ViewContext, ) -> Task, anyhow::Error>> { let pane = pane.unwrap_or_else(|| { - if !self.dock_active() { - self.active_pane().downgrade() - } else { - self.last_active_center_pane.clone().unwrap_or_else(|| { - self.panes - .first() - .expect("There must be an active pane") - .downgrade() - }) - } + self.last_active_center_pane.clone().unwrap_or_else(|| { + self.panes + .first() + .expect("There must be an active pane") + .downgrade() + }) }); let task = self.load_path(path.into(), cx); @@ -1568,9 +1661,6 @@ impl Workspace { .map(|ix| (pane.clone(), ix)) }); if let Some((pane, ix)) = result { - if &pane == self.dock_pane() { - Dock::show(self, false, cx); - } pane.update(cx, |pane, cx| pane.activate_item(ix, true, true, cx)); true } else { @@ -1616,16 +1706,7 @@ impl Workspace { status_bar.set_active_pane(&self.active_pane, cx); }); self.active_item_path_changed(cx); - - if &pane == self.dock_pane() { - Dock::show(self, false, cx); - } else { - self.last_active_center_pane = Some(pane.downgrade()); - if self.dock.is_anchored_at(DockAnchor::Expanded) { - Dock::hide(self, cx); - } - } - cx.notify(); + self.last_active_center_pane = Some(pane.downgrade()); } self.update_followers( @@ -1639,6 +1720,8 @@ impl Workspace { }), cx, ); + + cx.notify(); } fn handle_pane_event( @@ -1647,13 +1730,11 @@ impl Workspace { event: &pane::Event, cx: &mut ViewContext, ) { - let is_dock = &pane == self.dock.pane(); match event { - pane::Event::Split(direction) if !is_dock => { + pane::Event::Split(direction) => { self.split_pane(pane, *direction, cx); } - pane::Event::Remove if !is_dock => self.remove_pane(pane, cx), - pane::Event::Remove if is_dock => Dock::hide(self, cx), + pane::Event::Remove => self.remove_pane(pane, cx), pane::Event::ActivateItem { local } => { if *local { self.unfollow(&pane, cx); @@ -1679,7 +1760,14 @@ impl Workspace { pane::Event::Focus => { self.handle_pane_focused(pane.clone(), cx); } - _ => {} + pane::Event::ZoomIn => { + if pane == self.active_pane { + self.zoom_out(cx); + pane.update(cx, |pane, cx| pane.set_zoomed(true, cx)); + cx.notify(); + } + } + pane::Event::ZoomOut => self.zoom_out(cx), } self.serialize_workspace(cx); @@ -1691,11 +1779,6 @@ impl Workspace { direction: SplitDirection, cx: &mut ViewContext, ) -> Option> { - if &pane == self.dock_pane() { - warn!("Can't split dock pane."); - return None; - } - let item = pane.read(cx).active_item()?; let maybe_pane_handle = if let Some(clone) = item.clone_on_split(self.database_id(), cx) { let new_pane = self.add_pane(cx); @@ -1719,10 +1802,6 @@ impl Workspace { ) { let Some(pane_to_split) = pane_to_split.upgrade(cx) else { return; }; let Some(from) = from.upgrade(cx) else { return; }; - if &pane_to_split == self.dock_pane() { - warn!("Can't split dock pane."); - return; - } let new_pane = self.add_pane(cx); Pane::move_item(self, from.clone(), new_pane.clone(), item_id_to_move, 0, cx); @@ -1740,11 +1819,6 @@ impl Workspace { cx: &mut ViewContext, ) -> Option>> { let pane_to_split = pane_to_split.upgrade(cx)?; - if &pane_to_split == self.dock_pane() { - warn!("Can't split dock pane."); - return None; - } - let new_pane = self.add_pane(cx); self.center .split(&pane_to_split, &new_pane, split_direction) @@ -1781,14 +1855,6 @@ impl Workspace { &self.active_pane } - pub fn dock_pane(&self) -> &ViewHandle { - self.dock.pane() - } - - fn dock_active(&self) -> bool { - &self.active_pane == self.dock.pane() - } - fn project_remote_id_changed(&mut self, remote_id: Option, cx: &mut ViewContext) { if let Some(remote_id) = remote_id { self.remote_entity_subscription = Some( @@ -2530,23 +2596,65 @@ impl Workspace { } } + fn build_serialized_docks(this: &Workspace, cx: &AppContext) -> DockStructure { + let left_dock = this.left_dock.read(cx); + let left_visible = left_dock.is_open(); + let left_active_panel = left_dock.active_panel().and_then(|panel| { + Some( + cx.view_ui_name(panel.as_any().window_id(), panel.id())? + .to_string(), + ) + }); + + let right_dock = this.right_dock.read(cx); + let right_visible = right_dock.is_open(); + let right_active_panel = right_dock.active_panel().and_then(|panel| { + Some( + cx.view_ui_name(panel.as_any().window_id(), panel.id())? + .to_string(), + ) + }); + + let bottom_dock = this.bottom_dock.read(cx); + let bottom_visible = bottom_dock.is_open(); + let bottom_active_panel = bottom_dock.active_panel().and_then(|panel| { + Some( + cx.view_ui_name(panel.as_any().window_id(), panel.id())? + .to_string(), + ) + }); + + DockStructure { + left: DockData { + visible: left_visible, + active_panel: left_active_panel, + }, + right: DockData { + visible: right_visible, + active_panel: right_active_panel, + }, + bottom: DockData { + visible: bottom_visible, + active_panel: bottom_active_panel, + }, + } + } + if let Some(location) = self.location(cx) { // Load bearing special case: // - with_local_workspace() relies on this to not have other stuff open // when you open your log if !location.paths().is_empty() { - let dock_pane = serialize_pane_handle(self.dock.pane(), cx); let center_group = build_serialized_pane_group(&self.center.root, cx); + let docks = build_serialized_docks(self, cx); let serialized_workspace = SerializedWorkspace { id: self.database_id, location, - dock_position: self.dock.position(), - dock_pane, center_group, - left_sidebar_open: self.left_sidebar.read(cx).is_open(), bounds: Default::default(), display: Default::default(), + docks, }; cx.background() @@ -2564,26 +2672,14 @@ impl Workspace { ) -> Task, anyhow::Error>>>> { cx.spawn(|mut cx| async move { let result = async_iife! {{ - let (project, dock_pane_handle, old_center_pane) = + let (project, old_center_pane) = workspace.read_with(&cx, |workspace, _| { ( workspace.project().clone(), - workspace.dock_pane().downgrade(), workspace.last_active_center_pane.clone(), ) })?; - let dock_items = serialized_workspace - .dock_pane - .deserialize_to( - &project, - &dock_pane_handle, - serialized_workspace.id, - &workspace, - &mut cx, - ) - .await?; - let mut center_items = None; let mut center_group = None; // Traverse the splits tree and add to things @@ -2599,7 +2695,6 @@ impl Workspace { let mut opened_items = center_items .unwrap_or_default() .into_iter() - .chain(dock_items.into_iter()) .filter_map(|item| { let item = item?; let project_path = item.project_path(cx)?; @@ -2645,22 +2740,30 @@ impl Workspace { } } - if workspace.left_sidebar().read(cx).is_open() - != serialized_workspace.left_sidebar_open - { - workspace.toggle_sidebar(SidebarSide::Left, cx); - } - - // Note that without after_window, the focus_self() and - // the focus the dock generates start generating alternating - // focus due to the deferred execution each triggering each other - cx.after_window_update(move |workspace, cx| { - Dock::set_dock_position( - workspace, - serialized_workspace.dock_position, - false, - cx, - ); + let docks = serialized_workspace.docks; + workspace.left_dock.update(cx, |dock, cx| { + dock.set_open(docks.left.visible, cx); + if let Some(active_panel) = docks.left.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + }); + workspace.right_dock.update(cx, |dock, cx| { + dock.set_open(docks.right.visible, cx); + if let Some(active_panel) = docks.right.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } + }); + workspace.bottom_dock.update(cx, |dock, cx| { + dock.set_open(docks.bottom.visible, cx); + if let Some(active_panel) = docks.bottom.active_panel { + if let Some(ix) = dock.panel_index_for_ui_name(&active_panel, cx) { + dock.activate_panel(ix, cx); + } + } }); cx.notify(); @@ -2684,12 +2787,38 @@ impl Workspace { user_store: project.read(cx).user_store(), fs: project.read(cx).fs().clone(), build_window_options: |_, _, _| Default::default(), - initialize_workspace: |_, _, _| {}, - dock_default_item_factory: |_, _| None, + initialize_workspace: |_, _, _, _| Task::ready(Ok(())), background_actions: || &[], }); Self::new(0, project, app_state, cx) } + + fn render_dock(&self, position: DockPosition, cx: &WindowContext) -> Option> { + let dock = match position { + DockPosition::Left => &self.left_dock, + DockPosition::Right => &self.right_dock, + DockPosition::Bottom => &self.bottom_dock, + }; + let active_panel = dock.read(cx).active_panel()?; + let element = if Some(active_panel.as_any()) == self.zoomed(cx).as_ref() { + dock.read(cx).render_placeholder(cx) + } else { + ChildView::new(dock, cx).into_any() + }; + + Some( + element + .constrained() + .dynamically(move |constraint, _, cx| match position { + DockPosition::Left | DockPosition::Right => SizeConstraint::new( + Vector2F::new(20., constraint.min.y()), + Vector2F::new(cx.window_size().x() * 0.8, constraint.max.y()), + ), + _ => constraint, + }) + .into_any(), + ) + } } async fn open_items( @@ -2835,76 +2964,46 @@ impl View for Workspace { .with_child({ let project = self.project.clone(); Flex::row() - .with_children( - if self.left_sidebar.read(cx).active_item().is_some() { - Some( - ChildView::new(&self.left_sidebar, cx) - .constrained() - .dynamically(|constraint, _, cx| { - SizeConstraint::new( - Vector2F::new(20., constraint.min.y()), - Vector2F::new( - cx.window_size().x() * 0.8, - constraint.max.y(), - ), - ) - }), - ) - } else { - None - }, - ) + .with_children(self.render_dock(DockPosition::Left, cx)) .with_child( - FlexItem::new( - Flex::column() - .with_child( - FlexItem::new(self.center.render( - &project, - &theme, - &self.follower_states_by_leader, - self.active_call(), - self.active_pane(), - &self.app_state, - cx, - )) - .flex(1., true), - ) - .with_children(self.dock.render( + Flex::column() + .with_child( + FlexItem::new(self.center.render( + &project, &theme, - DockAnchor::Bottom, + &self.follower_states_by_leader, + self.active_call(), + self.active_pane(), + self.zoomed(cx).as_ref(), + &self.app_state, cx, - )), - ) - .flex(1., true), - ) - .with_children(self.dock.render(&theme, DockAnchor::Right, cx)) - .with_children( - if self.right_sidebar.read(cx).active_item().is_some() { - Some( - ChildView::new(&self.right_sidebar, cx) - .constrained() - .dynamically(|constraint, _, cx| { - SizeConstraint::new( - Vector2F::new(20., constraint.min.y()), - Vector2F::new( - cx.window_size().x() * 0.8, - constraint.max.y(), - ), - ) - }), + )) + .flex(1., true), ) - } else { - None - }, + .with_children( + self.render_dock(DockPosition::Bottom, cx), + ) + .flex(1., true), ) + .with_children(self.render_dock(DockPosition::Right, cx)) }) .with_child(Overlay::new( Stack::new() - .with_children(self.dock.render( - &theme, - DockAnchor::Expanded, - cx, - )) + .with_children(self.zoomed(cx).map(|zoomed| { + enum ZoomBackground {} + + ChildView::new(&zoomed, cx) + .contained() + .with_style(theme.workspace.zoomed_foreground) + .aligned() + .contained() + .with_style(theme.workspace.zoomed_background) + .mouse::(0) + .capture_all() + .on_down(MouseButton::Left, |_, this: &mut Self, cx| { + this.zoom_out(cx); + }) + })) .with_children(self.modal.as_ref().map(|modal| { ChildView::new(modal, cx) .contained() @@ -2929,6 +3028,7 @@ impl View for Workspace { if cx.is_self_focused() { cx.focus(&self.active_pane); } + cx.notify(); } } @@ -3038,28 +3138,11 @@ pub fn open_paths( .await, )) } else { - let contains_directory = - futures::future::join_all(abs_paths.iter().map(|path| app_state.fs.is_file(path))) - .await - .contains(&false); - - cx.update(|cx| { - let task = - Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx); - - cx.spawn(|mut cx| async move { - let (workspace, items) = task.await; - - workspace.update(&mut cx, |workspace, cx| { - if contains_directory { - workspace.toggle_sidebar(SidebarSide::Left, cx); - } - })?; - - anyhow::Ok((workspace, items)) + Ok(cx + .update(|cx| { + Workspace::new_local(abs_paths, app_state.clone(), requesting_window_id, cx) }) - }) - .await + .await) } }) } @@ -3148,12 +3231,17 @@ pub fn join_remote_project( let (_, workspace) = cx.add_window( (app_state.build_window_options)(None, None, cx.platform().as_ref()), - |cx| { - let mut workspace = Workspace::new(0, project, app_state.clone(), cx); - (app_state.initialize_workspace)(&mut workspace, &app_state, cx); - workspace - }, + |cx| Workspace::new(0, project, app_state.clone(), cx), ); + (app_state.initialize_workspace)( + workspace.downgrade(), + false, + app_state.clone(), + cx.clone(), + ) + .await + .log_err(); + workspace.downgrade() }; @@ -3256,7 +3344,10 @@ fn parse_pixel_position_env_var(value: &str) -> Option { #[cfg(test)] mod tests { use super::*; - use crate::item::test::{TestItem, TestItemEvent, TestProjectItem}; + use crate::{ + dock::test::{TestPanel, TestPanelEvent}, + item::test::{TestItem, TestItemEvent, TestProjectItem}, + }; use fs::FakeFs; use gpui::{executor::Deterministic, TestAppContext}; use project::{Project, ProjectEntryId}; @@ -3335,6 +3426,7 @@ mod tests { let project = Project::test(fs, ["root1".as_ref()], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let worktree_id = project.read_with(cx, |project, cx| { project.worktrees(cx).next().unwrap().read(cx).id() }); @@ -3377,12 +3469,11 @@ mod tests { }); // Close the active item - workspace - .update(cx, |workspace, cx| { - Pane::close_active_item(workspace, &Default::default(), cx).unwrap() - }) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + pane.close_active_item(&Default::default(), cx).unwrap() + }) + .await + .unwrap(); assert_eq!( cx.current_window_title(window_id).as_deref(), Some("one.txt — root1") @@ -3491,18 +3582,13 @@ mod tests { workspace.active_pane().clone() }); - let close_items = workspace.update(cx, |workspace, cx| { - pane.update(cx, |pane, cx| { - pane.activate_item(1, true, true, cx); - assert_eq!(pane.active_item().unwrap().id(), item2.id()); - }); - + let close_items = pane.update(cx, |pane, cx| { + pane.activate_item(1, true, true, cx); + assert_eq!(pane.active_item().unwrap().id(), item2.id()); let item1_id = item1.id(); let item3_id = item3.id(); let item4_id = item4.id(); - Pane::close_items(workspace, pane.clone(), cx, move |id| { - [item1_id, item3_id, item4_id].contains(&id) - }) + pane.close_items(cx, move |id| [item1_id, item3_id, item4_id].contains(&id)) }); cx.foreground().run_until_parked(); @@ -3636,10 +3722,7 @@ mod tests { // once for project entry 0, and once for project entry 2. After those two // prompts, the task should complete. - let close = workspace.update(cx, |workspace, cx| { - Pane::close_items(workspace, left_pane.clone(), cx, |_| true) - }); - + let close = left_pane.update(cx, |pane, cx| pane.close_items(cx, |_| true)); cx.foreground().run_until_parked(); left_pane.read_with(cx, |pane, cx| { assert_eq!( @@ -3673,6 +3756,7 @@ mod tests { let project = Project::test(fs, [], cx).await; let (window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let item = cx.add_view(window_id, |cx| { TestItem::new().with_project_items(&[TestProjectItem::new(1, "1.txt", cx)]) @@ -3753,11 +3837,7 @@ mod tests { item.is_dirty = true; }); - workspace - .update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); - Pane::close_items(workspace, pane, cx, move |id| id == item_id) - }) + pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)) .await .unwrap(); assert!(!cx.has_pending_prompt(window_id)); @@ -3778,10 +3858,8 @@ mod tests { item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); // Ensure autosave is prevented for deleted files also when closing the buffer. - let _close_items = workspace.update(cx, |workspace, cx| { - let pane = workspace.active_pane().clone(); - Pane::close_items(workspace, pane, cx, move |id| id == item_id) - }); + let _close_items = + pane.update(cx, |pane, cx| pane.close_items(cx, move |id| id == item_id)); deterministic.run_until_parked(); assert!(cx.has_pending_prompt(window_id)); item.read_with(cx, |item, _| assert_eq!(item.save_count, 5)); @@ -3843,6 +3921,175 @@ mod tests { }); } + #[gpui::test] + async fn test_panels(cx: &mut gpui::TestAppContext) { + init_test(cx); + let fs = FakeFs::new(cx.background()); + + let project = Project::test(fs, [], cx).await; + let (_window_id, workspace) = cx.add_window(|cx| Workspace::test_new(project, cx)); + + let (panel_1, panel_2) = workspace.update(cx, |workspace, cx| { + // Add panel_1 on the left, panel_2 on the right. + let panel_1 = cx.add_view(|_| TestPanel::new(DockPosition::Left)); + workspace.add_panel(panel_1.clone(), cx); + workspace + .left_dock() + .update(cx, |left_dock, cx| left_dock.set_open(true, cx)); + let panel_2 = cx.add_view(|_| TestPanel::new(DockPosition::Right)); + workspace.add_panel(panel_2.clone(), cx); + workspace + .right_dock() + .update(cx, |right_dock, cx| right_dock.set_open(true, cx)); + + let left_dock = workspace.left_dock(); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!( + left_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx) + ); + + left_dock.update(cx, |left_dock, cx| left_dock.resize_active_panel(1337., cx)); + assert_eq!( + workspace.right_dock().read(cx).active_panel().unwrap().id(), + panel_2.id() + ); + + (panel_1, panel_2) + }); + + // Move panel_1 to the right + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Right, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, it should now be visible now that it's been moved to the right. + // Since it was the only panel on the left, the left dock should now be closed. + assert!(!workspace.left_dock().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).active_panel().is_none()); + let right_dock = workspace.right_dock(); + assert_eq!( + right_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!(right_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + + // Now we move panel_2 to the left + panel_2.set_position(DockPosition::Left, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_2 was not visible on the right, we don't open the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // And the right dock is unaffected in it's displaying of panel_1 + assert!(workspace.right_dock().read(cx).is_open()); + assert_eq!( + workspace.right_dock().read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + }); + + // Move panel_1 back to the left + panel_1.update(cx, |panel_1, cx| { + panel_1.set_position(DockPosition::Left, cx) + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the right, we open the left dock and make panel_1 active. + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert_eq!(left_dock.read(cx).active_panel_size(cx).unwrap(), 1337.); + // And right the dock should be closed as it no longer has any panels. + assert!(!workspace.right_dock().read(cx).is_open()); + + // Now we move panel_1 to the bottom + panel_1.set_position(DockPosition::Bottom, cx); + }); + + workspace.update(cx, |workspace, cx| { + // Since panel_1 was visible on the left, we close the left dock. + assert!(!workspace.left_dock().read(cx).is_open()); + // The bottom dock is sized based on the panel's default size, + // since the panel orientation changed from vertical to horizontal. + let bottom_dock = workspace.bottom_dock(); + assert_eq!( + bottom_dock.read(cx).active_panel_size(cx).unwrap(), + panel_1.size(cx), + ); + // Close bottom dock and move panel_1 back to the left. + bottom_dock.update(cx, |bottom_dock, cx| bottom_dock.set_open(false, cx)); + panel_1.set_position(DockPosition::Left, cx); + }); + + // Emit activated event on panel 1 + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Activated)); + + // Now the left dock is open and panel_1 is active and focused. + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + assert!(panel_1.is_focused(cx)); + }); + + // Emit closed event on panel 2, which is not active + panel_2.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // Wo don't close the left dock, because panel_2 wasn't the active panel + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(left_dock.read(cx).is_open()); + assert_eq!( + left_dock.read(cx).active_panel().unwrap().id(), + panel_1.id() + ); + }); + + // Emitting a ZoomIn event shows the panel as zoomed. + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomIn)); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any())); + }); + + // If focus is transferred elsewhere in the workspace, the panel is no longer zoomed. + workspace.update(cx, |_, cx| cx.focus_self()); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), None); + }); + + // When focus is transferred back to the panel, it is zoomed again. + panel_1.update(cx, |_, cx| cx.focus_self()); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), Some(panel_1.clone().into_any())); + }); + + // Emitting a ZoomOut event unzooms the panel. + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::ZoomOut)); + workspace.read_with(cx, |workspace, cx| { + assert_eq!(workspace.zoomed(cx), None); + }); + + // Emit closed event on panel 1, which is active + panel_1.update(cx, |_, cx| cx.emit(TestPanelEvent::Closed)); + + // Now the left dock is closed, because panel_1 was the active panel + workspace.read_with(cx, |workspace, cx| { + let left_dock = workspace.left_dock(); + assert!(!left_dock.read(cx).is_open()); + }); + } + pub fn init_test(cx: &mut TestAppContext) { cx.foreground().forbid_parking(); cx.update(|cx| { diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 4ec0a22b06800869bce8dcbd0ef13a5e646b2c8b..4202c00a8d7a9fb6384280977493896ab5906fbc 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/crates/workspace/src/workspace_settings.rs @@ -1,8 +1,3 @@ -use anyhow::bail; -use db::sqlez::{ - bindable::{Bind, Column, StaticColumnCount}, - statement::Statement, -}; use schemars::JsonSchema; use serde::{Deserialize, Serialize}; use settings::Setting; @@ -13,7 +8,6 @@ pub struct WorkspaceSettings { pub confirm_quit: bool, pub show_call_status_icon: bool, pub autosave: AutosaveSetting, - pub default_dock_anchor: DockAnchor, pub git: GitSettings, } @@ -23,7 +17,6 @@ pub struct WorkspaceSettingsContent { pub confirm_quit: Option, pub show_call_status_icon: Option, pub autosave: Option, - pub default_dock_anchor: Option, pub git: Option, } @@ -36,15 +29,6 @@ pub enum AutosaveSetting { OnWindowChange, } -#[derive(PartialEq, Eq, Debug, Default, Copy, Clone, Hash, Serialize, Deserialize, JsonSchema)] -#[serde(rename_all = "snake_case")] -pub enum DockAnchor { - #[default] - Bottom, - Right, - Expanded, -} - #[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)] pub struct GitSettings { pub git_gutter: Option, @@ -59,35 +43,6 @@ pub enum GitGutterSetting { Hide, } -impl StaticColumnCount for DockAnchor {} - -impl Bind for DockAnchor { - fn bind(&self, statement: &Statement, start_index: i32) -> anyhow::Result { - match self { - DockAnchor::Bottom => "Bottom", - DockAnchor::Right => "Right", - DockAnchor::Expanded => "Expanded", - } - .bind(statement, start_index) - } -} - -impl Column for DockAnchor { - fn column(statement: &mut Statement, start_index: i32) -> anyhow::Result<(Self, i32)> { - String::column(statement, start_index).and_then(|(anchor_text, next_index)| { - Ok(( - match anchor_text.as_ref() { - "Bottom" => DockAnchor::Bottom, - "Right" => DockAnchor::Right, - "Expanded" => DockAnchor::Expanded, - _ => bail!("Stored dock anchor is incorrect"), - }, - next_index, - )) - }) - } -} - impl Setting for WorkspaceSettings { const KEY: Option<&'static str> = None; diff --git a/crates/zed/src/main.rs b/crates/zed/src/main.rs index 2f359240bc7a2ba295cf3d67579ea1ab5a3411da..c05efd3f027c6911dc5c40e9799758490919a338 100644 --- a/crates/zed/src/main.rs +++ b/crates/zed/src/main.rs @@ -56,8 +56,7 @@ use fs::RealFs; use staff_mode::StaffMode; use util::{channel::RELEASE_CHANNEL, paths, ResultExt, TryFutureExt}; use workspace::{ - dock::FocusDock, item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, - Workspace, + item::ItemHandle, notifications::NotifyResultExt, AppState, OpenSettings, Workspace, }; use zed::{ self, build_window_options, handle_keymap_file_changes, initialize_workspace, languages, menus, @@ -186,7 +185,6 @@ fn main() { fs, build_window_options, initialize_workspace, - dock_default_item_factory, background_actions, }); cx.set_global(Arc::downgrade(&app_state)); @@ -816,7 +814,6 @@ pub fn background_actions() -> &'static [(&'static str, &'static dyn Action)] { &[ ("Go to file", &file_finder::Toggle), ("Open 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/menus.rs b/crates/zed/src/menus.rs index 741b61f323d13cf3682a1c5c9460fb2f97a16661..a98147c51b09ff4e06b8583c68e1206c37e799c6 100644 --- a/crates/zed/src/menus.rs +++ b/crates/zed/src/menus.rs @@ -89,7 +89,7 @@ pub fn menus() -> Vec> { MenuItem::action("Zoom Out", super::DecreaseBufferFontSize), MenuItem::action("Reset Zoom", super::ResetBufferFontSize), MenuItem::separator(), - MenuItem::action("Toggle Left Sidebar", workspace::ToggleLeftSidebar), + MenuItem::action("Toggle Left Dock", workspace::ToggleLeftDock), MenuItem::submenu(Menu { name: "Editor Layout", items: vec![ diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 95d09e82ec445ea245508f8e31441772b118ba6f..f9c0a1855ee099ca7c4575ec4ca63c8462aabca9 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -18,10 +18,11 @@ use feedback::{ use futures::{channel::mpsc, StreamExt}; use gpui::{ actions, + anyhow::{self, Result}, geometry::vector::vec2f, impl_actions, platform::{Platform, PromptLevel, TitlebarOptions, WindowBounds, WindowKind, WindowOptions}, - AppContext, ViewContext, + AppContext, AsyncAppContext, Task, ViewContext, WeakViewHandle, }; pub use lsp; pub use project; @@ -31,13 +32,13 @@ use serde::Deserialize; use serde_json::to_string_pretty; use settings::{KeymapFileContent, SettingsStore, DEFAULT_SETTINGS_ASSET_PATH}; use std::{borrow::Cow, str, sync::Arc}; -use terminal_view::terminal_button::TerminalButton; +use terminal_view::terminal_panel::{self, TerminalPanel}; use util::{channel::ReleaseChannel, paths, ResultExt}; use uuid::Uuid; use welcome::BaseKeymap; pub use workspace; use workspace::{ - create_and_open_local_file, open_new, sidebar::SidebarSide, AppState, NewFile, NewWindow, + create_and_open_local_file, dock::PanelHandle, open_new, AppState, NewFile, NewWindow, Workspace, WorkspaceSettings, }; @@ -223,7 +224,14 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { |workspace: &mut Workspace, _: &project_panel::ToggleFocus, cx: &mut ViewContext| { - workspace.toggle_sidebar_item_focus(SidebarSide::Left, 0, cx); + workspace.toggle_panel_focus::(cx); + }, + ); + cx.add_action( + |workspace: &mut Workspace, + _: &terminal_panel::ToggleFocus, + cx: &mut ViewContext| { + workspace.toggle_panel_focus::(cx); }, ); cx.add_global_action({ @@ -252,85 +260,107 @@ pub fn init(app_state: &Arc, cx: &mut gpui::AppContext) { } pub fn initialize_workspace( - workspace: &mut Workspace, - app_state: &Arc, - cx: &mut ViewContext, -) { - let workspace_handle = cx.handle(); - cx.subscribe(&workspace_handle, { - move |workspace, _, event, cx| { - if let workspace::Event::PaneAdded(pane) = event { - pane.update(cx, |pane, cx| { - pane.toolbar().update(cx, |toolbar, cx| { - let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); - toolbar.add_item(breadcrumbs, cx); - let buffer_search_bar = cx.add_view(BufferSearchBar::new); - toolbar.add_item(buffer_search_bar, cx); - let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); - toolbar.add_item(project_search_bar, cx); - let submit_feedback_button = cx.add_view(|_| SubmitFeedbackButton::new()); - toolbar.add_item(submit_feedback_button, cx); - let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); - toolbar.add_item(feedback_info_text, cx); - let lsp_log_item = cx.add_view(|_| { - lsp_log::LspLogToolbarItemView::new(workspace.project().clone()) + workspace_handle: WeakViewHandle, + was_deserialized: bool, + app_state: Arc, + cx: AsyncAppContext, +) -> Task> { + cx.spawn(|mut cx| async move { + workspace_handle.update(&mut cx, |workspace, cx| { + let workspace_handle = cx.handle(); + cx.subscribe(&workspace_handle, { + move |workspace, _, event, cx| { + if let workspace::Event::PaneAdded(pane) = event { + pane.update(cx, |pane, cx| { + pane.toolbar().update(cx, |toolbar, cx| { + let breadcrumbs = cx.add_view(|_| Breadcrumbs::new(workspace)); + toolbar.add_item(breadcrumbs, cx); + let buffer_search_bar = cx.add_view(BufferSearchBar::new); + toolbar.add_item(buffer_search_bar, cx); + let project_search_bar = cx.add_view(|_| ProjectSearchBar::new()); + toolbar.add_item(project_search_bar, cx); + let submit_feedback_button = + cx.add_view(|_| SubmitFeedbackButton::new()); + toolbar.add_item(submit_feedback_button, cx); + let feedback_info_text = cx.add_view(|_| FeedbackInfoText::new()); + toolbar.add_item(feedback_info_text, cx); + let lsp_log_item = cx.add_view(|_| { + lsp_log::LspLogToolbarItemView::new(workspace.project().clone()) + }); + toolbar.add_item(lsp_log_item, cx); + }) }); - toolbar.add_item(lsp_log_item, cx); - }) - }); - } - } - }) - .detach(); + } + } + }) + .detach(); - cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); - cx.emit(workspace::Event::PaneAdded(workspace.dock_pane().clone())); + cx.emit(workspace::Event::PaneAdded(workspace.active_pane().clone())); - let collab_titlebar_item = - cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); - workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); + let collab_titlebar_item = + cx.add_view(|cx| CollabTitlebarItem::new(workspace, &workspace_handle, cx)); + workspace.set_titlebar_item(collab_titlebar_item.into_any(), cx); - let project_panel = ProjectPanel::new(workspace, cx); - workspace.left_sidebar().update(cx, |sidebar, cx| { - sidebar.add_item( - "icons/folder_tree_16.svg", - "Project Panel".to_string(), - project_panel, - cx, - ) - }); + let copilot = + cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); + let diagnostic_summary = + cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); + let activity_indicator = activity_indicator::ActivityIndicator::new( + workspace, + app_state.languages.clone(), + cx, + ); + let active_buffer_language = + cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); + let feedback_button = cx.add_view(|_| { + feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace) + }); + let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); + workspace.status_bar().update(cx, |status_bar, cx| { + status_bar.add_left_item(diagnostic_summary, cx); + status_bar.add_left_item(activity_indicator, cx); + status_bar.add_right_item(feedback_button, cx); + status_bar.add_right_item(copilot, cx); + status_bar.add_right_item(active_buffer_language, cx); + status_bar.add_right_item(cursor_position, cx); + }); - let toggle_terminal = cx.add_view(|cx| TerminalButton::new(workspace_handle.clone(), cx)); - let copilot = cx.add_view(|cx| copilot_button::CopilotButton::new(app_state.fs.clone(), cx)); - let diagnostic_summary = - cx.add_view(|cx| diagnostics::items::DiagnosticIndicator::new(workspace, cx)); - let activity_indicator = - activity_indicator::ActivityIndicator::new(workspace, app_state.languages.clone(), cx); - let active_buffer_language = - cx.add_view(|_| language_selector::ActiveBufferLanguage::new(workspace)); - let feedback_button = - cx.add_view(|_| feedback::deploy_feedback_button::DeployFeedbackButton::new(workspace)); - let cursor_position = cx.add_view(|_| editor::items::CursorPosition::new()); - workspace.status_bar().update(cx, |status_bar, cx| { - status_bar.add_left_item(diagnostic_summary, cx); - status_bar.add_left_item(activity_indicator, cx); - status_bar.add_right_item(toggle_terminal, cx); - status_bar.add_right_item(feedback_button, cx); - status_bar.add_right_item(copilot, cx); - status_bar.add_right_item(active_buffer_language, cx); - status_bar.add_right_item(cursor_position, cx); - }); + auto_update::notify_of_any_new_update(cx.weak_handle(), cx); - auto_update::notify_of_any_new_update(cx.weak_handle(), cx); + vim::observe_keystrokes(cx); - vim::observe_keystrokes(cx); + cx.on_window_should_close(|workspace, cx| { + if let Some(task) = workspace.close(&Default::default(), cx) { + task.detach_and_log_err(cx); + } + false + }); + })?; + + let project_panel = ProjectPanel::load(workspace_handle.clone(), cx.clone()); + let terminal_panel = TerminalPanel::load(workspace_handle.clone(), cx.clone()); + let (project_panel, terminal_panel) = futures::try_join!(project_panel, terminal_panel)?; + workspace_handle.update(&mut cx, |workspace, cx| { + let project_panel_position = project_panel.position(cx); + workspace.add_panel(project_panel, cx); + if !was_deserialized + && workspace + .project() + .read(cx) + .visible_worktrees(cx) + .any(|tree| { + tree.read(cx) + .root_entry() + .map_or(false, |entry| entry.is_dir()) + }) + { + workspace.toggle_dock(project_panel_position, cx); + } - cx.on_window_should_close(|workspace, cx| { - if let Some(task) = workspace.close(&Default::default(), cx) { - task.detach_and_log_err(cx); - } - false - }); + workspace.add_panel(terminal_panel, cx) + })?; + Ok(()) + }) } pub fn build_window_options( @@ -348,7 +378,8 @@ pub fn build_window_options( traffic_light_position: Some(vec2f(8., 8.)), }), center: false, - focus: true, + focus: false, + show: false, kind: WindowKind::Normal, is_movable: true, bounds, @@ -687,7 +718,7 @@ mod tests { .unwrap(); workspace_1.update(cx, |workspace, cx| { assert_eq!(workspace.worktrees(cx).count(), 2); - assert!(workspace.left_sidebar().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).is_open()); assert!(workspace.active_pane().is_focused(cx)); }); @@ -730,7 +761,7 @@ mod tests { .collect::>(), &[Path::new("/root/c").into(), Path::new("/root/d").into()] ); - assert!(workspace.left_sidebar().read(cx).is_open()); + assert!(workspace.left_dock().read(cx).is_open()); assert!(workspace.active_pane().is_focused(cx)); }); } @@ -755,6 +786,7 @@ mod tests { .unwrap() .downcast::() .unwrap(); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let editor = workspace.read_with(cx, |workspace, cx| { workspace .active_item(cx) @@ -777,9 +809,9 @@ mod tests { assert!(cx.is_window_edited(workspace.window_id())); // Closing the item restores the window's edited state. - let close = workspace.update(cx, |workspace, cx| { + let close = pane.update(cx, |pane, cx| { drop(editor); - Pane::close_active_item(workspace, &Default::default(), cx).unwrap() + pane.close_active_item(&Default::default(), cx).unwrap() }); executor.run_until_parked(); cx.simulate_prompt_answer(workspace.window_id(), 1); @@ -1364,7 +1396,7 @@ mod tests { cx.foreground().run_until_parked(); workspace.read_with(cx, |workspace, _| { - assert_eq!(workspace.panes().len(), 2); //Center pane + Dock pane + assert_eq!(workspace.panes().len(), 1); assert_eq!(workspace.active_pane(), &pane_1); }); @@ -1374,7 +1406,7 @@ mod tests { cx.foreground().run_until_parked(); workspace.read_with(cx, |workspace, cx| { - assert_eq!(workspace.panes().len(), 2); + assert_eq!(workspace.panes().len(), 1); assert!(workspace.active_item(cx).is_none()); }); @@ -1403,6 +1435,7 @@ mod tests { let project = Project::test(app_state.fs.clone(), ["/root".as_ref()], cx).await; let (_, workspace) = cx.add_window(|cx| Workspace::test_new(project.clone(), cx)); + let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone()); let entries = cx.read(|cx| workspace.file_project_paths(cx)); let file1 = entries[0].clone(); @@ -1520,14 +1553,13 @@ mod tests { // Go forward to an item that has been closed, ensuring it gets re-opened at the same // location. - workspace - .update(cx, |workspace, cx| { - let editor3_id = editor3.id(); - drop(editor3); - Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor3_id, cx) - }) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + let editor3_id = editor3.id(); + drop(editor3); + pane.close_item_by_id(editor3_id, cx) + }) + .await + .unwrap(); workspace .update(cx, |w, cx| Pane::go_forward(w, None, cx)) .await @@ -1556,14 +1588,13 @@ mod tests { ); // Go back to an item that has been closed and removed from disk, ensuring it gets skipped. - workspace - .update(cx, |workspace, cx| { - let editor2_id = editor2.id(); - drop(editor2); - Pane::close_item_by_id(workspace, workspace.active_pane().clone(), editor2_id, cx) - }) - .await - .unwrap(); + pane.update(cx, |pane, cx| { + let editor2_id = editor2.id(); + drop(editor2); + pane.close_item_by_id(editor2_id, cx) + }) + .await + .unwrap(); app_state .fs .remove_file(Path::new("/root/a/file2"), Default::default()) @@ -1712,34 +1743,22 @@ mod tests { assert_eq!(active_path(&workspace, cx), Some(file4.clone())); // Close all the pane items in some arbitrary order. - workspace - .update(cx, |workspace, cx| { - Pane::close_item_by_id(workspace, pane.clone(), file1_item_id, cx) - }) + pane.update(cx, |pane, cx| pane.close_item_by_id(file1_item_id, cx)) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file4.clone())); - workspace - .update(cx, |workspace, cx| { - Pane::close_item_by_id(workspace, pane.clone(), file4_item_id, cx) - }) + pane.update(cx, |pane, cx| pane.close_item_by_id(file4_item_id, cx)) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - workspace - .update(cx, |workspace, cx| { - Pane::close_item_by_id(workspace, pane.clone(), file2_item_id, cx) - }) + pane.update(cx, |pane, cx| pane.close_item_by_id(file2_item_id, cx)) .await .unwrap(); assert_eq!(active_path(&workspace, cx), Some(file3.clone())); - workspace - .update(cx, |workspace, cx| { - Pane::close_item_by_id(workspace, pane.clone(), file3_item_id, cx) - }) + pane.update(cx, |pane, cx| pane.close_item_by_id(file3_item_id, cx)) .await .unwrap(); assert_eq!(active_path(&workspace, cx), None); @@ -2070,6 +2089,8 @@ mod tests { editor::init(cx); project_panel::init_settings(cx); pane::init(cx); + project_panel::init(cx); + terminal_view::init(cx); app_state }) } diff --git a/styles/src/styleTree/statusBar.ts b/styles/src/styleTree/statusBar.ts index c55160c336275c841b04258836d3ed47f04704ea..eca537c150d566e7fe454d7f32b5a652cc5c6328 100644 --- a/styles/src/styleTree/statusBar.ts +++ b/styles/src/styleTree/statusBar.ts @@ -93,10 +93,11 @@ export default function statusBar(colorScheme: ColorScheme) { }, }, }, - sidebarButtons: { + panelButtons: { groupLeft: {}, + groupBottom: {}, groupRight: {}, - item: { + button: { ...statusContainer, iconSize: 16, iconColor: foreground(layer, "variant"), diff --git a/styles/src/styleTree/workspace.ts b/styles/src/styleTree/workspace.ts index 9d0c4de9f78eef989a71ee33c1b9da3fcc8f8a50..737d225784aeabe86edbdcf9a044748855ffc01f 100644 --- a/styles/src/styleTree/workspace.ts +++ b/styles/src/styleTree/workspace.ts @@ -118,9 +118,25 @@ export default function workspace(colorScheme: ColorScheme) { }, cursor: "Arrow", }, - sidebar: { - initialSize: 240, - border: border(layer, { left: true, right: true }), + zoomedBackground: { + padding: 10, + cursor: "Arrow", + background: withOpacity(background(colorScheme.lowest), 0.5) + }, + zoomedForeground: { + shadow: colorScheme.modalShadow, + border: border(colorScheme.highest, { overlay: true }), + }, + dock: { + left: { + border: border(layer, { right: true }), + }, + bottom: { + border: border(layer, { top: true }), + }, + right: { + border: border(layer, { left: true }), + } }, paneDivider: { color: borderColor(layer), @@ -310,19 +326,6 @@ export default function workspace(colorScheme: ColorScheme) { width: 400, margin: { right: 10, bottom: 10 }, }, - dock: { - initialSizeRight: 640, - initialSizeBottom: 304, - wash_color: withOpacity(background(colorScheme.highest), 0.5), - panel: { - border: border(colorScheme.middle), - }, - maximized: { - margin: 32, - border: border(colorScheme.highest, { overlay: true }), - shadow: colorScheme.modalShadow, - }, - }, dropTargetOverlayColor: withOpacity(foreground(layer, "variant"), 0.5), } }