workspace: Implement Extended Terminal Option (#26211)

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>
This commit is contained in:
Thomas Jensen 2025-04-11 18:18:36 +02:00 committed by GitHub
parent 2f5c662c42
commit 1df01eabfe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 409 additions and 79 deletions

5
assets/icons/layout.svg Normal file
View file

@ -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>

After

Width:  |  Height:  |  Size: 746 B

View file

@ -80,6 +80,8 @@
// Values are clamped to the [0.0, 1.0] range. // Values are clamped to the [0.0, 1.0] range.
"inactive_opacity": 1.0 "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" // The direction that you want to split panes horizontally. Defaults to "up"
"pane_split_direction_horizontal": "up", "pane_split_direction_horizontal": "up",
// The direction that you want to split panes horizontally. Defaults to "left" // The direction that you want to split panes horizontally. Defaults to "left"

View file

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

View file

@ -36,7 +36,7 @@ use ui::{
IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*, IconWithIndicator, Indicator, PopoverMenu, Tooltip, h_flex, prelude::*,
}; };
use util::ResultExt; use util::ResultExt;
use workspace::{Workspace, notifications::NotifyResultExt}; use workspace::{BottomDockLayout, Workspace, notifications::NotifyResultExt};
use zed_actions::{OpenBrowser, OpenRecent, OpenRemote}; use zed_actions::{OpenBrowser, OpenRecent, OpenRemote};
pub use onboarding_banner::restore_banner; pub use onboarding_banner::restore_banner;
@ -210,6 +210,7 @@ impl Render for TitleBar {
.pr_1() .pr_1()
.on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation()) .on_mouse_down(MouseButton::Left, |_, _, cx| cx.stop_propagation())
.children(self.render_call_controls(window, cx)) .children(self.render_call_controls(window, cx))
.child(self.render_bottom_dock_layout_menu(cx))
.map(|el| { .map(|el| {
let status = self.client.status(); let status = self.client.status();
let status = &*status.borrow(); 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 { pub fn render_sign_in_button(&mut self, _: &mut Context<Self>) -> Button {
let client = self.client.clone(); let client = self.client.clone();
Button::new("sign_in", "Sign in") Button::new("sign_in", "Sign in")

View file

@ -102,7 +102,7 @@ use ui::prelude::*;
use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true}; use util::{ResultExt, TryFutureExt, paths::SanitizedPath, serde::default_true};
use uuid::Uuid; use uuid::Uuid;
pub use workspace_settings::{ pub use workspace_settings::{
AutosaveSetting, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings, AutosaveSetting, BottomDockLayout, RestoreOnStartupBehavior, TabBarSettings, WorkspaceSettings,
}; };
use crate::notifications::NotificationId; use crate::notifications::NotificationId;
@ -819,6 +819,7 @@ pub struct Workspace {
center: PaneGroup, center: PaneGroup,
left_dock: Entity<Dock>, left_dock: Entity<Dock>,
bottom_dock: Entity<Dock>, bottom_dock: Entity<Dock>,
bottom_dock_layout: BottomDockLayout,
right_dock: Entity<Dock>, right_dock: Entity<Dock>,
panes: Vec<Entity<Pane>>, panes: Vec<Entity<Pane>>,
panes_by_item: HashMap<EntityId, WeakEntity<Pane>>, panes_by_item: HashMap<EntityId, WeakEntity<Pane>>,
@ -1044,6 +1045,7 @@ impl Workspace {
let modal_layer = cx.new(|_| ModalLayer::new()); let modal_layer = cx.new(|_| ModalLayer::new());
let toast_layer = cx.new(|_| ToastLayer::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 left_dock = Dock::new(DockPosition::Left, modal_layer.clone(), window, cx);
let bottom_dock = Dock::new(DockPosition::Bottom, 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); let right_dock = Dock::new(DockPosition::Right, modal_layer.clone(), window, cx);
@ -1141,6 +1143,7 @@ impl Workspace {
notifications: Default::default(), notifications: Default::default(),
left_dock, left_dock,
bottom_dock, bottom_dock,
bottom_dock_layout,
right_dock, right_dock,
project: project.clone(), project: project.clone(),
follower_states: Default::default(), follower_states: Default::default(),
@ -1349,6 +1352,26 @@ impl Workspace {
&self.bottom_dock &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> { pub fn right_dock(&self) -> &Entity<Dock> {
&self.right_dock &self.right_dock
} }
@ -5535,19 +5558,203 @@ impl Render for Workspace {
}, },
)) ))
}) })
.child({
match self.bottom_dock_layout {
BottomDockLayout::Full => div()
.flex()
.flex_col()
.h_full()
.child( .child(
div() div()
.flex() .flex()
.flex_row() .flex_row()
.h_full() .flex_1()
// Left Dock .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( .children(self.render_dock(
DockPosition::Left, DockPosition::Left,
&self.left_dock, &self.left_dock,
window, window,
cx, cx,
)) ))
// Panes
.child( .child(
div() div()
.flex() .flex()
@ -5585,14 +5792,14 @@ impl Render for Workspace {
cx, cx,
)), )),
) )
// Right Dock
.children(self.render_dock( .children(self.render_dock(
DockPosition::Right, DockPosition::Right,
&self.right_dock, &self.right_dock,
window, window,
cx, cx,
)), )),
) }
})
.children(self.zoomed.as_ref().and_then(|view| { .children(self.zoomed.as_ref().and_then(|view| {
let zoomed_view = view.upgrade()?; let zoomed_view = view.upgrade()?;
let div = div() let div = div()

View file

@ -10,6 +10,7 @@ use settings::{Settings, SettingsSources};
#[derive(Deserialize)] #[derive(Deserialize)]
pub struct WorkspaceSettings { pub struct WorkspaceSettings {
pub active_pane_modifiers: ActivePanelModifiers, pub active_pane_modifiers: ActivePanelModifiers,
pub bottom_dock_layout: BottomDockLayout,
pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal, pub pane_split_direction_horizontal: PaneSplitDirectionHorizontal,
pub pane_split_direction_vertical: PaneSplitDirectionVertical, pub pane_split_direction_vertical: PaneSplitDirectionVertical,
pub centered_layout: CenteredLayoutSettings, pub centered_layout: CenteredLayoutSettings,
@ -71,6 +72,20 @@ pub struct ActivePanelModifiers {
pub inactive_opacity: Option<f32>, 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)] #[derive(Copy, Clone, Default, Serialize, Deserialize, JsonSchema)]
#[serde(rename_all = "snake_case")] #[serde(rename_all = "snake_case")]
pub enum CloseWindowWhenNoItems { pub enum CloseWindowWhenNoItems {
@ -109,6 +124,10 @@ pub enum RestoreOnStartupBehavior {
pub struct WorkspaceSettingsContent { pub struct WorkspaceSettingsContent {
/// Active pane styling settings. /// Active pane styling settings.
pub active_pane_modifiers: Option<ActivePanelModifiers>, pub active_pane_modifiers: Option<ActivePanelModifiers>,
/// Layout mode for the bottom dock
///
/// Default: contained
pub bottom_dock_layout: Option<BottomDockLayout>,
/// Direction to split horizontally. /// Direction to split horizontally.
/// ///
/// Default: "up" /// Default: "up"