Author SHA1 Message Date
yenru0 66e2a957d9 gpui_windows: Report the global cursor state for drag tracking
Implement Platform::global_cursor_state with GetCursorPos scaled by the
DPI of the monitor under the cursor, and the left button state from
GetAsyncKeyState, so drags released outside every window reach the
tear-off handler on Windows too.
2026-10-16 16:55:38 +09:00
yenru0 b3cca826c8 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.
2026-10-14 05:10:05 +09:00
yenru0 a7b8190c24 gpui: Track in-app drags outside of every window
Platforms that can query the global cursor position (X11 via
QueryPointer, tests via an injected state) now report it through
Platform::global_cursor_state. While a drag is active, App samples that
state to keep drag_hover_window current: the drag ghost paints only in
the window under the cursor and in no window while the cursor is
outside all of them, fixing stale ghosts left behind in the source
window.

When the button is released outside every window, no window receives
the mouse-up, so the drag used to stick until the next click. The poll
now detects that release and hands the drag to a handler registered
with App::set_drag_dropped_outside_handler, letting the workspace layer
decide what a tear-off means.
2026-10-14 05:09:54 +09:00
15 changed files with 701 additions and 74 deletions

No files matched your search

+120 -6
View File
@@ -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;
+13 -6
View File
@@ -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()
+17
View File
@@ -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
}
+16 -5
View File
@@ -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,
+3 -1
View File
@@ -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,
}
+70 -5
View File
@@ -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,
+9 -4
View File
@@ -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"
}
+23 -4
View File
@@ -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
+1 -1
View File
@@ -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) }?;
+21
View File
@@ -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,
+19 -21
View File
@@ -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"));
+262 -7
View File
@@ -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);
}
}
+114 -9
View File
@@ -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>,