workspace: Move tabs across windows by drag and drop
Dropping a tab on another window's pane, or releasing it outside every window, previously had no defined behavior: the item entity would be re-parented into a pane whose project it does not belong to, or the drag would simply stick. Both gestures now transfer the item by round-tripping it through the workspace database: the source item is serialized, recreated in the target pane via the SerializableItem registry, and closed in its source window. Items that are dirty or not serializable snap back instead. A release outside every window opens a new window for the item, sized like the source window and placed at the cursor, clamped to the display. Workspace::new_local_with_bounds exposes the window placement for that; new_local keeps its previous behavior.
This commit is contained in:
1 parent
a7b8190c24
commit
b3cca826c8
3 files changed
+395
-37
No files matched your search
@@ -7,16 +7,16 @@ use std::{
|
||||
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
point, px, relative, size, BackgroundExecutor, Bounds, Context, Corners, Element,
|
||||
ElementId, Empty, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
|
||||
ParentElement, Pixels, Render, RenderImage, Style, Styled, Subscription, Task, Window,
|
||||
BackgroundExecutor, Bounds, Context, Corners, Element, ElementId, Empty, GlobalElementId,
|
||||
InspectorElementId, IntoElement, LayoutId, ParentElement, Pixels, Render, RenderImage, Style,
|
||||
Styled, Subscription, Task, Window, point, px, relative, size,
|
||||
};
|
||||
use util::ResultExt;
|
||||
use image::Frame;
|
||||
use settings::{
|
||||
BackgroundImageFit, BackgroundImagePosition, RegisterSetting, Settings, SettingsStore,
|
||||
};
|
||||
use smallvec::SmallVec;
|
||||
use util::ResultExt;
|
||||
|
||||
/// Default opacity of the editor and terminal surfaces drawn on top of
|
||||
/// the background image. Lower values let the image show through more.
|
||||
@@ -125,11 +125,9 @@ impl BackgroundImage {
|
||||
let path = path.clone();
|
||||
let path_for_check = path.clone();
|
||||
let cache_dir = cache_dir.clone();
|
||||
let load_task = executor
|
||||
.clone()
|
||||
.spawn(async move {
|
||||
load_background_image(&fs, &path, &cache_dir, executor.clone()).await
|
||||
});
|
||||
let load_task = executor.clone().spawn(async move {
|
||||
load_background_image(&fs, &path, &cache_dir, executor.clone()).await
|
||||
});
|
||||
let image = load_task.await;
|
||||
|
||||
this.update(cx, |this, cx| {
|
||||
@@ -335,7 +333,7 @@ async fn load_background_image(
|
||||
);
|
||||
load_from_cache(fs, path, cache_dir).await
|
||||
}
|
||||
}
|
||||
},
|
||||
futures::future::Either::Left((Err(error), _)) => {
|
||||
log::info!(
|
||||
"failed to load background image at {}: {error}",
|
||||
@@ -441,9 +439,7 @@ mod tests {
|
||||
cx: &mut TestAppContext,
|
||||
fs: Arc<FakeFs>,
|
||||
) -> gpui::Entity<BackgroundImage> {
|
||||
cx.update(|cx| {
|
||||
cx.new(|cx| BackgroundImage::new(fs, Arc::from(Path::new(CACHE_DIR)), cx))
|
||||
})
|
||||
cx.update(|cx| cx.new(|cx| BackgroundImage::new(fs, Arc::from(Path::new(CACHE_DIR)), cx)))
|
||||
}
|
||||
|
||||
/// A minimal 2x2 PNG of the given color.
|
||||
@@ -523,7 +519,8 @@ mod tests {
|
||||
settings_store(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/background.png", png_bytes(image::Rgba([255, 0, 0, 255]))).await;
|
||||
fs.insert_file("/background.png", png_bytes(image::Rgba([255, 0, 0, 255])))
|
||||
.await;
|
||||
|
||||
let background_image = new_background_image(cx, fs.clone());
|
||||
cx.run_until_parked();
|
||||
@@ -581,11 +578,10 @@ mod tests {
|
||||
settings_store(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let cache_path = cache_path_for(
|
||||
Path::new("/remote/background.png"),
|
||||
Path::new(CACHE_DIR),
|
||||
);
|
||||
fs.write(&cache_path, &png_bytes(image::Rgba([255, 0, 0, 255]))).await.unwrap();
|
||||
let cache_path = cache_path_for(Path::new("/remote/background.png"), Path::new(CACHE_DIR));
|
||||
fs.write(&cache_path, &png_bytes(image::Rgba([255, 0, 0, 255])))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let background_image = new_background_image(cx, fs);
|
||||
set_background_path(cx, Some("/remote/background.png"));
|
||||
@@ -618,7 +614,8 @@ mod tests {
|
||||
settings_store(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/broken.png", b"not an image".to_vec()).await;
|
||||
fs.insert_file("/broken.png", b"not an image".to_vec())
|
||||
.await;
|
||||
|
||||
let background_image = new_background_image(cx, fs);
|
||||
set_background_path(cx, Some("/broken.png"));
|
||||
@@ -634,7 +631,8 @@ mod tests {
|
||||
settings_store(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file("/background.png", png_bytes(image::Rgba([255, 0, 0, 255]))).await;
|
||||
fs.insert_file("/background.png", png_bytes(image::Rgba([255, 0, 0, 255])))
|
||||
.await;
|
||||
|
||||
let background_image = new_background_image(cx, fs);
|
||||
set_background_path(cx, Some("/background.png"));
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
use crate::{
|
||||
CloseWindow, NewCenterTerminal, NewFile, NewTerminal, OpenInTerminal, OpenOptions,
|
||||
OpenTerminal, OpenVisible, SplitDirection, ToggleFileFinder, ToggleProjectSymbols, ToggleZoom,
|
||||
Workspace, WorkspaceItemBuilder, ZoomIn, ZoomOut,
|
||||
OpenTerminal, OpenVisible, SerializableItemRegistry, SplitDirection, ToggleFileFinder,
|
||||
ToggleProjectSymbols, ToggleZoom, Workspace, WorkspaceId, WorkspaceItemBuilder, ZoomIn,
|
||||
ZoomOut,
|
||||
focus_follows_mouse::FocusFollowsMouse as _,
|
||||
invalid_item_view::InvalidItemView,
|
||||
item::{
|
||||
@@ -14,7 +15,7 @@ use crate::{
|
||||
toolbar::Toolbar,
|
||||
workspace_settings::{AutosaveSetting, FocusFollowsMouse, TabBarSettings, WorkspaceSettings},
|
||||
};
|
||||
use anyhow::Result;
|
||||
use anyhow::{Result, anyhow};
|
||||
use collections::{BTreeSet, HashMap, HashSet, VecDeque};
|
||||
use futures::{StreamExt, stream::FuturesUnordered};
|
||||
use git::{CopyFilePermalink, OpenFilePermalink};
|
||||
@@ -3884,9 +3885,27 @@ impl Pane {
|
||||
|
||||
let from_pane = dragged_tab.pane.clone();
|
||||
|
||||
let dragged_item = dragged_tab.item.boxed_clone();
|
||||
|
||||
self.workspace
|
||||
.update(cx, |_, cx| {
|
||||
cx.defer_in(window, move |workspace, window, cx| {
|
||||
let from_workspace_id =
|
||||
from_pane.read_with(cx, |pane, _| pane.workspace.entity_id());
|
||||
if from_workspace_id != cx.entity_id() {
|
||||
let to_pane = to_pane.downgrade();
|
||||
if let Some(transfer) =
|
||||
prepare_item_transfer(&*dragged_item, &from_pane, cx)
|
||||
{
|
||||
cx.spawn_in(window, async move |_, cx| {
|
||||
complete_item_transfer(transfer, to_pane, ix, cx)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if let Some(split_direction) = split_direction {
|
||||
to_pane = workspace.split_pane(to_pane, split_direction, window, cx);
|
||||
}
|
||||
@@ -5080,18 +5099,108 @@ impl Render for DraggedTab {
|
||||
}
|
||||
}
|
||||
|
||||
/// The pieces needed to recreate an item in another workspace by round
|
||||
/// tripping it through the workspace database. Item entities are bound to
|
||||
/// their workspace's project, so moves across windows cannot move the entity
|
||||
/// itself.
|
||||
pub(crate) struct ItemTransfer {
|
||||
kind: Arc<str>,
|
||||
source_workspace_id: WorkspaceId,
|
||||
item_id: EntityId,
|
||||
source_pane: Entity<Pane>,
|
||||
serialize_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
/// Prepares to recreate `item` in another workspace. Returns `None` when the
|
||||
/// item has unsaved changes or is not serializable, in which case the drag
|
||||
/// snaps back instead of moving the entity across projects.
|
||||
pub(crate) fn prepare_item_transfer(
|
||||
item: &dyn ItemHandle,
|
||||
source_pane: &Entity<Pane>,
|
||||
cx: &mut App,
|
||||
) -> Option<ItemTransfer> {
|
||||
if item.is_dirty(cx) {
|
||||
return None;
|
||||
}
|
||||
let serializable =
|
||||
SerializableItemRegistry::view_to_serializable_item_handle(item.to_any_view(), cx)?;
|
||||
let source_workspace = source_pane.read(cx).workspace.upgrade()?;
|
||||
let source_workspace_id = source_workspace.read(cx).database_id()?;
|
||||
let serialize_task = source_workspace.update(cx, |workspace, cx| {
|
||||
serializable.serialize(workspace, false, cx)
|
||||
})?;
|
||||
Some(ItemTransfer {
|
||||
kind: Arc::from(serializable.serialized_item_kind()),
|
||||
source_workspace_id,
|
||||
item_id: item.item_id(),
|
||||
source_pane: source_pane.clone(),
|
||||
serialize_task,
|
||||
})
|
||||
}
|
||||
|
||||
/// Recreates a transferred item at index `ix` of `to_pane`, then closes the
|
||||
/// source item in its own window.
|
||||
pub(crate) async fn complete_item_transfer(
|
||||
transfer: ItemTransfer,
|
||||
to_pane: WeakEntity<Pane>,
|
||||
ix: usize,
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Result<()> {
|
||||
transfer.serialize_task.await.log_err();
|
||||
let project = to_pane
|
||||
.update(cx, |pane, _| pane.project.upgrade())?
|
||||
.ok_or_else(|| anyhow!("target pane's project was dropped"))?;
|
||||
let workspace = to_pane.update(cx, |pane, _| pane.workspace.clone())?;
|
||||
let item = to_pane
|
||||
.update_in(cx, |_, window, cx| {
|
||||
SerializableItemRegistry::deserialize(
|
||||
&transfer.kind,
|
||||
project,
|
||||
workspace,
|
||||
transfer.source_workspace_id,
|
||||
transfer.item_id.as_u64(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?;
|
||||
let to_pane = to_pane
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("target pane was dropped"))?;
|
||||
to_pane.update_in(cx, |pane, window, cx| {
|
||||
pane.add_item(item, true, true, None, window, cx);
|
||||
})?;
|
||||
let last_index = to_pane.read_with(cx, |pane, _| pane.items.len().saturating_sub(1));
|
||||
if ix < last_index {
|
||||
to_pane.update_in(cx, |_, window, cx| {
|
||||
move_item(&to_pane, &to_pane, transfer.item_id, ix, true, window, cx);
|
||||
})?;
|
||||
}
|
||||
let source_workspace = transfer
|
||||
.source_pane
|
||||
.read_with(cx, |pane, _| pane.workspace.clone());
|
||||
source_workspace
|
||||
.update_in(cx, |_, window, cx| {
|
||||
transfer.source_pane.update(cx, |pane, cx| {
|
||||
pane.close_item_by_id(transfer.item_id, SaveIntent::Close, window, cx)
|
||||
})
|
||||
})?
|
||||
.await
|
||||
.log_err();
|
||||
Ok(())
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{cell::Cell, iter::zip, num::NonZero, rc::Rc};
|
||||
use std::{cell::Cell, iter::zip, num::NonZero, rc::Rc, sync::Arc};
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
Member,
|
||||
Member, MultiWorkspace,
|
||||
item::test::{TestItem, TestProjectItem},
|
||||
};
|
||||
use gpui::{
|
||||
AppContext, Axis, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
TestAppContext, VisualTestContext, size,
|
||||
AnyDrag, AppContext, Axis, Empty, Modifiers, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
MouseUpEvent, TestAppContext, VisualTestContext, point, px, size,
|
||||
};
|
||||
use project::FakeFs;
|
||||
use settings::SettingsStore;
|
||||
@@ -9664,4 +9773,150 @@ mod tests {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_tab_drop_across_workspaces_transfers_item(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project_a = Project::test(fs.clone(), None, cx).await;
|
||||
let project_b = Project::test(fs.clone(), None, cx).await;
|
||||
cx.update(crate::register_serializable_item::<TestItem>);
|
||||
|
||||
let (workspace_a, cx_a) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project_a.clone(), window, cx));
|
||||
workspace_a.update(cx_a, |workspace, _| {
|
||||
workspace.database_id = Some(WorkspaceId(1))
|
||||
});
|
||||
let pane_a = workspace_a.read_with(cx_a, |workspace, _| workspace.active_pane().clone());
|
||||
let item = cx_a.new(|cx| TestItem::new(cx).with_serialize(|| Some(Task::ready(Ok(())))));
|
||||
pane_a.update_in(cx_a, |pane, window, cx| {
|
||||
pane.add_item(Box::new(item.clone()), true, false, None, window, cx)
|
||||
});
|
||||
let source_workspace_id =
|
||||
workspace_a.read_with(cx_a, |workspace, _| workspace.database_id());
|
||||
|
||||
let window_b =
|
||||
cx_a.add_window(|window, cx| Workspace::test_new(project_b.clone(), window, cx));
|
||||
let cx_b = &mut VisualTestContext::from_window(window_b.into(), cx_a);
|
||||
cx_b.run_until_parked();
|
||||
let pane_b = window_b
|
||||
.update(cx_b, |workspace, _, _| workspace.active_pane().clone())
|
||||
.unwrap();
|
||||
|
||||
let dragged_tab = DraggedTab {
|
||||
pane: pane_a.clone(),
|
||||
item: Box::new(item.clone()),
|
||||
ix: 0,
|
||||
detail: 0,
|
||||
is_active: true,
|
||||
};
|
||||
pane_b.update_in(cx_b, |pane, window, cx| {
|
||||
pane.handle_tab_drop(&dragged_tab, 0, false, window, cx)
|
||||
});
|
||||
cx_a.run_until_parked();
|
||||
|
||||
assert_eq!(pane_a.read_with(cx_a, |pane, _| pane.items_len()), 0);
|
||||
let transferred = pane_b
|
||||
.read_with(cx_b, |pane, _| pane.items().next().cloned())
|
||||
.expect("item moved to the other workspace")
|
||||
.downcast::<TestItem>()
|
||||
.expect("transferred item is a test item");
|
||||
assert_eq!(
|
||||
transferred.read_with(cx_b, |item, _| item.workspace_id),
|
||||
source_workspace_id
|
||||
);
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
#[gpui::test]
|
||||
async fn test_tab_dropped_outside_opens_new_window(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), None, cx).await;
|
||||
cx.update(crate::register_serializable_item::<TestItem>);
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
workspace.update(cx, |workspace, _| {
|
||||
workspace.database_id = Some(WorkspaceId(1))
|
||||
});
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let item = cx.new(|cx| TestItem::new(cx).with_serialize(|| Some(Task::ready(Ok(())))));
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.add_item(Box::new(item.clone()), true, false, None, window, cx)
|
||||
});
|
||||
let source_workspace_id = workspace.read_with(cx, |workspace, _| workspace.database_id());
|
||||
|
||||
let dragged_tab = DraggedTab {
|
||||
pane: pane.clone(),
|
||||
item: Box::new(item.clone()),
|
||||
ix: 0,
|
||||
detail: 0,
|
||||
is_active: true,
|
||||
};
|
||||
let drag = AnyDrag {
|
||||
value: Arc::new(dragged_tab),
|
||||
view: cx.new(|_| Empty).into(),
|
||||
cursor_offset: point(px(0.), px(0.)),
|
||||
cursor_style: None,
|
||||
external_payload_source: None,
|
||||
};
|
||||
cx.update(|_, cx| crate::handle_tab_dropped_outside(&drag, point(px(400.), px(300.)), cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
let mut transferred_workspace_id = None;
|
||||
for window in cx.windows() {
|
||||
let Some(handle) = window.downcast::<MultiWorkspace>() else {
|
||||
continue;
|
||||
};
|
||||
let workspace = handle
|
||||
.update(cx, |multi_workspace, _, _| {
|
||||
multi_workspace.workspace().clone()
|
||||
})
|
||||
.unwrap();
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
if let Some(item) = pane.read_with(cx, |pane, _| pane.items().next().cloned()) {
|
||||
transferred_workspace_id = item
|
||||
.downcast::<TestItem>()
|
||||
.map(|item| item.read_with(cx, |item, _| item.workspace_id));
|
||||
}
|
||||
}
|
||||
assert_eq!(transferred_workspace_id, Some(source_workspace_id));
|
||||
assert_eq!(pane.read_with(cx, |pane, _| pane.items_len()), 0);
|
||||
}
|
||||
|
||||
#[allow(clippy::arc_with_non_send_sync)]
|
||||
#[gpui::test]
|
||||
async fn test_dirty_tab_dropped_outside_keeps_item(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
cx.update(crate::register_serializable_item::<TestItem>);
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs.clone(), None, cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let pane = workspace.read_with(cx, |workspace, _| workspace.active_pane().clone());
|
||||
let item = cx.new(|cx| TestItem::new(cx).with_dirty(true));
|
||||
pane.update_in(cx, |pane, window, cx| {
|
||||
pane.add_item(Box::new(item.clone()), true, false, None, window, cx)
|
||||
});
|
||||
|
||||
let dragged_tab = DraggedTab {
|
||||
pane: pane.clone(),
|
||||
item: Box::new(item.clone()),
|
||||
ix: 0,
|
||||
detail: 0,
|
||||
is_active: true,
|
||||
};
|
||||
let drag = AnyDrag {
|
||||
value: Arc::new(dragged_tab),
|
||||
view: cx.new(|_| Empty).into(),
|
||||
cursor_offset: point(px(0.), px(0.)),
|
||||
cursor_style: None,
|
||||
external_payload_source: None,
|
||||
};
|
||||
cx.update(|_, cx| crate::handle_tab_dropped_outside(&drag, point(px(400.), px(300.)), cx));
|
||||
cx.run_until_parked();
|
||||
|
||||
assert_eq!(cx.windows().len(), 1);
|
||||
assert_eq!(pane.read_with(cx, |pane, _| pane.items_len()), 1);
|
||||
}
|
||||
}
|
||||
@@ -62,13 +62,13 @@ use futures::{
|
||||
future::{Shared, try_join_all},
|
||||
};
|
||||
use gpui::{
|
||||
Action, AnyEntity, AnyView, AnyWeakView, App, AppContext, AsyncApp, AsyncWindowContext, Axis,
|
||||
Bounds, ClipboardItem, 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, TaskExt, Tiling, WeakEntity,
|
||||
WindowBounds, WindowHandle, WindowId, WindowOptions, actions, canvas, point, relative, size,
|
||||
transparent_black,
|
||||
Action, AnyDrag, AnyEntity, AnyView, AnyWeakView, App, AppContext, AsyncApp,
|
||||
AsyncWindowContext, Axis, Bounds, ClipboardItem, Context, CursorStyle, Decorations,
|
||||
DragMoveEvent, Entity, EntityId, EventEmitter, FocusHandle, Focusable, Global, HitboxBehavior,
|
||||
Hsla, KeyContext, Keystroke, ManagedView, MouseButton, PathPromptOptions, Pixels, Point,
|
||||
PromptLevel, Render, ResizeEdge, Size, Stateful, Subscription, SystemWindowTabController, Task,
|
||||
TaskExt, Tiling, WeakEntity, WindowBounds, WindowHandle, WindowId, WindowOptions, actions,
|
||||
canvas, point, px, relative, size, transparent_black,
|
||||
};
|
||||
pub use history_manager::*;
|
||||
pub use item::{
|
||||
@@ -166,8 +166,8 @@ pub use workspace_settings::{
|
||||
};
|
||||
use zed_actions::{Spawn, feedback::FileBugReport, theme::ToggleMode};
|
||||
|
||||
use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
|
||||
use crate::background_image::BackgroundImage;
|
||||
use crate::{dock::PanelSizeState, item::ItemBufferKind, notifications::NotificationId};
|
||||
use crate::{
|
||||
persistence::{
|
||||
SerializedAxis,
|
||||
@@ -999,6 +999,10 @@ pub fn init(app_state: Arc<AppState>, cx: &mut App) {
|
||||
|
||||
cx.on_app_quit(flush_windows_serialization_on_quit).detach();
|
||||
|
||||
cx.set_drag_dropped_outside_handler(|drag, position, cx| {
|
||||
handle_tab_dropped_outside(drag, position, cx);
|
||||
});
|
||||
|
||||
cx.on_action(|_: &CloseWindow, cx| Workspace::close_global(cx))
|
||||
.on_action(|_: &Reload, cx| reload(cx))
|
||||
.on_action(|action: &Open, cx: &mut App| {
|
||||
@@ -2178,6 +2182,30 @@ impl Workspace {
|
||||
init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
|
||||
open_mode: OpenMode,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<OpenResult>> {
|
||||
Self::new_local_with_bounds(
|
||||
abs_paths,
|
||||
app_state,
|
||||
requesting_window,
|
||||
env,
|
||||
init,
|
||||
open_mode,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as [`Workspace::new_local`], but places a newly created window at
|
||||
/// `window_bounds_override` instead of the persisted or cascaded defaults.
|
||||
pub fn new_local_with_bounds(
|
||||
abs_paths: Vec<PathBuf>,
|
||||
app_state: Arc<AppState>,
|
||||
requesting_window: Option<WindowHandle<MultiWorkspace>>,
|
||||
env: Option<HashMap<String, String>>,
|
||||
init: Option<Box<dyn FnOnce(&mut Workspace, &mut Window, &mut Context<Workspace>) + Send>>,
|
||||
open_mode: OpenMode,
|
||||
window_bounds_override: Option<Bounds<Pixels>>,
|
||||
cx: &mut App,
|
||||
) -> Task<anyhow::Result<OpenResult>> {
|
||||
let project_handle = Project::local(
|
||||
app_state.client.clone(),
|
||||
@@ -2315,7 +2343,8 @@ impl Workspace {
|
||||
})?;
|
||||
(window, workspace)
|
||||
} else {
|
||||
let window_bounds_override = window_bounds_env_override();
|
||||
let window_bounds_override =
|
||||
window_bounds_override.or_else(window_bounds_env_override);
|
||||
|
||||
let (window_bounds, display) = if let Some(bounds) = window_bounds_override {
|
||||
(Some(WindowBounds::Windowed(bounds)), None)
|
||||
@@ -12290,6 +12319,82 @@ pub fn clone_active_item(
|
||||
.detach();
|
||||
}
|
||||
|
||||
/// Opens a new window holding the dragged tab's item, for drags released
|
||||
/// outside of every application window.
|
||||
fn handle_tab_dropped_outside(drag: &AnyDrag, position: Point<Pixels>, cx: &mut App) {
|
||||
let Some(dragged_tab) = drag.value.downcast_ref::<DraggedTab>() else {
|
||||
return;
|
||||
};
|
||||
let source_pane = dragged_tab.pane.clone();
|
||||
let Some(transfer) = prepare_item_transfer(&*dragged_tab.item, &source_pane, cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(source_workspace) = source_pane.read(cx).workspace.upgrade() else {
|
||||
return;
|
||||
};
|
||||
let paths = source_workspace
|
||||
.read(cx)
|
||||
.root_paths(cx)
|
||||
.into_iter()
|
||||
.map(|path| path.to_path_buf())
|
||||
.collect();
|
||||
let app_state = source_workspace.read(cx).app_state.clone();
|
||||
let window_bounds = tear_off_window_bounds(position, cx);
|
||||
let open_task = Workspace::new_local_with_bounds(
|
||||
paths,
|
||||
app_state,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
OpenMode::NewWindow,
|
||||
Some(window_bounds),
|
||||
cx,
|
||||
);
|
||||
cx.spawn(async move |cx| {
|
||||
let opened = open_task.await?;
|
||||
let to_pane = opened
|
||||
.workspace
|
||||
.update(cx, |workspace, _| workspace.active_pane().downgrade());
|
||||
opened.window.update(cx, |_, window, cx| {
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
complete_item_transfer(transfer, to_pane, 0, cx)
|
||||
.await
|
||||
.log_err();
|
||||
})
|
||||
.detach();
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
|
||||
/// Bounds for a window torn off at `position`: the active window's size with
|
||||
/// its origin at the cursor, clamped inside the display's visible area.
|
||||
fn tear_off_window_bounds(position: Point<Pixels>, cx: &mut App) -> Bounds<Pixels> {
|
||||
let size = cx
|
||||
.active_window()
|
||||
.and_then(|window| {
|
||||
window
|
||||
.update(cx, |_, window, _| window.window_bounds().get_bounds().size)
|
||||
.ok()
|
||||
})
|
||||
.unwrap_or_else(|| size(px(1024.), px(768.)));
|
||||
let Some(display) = cx.primary_display() else {
|
||||
return Bounds::new(position, size);
|
||||
};
|
||||
let visible = display.visible_bounds();
|
||||
let max_origin = point(
|
||||
(visible.origin.x + visible.size.width - size.width).max(visible.origin.x),
|
||||
(visible.origin.y + visible.size.height - size.height).max(visible.origin.y),
|
||||
);
|
||||
let origin = point(
|
||||
position.x.clamp(visible.origin.x, max_origin.x),
|
||||
position.y.clamp(visible.origin.y, max_origin.y),
|
||||
);
|
||||
Bounds::new(origin, size)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct WorkspacePosition {
|
||||
pub window_bounds: Option<WindowBounds>,
|
||||
|
||||
Reference in new issue
Block a user