workspace: Handle non-cloneable items better (#41215)

When trying to split and clone a non clone-able workspace item we now
attempt split and move instead of doing nothing. Additionally we disable
the split menu buttons if we can't split the active item at all.

Release Notes:

- Improved handling of unsplittable panes
This commit is contained in:
Lukas Wirth
2025-10-26 13:24:26 +00:00
committed by GitHub
parent 61e4a1d16a
commit fd3ca0303f
23 changed files with 196 additions and 65 deletions
+18 -4
View File
@@ -213,16 +213,21 @@ pub trait Item: Focusable + EventEmitter<Self::Event> + Render + Sized {
ItemBufferKind::None
}
fn set_nav_history(&mut self, _: ItemNavHistory, _window: &mut Window, _: &mut Context<Self>) {}
fn can_split(&self) -> bool {
false
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
_window: &mut Window,
_: &mut Context<Self>,
workspace_id: Option<WorkspaceId>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Task<Option<Entity<Self>>>
where
Self: Sized,
{
Task::ready(None)
_ = (workspace_id, window, cx);
unimplemented!("clone_on_split() must be implemented if can_split() returns true")
}
fn is_dirty(&self, _: &App) -> bool {
false
@@ -418,6 +423,7 @@ pub trait ItemHandle: 'static + Send {
);
fn buffer_kind(&self, cx: &App) -> ItemBufferKind;
fn boxed_clone(&self) -> Box<dyn ItemHandle>;
fn can_split(&self, cx: &App) -> bool;
fn clone_on_split(
&self,
workspace_id: Option<WorkspaceId>,
@@ -631,6 +637,10 @@ impl<T: Item> ItemHandle for Entity<T> {
Box::new(self.clone())
}
fn can_split(&self, cx: &App) -> bool {
self.read(cx).can_split()
}
fn clone_on_split(
&self,
workspace_id: Option<WorkspaceId>,
@@ -1503,6 +1513,10 @@ pub mod test {
self.push_to_nav_history(cx);
}
fn can_split(&self) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
+35 -17
View File
@@ -3292,18 +3292,22 @@ impl Pane {
else {
return;
};
let task = item.clone_on_split(database_id, window, cx);
let to_pane = to_pane.downgrade();
cx.spawn_in(window, async move |_, cx| {
if let Some(item) = task.await {
to_pane
.update_in(cx, |pane, window, cx| {
pane.add_item(item, true, true, None, window, cx)
})
.ok();
}
})
.detach();
if item.can_split(cx) {
let task = item.clone_on_split(database_id, window, cx);
let to_pane = to_pane.downgrade();
cx.spawn_in(window, async move |_, cx| {
if let Some(item) = task.await {
to_pane
.update_in(cx, |pane, window, cx| {
pane.add_item(item, true, true, None, window, cx)
})
.ok();
}
})
.detach();
} else {
move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
}
} else {
move_item(&from_pane, &to_pane, item_id, ix, true, window, cx);
}
@@ -3597,6 +3601,11 @@ fn default_render_tab_bar_buttons(
if !pane.has_focus(window, cx) && !pane.context_menu_focused(window, cx) {
return (None, None);
}
let (can_clone, can_split_move) = match pane.active_item() {
Some(active_item) if active_item.can_split(cx) => (true, false),
Some(_) => (false, pane.items_len() > 1),
None => (false, false),
};
// Ideally we would return a vec of elements here to pass directly to the [TabBar]'s
// `end_slot`, but due to needing a view here that isn't possible.
let right_children = h_flex()
@@ -3633,17 +3642,26 @@ fn default_render_tab_bar_buttons(
.child(
PopoverMenu::new("pane-tab-bar-split")
.trigger_with_tooltip(
IconButton::new("split", IconName::Split).icon_size(IconSize::Small),
IconButton::new("split", IconName::Split)
.icon_size(IconSize::Small)
.disabled(!can_clone && !can_split_move),
Tooltip::text("Split Pane"),
)
.anchor(Corner::TopRight)
.with_handle(pane.split_item_context_menu_handle.clone())
.menu(move |window, cx| {
ContextMenu::build(window, cx, |menu, _, _| {
menu.action("Split Right", SplitRight.boxed_clone())
.action("Split Left", SplitLeft.boxed_clone())
.action("Split Up", SplitUp.boxed_clone())
.action("Split Down", SplitDown.boxed_clone())
if can_split_move {
menu.action("Split Right", SplitAndMoveRight.boxed_clone())
.action("Split Left", SplitAndMoveLeft.boxed_clone())
.action("Split Up", SplitAndMoveUp.boxed_clone())
.action("Split Down", SplitAndMoveDown.boxed_clone())
} else {
menu.action("Split Right", SplitRight.boxed_clone())
.action("Split Left", SplitLeft.boxed_clone())
.action("Split Up", SplitUp.boxed_clone())
.action("Split Down", SplitDown.boxed_clone())
}
})
.into()
}),
+4
View File
@@ -109,6 +109,10 @@ impl Item for SharedScreen {
self.nav_history = Some(history);
}
fn can_split(&self) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<WorkspaceId>,
+4
View File
@@ -97,6 +97,10 @@ impl Item for ThemePreview {
None
}
fn can_split(&self) -> bool {
true
}
fn clone_on_split(
&self,
_workspace_id: Option<crate::WorkspaceId>,
+54 -34
View File
@@ -3663,24 +3663,31 @@ impl Workspace {
};
if action.clone {
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
)
} else {
move_active_item(
&self.active_pane,
&destination,
action.focus,
true,
window,
cx,
)
if self
.active_pane
.read(cx)
.active_item()
.is_some_and(|item| item.can_split(cx))
{
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
);
return;
}
}
move_active_item(
&self.active_pane,
&destination,
action.focus,
true,
window,
cx,
)
}
pub fn activate_next_pane(&mut self, window: &mut Window, cx: &mut App) {
@@ -3841,24 +3848,31 @@ impl Workspace {
};
if action.clone {
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
)
} else {
move_active_item(
&self.active_pane,
&destination,
action.focus,
true,
window,
cx,
);
if self
.active_pane
.read(cx)
.active_item()
.is_some_and(|item| item.can_split(cx))
{
clone_active_item(
self.database_id(),
&self.active_pane,
&destination,
action.focus,
window,
cx,
);
return;
}
}
move_active_item(
&self.active_pane,
&destination,
action.focus,
true,
window,
cx,
);
}
pub fn bounding_box_for_pane(&self, pane: &Entity<Pane>) -> Option<Bounds<Pixels>> {
@@ -4141,6 +4155,9 @@ impl Workspace {
let Some(item) = pane.read(cx).active_item() else {
return Task::ready(None);
};
if !item.can_split(cx) {
return Task::ready(None);
}
let task = item.clone_on_split(self.database_id(), window, cx);
cx.spawn_in(window, async move |this, cx| {
if let Some(clone) = task.await {
@@ -8225,6 +8242,9 @@ pub fn clone_active_item(
let Some(active_item) = source.read(cx).active_item() else {
return;
};
if !active_item.can_split(cx) {
return;
}
let destination = destination.downgrade();
let task = active_item.clone_on_split(workspace_id, window, cx);
window