@@ -203,6 +203,8 @@ actions!(
CloseActiveDock,
/// Closes all docks.
CloseAllDocks,
+ /// Toggles all docks.
+ ToggleAllDocks,
/// Closes the current window.
CloseWindow,
/// Opens the feedback dialog.
@@ -1176,6 +1178,7 @@ pub struct Workspace {
_items_serializer: Task<Result<()>>,
session_id: Option<String>,
scheduled_tasks: Vec<Task<()>>,
+ last_open_dock_positions: Vec<DockPosition>,
}
impl EventEmitter<Event> for Workspace {}
@@ -1518,6 +1521,7 @@ impl Workspace {
session_id: Some(session_id),
scheduled_tasks: Vec::new(),
+ last_open_dock_positions: Vec::new(),
}
}
@@ -2987,12 +2991,17 @@ impl Workspace {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let dock = self.dock_at_position(dock_side);
let mut focus_center = false;
let mut reveal_dock = false;
+
+ let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
+ let was_visible = self.is_dock_at_position_open(dock_side, cx) && !other_is_zoomed;
+ if was_visible {
+ self.save_open_dock_positions(cx);
+ }
+
+ let dock = self.dock_at_position(dock_side);
dock.update(cx, |dock, cx| {
- let other_is_zoomed = self.zoomed.is_some() && self.zoomed_position != Some(dock_side);
- let was_visible = dock.is_open() && !other_is_zoomed;
dock.set_open(!was_visible, window, cx);
if dock.active_panel().is_none() {
@@ -3041,7 +3050,8 @@ impl Workspace {
}
fn close_active_dock(&mut self, window: &mut Window, cx: &mut Context<Self>) -> bool {
- if let Some(dock) = self.active_dock(window, cx) {
+ if let Some(dock) = self.active_dock(window, cx).cloned() {
+ self.save_open_dock_positions(cx);
dock.update(cx, |dock, cx| {
dock.set_open(false, window, cx);
});
@@ -3051,6 +3061,7 @@ impl Workspace {
}
pub fn close_all_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ self.save_open_dock_positions(cx);
for dock in self.all_docks() {
dock.update(cx, |dock, cx| {
dock.set_open(false, window, cx);
@@ -3062,6 +3073,67 @@ impl Workspace {
self.serialize_workspace(window, cx);
}
+ fn get_open_dock_positions(&self, cx: &Context<Self>) -> Vec<DockPosition> {
+ self.all_docks()
+ .into_iter()
+ .filter_map(|dock| {
+ let dock_ref = dock.read(cx);
+ if dock_ref.is_open() {
+ Some(dock_ref.position())
+ } else {
+ None
+ }
+ })
+ .collect()
+ }
+
+ /// Saves the positions of currently open docks.
+ ///
+ /// Updates `last_open_dock_positions` with positions of all currently open
+ /// docks, to later be restored by the 'Toggle All Docks' action.
+ fn save_open_dock_positions(&mut self, cx: &mut Context<Self>) {
+ let open_dock_positions = self.get_open_dock_positions(cx);
+ if !open_dock_positions.is_empty() {
+ self.last_open_dock_positions = open_dock_positions;
+ }
+ }
+
+ /// Toggles all docks between open and closed states.
+ ///
+ /// If any docks are open, closes all and remembers their positions. If all
+ /// docks are closed, restores the last remembered dock configuration.
+ fn toggle_all_docks(
+ &mut self,
+ _: &ToggleAllDocks,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let open_dock_positions = self.get_open_dock_positions(cx);
+
+ if !open_dock_positions.is_empty() {
+ self.close_all_docks(window, cx);
+ } else if !self.last_open_dock_positions.is_empty() {
+ self.restore_last_open_docks(window, cx);
+ }
+ }
+
+ /// Reopens docks from the most recently remembered configuration.
+ ///
+ /// Opens all docks whose positions are stored in `last_open_dock_positions`
+ /// and clears the stored positions.
+ fn restore_last_open_docks(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ let positions_to_open = std::mem::take(&mut self.last_open_dock_positions);
+
+ for position in positions_to_open {
+ let dock = self.dock_at_position(position);
+ dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
+ }
+
+ cx.focus_self(window);
+ cx.notify();
+ self.serialize_workspace(window, cx);
+ }
+
/// Transfer focus to the panel of the given type.
pub fn focus_panel<T: Panel>(
&mut self,
@@ -5761,6 +5833,7 @@ impl Workspace {
workspace.close_all_docks(window, cx);
}),
)
+ .on_action(cx.listener(Self::toggle_all_docks))
.on_action(cx.listener(
|workspace: &mut Workspace, _: &ClearAllNotifications, _, cx| {
workspace.clear_all_notifications(cx);
@@ -9206,6 +9279,238 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_toggle_all_docks(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ let project = Project::test(fs, [], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Open two docks
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+
+ left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
+ right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
+
+ assert!(left_dock.read(cx).is_open());
+ assert!(right_dock.read(cx).is_open());
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Toggle all docks - should close both
+ workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
+
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+ assert!(!left_dock.read(cx).is_open());
+ assert!(!right_dock.read(cx).is_open());
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Toggle again - should reopen both
+ workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
+
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+ assert!(left_dock.read(cx).is_open());
+ assert!(right_dock.read(cx).is_open());
+ });
+ }
+
+ #[gpui::test]
+ async fn test_toggle_all_with_manual_close(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ let project = Project::test(fs, [], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Open two docks
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+
+ left_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
+ right_dock.update(cx, |dock, cx| dock.set_open(true, window, cx));
+
+ assert!(left_dock.read(cx).is_open());
+ assert!(right_dock.read(cx).is_open());
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Close them manually
+ workspace.toggle_dock(DockPosition::Left, window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+ assert!(!left_dock.read(cx).is_open());
+ assert!(!right_dock.read(cx).is_open());
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ // Toggle all docks - only last closed (right dock) should reopen
+ workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
+
+ let left_dock = workspace.dock_at_position(DockPosition::Left);
+ let right_dock = workspace.dock_at_position(DockPosition::Right);
+ assert!(!left_dock.read(cx).is_open());
+ assert!(right_dock.read(cx).is_open());
+ });
+ }
+
+ #[gpui::test]
+ async fn test_toggle_all_docks_after_dock_move(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ let project = Project::test(fs, [], cx).await;
+ let (workspace, cx) =
+ cx.add_window_view(|window, cx| Workspace::test_new(project, window, cx));
+
+ // Open two docks (left and right) with one panel each
+ let (left_panel, right_panel) = workspace.update_in(cx, |workspace, window, cx| {
+ let left_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, cx));
+ workspace.add_panel(left_panel.clone(), window, cx);
+
+ let right_panel = cx.new(|cx| TestPanel::new(DockPosition::Right, cx));
+ workspace.add_panel(right_panel.clone(), window, cx);
+
+ workspace.toggle_dock(DockPosition::Left, window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+
+ // Verify initial state
+ assert!(
+ workspace.left_dock().read(cx).is_open(),
+ "Left dock should be open"
+ );
+ assert_eq!(
+ workspace
+ .left_dock()
+ .read(cx)
+ .visible_panel()
+ .unwrap()
+ .panel_id(),
+ left_panel.panel_id(),
+ "Left panel should be visible in left dock"
+ );
+ assert!(
+ workspace.right_dock().read(cx).is_open(),
+ "Right dock should be open"
+ );
+ assert_eq!(
+ workspace
+ .right_dock()
+ .read(cx)
+ .visible_panel()
+ .unwrap()
+ .panel_id(),
+ right_panel.panel_id(),
+ "Right panel should be visible in right dock"
+ );
+ assert!(
+ !workspace.bottom_dock().read(cx).is_open(),
+ "Bottom dock should be closed"
+ );
+
+ (left_panel, right_panel)
+ });
+
+ // Focus the left panel and move it to the next position (bottom dock)
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.toggle_panel_focus::<TestPanel>(window, cx); // Focus left panel
+ assert!(
+ left_panel.read(cx).focus_handle(cx).is_focused(window),
+ "Left panel should be focused"
+ );
+ });
+
+ cx.dispatch_action(MoveFocusedPanelToNextPosition);
+
+ // Verify the left panel has moved to the bottom dock, and the bottom dock is now open
+ workspace.update(cx, |workspace, cx| {
+ assert!(
+ !workspace.left_dock().read(cx).is_open(),
+ "Left dock should be closed"
+ );
+ assert!(
+ workspace.bottom_dock().read(cx).is_open(),
+ "Bottom dock should now be open"
+ );
+ assert_eq!(
+ left_panel.read(cx).position,
+ DockPosition::Bottom,
+ "Left panel should now be in the bottom dock"
+ );
+ assert_eq!(
+ workspace
+ .bottom_dock()
+ .read(cx)
+ .visible_panel()
+ .unwrap()
+ .panel_id(),
+ left_panel.panel_id(),
+ "Left panel should be the visible panel in the bottom dock"
+ );
+ });
+
+ // Toggle all docks off
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
+ assert!(
+ !workspace.left_dock().read(cx).is_open(),
+ "Left dock should be closed"
+ );
+ assert!(
+ !workspace.right_dock().read(cx).is_open(),
+ "Right dock should be closed"
+ );
+ assert!(
+ !workspace.bottom_dock().read(cx).is_open(),
+ "Bottom dock should be closed"
+ );
+ });
+
+ // Toggle all docks back on and verify positions are restored
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.toggle_all_docks(&ToggleAllDocks, window, cx);
+ assert!(
+ !workspace.left_dock().read(cx).is_open(),
+ "Left dock should remain closed"
+ );
+ assert!(
+ workspace.right_dock().read(cx).is_open(),
+ "Right dock should remain open"
+ );
+ assert!(
+ workspace.bottom_dock().read(cx).is_open(),
+ "Bottom dock should remain open"
+ );
+ assert_eq!(
+ left_panel.read(cx).position,
+ DockPosition::Bottom,
+ "Left panel should remain in the bottom dock"
+ );
+ assert_eq!(
+ right_panel.read(cx).position,
+ DockPosition::Right,
+ "Right panel should remain in the right dock"
+ );
+ assert_eq!(
+ workspace
+ .bottom_dock()
+ .read(cx)
+ .visible_panel()
+ .unwrap()
+ .panel_id(),
+ left_panel.panel_id(),
+ "Left panel should be the visible panel in the right dock"
+ );
+ });
+ }
+
#[gpui::test]
async fn test_join_pane_into_next(cx: &mut gpui::TestAppContext) {
init_test(cx);