Cargo.lock 🔗
@@ -13302,7 +13302,6 @@ dependencies = [
"collections",
"command_palette_hooks",
"criterion",
- "db",
"editor",
"feature_flags",
"file_icons",
Max Brunsfeld created
Release Notes:
- The agent panel now has a flexible width, similar to the center panes
of the workspace.
Cargo.lock | 1
crates/agent_ui/src/agent_panel.rs | 48 -
crates/collab_ui/src/collab_panel.rs | 42
crates/collab_ui/src/notification_panel.rs | 66 --
crates/debugger_ui/src/debugger_panel.rs | 17
crates/git_ui/src/git_panel.rs | 17
crates/outline_panel/src/outline_panel.rs | 28
crates/project_panel/Cargo.toml | 1
crates/project_panel/src/project_panel.rs | 90 ---
crates/terminal_view/src/persistence.rs | 11
crates/terminal_view/src/terminal_panel.rs | 27
crates/workspace/src/dock.rs | 332 +++++++++++-
crates/workspace/src/pane_group.rs | 45 +
crates/workspace/src/workspace.rs | 655 ++++++++++++++++++++++-
crates/zed/src/visual_test_runner.rs | 3
15 files changed, 1,022 insertions(+), 361 deletions(-)
@@ -13302,7 +13302,6 @@ dependencies = [
"collections",
"command_palette_hooks",
"criterion",
- "db",
"editor",
"feature_flags",
"file_icons",
@@ -131,7 +131,6 @@ fn read_legacy_serialized_panel(kvp: &KeyValueStore) -> Option<SerializedAgentPa
#[derive(Serialize, Deserialize, Debug)]
struct SerializedAgentPanel {
- width: Option<Pixels>,
selected_agent: Option<AgentType>,
#[serde(default)]
last_active_thread: Option<SerializedActiveThread>,
@@ -743,8 +742,6 @@ pub struct AgentPanel {
agent_navigation_menu_handle: PopoverMenuHandle<ContextMenu>,
agent_navigation_menu: Option<Entity<ContextMenu>>,
_extension_subscription: Option<Subscription>,
- width: Option<Pixels>,
- height: Option<Pixels>,
zoomed: bool,
pending_serialization: Option<Task<Result<()>>>,
onboarding: Entity<AgentPanelOnboarding>,
@@ -766,7 +763,6 @@ impl AgentPanel {
return;
};
- let width = self.width;
let selected_agent_type = self.selected_agent_type.clone();
let start_thread_in = Some(self.start_thread_in);
@@ -787,7 +783,6 @@ impl AgentPanel {
save_serialized_panel(
workspace_id,
SerializedAgentPanel {
- width,
selected_agent: Some(selected_agent_type),
last_active_thread,
start_thread_in,
@@ -876,7 +871,6 @@ impl AgentPanel {
if let Some(serialized_panel) = &serialized_panel {
panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|w| w.round());
if let Some(selected_agent) = serialized_panel.selected_agent.clone() {
panel.selected_agent_type = selected_agent;
}
@@ -1079,8 +1073,6 @@ impl AgentPanel {
agent_navigation_menu_handle: PopoverMenuHandle::default(),
agent_navigation_menu: None,
_extension_subscription: extension_subscription,
- width: None,
- height: None,
zoomed: false,
pending_serialization: None,
onboarding,
@@ -3150,23 +3142,16 @@ impl Panel for AgentPanel {
});
}
- fn size(&self, window: &Window, cx: &App) -> Pixels {
+ fn default_size(&self, window: &Window, cx: &App) -> Pixels {
let settings = AgentSettings::get_global(cx);
match self.position(window, cx) {
- DockPosition::Left | DockPosition::Right => {
- self.width.unwrap_or(settings.default_width)
- }
- DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
+ DockPosition::Left | DockPosition::Right => settings.default_width,
+ DockPosition::Bottom => settings.default_height,
}
}
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
- match self.position(window, cx) {
- DockPosition::Left | DockPosition::Right => self.width = size,
- DockPosition::Bottom => self.height = size,
- }
- self.serialize(cx);
- cx.notify();
+ fn supports_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
+ true
}
fn set_active(&mut self, active: bool, window: &mut Window, cx: &mut Context<Self>) {
@@ -5055,16 +5040,12 @@ mod tests {
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
- // --- Set up workspace A: width=300, with an active thread ---
+ // --- Set up workspace A: with an active thread ---
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_a.clone(), cx));
cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
});
- panel_a.update(cx, |panel, _cx| {
- panel.width = Some(px(300.0));
- });
-
panel_a.update_in(cx, |panel, window, cx| {
panel.open_external_thread_with_server(
Rc::new(StubAgentServer::default_response()),
@@ -5084,14 +5065,13 @@ mod tests {
let agent_type_a = panel_a.read_with(cx, |panel, _cx| panel.selected_agent_type.clone());
- // --- Set up workspace B: ClaudeCode, width=400, no active thread ---
+ // --- Set up workspace B: ClaudeCode, no active thread ---
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project_b.clone(), cx));
cx.new(|cx| AgentPanel::new(workspace, text_thread_store, None, window, cx))
});
panel_b.update(cx, |panel, _cx| {
- panel.width = Some(px(400.0));
panel.selected_agent_type = AgentType::Custom {
id: "claude-acp".into(),
};
@@ -5117,13 +5097,8 @@ mod tests {
.expect("panel B load should succeed");
cx.run_until_parked();
- // Workspace A should restore its thread, width, and agent type
+ // Workspace A should restore its thread and agent type
loaded_a.read_with(cx, |panel, _cx| {
- assert_eq!(
- panel.width,
- Some(px(300.0)),
- "workspace A width should be restored"
- );
assert_eq!(
panel.selected_agent_type, agent_type_a,
"workspace A agent type should be restored"
@@ -5134,13 +5109,8 @@ mod tests {
);
});
- // Workspace B should restore its own width and agent type, with no thread
+ // Workspace B should restore its own agent type, with no thread
loaded_b.read_with(cx, |panel, _cx| {
- assert_eq!(
- panel.width,
- Some(px(400.0)),
- "workspace B width should be restored"
- );
assert_eq!(
panel.selected_agent_type,
AgentType::Custom {
@@ -237,7 +237,6 @@ impl ChannelEditingState {
}
pub struct CollabPanel {
- width: Option<Pixels>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
channel_clipboard: Option<ChannelMoveClipboard>,
@@ -263,7 +262,6 @@ pub struct CollabPanel {
#[derive(Serialize, Deserialize)]
struct SerializedCollabPanel {
- width: Option<Pixels>,
collapsed_channels: Option<Vec<u64>>,
}
@@ -371,7 +369,6 @@ impl CollabPanel {
.detach();
let mut this = Self {
- width: None,
focus_handle: cx.focus_handle(),
channel_clipboard: None,
fs: workspace.app_state().fs.clone(),
@@ -461,7 +458,6 @@ impl CollabPanel {
let panel = CollabPanel::new(workspace, window, cx);
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|w| w.round());
panel.collapsed_channels = serialized_panel
.collapsed_channels
.unwrap_or_else(Vec::new)
@@ -492,19 +488,17 @@ impl CollabPanel {
else {
return;
};
- let width = self.width;
- let collapsed_channels = self.collapsed_channels.clone();
+ let collapsed_channels = if self.collapsed_channels.is_empty() {
+ None
+ } else {
+ Some(self.collapsed_channels.iter().map(|id| id.0).collect())
+ };
let kvp = KeyValueStore::global(cx);
self.pending_serialization = cx.background_spawn(
async move {
kvp.write_kvp(
serialization_key,
- serde_json::to_string(&SerializedCollabPanel {
- width,
- collapsed_channels: Some(
- collapsed_channels.iter().map(|cid| cid.0).collect(),
- ),
- })?,
+ serde_json::to_string(&SerializedCollabPanel { collapsed_channels })?,
)
.await?;
anyhow::Ok(())
@@ -2915,7 +2909,16 @@ impl CollabPanel {
Some(result)
};
- let width = self.width.unwrap_or(px(240.));
+ let width = self
+ .workspace
+ .read_with(cx, |workspace, cx| {
+ workspace
+ .panel_size_state::<Self>(cx)
+ .and_then(|size_state| size_state.size)
+ })
+ .ok()
+ .flatten()
+ .unwrap_or(px(240.));
let root_id = channel.root_id();
div()
@@ -3193,17 +3196,8 @@ impl Panel for CollabPanel {
});
}
- fn size(&self, _window: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| CollaborationPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- cx.notify();
- cx.defer_in(window, |this, _, cx| {
- this.serialize(cx);
- });
+ fn default_size(&self, _window: &Window, cx: &App) -> Pixels {
+ CollaborationPanelSettings::get_global(cx).default_width
}
fn icon(&self, _window: &Window, cx: &App) -> Option<ui::IconName> {
@@ -3,7 +3,6 @@ use anyhow::Result;
use channel::ChannelStore;
use client::{ChannelId, Client, Notification, User, UserStore};
use collections::HashMap;
-use db::kvp::KeyValueStore;
use futures::StreamExt;
use gpui::{
AnyElement, App, AsyncWindowContext, ClickEvent, Context, DismissEvent, Element, Entity,
@@ -14,14 +13,14 @@ use gpui::{
use notifications::{NotificationEntry, NotificationEvent, NotificationStore};
use project::Fs;
use rpc::proto;
-use serde::{Deserialize, Serialize};
+
use settings::{Settings, SettingsStore};
use std::{sync::Arc, time::Duration};
use time::{OffsetDateTime, UtcOffset};
use ui::{
Avatar, Button, Icon, IconButton, IconName, Label, Tab, Tooltip, h_flex, prelude::*, v_flex,
};
-use util::{ResultExt, TryFutureExt};
+use util::ResultExt;
use workspace::notifications::{
Notification as WorkspaceNotification, NotificationId, SuppressEvent,
};
@@ -41,10 +40,8 @@ pub struct NotificationPanel {
channel_store: Entity<ChannelStore>,
notification_store: Entity<NotificationStore>,
fs: Arc<dyn Fs>,
- width: Option<Pixels>,
active: bool,
notification_list: ListState,
- pending_serialization: Task<Option<()>>,
subscriptions: Vec<gpui::Subscription>,
workspace: WeakEntity<Workspace>,
current_notification_toast: Option<(u64, Task<()>)>,
@@ -54,11 +51,6 @@ pub struct NotificationPanel {
unseen_notifications: Vec<NotificationEntry>,
}
-#[derive(Serialize, Deserialize)]
-struct SerializedNotificationPanel {
- width: Option<Pixels>,
-}
-
#[derive(Debug)]
pub enum Event {
DockPositionChanged,
@@ -146,15 +138,13 @@ impl NotificationPanel {
channel_store: ChannelStore::global(cx),
notification_store: NotificationStore::global(cx),
notification_list,
- pending_serialization: Task::ready(None),
workspace: workspace_handle,
focus_handle: cx.focus_handle(),
+ subscriptions: Default::default(),
current_notification_toast: None,
- subscriptions: Vec::new(),
active: false,
- mark_as_read_tasks: HashMap::default(),
- width: None,
- unseen_notifications: Vec::new(),
+ mark_as_read_tasks: Default::default(),
+ unseen_notifications: Default::default(),
};
let mut old_dock_position = this.position(window, cx);
@@ -186,43 +176,10 @@ impl NotificationPanel {
cx: AsyncWindowContext,
) -> Task<Result<Entity<Self>>> {
cx.spawn(async move |cx| {
- let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
- let serialized_panel =
- if let Some(panel) = kvp.read_kvp(NOTIFICATION_PANEL_KEY).log_err().flatten() {
- Some(serde_json::from_str::<SerializedNotificationPanel>(&panel)?)
- } else {
- None
- };
-
- workspace.update_in(cx, |workspace, window, cx| {
- let panel = Self::new(workspace, window, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|w| w.round());
- cx.notify();
- });
- }
- panel
- })
+ workspace.update_in(cx, |workspace, window, cx| Self::new(workspace, window, cx))
})
}
- fn serialize(&mut self, cx: &mut Context<Self>) {
- let width = self.width;
- let kvp = KeyValueStore::global(cx);
- self.pending_serialization = cx.background_spawn(
- async move {
- kvp.write_kvp(
- NOTIFICATION_PANEL_KEY.into(),
- serde_json::to_string(&SerializedNotificationPanel { width })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
fn render_notification(
&mut self,
ix: usize,
@@ -632,15 +589,8 @@ impl Panel for NotificationPanel {
});
}
- fn size(&self, _: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| NotificationPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
+ fn default_size(&self, _: &Window, cx: &App) -> Pixels {
+ NotificationPanelSettings::get_global(cx).default_width
}
fn set_active(&mut self, active: bool, _: &mut Window, cx: &mut Context<Self>) {
@@ -55,7 +55,6 @@ impl FeatureFlag for DebuggerHistoryFeatureFlag {
const DEBUG_PANEL_KEY: &str = "DebugPanel";
pub struct DebugPanel {
- size: Pixels,
active_session: Option<Entity<DebugSession>>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
@@ -93,7 +92,6 @@ impl DebugPanel {
);
Self {
- size: px(300.),
sessions_with_children: Default::default(),
active_session: None,
focus_handle,
@@ -1572,12 +1570,8 @@ impl Panel for DebugPanel {
});
}
- fn size(&self, _window: &Window, _: &App) -> Pixels {
- self.size
- }
-
- fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _cx: &mut Context<Self>) {
- self.size = size.unwrap_or(px(300.));
+ fn default_size(&self, _window: &Window, _: &App) -> Pixels {
+ px(300.)
}
fn remote_id() -> Option<proto::PanelId> {
@@ -1637,13 +1631,6 @@ impl Render for DebugPanel {
}
v_flex()
- .when(!self.is_zoomed, |this| {
- this.when_else(
- self.position(window, cx) == DockPosition::Bottom,
- |this| this.max_h(self.size),
- |this| this.max_w(self.size),
- )
- })
.size_full()
.key_context("DebugPanel")
.child(h_flex().children(self.top_controls_strip(window, cx)))
@@ -258,7 +258,6 @@ pub enum Event {
#[derive(Serialize, Deserialize)]
struct SerializedGitPanel {
- width: Option<Pixels>,
#[serde(default)]
amend_pending: bool,
#[serde(default)]
@@ -645,7 +644,6 @@ pub struct GitPanel {
tracked_count: usize,
tracked_staged_count: usize,
update_visible_entries_task: Task<()>,
- width: Option<Pixels>,
pub(crate) workspace: WeakEntity<Workspace>,
context_menu: Option<(Entity<ContextMenu>, Point<Pixels>, Subscription)>,
modal_open: bool,
@@ -832,7 +830,6 @@ impl GitPanel {
tracked_count: 0,
tracked_staged_count: 0,
update_visible_entries_task: Task::ready(()),
- width: None,
show_placeholders: false,
local_committer: None,
local_committer_task: None,
@@ -925,7 +922,6 @@ impl GitPanel {
}
fn serialize(&mut self, cx: &mut Context<Self>) {
- let width = self.width;
let amend_pending = self.amend_pending;
let signoff_enabled = self.signoff_enabled;
let kvp = KeyValueStore::global(cx);
@@ -952,7 +948,6 @@ impl GitPanel {
kvp.write_kvp(
serialization_key,
serde_json::to_string(&SerializedGitPanel {
- width,
amend_pending,
signoff_enabled,
})?,
@@ -5564,7 +5559,6 @@ impl GitPanel {
if let Some(serialized_panel) = serialized_panel {
panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width;
panel.amend_pending = serialized_panel.amend_pending;
panel.signoff_enabled = serialized_panel.signoff_enabled;
cx.notify();
@@ -5793,15 +5787,8 @@ impl Panel for GitPanel {
});
}
- fn size(&self, _: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| GitPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, _: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- self.serialize(cx);
- cx.notify();
+ fn default_size(&self, _: &Window, cx: &App) -> Pixels {
+ GitPanelSettings::get_global(cx).default_width
}
fn icon(&self, _: &Window, cx: &App) -> Option<ui::IconName> {
@@ -108,7 +108,6 @@ type HighlightStyleData = Arc<OnceLock<Vec<(Range<usize>, HighlightStyle)>>>;
pub struct OutlinePanel {
fs: Arc<dyn Fs>,
- width: Option<Pixels>,
project: Entity<Project>,
workspace: WeakEntity<Workspace>,
active: bool,
@@ -663,7 +662,6 @@ pub enum Event {
#[derive(Serialize, Deserialize)]
struct SerializedOutlinePanel {
- width: Option<Pixels>,
active: Option<bool>,
}
@@ -710,12 +708,7 @@ impl OutlinePanel {
workspace.update_in(&mut cx, |workspace, window, cx| {
let panel = Self::new(workspace, serialized_panel.as_ref(), window, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|px| px.round());
- cx.notify();
- });
- }
+ panel.update(cx, |_, cx| cx.notify());
panel
})
}
@@ -911,7 +904,6 @@ impl OutlinePanel {
unfolded_dirs: HashMap::default(),
selected_entry: SelectedEntry::None,
context_menu: None,
- width: None,
active_item: None,
pending_serialization: Task::ready(None),
new_entries_for_fs_update: HashSet::default(),
@@ -958,14 +950,13 @@ impl OutlinePanel {
else {
return;
};
- let width = self.width;
- let active = Some(self.active);
+ let active = self.active.then_some(true);
let kvp = KeyValueStore::global(cx);
self.pending_serialization = cx.background_spawn(
async move {
kvp.write_kvp(
serialization_key,
- serde_json::to_string(&SerializedOutlinePanel { width, active })?,
+ serde_json::to_string(&SerializedOutlinePanel { active })?,
)
.await?;
anyhow::Ok(())
@@ -5000,17 +4991,8 @@ impl Panel for OutlinePanel {
});
}
- fn size(&self, _: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| OutlinePanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- cx.notify();
- cx.defer_in(window, |this, _, cx| {
- this.serialize(cx);
- });
+ fn default_size(&self, _: &Window, cx: &App) -> Pixels {
+ OutlinePanelSettings::get_global(cx).default_width
}
fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
@@ -20,7 +20,6 @@ doctest = false
anyhow.workspace = true
collections.workspace = true
command_palette_hooks.workspace = true
-db.workspace = true
editor.workspace = true
file_icons.workspace = true
git_ui.workspace = true
@@ -6,7 +6,6 @@ use anyhow::{Context as _, Result};
use client::{ErrorCode, ErrorExt};
use collections::{BTreeSet, HashMap, hash_map};
use command_palette_hooks::CommandPaletteFilter;
-use db::kvp::KeyValueStore;
use editor::{
Editor, EditorEvent, MultiBufferOffset,
items::{
@@ -42,7 +41,7 @@ use project::{
use project_panel_settings::ProjectPanelSettings;
use rayon::slice::ParallelSliceMut;
use schemars::JsonSchema;
-use serde::{Deserialize, Serialize};
+use serde::Deserialize;
use settings::{
DockSide, ProjectPanelEntrySpacing, Settings, SettingsStore, ShowDiagnostics, ShowIndentGuides,
update_settings_file,
@@ -148,8 +147,6 @@ pub struct ProjectPanel {
clipboard: Option<ClipboardEntry>,
_dragged_entry_destination: Option<Arc<Path>>,
workspace: WeakEntity<Workspace>,
- width: Option<Pixels>,
- pending_serialization: Task<Option<()>>,
diagnostics: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticSeverity>,
diagnostic_counts: HashMap<(WorktreeId, Arc<RelPath>), DiagnosticCount>,
diagnostic_summary_update: Task<()>,
@@ -608,11 +605,6 @@ pub enum Event {
Focus,
}
-#[derive(Serialize, Deserialize)]
-struct SerializedProjectPanel {
- width: Option<Pixels>,
-}
-
struct DraggedProjectEntryView {
selection: SelectedEntry,
icon: Option<SharedString>,
@@ -878,8 +870,6 @@ impl ProjectPanel {
clipboard: None,
_dragged_entry_destination: None,
workspace: workspace.weak_handle(),
- width: None,
- pending_serialization: Task::ready(None),
diagnostics: Default::default(),
diagnostic_counts: Default::default(),
diagnostic_summary_update: Task::ready(()),
@@ -1000,37 +990,8 @@ impl ProjectPanel {
workspace: WeakEntity<Workspace>,
mut cx: AsyncWindowContext,
) -> Result<Entity<Self>> {
- let serialized_panel = match workspace
- .read_with(&cx, |workspace, _| {
- ProjectPanel::serialization_key(workspace)
- })
- .ok()
- .flatten()
- {
- Some(serialization_key) => {
- let kvp = cx.update(|_, cx| KeyValueStore::global(cx))?;
- cx.background_spawn(async move { kvp.read_kvp(&serialization_key) })
- .await
- .context("loading project panel")
- .log_err()
- .flatten()
- .map(|panel| serde_json::from_str::<SerializedProjectPanel>(&panel))
- .transpose()
- .log_err()
- .flatten()
- }
- None => None,
- };
-
workspace.update_in(&mut cx, |workspace, window, cx| {
- let panel = ProjectPanel::new(workspace, window, cx);
- if let Some(serialized_panel) = serialized_panel {
- panel.update(cx, |panel, cx| {
- panel.width = serialized_panel.width.map(|px| px.round());
- cx.notify();
- });
- }
- panel
+ ProjectPanel::new(workspace, window, cx)
})
}
@@ -1104,40 +1065,6 @@ impl ProjectPanel {
.or_insert(diagnostic_severity);
}
- fn serialization_key(workspace: &Workspace) -> Option<String> {
- workspace
- .database_id()
- .map(|id| i64::from(id).to_string())
- .or(workspace.session_id())
- .map(|id| format!("{}-{:?}", PROJECT_PANEL_KEY, id))
- }
-
- fn serialize(&mut self, cx: &mut Context<Self>) {
- let Some(serialization_key) = self
- .workspace
- .read_with(cx, |workspace, _| {
- ProjectPanel::serialization_key(workspace)
- })
- .ok()
- .flatten()
- else {
- return;
- };
- let width = self.width;
- let kvp = KeyValueStore::global(cx);
- self.pending_serialization = cx.background_spawn(
- async move {
- kvp.write_kvp(
- serialization_key,
- serde_json::to_string(&SerializedProjectPanel { width })?,
- )
- .await?;
- anyhow::Ok(())
- }
- .log_err(),
- );
- }
-
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
if !self.focus_handle.contains_focused(window, cx) {
cx.emit(Event::Focus);
@@ -7252,17 +7179,8 @@ impl Panel for ProjectPanel {
});
}
- fn size(&self, _: &Window, cx: &App) -> Pixels {
- self.width
- .unwrap_or_else(|| ProjectPanelSettings::get_global(cx).default_width)
- }
-
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
- self.width = size;
- cx.notify();
- cx.defer_in(window, |this, _, cx| {
- this.serialize(cx);
- });
+ fn default_size(&self, _: &Window, cx: &App) -> Pixels {
+ ProjectPanelSettings::get_global(cx).default_width
}
fn icon(&self, _: &Window, cx: &App) -> Option<IconName> {
@@ -6,7 +6,7 @@ use gpui::{AppContext as _, AsyncWindowContext, Axis, Entity, Task, WeakEntity};
use project::Project;
use serde::{Deserialize, Serialize};
use std::path::PathBuf;
-use ui::{App, Context, Pixels, Window};
+use ui::{App, Context, Window};
use util::ResultExt as _;
use db::{
@@ -97,12 +97,7 @@ pub(crate) fn deserialize_terminal_panel(
) -> Task<anyhow::Result<Entity<TerminalPanel>>> {
window.spawn(cx, async move |cx| {
let terminal_panel = workspace.update_in(cx, |workspace, window, cx| {
- cx.new(|cx| {
- let mut panel = TerminalPanel::new(workspace, window, cx);
- panel.height = serialized_panel.height.map(|h| h.round());
- panel.width = serialized_panel.width.map(|w| w.round());
- panel
- })
+ cx.new(|cx| TerminalPanel::new(workspace, window, cx))
})?;
match &serialized_panel.items {
SerializedItems::NoSplits(item_ids) => {
@@ -317,8 +312,6 @@ pub(crate) struct SerializedTerminalPanel {
pub items: SerializedItems,
// A deprecated field, kept for backwards compatibility for the code before terminal splits were introduced.
pub active_item_id: Option<u64>,
- pub width: Option<Pixels>,
- pub height: Option<Pixels>,
}
#[derive(Debug, Serialize, Deserialize)]
@@ -79,8 +79,6 @@ pub struct TerminalPanel {
pub(crate) center: PaneGroup,
fs: Arc<dyn Fs>,
workspace: WeakEntity<Workspace>,
- pub(crate) width: Option<Pixels>,
- pub(crate) height: Option<Pixels>,
pending_serialization: Task<Option<()>>,
pending_terminals_to_add: usize,
deferred_tasks: HashMap<TaskId, Task<()>>,
@@ -100,8 +98,6 @@ impl TerminalPanel {
fs: workspace.app_state().fs.clone(),
workspace: workspace.weak_handle(),
pending_serialization: Task::ready(None),
- width: None,
- height: None,
pending_terminals_to_add: 0,
deferred_tasks: HashMap::default(),
assistant_enabled: false,
@@ -928,8 +924,6 @@ impl TerminalPanel {
}
fn serialize(&mut self, cx: &mut Context<Self>) {
- let height = self.height;
- let width = self.width;
let Some(serialization_key) = self
.workspace
.read_with(cx, |workspace, _| {
@@ -960,8 +954,6 @@ impl TerminalPanel {
serde_json::to_string(&SerializedTerminalPanel {
items,
active_item_id: None,
- height,
- width,
})?,
)
.await?;
@@ -1553,27 +1545,14 @@ impl Panel for TerminalPanel {
});
}
- fn size(&self, window: &Window, cx: &App) -> Pixels {
+ fn default_size(&self, window: &Window, cx: &App) -> Pixels {
let settings = TerminalSettings::get_global(cx);
match self.position(window, cx) {
- DockPosition::Left | DockPosition::Right => {
- self.width.unwrap_or(settings.default_width)
- }
- DockPosition::Bottom => self.height.unwrap_or(settings.default_height),
+ DockPosition::Left | DockPosition::Right => settings.default_width,
+ DockPosition::Bottom => settings.default_height,
}
}
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>) {
- match self.position(window, cx) {
- DockPosition::Left | DockPosition::Right => self.width = size,
- DockPosition::Bottom => self.height = size,
- }
- cx.notify();
- cx.defer_in(window, |this, _, cx| {
- this.serialize(cx);
- })
- }
-
fn is_zoomed(&self, _window: &Window, cx: &App) -> bool {
self.active_pane.read(cx).is_zoomed()
}
@@ -3,6 +3,7 @@ use crate::{DraggedDock, Event, ModalLayer, Pane};
use crate::{Workspace, status_bar::StatusItemView};
use anyhow::Context as _;
use client::proto;
+use db::kvp::KeyValueStore;
use gpui::{
Action, AnyView, App, Axis, Context, Corner, Entity, EntityId, EventEmitter, FocusHandle,
@@ -10,6 +11,7 @@ use gpui::{
Render, SharedString, StyleRefinement, Styled, Subscription, WeakEntity, Window, deferred, div,
px,
};
+use serde::{Deserialize, Serialize};
use settings::SettingsStore;
use std::sync::Arc;
use ui::{
@@ -35,8 +37,14 @@ pub trait Panel: Focusable + EventEmitter<PanelEvent> + Render + Sized {
fn position(&self, window: &Window, cx: &App) -> DockPosition;
fn position_is_valid(&self, position: DockPosition) -> bool;
fn set_position(&mut self, position: DockPosition, window: &mut Window, cx: &mut Context<Self>);
- fn size(&self, window: &Window, cx: &App) -> Pixels;
- fn set_size(&mut self, size: Option<Pixels>, window: &mut Window, cx: &mut Context<Self>);
+ fn default_size(&self, window: &Window, cx: &App) -> Pixels;
+ fn initial_size_state(&self, _window: &Window, _cx: &App) -> PanelSizeState {
+ PanelSizeState::default()
+ }
+ fn size_state_changed(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
+ fn supports_flexible_size(&self, _window: &Window, _cx: &App) -> bool {
+ false
+ }
fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
fn toggle_action(&self) -> Box<dyn Action>;
@@ -75,8 +83,10 @@ pub trait PanelHandle: Send + Sync {
fn set_active(&self, active: bool, window: &mut Window, cx: &mut App);
fn remote_id(&self) -> Option<proto::PanelId>;
fn pane(&self, cx: &App) -> Option<Entity<Pane>>;
- fn size(&self, window: &Window, cx: &App) -> Pixels;
- fn set_size(&self, size: Option<Pixels>, window: &mut Window, cx: &mut App);
+ fn default_size(&self, window: &Window, cx: &App) -> Pixels;
+ fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState;
+ fn size_state_changed(&self, window: &mut Window, cx: &mut App);
+ fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool;
fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName>;
fn icon_tooltip(&self, window: &Window, cx: &App) -> Option<&'static str>;
fn toggle_action(&self, window: &Window, cx: &App) -> Box<dyn Action>;
@@ -150,12 +160,20 @@ where
T::remote_id()
}
- fn size(&self, window: &Window, cx: &App) -> Pixels {
- self.read(cx).size(window, cx)
+ fn default_size(&self, window: &Window, cx: &App) -> Pixels {
+ self.read(cx).default_size(window, cx)
+ }
+
+ fn initial_size_state(&self, window: &Window, cx: &App) -> PanelSizeState {
+ self.read(cx).initial_size_state(window, cx)
+ }
+
+ fn size_state_changed(&self, window: &mut Window, cx: &mut App) {
+ self.update(cx, |this, cx| this.size_state_changed(window, cx))
}
- fn set_size(&self, size: Option<Pixels>, window: &mut Window, cx: &mut App) {
- self.update(cx, |this, cx| this.set_size(size, window, cx))
+ fn supports_flexible_size(&self, window: &Window, cx: &App) -> bool {
+ self.read(cx).supports_flexible_size(window, cx)
}
fn icon(&self, window: &Window, cx: &App) -> Option<ui::IconName> {
@@ -262,8 +280,16 @@ impl DockPosition {
}
}
+#[derive(Clone, Copy, Debug, Default, PartialEq, Serialize, Deserialize)]
+pub struct PanelSizeState {
+ pub size: Option<Pixels>,
+ #[serde(default)]
+ pub flexible_size_ratio: Option<f32>,
+}
+
struct PanelEntry {
panel: Arc<dyn PanelHandle>,
+ size_state: PanelSizeState,
_subscriptions: [Subscription; 3],
}
@@ -272,6 +298,8 @@ pub struct PanelButtons {
_settings_subscription: Subscription,
}
+pub(crate) const PANEL_SIZE_STATE_KEY: &str = "dock_panel_size";
+
impl Dock {
pub fn new(
position: DockPosition,
@@ -493,20 +521,34 @@ impl Dock {
return;
};
+ let panel_id = Entity::entity_id(&panel);
let was_visible = this.is_open()
- && this.visible_panel().is_some_and(|active_panel| {
- active_panel.panel_id() == Entity::entity_id(&panel)
- });
+ && this
+ .visible_panel()
+ .is_some_and(|active_panel| active_panel.panel_id() == panel_id);
+ let size_state = this
+ .panel_entries
+ .iter()
+ .find(|entry| entry.panel.panel_id() == panel_id)
+ .map(|entry| entry.size_state)
+ .unwrap_or_default();
+
+ let previous_axis = this.position.axis();
+ let next_axis = new_position.axis();
+ let size_state = if previous_axis == next_axis {
+ size_state
+ } else {
+ PanelSizeState::default()
+ };
this.remove_panel(&panel, window, cx);
- new_dock.update(cx, |new_dock, cx| {
- new_dock.remove_panel(&panel, window, cx);
- });
-
new_dock.update(cx, |new_dock, cx| {
let index =
new_dock.add_panel(panel.clone(), workspace.clone(), window, cx);
+ if let Some(added_panel) = new_dock.panel_for_id(panel_id).cloned() {
+ new_dock.set_panel_size_state(added_panel.as_ref(), size_state, cx);
+ }
if was_visible {
new_dock.set_open(true, window, cx);
new_dock.activate_panel(index, window, cx);
@@ -597,10 +639,13 @@ impl Dock {
{
*active_index += 1;
}
+ let size_state = panel.read(cx).initial_size_state(window, cx);
+
self.panel_entries.insert(
index,
PanelEntry {
panel: Arc::new(panel.clone()),
+ size_state,
_subscriptions: subscriptions,
},
);
@@ -718,28 +763,113 @@ impl Dock {
self.panel_entries
.iter()
.find(|entry| entry.panel.panel_id() == panel.panel_id())
- .map(|entry| entry.panel.size(window, cx))
+ .map(|entry| self.resolved_panel_size(entry, window, cx))
}
pub fn active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
if self.is_open {
self.active_panel_entry()
- .map(|entry| entry.panel.size(window, cx))
+ .map(|entry| self.resolved_panel_size(entry, window, cx))
} else {
None
}
}
+ pub fn stored_panel_size(
+ &self,
+ panel: &dyn PanelHandle,
+ window: &Window,
+ cx: &App,
+ ) -> Option<Pixels> {
+ self.panel_entries
+ .iter()
+ .find(|entry| entry.panel.panel_id() == panel.panel_id())
+ .map(|entry| {
+ entry
+ .size_state
+ .size
+ .unwrap_or_else(|| entry.panel.default_size(window, cx))
+ })
+ }
+
+ pub fn stored_panel_size_state(&self, panel: &dyn PanelHandle) -> Option<PanelSizeState> {
+ self.panel_entries
+ .iter()
+ .find(|entry| entry.panel.panel_id() == panel.panel_id())
+ .map(|entry| entry.size_state)
+ }
+
+ pub fn stored_active_panel_size(&self, window: &Window, cx: &App) -> Option<Pixels> {
+ if self.is_open {
+ self.active_panel_entry().map(|entry| {
+ entry
+ .size_state
+ .size
+ .unwrap_or_else(|| entry.panel.default_size(window, cx))
+ })
+ } else {
+ None
+ }
+ }
+
+ pub fn set_panel_size_state(
+ &mut self,
+ panel: &dyn PanelHandle,
+ size_state: PanelSizeState,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ if let Some(entry) = self
+ .panel_entries
+ .iter_mut()
+ .find(|entry| entry.panel.panel_id() == panel.panel_id())
+ {
+ entry.size_state = size_state;
+ cx.notify();
+ true
+ } else {
+ false
+ }
+ }
+
pub fn resize_active_panel(
&mut self,
size: Option<Pixels>,
window: &mut Window,
cx: &mut Context<Self>,
) {
- if let Some(entry) = self.active_panel_entry() {
+ let ratio = size.and_then(|size| self.flexible_size_ratio_for_pixels(size, window, cx));
+ self.resize_active_panel_with_ratio(size, ratio, window, cx);
+ }
+
+ pub fn resize_active_panel_with_ratio(
+ &mut self,
+ size: Option<Pixels>,
+ ratio: Option<f32>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ if let Some(index) = self.active_panel_index
+ && let Some(entry) = self.panel_entries.get_mut(index)
+ {
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
- entry.panel.set_size(size, window, cx);
+ if entry.panel.supports_flexible_size(window, cx) {
+ entry.size_state.flexible_size_ratio = ratio;
+ } else {
+ entry.size_state.size = size;
+ }
+
+ let panel_key = entry.panel.panel_key();
+ let size_state = entry.size_state;
+ let workspace = self.workspace.clone();
+ entry.panel.size_state_changed(window, cx);
+ cx.defer(move |cx| {
+ if let Some(workspace) = workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ workspace.persist_panel_size_state(panel_key, size_state, cx);
+ });
+ }
+ });
cx.notify();
}
}
@@ -750,10 +880,41 @@ impl Dock {
window: &mut Window,
cx: &mut Context<Self>,
) {
+ let ratio = size.and_then(|size| self.flexible_size_ratio_for_pixels(size, window, cx));
+ self.resize_all_panels_with_ratio(size, ratio, window, cx);
+ }
+
+ pub fn resize_all_panels_with_ratio(
+ &mut self,
+ size: Option<Pixels>,
+ ratio: Option<f32>,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ let mut size_states_to_persist = Vec::new();
+
for entry in &mut self.panel_entries {
let size = size.map(|size| size.max(RESIZE_HANDLE_SIZE).round());
- entry.panel.set_size(size, window, cx);
+ if entry.panel.supports_flexible_size(window, cx) {
+ entry.size_state.flexible_size_ratio = ratio;
+ } else {
+ entry.size_state.size = size;
+ }
+ entry.panel.size_state_changed(window, cx);
+ size_states_to_persist.push((entry.panel.panel_key(), entry.size_state));
}
+
+ let workspace = self.workspace.clone();
+ cx.defer(move |cx| {
+ if let Some(workspace) = workspace.upgrade() {
+ workspace.update(cx, |workspace, cx| {
+ for (panel_key, size_state) in size_states_to_persist {
+ workspace.persist_panel_size_state(panel_key, size_state, cx);
+ }
+ });
+ }
+ });
+
cx.notify();
}
@@ -772,21 +933,110 @@ impl Dock {
dispatch_context
}
- pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &mut Window, cx: &mut App) {
+ pub fn clamp_panel_size(&mut self, max_size: Pixels, window: &Window, cx: &mut App) {
let max_size = (max_size - RESIZE_HANDLE_SIZE).abs();
- for panel in self.panel_entries.iter().map(|entry| &entry.panel) {
- if panel.size(window, cx) > max_size {
- panel.set_size(Some(max_size.max(RESIZE_HANDLE_SIZE)), window, cx);
+ for entry in &mut self.panel_entries {
+ if entry.panel.supports_flexible_size(window, cx) {
+ continue;
+ }
+
+ let size = entry
+ .size_state
+ .size
+ .unwrap_or_else(|| entry.panel.default_size(window, cx));
+ if size > max_size {
+ entry.size_state.size = Some(max_size.max(RESIZE_HANDLE_SIZE));
+ }
+ }
+ }
+
+ fn resolved_panel_size(&self, entry: &PanelEntry, window: &Window, cx: &App) -> Pixels {
+ if self.position.axis() == Axis::Horizontal
+ && entry.panel.supports_flexible_size(window, cx)
+ {
+ if let Some(workspace) = self.workspace.upgrade() {
+ let workspace = workspace.read(cx);
+ return resolve_panel_size(
+ entry.size_state,
+ entry.panel.as_ref(),
+ self.position,
+ workspace,
+ window,
+ cx,
+ );
}
}
+ entry
+ .size_state
+ .size
+ .unwrap_or_else(|| entry.panel.default_size(window, cx))
+ }
+
+ fn flexible_size_ratio_for_pixels(
+ &self,
+ size: Pixels,
+ window: &Window,
+ cx: &App,
+ ) -> Option<f32> {
+ let workspace = self.workspace.upgrade()?;
+ workspace
+ .read(cx)
+ .flexible_dock_ratio_for_size(self.position, size, window, cx)
+ }
+
+ pub(crate) fn load_persisted_size_state(
+ workspace: &Workspace,
+ panel_key: &'static str,
+ cx: &App,
+ ) -> Option<PanelSizeState> {
+ let workspace_id = workspace
+ .database_id()
+ .map(|id| i64::from(id).to_string())
+ .or(workspace.session_id())?;
+ let kvp = KeyValueStore::global(cx);
+ let scope = kvp.scoped(PANEL_SIZE_STATE_KEY);
+ scope
+ .read(&format!("{workspace_id}:{panel_key}"))
+ .log_err()
+ .flatten()
+ .and_then(|json| serde_json::from_str::<PanelSizeState>(&json).log_err())
+ }
+}
+
+pub(crate) fn resolve_panel_size(
+ size_state: PanelSizeState,
+ panel: &dyn PanelHandle,
+ position: DockPosition,
+ workspace: &Workspace,
+ window: &Window,
+ cx: &App,
+) -> Pixels {
+ if position.axis() == Axis::Horizontal && panel.supports_flexible_size(window, cx) {
+ let ratio = size_state
+ .flexible_size_ratio
+ .or_else(|| workspace.default_flexible_dock_ratio(position, cx));
+
+ if let Some(ratio) = ratio {
+ return workspace
+ .flexible_dock_size(position, ratio, window, cx)
+ .unwrap_or_else(|| {
+ size_state
+ .size
+ .unwrap_or_else(|| panel.default_size(window, cx))
+ });
+ }
}
+
+ size_state
+ .size
+ .unwrap_or_else(|| panel.default_size(window, cx))
}
impl Render for Dock {
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let dispatch_context = Self::dispatch_context();
if let Some(entry) = self.visible_entry() {
- let size = entry.panel.size(window, cx);
+ let size = self.resolved_panel_size(entry, window, cx);
let position = self.position;
let create_resize_handle = || {
@@ -1046,7 +1296,8 @@ pub mod test {
pub zoomed: bool,
pub active: bool,
pub focus_handle: FocusHandle,
- pub size: Pixels,
+ pub default_size: Pixels,
+ pub flexible: bool,
pub activation_priority: u32,
}
actions!(test_only, [ToggleTestPanel]);
@@ -1060,10 +1311,22 @@ pub mod test {
zoomed: false,
active: false,
focus_handle: cx.focus_handle(),
- size: px(300.),
+ default_size: px(300.),
+ flexible: false,
activation_priority,
}
}
+
+ pub fn new_flexible(
+ position: DockPosition,
+ activation_priority: u32,
+ cx: &mut App,
+ ) -> Self {
+ Self {
+ flexible: true,
+ ..Self::new(position, activation_priority, cx)
+ }
+ }
}
impl Render for TestPanel {
@@ -1094,12 +1357,19 @@ pub mod test {
cx.update_global::<SettingsStore, _>(|_, _| {});
}
- fn size(&self, _window: &Window, _: &App) -> Pixels {
- self.size
+ fn default_size(&self, _window: &Window, _: &App) -> Pixels {
+ self.default_size
+ }
+
+ fn initial_size_state(&self, _window: &Window, _: &App) -> PanelSizeState {
+ PanelSizeState {
+ size: None,
+ flexible_size_ratio: None,
+ }
}
- fn set_size(&mut self, size: Option<Pixels>, _window: &mut Window, _: &mut Context<Self>) {
- self.size = size.unwrap_or(px(300.));
+ fn supports_flexible_size(&self, _window: &Window, _: &App) -> bool {
+ self.flexible
}
fn icon(&self, _window: &Window, _: &App) -> Option<ui::IconName> {
@@ -97,6 +97,10 @@ impl PaneGroup {
}
}
+ pub fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
+ self.root.width_fraction_for_pane(pane)
+ }
+
pub fn pane_at_pixel_position(&self, coordinate: Point<Pixels>) -> Option<&Entity<Pane>> {
match &self.root {
Member::Pane(pane) => Some(pane),
@@ -301,6 +305,13 @@ impl Member {
}),
}
}
+
+ fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
+ match self {
+ Member::Pane(found) => (found == pane).then_some(1.0),
+ Member::Axis(axis) => axis.width_fraction_for_pane(pane),
+ }
+ }
}
#[derive(Clone, Copy)]
@@ -884,6 +895,40 @@ impl PaneAxis {
None
}
+ fn width_fraction_for_pane(&self, pane: &Entity<Pane>) -> Option<f32> {
+ let flexes = self.flexes.lock();
+ let total_flex = flexes.iter().copied().sum::<f32>();
+
+ for (index, member) in self.members.iter().enumerate() {
+ let child_fraction = if total_flex > 0.0 {
+ flexes[index] / total_flex
+ } else {
+ 1.0 / self.members.len() as f32
+ };
+
+ match member {
+ Member::Pane(found) => {
+ if found == pane {
+ return Some(match self.axis {
+ Axis::Horizontal => child_fraction,
+ Axis::Vertical => 1.0,
+ });
+ }
+ }
+ Member::Axis(axis) => {
+ if let Some(descendant_fraction) = axis.width_fraction_for_pane(pane) {
+ return Some(match self.axis {
+ Axis::Horizontal => child_fraction * descendant_fraction,
+ Axis::Vertical => descendant_fraction,
+ });
+ }
+ }
+ }
+ }
+
+ None
+ }
+
fn render(
&self,
basis: usize,
@@ -51,8 +51,8 @@ use futures::{
future::{Shared, try_join_all},
};
use gpui::{
- Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Bounds, Context,
- CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
+ Action, AnyEntity, AnyView, AnyWeakView, App, AsyncApp, AsyncWindowContext, Axis, Bounds,
+ Context, CursorStyle, Decorations, DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle,
Focusable, Global, HitboxBehavior, Hsla, KeyContext, Keystroke, ManagedView, MouseButton,
PathPromptOptions, Point, PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription,
SystemWindowTabController, Task, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId,
@@ -2127,6 +2127,159 @@ impl Workspace {
}
}
+ pub fn panel_size_state<T: Panel>(&self, cx: &App) -> Option<dock::PanelSizeState> {
+ self.all_docks().into_iter().find_map(|dock| {
+ let dock = dock.read(cx);
+ let panel = dock.panel::<T>()?;
+ dock.stored_panel_size_state(&panel)
+ })
+ }
+
+ pub fn persisted_panel_size_state(
+ &self,
+ panel_key: &'static str,
+ cx: &App,
+ ) -> Option<dock::PanelSizeState> {
+ dock::Dock::load_persisted_size_state(self, panel_key, cx)
+ }
+
+ pub fn persist_panel_size_state(
+ &self,
+ panel_key: &str,
+ size_state: dock::PanelSizeState,
+ cx: &mut App,
+ ) {
+ let Some(workspace_id) = self
+ .database_id()
+ .map(|id| i64::from(id).to_string())
+ .or(self.session_id())
+ else {
+ return;
+ };
+
+ let kvp = db::kvp::KeyValueStore::global(cx);
+ let panel_key = panel_key.to_string();
+ cx.background_spawn(async move {
+ let scope = kvp.scoped(dock::PANEL_SIZE_STATE_KEY);
+ scope
+ .write(
+ format!("{workspace_id}:{panel_key}"),
+ serde_json::to_string(&size_state)?,
+ )
+ .await
+ })
+ .detach_and_log_err(cx);
+ }
+
+ pub fn set_panel_size_state<T: Panel>(
+ &mut self,
+ size_state: dock::PanelSizeState,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> bool {
+ let Some(panel) = self.panel::<T>(cx) else {
+ return false;
+ };
+
+ let dock = self.dock_at_position(panel.position(window, cx));
+ let did_set = dock.update(cx, |dock, cx| {
+ dock.set_panel_size_state(&panel, size_state, cx)
+ });
+
+ if did_set {
+ self.persist_panel_size_state(T::panel_key(), size_state, cx);
+ }
+
+ did_set
+ }
+
+ pub fn flexible_dock_size(
+ &self,
+ position: DockPosition,
+ ratio: f32,
+ window: &Window,
+ cx: &App,
+ ) -> Option<Pixels> {
+ if position.axis() != Axis::Horizontal {
+ return None;
+ }
+
+ let available_width = self.available_width_for_horizontal_dock(position, window, cx)?;
+ Some((available_width * ratio.clamp(0.0, 1.0)).max(RESIZE_HANDLE_SIZE))
+ }
+
+ pub fn resolved_dock_panel_size(
+ &self,
+ dock: &Dock,
+ panel: &dyn PanelHandle,
+ window: &Window,
+ cx: &App,
+ ) -> Pixels {
+ let size_state = dock.stored_panel_size_state(panel).unwrap_or_default();
+ dock::resolve_panel_size(size_state, panel, dock.position(), self, window, cx)
+ }
+
+ pub fn flexible_dock_ratio_for_size(
+ &self,
+ position: DockPosition,
+ size: Pixels,
+ window: &Window,
+ cx: &App,
+ ) -> Option<f32> {
+ if position.axis() != Axis::Horizontal {
+ return None;
+ }
+
+ let available_width = self.available_width_for_horizontal_dock(position, window, cx)?;
+ let available_width = available_width.max(RESIZE_HANDLE_SIZE);
+ Some((size / available_width).clamp(0.0, 1.0))
+ }
+
+ fn available_width_for_horizontal_dock(
+ &self,
+ position: DockPosition,
+ window: &Window,
+ cx: &App,
+ ) -> Option<Pixels> {
+ let workspace_width = self.bounds.size.width;
+ if workspace_width <= Pixels::ZERO {
+ return None;
+ }
+
+ let opposite_position = match position {
+ DockPosition::Left => DockPosition::Right,
+ DockPosition::Right => DockPosition::Left,
+ DockPosition::Bottom => return None,
+ };
+
+ let opposite_width = self
+ .dock_at_position(opposite_position)
+ .read(cx)
+ .stored_active_panel_size(window, cx)
+ .unwrap_or(Pixels::ZERO);
+
+ Some((workspace_width - opposite_width).max(RESIZE_HANDLE_SIZE))
+ }
+
+ pub fn default_flexible_dock_ratio(&self, position: DockPosition, cx: &App) -> Option<f32> {
+ if position.axis() != Axis::Horizontal {
+ return None;
+ }
+
+ if self
+ .center
+ .panes()
+ .iter()
+ .all(|pane| pane.read(cx).items_len() == 0)
+ {
+ return Some(1.0);
+ }
+
+ let pane = self.last_active_center_pane.clone()?.upgrade()?;
+ let pane_fraction = self.center.width_fraction_for_pane(&pane).unwrap_or(1.0);
+ Some((pane_fraction / (1.0 + pane_fraction)).clamp(0.0, 1.0))
+ }
+
pub fn is_edited(&self) -> bool {
self.window_edited
}
@@ -2144,9 +2297,25 @@ impl Workspace {
let dock_position = panel.position(window, cx);
let dock = self.dock_at_position(dock_position);
let any_panel = panel.to_any();
+ let persisted_size_state =
+ self.persisted_panel_size_state(T::panel_key(), cx)
+ .or_else(|| {
+ load_legacy_panel_size(T::panel_key(), dock_position, self, cx).map(|size| {
+ let state = dock::PanelSizeState {
+ size: Some(size),
+ flexible_size_ratio: None,
+ };
+ self.persist_panel_size_state(T::panel_key(), state, cx);
+ state
+ })
+ });
dock.update(cx, |dock, cx| {
- dock.add_panel(panel, self.weak_self.clone(), window, cx)
+ let index = dock.add_panel(panel.clone(), self.weak_self.clone(), window, cx);
+ if let Some(size_state) = persisted_size_state {
+ dock.set_panel_size_state(&panel, size_state, cx);
+ }
+ index
});
cx.emit(Event::PanelAdded(any_panel));
@@ -4730,11 +4899,15 @@ impl Workspace {
.into_iter()
.find(|dock| dock.focus_handle(cx).contains_focused(window, cx));
- if let Some(dock) = active_dock {
- let Some(panel_size) = dock.read(cx).active_panel_size(window, cx) else {
+ if let Some(dock_entity) = active_dock {
+ let dock = dock_entity.read(cx);
+ let Some(panel_size) = dock
+ .active_panel()
+ .map(|panel| self.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ else {
return;
};
- match dock.read(cx).position() {
+ match dock.position() {
DockPosition::Left => self.resize_left_dock(panel_size + amount, window, cx),
DockPosition::Bottom => self.resize_bottom_dock(panel_size + amount, window, cx),
DockPosition::Right => self.resize_right_dock(panel_size + amount, window, cx),
@@ -6754,24 +6927,33 @@ impl Workspace {
|workspace: &mut Workspace, _: &ResetActiveDockSize, window, cx| {
for dock in workspace.all_docks() {
if dock.focus_handle(cx).contains_focused(window, cx) {
- let Some(panel) = dock.read(cx).active_panel() else {
- return;
- };
-
- // Set to `None`, then the size will fall back to the default.
- panel.clone().set_size(None, window, cx);
-
+ let panel = dock.read(cx).active_panel().cloned();
+ if let Some(panel) = panel {
+ dock.update(cx, |dock, cx| {
+ dock.set_panel_size_state(
+ panel.as_ref(),
+ dock::PanelSizeState::default(),
+ cx,
+ );
+ });
+ }
return;
}
}
},
))
.on_action(cx.listener(
- |workspace: &mut Workspace, _: &ResetOpenDocksSize, window, cx| {
+ |workspace: &mut Workspace, _: &ResetOpenDocksSize, _window, cx| {
for dock in workspace.all_docks() {
- if let Some(panel) = dock.read(cx).visible_panel() {
- // Set to `None`, then the size will fall back to the default.
- panel.clone().set_size(None, window, cx);
+ let panel = dock.read(cx).visible_panel().cloned();
+ if let Some(panel) = panel {
+ dock.update(cx, |dock, cx| {
+ dock.set_panel_size_state(
+ panel.as_ref(),
+ dock::PanelSizeState::default(),
+ cx,
+ );
+ });
}
}
},
@@ -7183,21 +7365,22 @@ impl Workspace {
self.right_dock.read_with(cx, |right_dock, cx| {
let right_dock_size = right_dock
- .active_panel_size(window, cx)
+ .stored_active_panel_size(window, cx)
.unwrap_or(Pixels::ZERO);
if right_dock_size + size > workspace_width {
size = workspace_width - right_dock_size
}
});
+ let ratio = self.flexible_dock_ratio_for_size(DockPosition::Left, size, window, cx);
self.left_dock.update(cx, |left_dock, cx| {
if WorkspaceSettings::get_global(cx)
.resize_all_panels_in_dock
.contains(&DockPosition::Left)
{
- left_dock.resize_all_panels(Some(size), window, cx);
+ left_dock.resize_all_panels_with_ratio(Some(size), ratio, window, cx);
} else {
- left_dock.resize_active_panel(Some(size), window, cx);
+ left_dock.resize_active_panel_with_ratio(Some(size), ratio, window, cx);
}
});
}
@@ -7207,20 +7390,21 @@ impl Workspace {
let mut size = new_size.min(workspace_width - RESIZE_HANDLE_SIZE);
self.left_dock.read_with(cx, |left_dock, cx| {
let left_dock_size = left_dock
- .active_panel_size(window, cx)
+ .stored_active_panel_size(window, cx)
.unwrap_or(Pixels::ZERO);
if left_dock_size + size > workspace_width {
size = workspace_width - left_dock_size
}
});
+ let ratio = self.flexible_dock_ratio_for_size(DockPosition::Right, size, window, cx);
self.right_dock.update(cx, |right_dock, cx| {
if WorkspaceSettings::get_global(cx)
.resize_all_panels_in_dock
.contains(&DockPosition::Right)
{
- right_dock.resize_all_panels(Some(size), window, cx);
+ right_dock.resize_all_panels_with_ratio(Some(size), ratio, window, cx);
} else {
- right_dock.resize_active_panel(Some(size), window, cx);
+ right_dock.resize_active_panel_with_ratio(Some(size), ratio, window, cx);
}
});
}
@@ -7615,7 +7799,10 @@ fn adjust_active_dock_size_by_px(
return;
};
let dock = active_dock.read(cx);
- let Some(panel_size) = dock.active_panel_size(window, cx) else {
+ let Some(panel_size) = dock
+ .active_panel()
+ .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ else {
return;
};
let dock_pos = dock.position();
@@ -7631,10 +7818,12 @@ fn adjust_open_docks_size_by_px(
let docks = workspace
.all_docks()
.into_iter()
- .filter_map(|dock| {
- if dock.read(cx).is_open() {
- let dock = dock.read(cx);
- let panel_size = dock.active_panel_size(window, cx)?;
+ .filter_map(|dock_entity| {
+ let dock = dock_entity.read(cx);
+ if dock.is_open() {
+ let panel_size = dock.active_panel().map(|panel| {
+ workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)
+ })?;
let dock_pos = dock.position();
Some((panel_size, dock_pos, px))
} else {
@@ -10111,6 +10300,57 @@ pub fn with_active_or_new_workspace(
}
}
+/// Reads a panel's pixel size from its legacy KVP format and deletes the legacy
+/// key. This migration path only runs once per panel per workspace.
+fn load_legacy_panel_size(
+ panel_key: &str,
+ dock_position: DockPosition,
+ workspace: &Workspace,
+ cx: &mut App,
+) -> Option<Pixels> {
+ #[derive(Deserialize)]
+ struct LegacyPanelState {
+ #[serde(default)]
+ width: Option<Pixels>,
+ #[serde(default)]
+ height: Option<Pixels>,
+ }
+
+ let workspace_id = workspace
+ .database_id()
+ .map(|id| i64::from(id).to_string())
+ .or_else(|| workspace.session_id())?;
+
+ let legacy_key = match panel_key {
+ "ProjectPanel" => {
+ format!("{}-{:?}", "ProjectPanel", workspace_id)
+ }
+ "OutlinePanel" => {
+ format!("{}-{:?}", "OutlinePanel", workspace_id)
+ }
+ "GitPanel" => {
+ format!("{}-{:?}", "GitPanel", workspace_id)
+ }
+ "TerminalPanel" => {
+ format!("{:?}-{:?}", "TerminalPanel", workspace_id)
+ }
+ _ => return None,
+ };
+
+ let kvp = db::kvp::KeyValueStore::global(cx);
+ let json = kvp.read_kvp(&legacy_key).log_err().flatten()?;
+ let state = serde_json::from_str::<LegacyPanelState>(&json).log_err()?;
+ let size = match dock_position {
+ DockPosition::Bottom => state.height,
+ DockPosition::Left | DockPosition::Right => state.width,
+ }?;
+
+ cx.background_spawn(async move { kvp.delete_kvp(legacy_key).await })
+ .detach_and_log_err(cx);
+
+ Some(size)
+}
+
#[cfg(test)]
mod tests {
use std::{cell::RefCell, rc::Rc, sync::Arc, time::Duration};
@@ -11887,6 +12127,357 @@ mod tests {
assert_eq!(active_item.item_id(), last_item.item_id());
});
}
+
+ #[gpui::test]
+ async fn test_flexible_dock_sizing(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ let project = Project::test(fs, [], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.bounds.size.width = px(800.);
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
+ workspace.add_panel(panel, window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+
+ let dock = workspace.right_dock().read(cx);
+ let workspace_width = workspace.bounds.size.width;
+ let expanded_width = dock
+ .active_panel()
+ .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ .expect("flexible dock should fill the center when there are no tabs");
+
+ assert_eq!(expanded_width, workspace_width);
+ });
+
+ let (panel, resized_width, ratio_basis_width) =
+ workspace.update_in(cx, |workspace, window, cx| {
+ let item = cx.new(|cx| {
+ TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+ });
+ workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
+
+ let dock = workspace.right_dock().read(cx);
+ let workspace_width = workspace.bounds.size.width;
+ let initial_width = dock
+ .active_panel()
+ .map(|panel| {
+ workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)
+ })
+ .expect("flexible dock should have an initial width");
+
+ assert_eq!(initial_width, workspace_width / 2.);
+
+ workspace.resize_right_dock(px(300.), window, cx);
+
+ let dock = workspace.right_dock().read(cx);
+ let resized_width = dock
+ .active_panel()
+ .map(|panel| {
+ workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx)
+ })
+ .expect("flexible dock should keep its resized width");
+
+ assert_eq!(resized_width, px(300.));
+
+ let panel = workspace
+ .right_dock()
+ .read(cx)
+ .visible_panel()
+ .expect("flexible dock should have a visible panel")
+ .panel_id();
+
+ (panel, resized_width, workspace_width)
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+
+ let dock = workspace.right_dock().read(cx);
+ let reopened_width = dock
+ .active_panel()
+ .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ .expect("flexible dock should restore when reopened");
+
+ assert_eq!(reopened_width, resized_width);
+
+ let right_dock = workspace.right_dock().read(cx);
+ let flexible_panel = right_dock
+ .visible_panel()
+ .expect("flexible dock should still have a visible panel");
+ assert_eq!(flexible_panel.panel_id(), panel);
+ assert_eq!(
+ right_dock
+ .stored_panel_size_state(flexible_panel.as_ref())
+ .and_then(|size_state| size_state.flexible_size_ratio),
+ Some(resized_width.to_f64() as f32 / workspace.bounds.size.width.to_f64() as f32)
+ );
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.split_pane(
+ workspace.active_pane().clone(),
+ SplitDirection::Right,
+ window,
+ cx,
+ );
+
+ let dock = workspace.right_dock().read(cx);
+ let split_width = dock
+ .active_panel()
+ .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ .expect("flexible dock should keep its user-resized proportion");
+
+ assert_eq!(split_width, px(300.));
+
+ workspace.bounds.size.width = px(1600.);
+
+ let dock = workspace.right_dock().read(cx);
+ let resized_window_width = dock
+ .active_panel()
+ .map(|panel| workspace.resolved_dock_panel_size(&dock, panel.as_ref(), window, cx))
+ .expect("flexible dock should preserve proportional size on window resize");
+
+ assert_eq!(
+ resized_window_width,
+ workspace.bounds.size.width
+ * (resized_width.to_f64() as f32 / ratio_basis_width.to_f64() as f32)
+ );
+ });
+ }
+
+ #[gpui::test]
+ async fn test_panel_size_state_persistence(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ // Fixed-width panel: pixel size is persisted to KVP and restored on re-add.
+ {
+ let project = Project::test(fs.clone(), [], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ workspace.bounds.size.width = px(800.);
+ });
+
+ let panel = workspace.update_in(cx, |workspace, window, cx| {
+ let panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
+ workspace.add_panel(panel.clone(), window, cx);
+ workspace.toggle_dock(DockPosition::Left, window, cx);
+ panel
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.resize_left_dock(px(350.), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ let persisted = workspace.read_with(cx, |workspace, cx| {
+ workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
+ });
+ assert_eq!(
+ persisted.and_then(|s| s.size),
+ Some(px(350.)),
+ "fixed-width panel size should be persisted to KVP"
+ );
+
+ // Remove the panel and re-add a fresh instance with the same key.
+ // The new instance should have its size state restored from KVP.
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.remove_panel(&panel, window, cx);
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let new_panel = cx.new(|cx| TestPanel::new(DockPosition::Left, 100, cx));
+ workspace.add_panel(new_panel, window, cx);
+
+ let left_dock = workspace.left_dock().read(cx);
+ let size_state = left_dock
+ .panel::<TestPanel>()
+ .and_then(|p| left_dock.stored_panel_size_state(&p));
+ assert_eq!(
+ size_state.and_then(|s| s.size),
+ Some(px(350.)),
+ "re-added fixed-width panel should restore persisted size from KVP"
+ );
+ });
+ }
+
+ // Flexible panel: both pixel size and ratio are persisted and restored.
+ {
+ let project = Project::test(fs.clone(), [], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.set_random_database_id();
+ workspace.bounds.size.width = px(800.);
+ });
+
+ let panel = workspace.update_in(cx, |workspace, window, cx| {
+ let item = cx.new(|cx| {
+ TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+ });
+ workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
+
+ let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
+ workspace.add_panel(panel.clone(), window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+ panel
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.resize_right_dock(px(300.), window, cx);
+ });
+
+ cx.run_until_parked();
+
+ let persisted = workspace
+ .read_with(cx, |workspace, cx| {
+ workspace.persisted_panel_size_state(TestPanel::panel_key(), cx)
+ })
+ .expect("flexible panel state should be persisted to KVP");
+ assert_eq!(
+ persisted.size, None,
+ "flexible panel should not persist a redundant pixel size"
+ );
+ let original_ratio = persisted
+ .flexible_size_ratio
+ .expect("flexible panel ratio should be persisted");
+
+ // Remove the panel and re-add: both size and ratio should be restored.
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.remove_panel(&panel, window, cx);
+ });
+
+ workspace.update_in(cx, |workspace, window, cx| {
+ let new_panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Right, 100, cx));
+ workspace.add_panel(new_panel, window, cx);
+
+ let right_dock = workspace.right_dock().read(cx);
+ let size_state = right_dock
+ .panel::<TestPanel>()
+ .and_then(|p| right_dock.stored_panel_size_state(&p))
+ .expect("re-added flexible panel should have restored size state from KVP");
+ assert_eq!(
+ size_state.size, None,
+ "re-added flexible panel should not have a persisted pixel size"
+ );
+ assert_eq!(
+ size_state.flexible_size_ratio,
+ Some(original_ratio),
+ "re-added flexible panel should restore persisted ratio"
+ );
+ });
+ }
+ }
+
+ #[gpui::test]
+ async fn test_flexible_panel_left_dock_sizing(cx: &mut gpui::TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+
+ let project = Project::test(fs, [], cx).await;
+ let (multi_workspace, cx) =
+ cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
+ let workspace = multi_workspace.read_with(cx, |mw, _| mw.workspace().clone());
+
+ workspace.update(cx, |workspace, _cx| {
+ workspace.bounds.size.width = px(900.);
+ });
+
+ // Step 1: Add a tab to the center pane then open a flexible panel in the left
+ // dock. With one full-width center pane the default ratio is 0.5, so the panel
+ // and the center pane each take half the workspace width.
+ workspace.update_in(cx, |workspace, window, cx| {
+ let item = cx.new(|cx| {
+ TestItem::new(cx).with_project_items(&[TestProjectItem::new(1, "one.txt", cx)])
+ });
+ workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
+
+ let panel = cx.new(|cx| TestPanel::new_flexible(DockPosition::Left, 100, cx));
+ workspace.add_panel(panel, window, cx);
+ workspace.toggle_dock(DockPosition::Left, window, cx);
+
+ let left_dock = workspace.left_dock().read(cx);
+ let left_width = left_dock
+ .active_panel()
+ .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx))
+ .expect("left dock should have an active panel");
+
+ assert_eq!(
+ left_width,
+ workspace.bounds.size.width / 2.,
+ "flexible left panel should split evenly with the center pane"
+ );
+ });
+
+ // Step 2: Split the center pane vertically (top/bottom). Vertical splits do not
+ // change horizontal width fractions, so the flexible panel stays at the same
+ // width as each half of the split.
+ workspace.update_in(cx, |workspace, window, cx| {
+ workspace.split_pane(
+ workspace.active_pane().clone(),
+ SplitDirection::Down,
+ window,
+ cx,
+ );
+
+ let left_dock = workspace.left_dock().read(cx);
+ let left_width = left_dock
+ .active_panel()
+ .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx))
+ .expect("left dock should still have an active panel after vertical split");
+
+ assert_eq!(
+ left_width,
+ workspace.bounds.size.width / 2.,
+ "flexible left panel width should match each vertically-split pane"
+ );
+ });
+
+ // Step 3: Open a fixed-width panel in the right dock. The right dock's default
+ // size reduces the available width, so the flexible left panel and the center
+ // panes all shrink proportionally to accommodate it.
+ workspace.update_in(cx, |workspace, window, cx| {
+ let panel = cx.new(|cx| TestPanel::new(DockPosition::Right, 200, cx));
+ workspace.add_panel(panel, window, cx);
+ workspace.toggle_dock(DockPosition::Right, window, cx);
+
+ let right_dock = workspace.right_dock().read(cx);
+ let right_width = right_dock
+ .active_panel()
+ .map(|p| workspace.resolved_dock_panel_size(&right_dock, p.as_ref(), window, cx))
+ .expect("right dock should have an active panel");
+
+ let left_dock = workspace.left_dock().read(cx);
+ let left_width = left_dock
+ .active_panel()
+ .map(|p| workspace.resolved_dock_panel_size(&left_dock, p.as_ref(), window, cx))
+ .expect("left dock should still have an active panel");
+
+ let available_width = workspace.bounds.size.width - right_width;
+ assert_eq!(
+ left_width,
+ available_width / 2.,
+ "flexible left panel should shrink proportionally as the right dock takes space"
+ );
+ });
+ }
+
struct TestModal(FocusHandle);
impl TestModal {
@@ -11940,12 +12531,10 @@ mod tests {
);
assert_eq!(
left_dock.read(cx).active_panel_size(window, cx).unwrap(),
- panel_1.size(window, cx)
+ px(300.)
);
- left_dock.update(cx, |left_dock, cx| {
- left_dock.resize_active_panel(Some(px(1337.)), window, cx)
- });
+ workspace.resize_left_dock(px(1337.), window, cx);
assert_eq!(
workspace
.right_dock()
@@ -12031,7 +12620,7 @@ mod tests {
let bottom_dock = workspace.bottom_dock();
assert_eq!(
bottom_dock.read(cx).active_panel_size(window, cx).unwrap(),
- panel_1.size(window, cx),
+ px(300.),
);
// Close bottom dock and move panel_1 back to the left.
bottom_dock.update(cx, |bottom_dock, cx| {
@@ -119,7 +119,7 @@ use {
time::Duration,
},
util::ResultExt as _,
- workspace::{AppState, MultiWorkspace, Panel as _, Workspace},
+ workspace::{AppState, MultiWorkspace, Workspace},
zed_actions::OpenSettingsAt,
};
@@ -3545,7 +3545,6 @@ edition = "2021"
new_workspace.update(cx, |workspace, cx| {
if let Some(new_panel) = workspace.panel::<AgentPanel>(cx) {
new_panel.update(cx, |panel, cx| {
- panel.set_size(Some(px(480.0)), window, cx);
panel.open_external_thread_with_server(stub_agent.clone(), window, cx);
});
}