Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
66e2a957d9 | ||
|
|
b3cca826c8 | ||
|
|
a7b8190c24 |
No files matched your search
+120
-6
@@ -44,14 +44,14 @@ use crate::{
|
||||
Action, ActionBuildError, ActionRegistry, ActivityGuard, Any, AnyView, AnyWindowHandle,
|
||||
AppContext, Arena, ArenaBox, Asset, AssetSource, BackgroundExecutor, Bounds, ClipboardItem,
|
||||
ClipboardReadError, CursorStyle, DispatchPhase, DisplayId, EventEmitter, ExternalDragPayload,
|
||||
FocusHandle, FocusMap, ForegroundExecutor, Global, KeyBinding, KeyContext, Keymap, Keystroke,
|
||||
LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions, Pixels, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point, Priority,
|
||||
PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
|
||||
FocusHandle, FocusMap, ForegroundExecutor, Global, GlobalCursorState, KeyBinding, KeyContext,
|
||||
Keymap, Keystroke, LayoutId, Menu, MenuItem, MissingGlyph, OwnedMenu, PathPromptOptions,
|
||||
Pixels, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, Point,
|
||||
Priority, PromptBuilder, PromptButton, PromptHandle, PromptLevel, Render, RenderImage,
|
||||
RenderablePromptHandle, Reservation, ScreenCaptureSource, SharedString, SubscriberSet,
|
||||
Subscription, SvgRenderer, SystemNotification, SystemNotificationResponse, Task,
|
||||
TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowButtonLayout,
|
||||
WindowHandle, WindowId, WindowInvalidator,
|
||||
TextRenderingMode, TextSystem, ThermalState, Window, WindowAppearance, WindowBounds,
|
||||
WindowButtonLayout, WindowHandle, WindowId, WindowInvalidator,
|
||||
colors::{Colors, GlobalColors},
|
||||
hash, init_app_menus,
|
||||
};
|
||||
@@ -738,6 +738,10 @@ enum PlatformOwnedDragState {
|
||||
RestoredInSourceWindow,
|
||||
}
|
||||
|
||||
/// How often the global cursor state is sampled while an in-app drag is
|
||||
/// active, to notice releases that happen outside of every window.
|
||||
const DRAG_POLL_INTERVAL: Duration = Duration::from_millis(30);
|
||||
|
||||
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
|
||||
/// Other [Context] derefs to this type.
|
||||
/// You need a reference to an `App` to access the state of a [Entity].
|
||||
@@ -749,6 +753,10 @@ pub struct App {
|
||||
pub(crate) actions: Rc<ActionRegistry>,
|
||||
pub(crate) active_drag: Option<AnyDrag>,
|
||||
platform_owned_drag: Option<PlatformOwnedDrag>,
|
||||
pub(crate) drag_hover_window: Option<Option<WindowId>>,
|
||||
drag_poll_running: bool,
|
||||
drag_poll_task: Option<Task<()>>,
|
||||
drag_dropped_outside_handler: Option<Box<dyn FnMut(&AnyDrag, Point<Pixels>, &mut App)>>,
|
||||
pub(crate) background_executor: BackgroundExecutor,
|
||||
pub(crate) foreground_executor: ForegroundExecutor,
|
||||
#[cfg(feature = "profiler")]
|
||||
@@ -875,6 +883,10 @@ impl App {
|
||||
pending_updates: 0,
|
||||
active_drag: None,
|
||||
platform_owned_drag: None,
|
||||
drag_hover_window: None,
|
||||
drag_poll_running: false,
|
||||
drag_poll_task: None,
|
||||
drag_dropped_outside_handler: None,
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
#[cfg(feature = "profiler")]
|
||||
@@ -2636,6 +2648,7 @@ impl App {
|
||||
pub fn stop_active_drag(&mut self, window: &mut Window) -> bool {
|
||||
if self.active_drag.is_some() {
|
||||
self.active_drag = None;
|
||||
self.drag_hover_window = None;
|
||||
if self.platform_owned_drag.as_ref().is_some_and(|drag| {
|
||||
drag.source_window == window.window_handle().window_id()
|
||||
&& matches!(&drag.state, PlatformOwnedDragState::RestoredInSourceWindow)
|
||||
@@ -2649,6 +2662,107 @@ impl App {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers a handler invoked when an in-app drag is released outside of
|
||||
/// every application window, on platforms that report the global cursor
|
||||
/// state.
|
||||
pub fn set_drag_dropped_outside_handler(
|
||||
&mut self,
|
||||
handler: impl FnMut(&AnyDrag, Point<Pixels>, &mut App) + 'static,
|
||||
) {
|
||||
self.drag_dropped_outside_handler = Some(Box::new(handler));
|
||||
}
|
||||
|
||||
pub(crate) fn should_paint_drag_in(&self, window_id: WindowId) -> bool {
|
||||
match self.drag_hover_window {
|
||||
None => true,
|
||||
Some(hovered_window) => hovered_window == Some(window_id),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn set_drag_hover_window(&mut self, hovered: Option<Option<WindowId>>) {
|
||||
if self.drag_hover_window == hovered {
|
||||
return;
|
||||
}
|
||||
let previous = mem::replace(&mut self.drag_hover_window, hovered).flatten();
|
||||
let next = hovered.flatten();
|
||||
for window_id in [previous, next].into_iter().flatten() {
|
||||
if let Some(handle) = self.window_handles.get(&window_id).copied() {
|
||||
handle
|
||||
.update(self, |_, window, _| window.refresh())
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn ensure_drag_poll_running(&mut self) {
|
||||
if self.drag_poll_running || self.active_drag.is_none() {
|
||||
return;
|
||||
}
|
||||
if self.platform.global_cursor_state().is_none() {
|
||||
return;
|
||||
}
|
||||
self.drag_poll_running = true;
|
||||
self.drag_poll_task = Some(self.spawn(async move |cx| {
|
||||
loop {
|
||||
cx.background_executor.timer(DRAG_POLL_INTERVAL).await;
|
||||
let Some(state) = cx.update(|cx: &mut App| cx.platform.global_cursor_state())
|
||||
else {
|
||||
break;
|
||||
};
|
||||
let finished = cx.update(|cx: &mut App| cx.poll_drag_state(state));
|
||||
if finished {
|
||||
break;
|
||||
}
|
||||
}
|
||||
cx.update(|cx: &mut App| cx.drag_poll_running = false);
|
||||
}));
|
||||
}
|
||||
|
||||
fn poll_drag_state(&mut self, state: GlobalCursorState) -> bool {
|
||||
if self.active_drag.is_none() {
|
||||
self.set_drag_hover_window(None);
|
||||
return true;
|
||||
}
|
||||
let hovered_window = self.window_under_point(state.position);
|
||||
self.set_drag_hover_window(Some(hovered_window));
|
||||
if state.left_button_pressed {
|
||||
return false;
|
||||
}
|
||||
if hovered_window.is_none()
|
||||
&& let Some(drag) = self.active_drag.take()
|
||||
{
|
||||
self.set_drag_hover_window(None);
|
||||
let mut handler = self.drag_dropped_outside_handler.take();
|
||||
if let Some(handler) = handler.as_mut() {
|
||||
handler(&drag, state.position, self);
|
||||
}
|
||||
self.drag_dropped_outside_handler = handler;
|
||||
self.refresh_windows();
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn window_under_point(&mut self, position: Point<Pixels>) -> Option<WindowId> {
|
||||
let ordered_handles = self
|
||||
.platform
|
||||
.window_stack()
|
||||
.unwrap_or_else(|| self.window_handles.values().copied().collect());
|
||||
for handle in ordered_handles {
|
||||
let Ok(bounds) = handle.update(self, |_, window, _| window.window_bounds()) else {
|
||||
continue;
|
||||
};
|
||||
let bounds = match bounds {
|
||||
WindowBounds::Windowed(bounds)
|
||||
| WindowBounds::Maximized(bounds)
|
||||
| WindowBounds::Fullscreen(bounds) => bounds,
|
||||
};
|
||||
if bounds.contains(&position) {
|
||||
return Some(handle.window_id());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn hand_active_drag_to_platform(&mut self, source_window: WindowId) -> bool {
|
||||
let Some(drag) = self.active_drag.take() else {
|
||||
return false;
|
||||
|
||||
@@ -1,12 +1,13 @@
|
||||
use crate::{
|
||||
Action, AnyView, AnyWindowHandle, App, AppCell, AppContext, AsyncApp, AvailableSpace,
|
||||
BackgroundExecutor, BorrowAppContext, Bounds, Capslock, ClipboardItem, DrawPhase, Drawable,
|
||||
Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, InputEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Pixels, Platform, PlatformTextSystem, Point, Render, Result, SharedString, Size,
|
||||
SystemNotification, SystemNotificationResponse, Task, TestDispatcher, TestPlatform,
|
||||
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
|
||||
WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode, window::ElementArenaScope,
|
||||
Element, Empty, EntityId, EventEmitter, ForegroundExecutor, Global, GlobalCursorState,
|
||||
InputEvent, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent,
|
||||
MouseMoveEvent, MouseUpEvent, Pixels, Platform, PlatformTextSystem, Point, Render, Result,
|
||||
SharedString, Size, SystemNotification, SystemNotificationResponse, Task, TestDispatcher,
|
||||
TestPlatform, TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window,
|
||||
WindowBounds, WindowHandle, WindowOptions, WindowVisibility, app::GpuiMode,
|
||||
window::ElementArenaScope,
|
||||
};
|
||||
use anyhow::{anyhow, bail};
|
||||
use futures::{Stream, StreamExt, channel::oneshot};
|
||||
@@ -500,6 +501,12 @@ impl TestAppContext {
|
||||
self.test_platform.set_screen_capture_sources(sources);
|
||||
}
|
||||
|
||||
/// Sets the global cursor state reported by the test platform, for
|
||||
/// exercising drags that leave every window.
|
||||
pub fn set_global_cursor_state(&self, state: Option<GlobalCursorState>) {
|
||||
self.test_platform.set_global_cursor_state(state);
|
||||
}
|
||||
|
||||
/// Returns all windows open in the test.
|
||||
pub fn windows(&self) -> Vec<AnyWindowHandle> {
|
||||
self.app.borrow().windows()
|
||||
|
||||
@@ -182,6 +182,16 @@ pub fn guess_compositor() -> &'static str {
|
||||
}
|
||||
}
|
||||
|
||||
/// The global cursor position and mouse button state, used to follow
|
||||
/// in-app drags once the cursor leaves every application window.
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq)]
|
||||
pub struct GlobalCursorState {
|
||||
/// The cursor position in global display coordinates.
|
||||
pub position: Point<Pixels>,
|
||||
/// Whether the left mouse button is currently pressed.
|
||||
pub left_button_pressed: bool,
|
||||
}
|
||||
|
||||
#[expect(missing_docs)]
|
||||
pub trait Platform: 'static {
|
||||
fn background_executor(&self) -> BackgroundExecutor;
|
||||
@@ -203,6 +213,13 @@ pub trait Platform: 'static {
|
||||
None
|
||||
}
|
||||
|
||||
/// The global cursor position and button state, for platforms that can
|
||||
/// query it without an active pointer grab. Returning `None` disables
|
||||
/// following drags outside of the application's windows.
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn is_screen_capture_supported(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
@@ -4,11 +4,11 @@ use crate::NoopTextSystem;
|
||||
use crate::PathPromptOptions;
|
||||
use crate::{
|
||||
ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, OwnedMenu, Platform, PlatformDisplay,
|
||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SharedString,
|
||||
SourceMetadata, SystemNotification, SystemNotificationResponse, Task, TestDisplay, TestWindow,
|
||||
ThermalState, WindowAppearance, WindowParams, size,
|
||||
DummyKeyboardMapper, ForegroundExecutor, GlobalCursorState, Keymap, OwnedMenu, Platform,
|
||||
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||
SharedString, SourceMetadata, SystemNotification, SystemNotificationResponse, Task,
|
||||
TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
||||
};
|
||||
use anyhow::Result;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
@@ -42,6 +42,7 @@ pub(crate) struct TestPlatform {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) prompts: RefCell<TestPrompts>,
|
||||
screen_capture_sources: RefCell<Vec<TestScreenCaptureSource>>,
|
||||
pub(crate) global_cursor_state: RefCell<Option<GlobalCursorState>>,
|
||||
pub opened_url: RefCell<Option<String>>,
|
||||
pub(crate) system_notifications: RefCell<TestSystemNotifications>,
|
||||
pub text_system: Arc<dyn PlatformTextSystem>,
|
||||
@@ -153,6 +154,7 @@ impl TestPlatform {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
prompts: Default::default(),
|
||||
screen_capture_sources: Default::default(),
|
||||
global_cursor_state: Default::default(),
|
||||
active_cursor: Default::default(),
|
||||
active_display: Rc::new(TestDisplay::new()),
|
||||
active_window: Default::default(),
|
||||
@@ -174,6 +176,11 @@ impl TestPlatform {
|
||||
})
|
||||
}
|
||||
|
||||
/// Sets the global cursor state reported by [`Platform::global_cursor_state`].
|
||||
pub fn set_global_cursor_state(&self, state: Option<GlobalCursorState>) {
|
||||
*self.global_cursor_state.borrow_mut() = state;
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn simulate_new_path_selection(
|
||||
&self,
|
||||
@@ -489,6 +496,10 @@ impl Platform for TestPlatform {
|
||||
.map(|window| window.0.lock().handle)
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
*self.global_cursor_state.borrow()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
|
||||
@@ -1442,7 +1442,9 @@ pub fn font_name_with_fallbacks_shared<'a>(
|
||||
// IBM Plex/Lilex, and so retained here for backward compatibility.
|
||||
match name.as_str() {
|
||||
".SystemUIFont" => system,
|
||||
".GwenSans" | ".ZedSans" | "Zed Plex Sans" => const { &SharedString::new_static("IBM Plex Sans") },
|
||||
".GwenSans" | ".ZedSans" | "Zed Plex Sans" => {
|
||||
const { &SharedString::new_static("IBM Plex Sans") }
|
||||
}
|
||||
".GwenMono" | ".ZedMono" | "Zed Plex Mono" => const { &SharedString::new_static("Lilex") },
|
||||
_ => name,
|
||||
}
|
||||
|
||||
@@ -3563,7 +3563,9 @@ impl Window {
|
||||
element.prepaint_as_root(Point::default(), root_size.into(), self, cx);
|
||||
prompt_element = Some(element);
|
||||
self.prompt = Some(prompt);
|
||||
} else if let Some(active_drag) = cx.active_drag.take() {
|
||||
} else if cx.should_paint_drag_in(self.handle.window_id())
|
||||
&& let Some(active_drag) = cx.active_drag.take()
|
||||
{
|
||||
let mut element = active_drag.view.clone().into_any_element();
|
||||
let offset = self.mouse_position() - active_drag.cursor_offset;
|
||||
element.prepaint_as_root(offset, AvailableSpace::min_size(), self, cx);
|
||||
@@ -5789,14 +5791,17 @@ impl Window {
|
||||
self.rendered_frame.mouse_listeners = mouse_listeners;
|
||||
|
||||
if cx.has_active_drag() {
|
||||
cx.ensure_drag_poll_running();
|
||||
if event.is::<MouseMoveEvent>() {
|
||||
// If this was a mouse move event, redraw the window so that the
|
||||
// active drag can follow the mouse cursor.
|
||||
cx.set_drag_hover_window(Some(Some(self.handle.window_id())));
|
||||
self.refresh();
|
||||
} else if event.is::<MouseUpEvent>() {
|
||||
// If this was a mouse up event, cancel the active drag and redraw
|
||||
// the window.
|
||||
cx.active_drag = None;
|
||||
cx.set_drag_hover_window(None);
|
||||
self.refresh();
|
||||
}
|
||||
}
|
||||
@@ -7640,15 +7645,16 @@ mod tests {
|
||||
cell::{Cell, RefCell},
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use crate::{
|
||||
AnyWindowHandle, AppContext as _, Bounds, ContentMask, Context, DispatchPhase,
|
||||
AnyDrag, AnyWindowHandle, AppContext as _, Bounds, ContentMask, Context, DispatchPhase,
|
||||
DragMoveEvent, Empty, ExternalDragPayload, ExternalPaths, FileDragPaths, FileDropEvent,
|
||||
FocusHandle, InputEvent as _, InteractiveElement as _, IntoElement, KeyDownEvent,
|
||||
Keystroke, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent, ParentElement,
|
||||
Pixels, PlatformInput, Point, Render, RequestFrameOptions, ScaledPixels,
|
||||
FocusHandle, GlobalCursorState, InputEvent as _, InteractiveElement as _, IntoElement,
|
||||
KeyDownEvent, Keystroke, LongPressEvent, MouseButton, MouseDownEvent, MouseMoveEvent,
|
||||
ParentElement, Pixels, PlatformInput, Point, Render, RequestFrameOptions, ScaledPixels,
|
||||
StatefulInteractiveElement as _, Styled, TestAppContext, TouchDragEvent, TouchEvent,
|
||||
TouchId, TouchPhase, Underline, UnderlineStyle, Window, WindowAppearance, WindowOptions,
|
||||
canvas, div, hsla, point, px, size,
|
||||
@@ -9109,6 +9115,65 @@ mod tests {
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_drag_release_outside_windows_notifies_handler(cx: &mut TestAppContext) {
|
||||
let dropped_position = Rc::new(RefCell::new(None));
|
||||
let observed_position = dropped_position.clone();
|
||||
cx.update(|cx| {
|
||||
cx.set_drag_dropped_outside_handler(move |_drag, position, _cx| {
|
||||
*observed_position.borrow_mut() = Some(position);
|
||||
});
|
||||
});
|
||||
|
||||
let window: AnyWindowHandle = cx.add_window(|_, _| EmptyView).into();
|
||||
cx.set_global_cursor_state(Some(GlobalCursorState {
|
||||
position: point(px(10.), px(10.)),
|
||||
left_button_pressed: true,
|
||||
}));
|
||||
cx.update_window(window, |_, window, cx| {
|
||||
cx.active_drag = Some(AnyDrag {
|
||||
value: Arc::new("payload"),
|
||||
view: cx.new(|_| Empty).into(),
|
||||
cursor_offset: point(px(0.), px(0.)),
|
||||
cursor_style: None,
|
||||
external_payload_source: None,
|
||||
});
|
||||
cx.ensure_drag_poll_running();
|
||||
window.refresh();
|
||||
})
|
||||
.unwrap();
|
||||
cx.executor().advance_clock(Duration::from_millis(50));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
cx.read(|cx| cx.drag_hover_window),
|
||||
Some(Some(window.window_id()))
|
||||
);
|
||||
|
||||
// While the button stays pressed outside every window, the drag lives
|
||||
// on but no window is asked to paint the ghost.
|
||||
cx.set_global_cursor_state(Some(GlobalCursorState {
|
||||
position: point(px(10_000.), px(10_000.)),
|
||||
left_button_pressed: true,
|
||||
}));
|
||||
cx.executor().advance_clock(Duration::from_millis(50));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(cx.read(|cx| cx.drag_hover_window), Some(None));
|
||||
assert!(cx.read(|cx| cx.active_drag.is_some()));
|
||||
|
||||
// Releasing outside every window hands the drag to the handler.
|
||||
cx.set_global_cursor_state(Some(GlobalCursorState {
|
||||
position: point(px(10_000.), px(10_000.)),
|
||||
left_button_pressed: false,
|
||||
}));
|
||||
cx.executor().advance_clock(Duration::from_millis(50));
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
*dropped_position.borrow(),
|
||||
Some(point(px(10_000.), px(10_000.)))
|
||||
);
|
||||
assert!(cx.read(|cx| cx.active_drag.is_none()));
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(test, any(feature = "inspector", debug_assertions)))]
|
||||
|
||||
@@ -7,8 +7,8 @@ use gpui_util::ResultExt;
|
||||
use crate::linux::headless::window::{HeadlessDisplay, HeadlessWindow};
|
||||
use crate::linux::{LinuxClient, LinuxCommon, LinuxKeyboardLayout};
|
||||
use gpui::{
|
||||
AnyWindowHandle, CursorStyle, DisplayId, PlatformDisplay, PlatformKeyboardLayout,
|
||||
PlatformWindow, WindowParams,
|
||||
AnyWindowHandle, CursorStyle, DisplayId, GlobalCursorState, PlatformDisplay,
|
||||
PlatformKeyboardLayout, PlatformWindow, WindowParams,
|
||||
};
|
||||
|
||||
pub struct HeadlessClientState {
|
||||
@@ -97,6 +97,10 @@ impl LinuxClient for HeadlessClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
_handle: AnyWindowHandle,
|
||||
|
||||
@@ -33,10 +33,10 @@ use xkbcommon::xkb::{self, Keycode, Keysym, State};
|
||||
use crate::linux::{LinuxDispatcher, PriorityQueueCalloopReceiver};
|
||||
use gpui::{
|
||||
Action, ActivityGuard, AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle,
|
||||
DisplayId, ForegroundExecutor, Keymap, Menu, MenuItem, OwnedMenu, PathPromptOptions, Platform,
|
||||
PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
||||
PlatformWindow, Result, RunnableVariant, Task, ThermalState, WindowAppearance,
|
||||
WindowButtonLayout, WindowParams,
|
||||
DisplayId, ForegroundExecutor, GlobalCursorState, Keymap, Menu, MenuItem, OwnedMenu,
|
||||
PathPromptOptions, Platform, PlatformDisplay, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||
PlatformTextSystem, PlatformWindow, Result, RunnableVariant, Task, ThermalState,
|
||||
WindowAppearance, WindowButtonLayout, WindowParams,
|
||||
};
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
use gpui::{Pixels, Point, px};
|
||||
@@ -101,6 +101,7 @@ pub(crate) trait LinuxClient {
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem>;
|
||||
fn active_window(&self) -> Option<AnyWindowHandle>;
|
||||
fn window_stack(&self) -> Option<Vec<AnyWindowHandle>>;
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState>;
|
||||
fn run(&self);
|
||||
|
||||
#[cfg(any(feature = "wayland", feature = "x11"))]
|
||||
@@ -436,6 +437,10 @@ impl<P: LinuxClient + 'static> Platform for LinuxPlatform<P> {
|
||||
self.inner.window_stack()
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
self.inner.global_cursor_state()
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
|
||||
@@ -97,9 +97,9 @@ use crate::linux::{
|
||||
};
|
||||
use gpui::{
|
||||
AnyWindowHandle, Bounds, Capslock, CursorStyle, DevicePixels, DisplayId, ExternalDragPayload,
|
||||
FileDragPaths, FileDropEvent, ForegroundExecutor, KeyDownEvent, KeyUpEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent, MouseMoveEvent,
|
||||
MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput,
|
||||
FileDragPaths, FileDropEvent, ForegroundExecutor, GlobalCursorState, KeyDownEvent, KeyUpEvent,
|
||||
Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseExitEvent,
|
||||
MouseMoveEvent, MouseUpEvent, NavigationDirection, Pixels, PlatformDisplay, PlatformInput,
|
||||
PlatformKeyboardLayout, PlatformWindow, Point, ScrollDelta, ScrollWheelEvent, SharedString,
|
||||
Size, TouchPhase, WindowButtonLayout, WindowKind, WindowParams, point, profiler, px, size,
|
||||
};
|
||||
@@ -1368,6 +1368,10 @@ impl LinuxClient for WaylandClient {
|
||||
None
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
None
|
||||
}
|
||||
|
||||
fn compositor_name(&self) -> &'static str {
|
||||
"Wayland"
|
||||
}
|
||||
|
||||
@@ -60,10 +60,11 @@ use crate::linux::{
|
||||
use crate::linux::{LinuxCommon, LinuxKeyboardLayout, X11Window, modifiers_from_xinput_info};
|
||||
|
||||
use gpui::{
|
||||
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent, Keystroke,
|
||||
Modifiers, ModifiersChangedEvent, MouseButton, Pixels, PlatformDisplay, PlatformInput,
|
||||
PlatformKeyboardLayout, PlatformWindow, Point, RequestFrameOptions, ScrollDelta, Size,
|
||||
TouchPhase, WindowButtonLayout, WindowParams, WindowVisibility, point, px,
|
||||
AnyWindowHandle, Bounds, ClipboardItem, CursorStyle, DisplayId, FileDropEvent,
|
||||
GlobalCursorState, Keystroke, Modifiers, ModifiersChangedEvent, MouseButton, Pixels,
|
||||
PlatformDisplay, PlatformInput, PlatformKeyboardLayout, PlatformWindow, Point,
|
||||
RequestFrameOptions, ScrollDelta, Size, TouchPhase, WindowButtonLayout, WindowParams,
|
||||
WindowVisibility, point, px,
|
||||
};
|
||||
use gpui_wgpu::{CompositorGpuHint, GpuContext};
|
||||
|
||||
@@ -1878,6 +1879,24 @@ impl LinuxClient for X11Client {
|
||||
Some(handles)
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
let state = self.0.borrow();
|
||||
let root = state.xcb_connection.setup().roots[state.x_root_index].root;
|
||||
let scale_factor = state.scale_factor;
|
||||
let reply = get_reply(
|
||||
|| "Failed to query pointer position",
|
||||
state.xcb_connection.query_pointer(root),
|
||||
)
|
||||
.log_err()?;
|
||||
Some(GlobalCursorState {
|
||||
position: point(
|
||||
px(reply.root_x as f32 / scale_factor),
|
||||
px(reply.root_y as f32 / scale_factor),
|
||||
),
|
||||
left_button_pressed: reply.mask.contains(xproto::KeyButMask::BUTTON1),
|
||||
})
|
||||
}
|
||||
|
||||
fn window_identifier(&self) -> impl Future<Output = Option<WindowIdentifier>> + Send + 'static {
|
||||
let state = self.0.borrow();
|
||||
state
|
||||
|
||||
@@ -201,7 +201,7 @@ fn generate_uuid(device_name: &[u16]) -> Uuid {
|
||||
Uuid::new_v5(&Uuid::NAMESPACE_DNS, &name)
|
||||
}
|
||||
|
||||
fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result<f32> {
|
||||
pub(crate) fn get_scale_factor_for_monitor(monitor: HMONITOR) -> Result<f32> {
|
||||
let mut dpi_x = 0;
|
||||
let mut dpi_y = 0;
|
||||
unsafe { GetDpiForMonitor(monitor, MDT_EFFECTIVE_DPI, &mut dpi_x, &mut dpi_y) }?;
|
||||
|
||||
@@ -636,6 +636,27 @@ impl Platform for WindowsPlatform {
|
||||
.map(|inner| inner.handle)
|
||||
}
|
||||
|
||||
fn global_cursor_state(&self) -> Option<GlobalCursorState> {
|
||||
let mut point: POINT = unsafe { std::mem::zeroed() };
|
||||
unsafe { GetCursorPos(&mut point) }
|
||||
.context("unable to get cursor position")
|
||||
.log_err()?;
|
||||
let scale_factor = unsafe {
|
||||
let monitor = MonitorFromPoint(point, MONITOR_DEFAULTTONEAREST);
|
||||
if monitor.is_invalid() {
|
||||
1.0
|
||||
} else {
|
||||
get_scale_factor_for_monitor(monitor).log_err()?
|
||||
}
|
||||
};
|
||||
let left_button_pressed =
|
||||
unsafe { GetAsyncKeyState(VK_LBUTTON.0 as i32) } as u16 & 0x8000 != 0;
|
||||
Some(GlobalCursorState {
|
||||
position: logical_point(point.x as f32, point.y as f32, scale_factor),
|
||||
left_button_pressed,
|
||||
})
|
||||
}
|
||||
|
||||
fn open_window(
|
||||
&self,
|
||||
handle: AnyWindowHandle,
|
||||
|
||||
@@ -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