workspace: Implement Extended Terminal Option (#26211)

Thomas Jensen and Mikayla Maki created

Closes #10211 
Closes #7575 

Screenshot of feature:
![Screenshot 2025-03-06 at 1 08
13 PM](https://github.com/user-attachments/assets/73cc4519-248b-4264-9ce8-42d0980cf73c)

Screenshot of proposed menu:
![Screenshot 2025-03-06 at 1 14
30 PM](https://github.com/user-attachments/assets/efc7c18a-a2a5-491f-b3e5-5ed181f23906)

Screenshot of proposed menu closed:
![Screenshot 2025-03-06 at 1 14
57 PM](https://github.com/user-attachments/assets/0b42829c-abe3-48aa-9b81-30a0aeeac8fd)

Release Notes:

- Configuration of bottom_dock_layout in settings.json
- Layout Mode button in Title Bar
- 4 different layout modes for the bottom dock: contained (default),
full (extends below both docks), left-aligned, right-aligned (extends
only below the respective dock)

---------

Co-authored-by: Mikayla Maki <mikayla.c.maki@gmail.com>

Change summary

assets/icons/layout.svg                    |   5 
assets/settings/default.json               |   2 
crates/icons/src/icons.rs                  |  39 +-
crates/title_bar/src/title_bar.rs          |  98 +++++++
crates/workspace/src/workspace.rs          | 325 +++++++++++++++++++----
crates/workspace/src/workspace_settings.rs |  19 +
6 files changed, 409 insertions(+), 79 deletions(-)

Detailed changes

assets/icons/layout.svg 🔗

@@ -0,0 +1,5 @@
+<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
+<path d="M20 14H4C3.44772 14 3 14.4477 3 15V20C3 20.5523 3.44772 21 4 21H20C20.5523 21 21 20.5523 21 20V15C21 14.4477 20.5523 14 20 14Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M11 3H4C3.44772 3 3 3.44772 3 4V9C3 9.55228 3.44772 10 4 10H11C11.5523 10 12 9.55228 12 9V4C12 3.44772 11.5523 3 11 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+<path d="M20 3H17C16.4477 3 16 3.44772 16 4V9C16 9.55228 16.4477 10 17 10H20C20.5523 10 21 9.55228 21 9V4C21 3.44772 20.5523 3 20 3Z" stroke="black" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
+</svg>

assets/settings/default.json 🔗

@@ -80,6 +80,8 @@
     // Values are clamped to the [0.0, 1.0] range.
     "inactive_opacity": 1.0
   },
+  // Layout mode of the bottom dock. Defaults to "contained"
+  "bottom_dock_layout": "contained",
   // The direction that you want to split panes horizontally. Defaults to "up"
   "pane_split_direction_horizontal": "up",
   // The direction that you want to split panes horizontally. Defaults to "left"

crates/icons/src/icons.rs 🔗

@@ -10,8 +10,8 @@ use strum::{EnumIter, EnumString, IntoStaticStr};
 pub enum IconName {
     Ai,
     AiAnthropic,
-    AiBedrock,
     AiAnthropicHosted,
+    AiBedrock,
     AiDeepSeek,
     AiEdit,
     AiGoogle,
@@ -61,6 +61,7 @@ pub enum IconName {
     CircleOff,
     Clipboard,
     Close,
+    Cloud,
     Code,
     Cog,
     Command,
@@ -74,22 +75,22 @@ pub enum IconName {
     CountdownTimer,
     CursorIBeam,
     Dash,
+    DatabaseZap,
+    Debug,
     DebugBreakpoint,
+    DebugContinue,
     DebugDisabledBreakpoint,
     DebugDisabledLogBreakpoint,
+    DebugDisconnect,
     DebugIgnoreBreakpoints,
+    DebugLogBreakpoint,
     DebugPause,
-    DebugContinue,
-    DebugStepOver,
+    DebugRestart,
+    DebugStepBack,
     DebugStepInto,
     DebugStepOut,
-    DebugStepBack,
-    DebugRestart,
-    Debug,
+    DebugStepOver,
     DebugStop,
-    DebugDisconnect,
-    DebugLogBreakpoint,
-    DatabaseZap,
     Delete,
     Diff,
     Disconnected,
@@ -99,18 +100,18 @@ pub enum IconName {
     Envelope,
     Eraser,
     Escape,
-    ExpandVertical,
     Exit,
-    ExternalLink,
-    ExpandUp,
     ExpandDown,
+    ExpandUp,
+    ExpandVertical,
+    ExternalLink,
     Eye,
     File,
     FileCode,
     FileCreate,
     FileDelete,
-    FileDoc,
     FileDiff,
+    FileDoc,
     FileGeneric,
     FileGit,
     FileLock,
@@ -133,16 +134,17 @@ pub enum IconName {
     GenericMaximize,
     GenericMinimize,
     GenericRestore,
-    Github,
-    Globe,
     GitBranch,
     GitBranchSmall,
+    Github,
+    Globe,
     Hash,
     HistoryRerun,
     Indicator,
     Info,
     InlayHint,
     Keyboard,
+    Layout,
     Library,
     LightBulb,
     LineHeight,
@@ -155,7 +157,6 @@ pub enum IconName {
     Maximize,
     Menu,
     MessageBubbles,
-    Cloud,
     Mic,
     MicMute,
     Microscope,
@@ -227,8 +228,8 @@ pub enum IconName {
     Tab,
     Terminal,
     TextSnippet,
-    ThumbsUp,
     ThumbsDown,
+    ThumbsUp,
     Trash,
     TrashAlt,
     Triangle,
@@ -247,10 +248,10 @@ pub enum IconName {
     ZedAssistant,
     ZedAssistantFilled,
     ZedPredict,
-    ZedPredictUp,
-    ZedPredictDown,
     ZedPredictDisabled,
+    ZedPredictDown,
     ZedPredictError,
+    ZedPredictUp,
     ZedXCopilot,
 }
 

crates/title_bar/src/title_bar.rs 🔗

@@ -36,7 +36,7 @@ use ui::{
     IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
 };
 use util::ResultExt;
-use workspace::{Workspace, notifications::NotifyResultExt};
+use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
 use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
 
 pub use onboarding_banner::restore_banner;
@@ -210,6 +210,7 @@ impl Render for TitleBar {
                             .pr_1()
                             .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
                             .children(self.render_call_controls(window, cx))
+                            .child(self.render_bottom_dock_layout_menu(cx))
                             .map(|el| {
                                 let status = self.client.status();
                                 let status = &*status.borrow();
@@ -622,6 +623,101 @@ impl TitleBar {
         }
     }
 
+    pub fn render_bottom_dock_layout_menu(&self, cx: &mut Context<Self>) -> impl IntoElement {
+        let workspace = self.workspace.upgrade().unwrap();
+        let current_layout = workspace.update(cx, |workspace, _cx| workspace.bottom_dock_layout());
+
+        PopoverMenu::new("layout-menu")
+            .trigger(
+                IconButton::new("toggle_layout", IconName::Layout)
+                    .icon_size(IconSize::Small)
+                    .tooltip(Tooltip::text("Toggle Layout Menu")),
+            )
+            .anchor(gpui::Corner::TopRight)
+            .menu(move |window, cx| {
+                ContextMenu::build(window, cx, {
+                    let workspace = workspace.clone();
+                    move |menu, _, _| {
+                        menu.label("Bottom Dock")
+                            .separator()
+                            .toggleable_entry(
+                                "Contained",
+                                current_layout == BottomDockLayout::Contained,
+                                ui::IconPosition::End,
+                                None,
+                                {
+                                    let workspace = workspace.clone();
+                                    move |window, cx| {
+                                        workspace.update(cx, |workspace, cx| {
+                                            workspace.set_bottom_dock_layout(
+                                                BottomDockLayout::Contained,
+                                                window,
+                                                cx,
+                                            );
+                                        });
+                                    }
+                                },
+                            )
+                            .toggleable_entry(
+                                "Full",
+                                current_layout == BottomDockLayout::Full,
+                                ui::IconPosition::End,
+                                None,
+                                {
+                                    let workspace = workspace.clone();
+                                    move |window, cx| {
+                                        workspace.update(cx, |workspace, cx| {
+                                            workspace.set_bottom_dock_layout(
+                                                BottomDockLayout::Full,
+                                                window,
+                                                cx,
+                                            );
+                                        });
+                                    }
+                                },
+                            )
+                            .toggleable_entry(
+                                "Left Aligned",
+                                current_layout == BottomDockLayout::LeftAligned,
+                                ui::IconPosition::End,
+                                None,
+                                {
+                                    let workspace = workspace.clone();
+                                    move |window, cx| {
+                                        workspace.update(cx, |workspace, cx| {
+                                            workspace.set_bottom_dock_layout(
+                                                BottomDockLayout::LeftAligned,
+                                                window,
+                                                cx,
+                                            );
+                                        });
+                                    }
+                                },
+                            )
+                            .toggleable_entry(
+                                "Right Aligned",
+                                current_layout == BottomDockLayout::RightAligned,
+                                ui::IconPosition::End,
+                                None,
+                                {
+                                    let workspace = workspace.clone();
+                                    move |window, cx| {
+                                        workspace.update(cx, |workspace, cx| {
+                                            workspace.set_bottom_dock_layout(
+                                                BottomDockLayout::RightAligned,
+                                                window,
+                                                cx,
+                                            );
+                                        });
+                                    }
+                                },
+                            )
+                    }
+                })
+                .into()
+            })
+    }
+
     pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
         let client = self.client.clone();
         Button::new("sign_in", "Sign in")

crates/workspace/src/workspace.rs 🔗

@@ -102,7 +102,7 @@ use ui::prelude::*;
 use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
 use uuid::Uuid;
 pub use workspace_settings::{
-    AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
+    AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
 };
 
 use crate::notifications::NotificationId;
@@ -819,6 +819,7 @@ pub struct Workspace {
     center: PaneGroup,
     left_dock: Entity<Dock>,
     bottom_dock: Entity<Dock>,
+    bottom_dock_layout: BottomDockLayout,
     right_dock: Entity<Dock>,
     panes: Vec<Entity<Pane>>,
     panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
@@ -1044,6 +1045,7 @@ impl Workspace {
         let modal_layer = cx.new(|_| ModalLayer::new());
         let toast_layer = cx.new(|_| ToastLayer::new());
 
+        let bottom_dock_layout = WorkspaceSettings::get_global(cx).bottom_dock_layout;
         let left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
         let bottom_dock = Dock::new(DockPosition::Bottom, modal_layer.clone(), window, cx);
         let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
@@ -1141,6 +1143,7 @@ impl Workspace {
             notifications: Default::default(),
             left_dock,
             bottom_dock,
+            bottom_dock_layout,
             right_dock,
             project: project.clone(),
             follower_states: Default::default(),
@@ -1349,6 +1352,26 @@ impl Workspace {
         &self.bottom_dock
     }
 
+    pub fn bottom_dock_layout(&self) -> BottomDockLayout {
+        self.bottom_dock_layout
+    }
+
+    pub fn set_bottom_dock_layout(
+        &mut self,
+        layout: BottomDockLayout,
+        window: &mut Window,
+        cx: &mut Context<Self>,
+    ) {
+        let fs = self.project().read(cx).fs();
+        settings::update_settings_file::<WorkspaceSettings>(fs.clone(), cx, move |content, _cx| {
+            content.bottom_dock_layout = Some(layout);
+        });
+
+        self.bottom_dock_layout = layout;
+        cx.notify();
+        self.serialize_workspace(window, cx);
+    }
+
     pub fn right_dock(&self) -> &Entity<Dock> {
         &self.right_dock
     }
@@ -5535,64 +5558,248 @@ impl Render for Workspace {
                                         },
                                     ))
                                 })
-                                .child(
-                                    div()
-                                        .flex()
-                                        .flex_row()
-                                        .h_full()
-                                        // Left Dock
-                                        .children(self.render_dock(
-                                            DockPosition::Left,
-                                            &self.left_dock,
-                                            window,
-                                            cx,
-                                        ))
-                                        // Panes
-                                        .child(
-                                            div()
-                                                .flex()
-                                                .flex_col()
-                                                .flex_1()
-                                                .overflow_hidden()
-                                                .child(
-                                                    h_flex()
-                                                        .flex_1()
-                                                        .when_some(paddings.0, |this, p| {
-                                                            this.child(p.border_r_1())
-                                                        })
-                                                        .child(self.center.render(
-                                                            self.zoomed.as_ref(),
-                                                            &PaneRenderContext {
-                                                                follower_states:
-                                                                    &self.follower_states,
-                                                                active_call: self.active_call(),
-                                                                active_pane: &self.active_pane,
-                                                                app_state: &self.app_state,
-                                                                project: &self.project,
-                                                                workspace: &self.weak_self,
-                                                            },
-                                                            window,
-                                                            cx,
-                                                        ))
-                                                        .when_some(paddings.1, |this, p| {
-                                                            this.child(p.border_l_1())
-                                                        }),
-                                                )
-                                                .children(self.render_dock(
-                                                    DockPosition::Bottom,
-                                                    &self.bottom_dock,
-                                                    window,
-                                                    cx,
-                                                )),
-                                        )
-                                        // Right Dock
-                                        .children(self.render_dock(
-                                            DockPosition::Right,
-                                            &self.right_dock,
-                                            window,
-                                            cx,
-                                        )),
-                                )
+                                .child({
+                                    match self.bottom_dock_layout {
+                                        BottomDockLayout::Full => div()
+                                            .flex()
+                                            .flex_col()
+                                            .h_full()
+                                            .child(
+                                                div()
+                                                    .flex()
+                                                    .flex_row()
+                                                    .flex_1()
+                                                    .overflow_hidden()
+                                                    .children(self.render_dock(
+                                                        DockPosition::Left,
+                                                        &self.left_dock,
+                                                        window,
+                                                        cx,
+                                                    ))
+                                                    .child(
+                                                        div()
+                                                            .flex()
+                                                            .flex_col()
+                                                            .flex_1()
+                                                            .overflow_hidden()
+                                                            .child(
+                                                                h_flex()
+                                                                    .flex_1()
+                                                                    .when_some(
+                                                                        paddings.0,
+                                                                        |this, p| {
+                                                                            this.child(
+                                                                                p.border_r_1(),
+                                                                            )
+                                                                        },
+                                                                    )
+                                                                    .child(self.center.render(
+                                                                        self.zoomed.as_ref(),
+                                                                        &PaneRenderContext {
+                                                                            follower_states:
+                                                                                &self.follower_states,
+                                                                            active_call: self.active_call(),
+                                                                            active_pane: &self.active_pane,
+                                                                            app_state: &self.app_state,
+                                                                            project: &self.project,
+                                                                            workspace: &self.weak_self,
+                                                                        },
+                                                                        window,
+                                                                        cx,
+                                                                    ))
+                                                                    .when_some(
+                                                                        paddings.1,
+                                                                        |this, p| {
+                                                                            this.child(
+                                                                                p.border_l_1(),
+                                                                            )
+                                                                        },
+                                                                    ),
+                                                            ),
+                                                    )
+                                                    .children(self.render_dock(
+                                                        DockPosition::Right,
+                                                        &self.right_dock,
+                                                        window,
+                                                        cx,
+                                                    )),
+                                            )
+                                            .child(div().w_full().children(self.render_dock(
+                                                DockPosition::Bottom,
+                                                &self.bottom_dock,
+                                                window,
+                                                cx
+                                            ))),
+
+                                        BottomDockLayout::LeftAligned => div()
+                                            .flex()
+                                            .flex_row()
+                                            .h_full()
+                                            .child(
+                                                div()
+                                                    .flex()
+                                                    .flex_col()
+                                                    .flex_1()
+                                                    .h_full()
+                                                    .child(
+                                                        div()
+                                                            .flex()
+                                                            .flex_row()
+                                                            .flex_1()
+                                                            .children(self.render_dock(DockPosition::Left, &self.left_dock, window, cx))
+                                                            .child(
+                                                                div()
+                                                                    .flex()
+                                                                    .flex_col()
+                                                                    .flex_1()
+                                                                    .overflow_hidden()
+                                                                    .child(
+                                                                        h_flex()
+                                                                            .flex_1()
+                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
+                                                                            .child(self.center.render(
+                                                                                self.zoomed.as_ref(),
+                                                                                &PaneRenderContext {
+                                                                                    follower_states:
+                                                                                        &self.follower_states,
+                                                                                    active_call: self.active_call(),
+                                                                                    active_pane: &self.active_pane,
+                                                                                    app_state: &self.app_state,
+                                                                                    project: &self.project,
+                                                                                    workspace: &self.weak_self,
+                                                                                },
+                                                                                window,
+                                                                                cx,
+                                                                            ))
+                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
+                                                                    )
+                                                            )
+                                                    )
+                                                    .child(
+                                                        div()
+                                                            .w_full()
+                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
+                                                    ),
+                                            )
+                                            .children(self.render_dock(
+                                                DockPosition::Right,
+                                                &self.right_dock,
+                                                window,
+                                                cx,
+                                            )),
+
+                                        BottomDockLayout::RightAligned => div()
+                                            .flex()
+                                            .flex_row()
+                                            .h_full()
+                                            .children(self.render_dock(
+                                                DockPosition::Left,
+                                                &self.left_dock,
+                                                window,
+                                                cx,
+                                            ))
+                                            .child(
+                                                div()
+                                                    .flex()
+                                                    .flex_col()
+                                                    .flex_1()
+                                                    .h_full()
+                                                    .child(
+                                                        div()
+                                                            .flex()
+                                                            .flex_row()
+                                                            .flex_1()
+                                                            .child(
+                                                                div()
+                                                                    .flex()
+                                                                    .flex_col()
+                                                                    .flex_1()
+                                                                    .overflow_hidden()
+                                                                    .child(
+                                                                        h_flex()
+                                                                            .flex_1()
+                                                                            .when_some(paddings.0, |this, p| this.child(p.border_r_1()))
+                                                                            .child(self.center.render(
+                                                                                self.zoomed.as_ref(),
+                                                                                &PaneRenderContext {
+                                                                                    follower_states:
+                                                                                        &self.follower_states,
+                                                                                    active_call: self.active_call(),
+                                                                                    active_pane: &self.active_pane,
+                                                                                    app_state: &self.app_state,
+                                                                                    project: &self.project,
+                                                                                    workspace: &self.weak_self,
+                                                                                },
+                                                                                window,
+                                                                                cx,
+                                                                            ))
+                                                                            .when_some(paddings.1, |this, p| this.child(p.border_l_1())),
+                                                                    )
+                                                            )
+                                                            .children(self.render_dock(DockPosition::Right, &self.right_dock, window, cx))
+                                                    )
+                                                    .child(
+                                                        div()
+                                                            .w_full()
+                                                            .children(self.render_dock(DockPosition::Bottom, &self.bottom_dock, window, cx))
+                                                    ),
+                                            ),
+
+                                        BottomDockLayout::Contained => div()
+                                            .flex()
+                                            .flex_row()
+                                            .h_full()
+                                            .children(self.render_dock(
+                                                DockPosition::Left,
+                                                &self.left_dock,
+                                                window,
+                                                cx,
+                                            ))
+                                            .child(
+                                                div()
+                                                    .flex()
+                                                    .flex_col()
+                                                    .flex_1()
+                                                    .overflow_hidden()
+                                                    .child(
+                                                        h_flex()
+                                                            .flex_1()
+                                                            .when_some(paddings.0, |this, p| {
+                                                                this.child(p.border_r_1())
+                                                            })
+                                                            .child(self.center.render(
+                                                                self.zoomed.as_ref(),
+                                                                &PaneRenderContext {
+                                                                    follower_states:
+                                                                        &self.follower_states,
+                                                                    active_call: self.active_call(),
+                                                                    active_pane: &self.active_pane,
+                                                                    app_state: &self.app_state,
+                                                                    project: &self.project,
+                                                                    workspace: &self.weak_self,
+                                                                },
+                                                                window,
+                                                                cx,
+                                                            ))
+                                                            .when_some(paddings.1, |this, p| {
+                                                                this.child(p.border_l_1())
+                                                            }),
+                                                    )
+                                                    .children(self.render_dock(
+                                                        DockPosition::Bottom,
+                                                        &self.bottom_dock,
+                                                        window,
+                                                        cx,
+                                                    )),
+                                            )
+                                            .children(self.render_dock(
+                                                DockPosition::Right,
+                                                &self.right_dock,
+                                                window,
+                                                cx,
+                                            )),
+                                    }
+                                })
                                 .children(self.zoomed.as_ref().and_then(|view| {
                                     let zoomed_view = view.upgrade()?;
                                     let div = div()

crates/workspace/src/workspace_settings.rs 🔗

@@ -10,6 +10,7 @@ use settings::{Settings, SettingsSources};
 #[derive(Deserialize)]
 pub struct WorkspaceSettings {
     pub active_pane_modifiers: ActivePanelModifiers,
+    pub bottom_dock_layout: BottomDockLayout,
     pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal,
     pub pane_split_direction_vertical: PaneSplitDirectionVertical,
     pub centered_layout: CenteredLayoutSettings,
@@ -71,6 +72,20 @@ pub struct ActivePanelModifiers {
     pub inactive_opacity: Option<f32>,
 }
 
+#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, PartialEq, JsonSchema)]
+#[serde(rename_all = "snake_case")]
+pub enum BottomDockLayout {
+    /// Contained between the left and right docks
+    #[default]
+    Contained,
+    /// Takes up the full width of the window
+    Full,
+    /// Extends under the left dock while snapping to the right dock
+    LeftAligned,
+    /// Extends under the right dock while snapping to the left dock
+    RightAligned,
+}
+
 #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum CloseWindowWhenNoItems {
@@ -109,6 +124,10 @@ pub enum RestoreOnStartupBehavior {
 pub struct WorkspaceSettingsContent {
     /// Active pane styling settings.
     pub active_pane_modifiers: Option<ActivePanelModifiers>,
+    /// Layout mode for the bottom dock
+    ///
+    /// Default: contained
+    pub bottom_dock_layout: Option<BottomDockLayout>,
     /// Direction to split horizontally.
     ///
     /// Default: "up"