diff --git a/assets/icons/layout.svg b/assets/icons/layout.svg new file mode 100644 index 0000000000..79464013b1 --- /dev/null +++ b/assets/icons/layout.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/assets/settings/default.json b/assets/settings/default.json index 2a845fac0b..df1a4a01af 100644 --- a/assets/settings/default.json +++ b/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" diff --git a/crates/icons/src/icons.rs b/crates/icons/src/icons.rs index aa8dcaf587..6c448c03ed 100644 --- a/crates/icons/src/icons.rs +++ b/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, } diff --git a/crates/title_bar/src/title_bar.rs b/crates/title_bar/src/title_bar.rs index ed929cb1f3..225c3613ce 100644 --- a/crates/title_bar/src/title_bar.rs +++ b/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) -> 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) -> Button { let client = self.client.clone(); Button::new("sign_in", "Sign in") diff --git a/crates/workspace/src/workspace.rs b/crates/workspace/src/workspace.rs index 5a9dce7c01..01d836f48d 100644 --- a/crates/workspace/src/workspace.rs +++ b/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, bottom_dock: Entity, + bottom_dock_layout: BottomDockLayout, right_dock: Entity, panes: Vec>, panes_by_item: HashMap>, @@ -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, + ) { + let fs = self.project().read(cx).fs(); + settings::update_settings_file::(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 { &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() diff --git a/crates/workspace/src/workspace_settings.rs b/crates/workspace/src/workspace_settings.rs index 2dda042288..a61a987b1c 100644 --- a/crates/workspace/src/workspace_settings.rs +++ b/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, } +#[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, + /// Layout mode for the bottom dock + /// + /// Default: contained + pub bottom_dock_layout: Option, /// Direction to split horizontally. /// /// Default: "up"