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.
"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"

View file

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

View file

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

View file

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

View file

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