diff --git a/src/Dock.Avalonia/Internal/DragPreviewHelper.cs b/src/Dock.Avalonia/Internal/DragPreviewHelper.cs index 4da351dd4..beb868b69 100644 --- a/src/Dock.Avalonia/Internal/DragPreviewHelper.cs +++ b/src/Dock.Avalonia/Internal/DragPreviewHelper.cs @@ -1,6 +1,9 @@ +using System; using System.Linq; +using System.Runtime.InteropServices; using Avalonia; using Avalonia.Controls; +using Avalonia.Threading; using Avalonia.VisualTree; using Dock.Avalonia.Controls; using Dock.Model.Core; @@ -17,19 +20,25 @@ internal class DragPreviewHelper private static bool s_managedTemplatesInitialized; private static DragPreviewControl? s_managedControl; private static ManagedWindowLayer? s_managedLayer; + private static readonly bool s_useWindowMoveCoalescing = RuntimeInformation.IsOSPlatform(OSPlatform.OSX); + private static PixelPoint s_pendingWindowPosition; + private static string s_pendingStatus = string.Empty; + private static bool s_hasPendingWindowMove; + private static bool s_windowMoveFlushScheduled; + private static bool s_windowSizeFrozen; + private static bool s_windowSizeFreezeScheduled; + private static double s_frozenWindowWidthPixels; + private static double s_frozenWindowHeightPixels; + private static double s_frozenContentWidthPixels = double.NaN; + private static double s_frozenContentHeightPixels = double.NaN; + private static double s_lastFrozenWindowScaling = 1.0; + private static int s_windowMoveSessionId; + private static long s_windowMovePostSequence; private static PixelPoint GetPositionWithinWindow(Window window, PixelPoint position, PixelPoint offset) { - var screen = window.Screens.ScreenFromPoint(position); - if (screen is { }) - { - var target = position + offset; - if (screen.WorkingArea.Contains(target)) - { - return target; - } - } - return position; + _ = window; + return position + offset; } private static Size GetPreviewSize(IDockable dockable) @@ -98,7 +107,186 @@ private static double ClampOpacity(double value) return value > 1.0 ? 1.0 : value; } - public void Show(IDockable dockable, PixelPoint position, PixelPoint offset, Visual? context = null) + private static void QueueWindowMove(DragPreviewWindow window, DragPreviewControl control, PixelPoint targetPosition, string status) + { + var currentPosition = window.Position; + var currentStatus = control.Status; + + if (s_windowMoveFlushScheduled && s_hasPendingWindowMove) + { + if (s_pendingWindowPosition == targetPosition + && string.Equals(s_pendingStatus, status, StringComparison.Ordinal)) + { + return; + } + } + + if (!s_windowMoveFlushScheduled + && currentPosition == targetPosition + && string.Equals(currentStatus, status, StringComparison.Ordinal)) + { + return; + } + + if (!s_useWindowMoveCoalescing) + { + ApplyWindowMove(window, control, targetPosition, status); + return; + } + + s_pendingWindowPosition = targetPosition; + s_pendingStatus = status; + s_hasPendingWindowMove = true; + if (!s_windowMoveFlushScheduled) + { + s_windowMoveFlushScheduled = true; + var sessionId = s_windowMoveSessionId; + var postSequence = ++s_windowMovePostSequence; + Dispatcher.UIThread.Post(() => FlushPendingWindowMove(sessionId, postSequence), DispatcherPriority.Render); + } + } + + private static void FlushPendingWindowMove(int sessionId, long postSequence) + { + lock (s_sync) + { + if (sessionId != s_windowMoveSessionId || postSequence != s_windowMovePostSequence) + { + return; + } + + s_windowMoveFlushScheduled = false; + if (!s_hasPendingWindowMove || s_window is null || s_control is null) + { + s_hasPendingWindowMove = false; + return; + } + + ApplyWindowMove(s_window, s_control, s_pendingWindowPosition, s_pendingStatus); + s_hasPendingWindowMove = false; + + if (s_hasPendingWindowMove && !s_windowMoveFlushScheduled) + { + s_windowMoveFlushScheduled = true; + var nextSessionId = s_windowMoveSessionId; + var nextPostSequence = ++s_windowMovePostSequence; + Dispatcher.UIThread.Post(() => FlushPendingWindowMove(nextSessionId, nextPostSequence), DispatcherPriority.Render); + } + } + } + + private static void ApplyWindowMove(DragPreviewWindow window, DragPreviewControl control, PixelPoint targetPosition, string status) + { + var hadStatus = !string.IsNullOrEmpty(control.Status); + if (!string.Equals(control.Status, status, StringComparison.Ordinal)) + { + control.Status = status; + } + + var currentPosition = window.Position; + var willMove = currentPosition != targetPosition; + if (willMove) + { + window.Position = targetPosition; + } + + var appliedPosition = willMove ? targetPosition : currentPosition; + + if (s_windowSizeFrozen) + { + MaintainFrozenWindowSize(window, control); + } + + if (!s_windowSizeFrozen && !s_windowSizeFreezeScheduled && !hadStatus && !string.IsNullOrEmpty(status)) + { + s_windowSizeFreezeScheduled = true; + Dispatcher.UIThread.Post(FreezeWindowSizeIfNeeded, DispatcherPriority.Render); + } + } + + private static double GetWindowScaling(Window window) + { + var scaling = window.RenderScaling; + return scaling > 0 ? scaling : 1.0; + } + + private static void MaintainFrozenWindowSize(DragPreviewWindow window, DragPreviewControl control) + { + var scaling = GetWindowScaling(window); + if (Math.Abs(scaling - s_lastFrozenWindowScaling) < 0.0001) + { + return; + } + + s_lastFrozenWindowScaling = scaling; + + var width = s_frozenWindowWidthPixels / scaling; + var height = s_frozenWindowHeightPixels / scaling; + + if (Math.Abs(window.Width - width) > 0.01) + { + window.Width = width; + } + + if (Math.Abs(window.Height - height) > 0.01) + { + window.Height = height; + } + + if (!double.IsNaN(s_frozenContentWidthPixels)) + { + var contentWidth = s_frozenContentWidthPixels / scaling; + if (Math.Abs(control.PreviewContentWidth - contentWidth) > 0.01) + { + control.PreviewContentWidth = contentWidth; + } + } + + if (!double.IsNaN(s_frozenContentHeightPixels)) + { + var contentHeight = s_frozenContentHeightPixels / scaling; + if (Math.Abs(control.PreviewContentHeight - contentHeight) > 0.01) + { + control.PreviewContentHeight = contentHeight; + } + } + } + + private static void FreezeWindowSizeIfNeeded() + { + lock (s_sync) + { + s_windowSizeFreezeScheduled = false; + if (s_windowSizeFrozen || s_window is null || !s_window.IsVisible) + { + return; + } + + var bounds = s_window.Bounds; + if (bounds.Width <= 0 || bounds.Height <= 0) + { + return; + } + + var scaling = GetWindowScaling(s_window); + s_lastFrozenWindowScaling = scaling; + s_frozenWindowWidthPixels = bounds.Width * scaling; + s_frozenWindowHeightPixels = bounds.Height * scaling; + s_frozenContentWidthPixels = !double.IsNaN(s_control?.PreviewContentWidth ?? double.NaN) + ? (s_control!.PreviewContentWidth * scaling) + : double.NaN; + s_frozenContentHeightPixels = !double.IsNaN(s_control?.PreviewContentHeight ?? double.NaN) + ? (s_control!.PreviewContentHeight * scaling) + : double.NaN; + + s_window.SizeToContent = SizeToContent.Manual; + s_window.Width = bounds.Width; + s_window.Height = bounds.Height; + s_windowSizeFrozen = true; + } + } + + public void Show(IDockable dockable, PixelPoint position, PixelPoint offset, Visual? context = null, Size? preferredSize = null) { lock (s_sync) { @@ -149,6 +337,19 @@ public void Show(IDockable dockable, PixelPoint position, PixelPoint offset, Vis s_control.Status = string.Empty; s_window.Opacity = ClampOpacity(DockSettings.DragPreviewOpacity); s_window.Position = GetPositionWithinWindow(s_window, position, offset); + s_pendingWindowPosition = s_window.Position; + s_pendingStatus = s_control.Status; + s_hasPendingWindowMove = false; + s_windowMoveFlushScheduled = false; + s_windowMoveSessionId++; + s_windowMovePostSequence = 0; + s_windowSizeFrozen = false; + s_windowSizeFreezeScheduled = false; + s_frozenWindowWidthPixels = 0; + s_frozenWindowHeightPixels = 0; + s_frozenContentWidthPixels = double.NaN; + s_frozenContentHeightPixels = double.NaN; + s_lastFrozenWindowScaling = 1.0; if (!s_window.IsVisible) { @@ -172,8 +373,8 @@ public void Move(PixelPoint position, PixelPoint offset, string status) return; } - s_control.Status = status; - s_window.Position = GetPositionWithinWindow(s_window, position, offset); + var targetPosition = GetPositionWithinWindow(s_window, position, offset); + QueueWindowMove(s_window, s_control, targetPosition, status); } } @@ -202,6 +403,17 @@ public void Hide() return; } + s_hasPendingWindowMove = false; + s_windowMoveFlushScheduled = false; + s_windowMoveSessionId++; + s_windowMovePostSequence = 0; + s_windowSizeFrozen = false; + s_windowSizeFreezeScheduled = false; + s_frozenWindowWidthPixels = 0; + s_frozenWindowHeightPixels = 0; + s_frozenContentWidthPixels = double.NaN; + s_frozenContentHeightPixels = double.NaN; + s_lastFrozenWindowScaling = 1.0; s_window.Close(); s_window = null; s_control = null; @@ -354,9 +566,15 @@ private static void MoveManaged(ManagedWindowLayer layer, PixelPoint position, P return; } - s_managedControl.Status = status; - s_managedControl.Measure(Size.Infinity); var localPosition = GetManagedPosition(layer, position, offset); + if (!string.Equals(s_managedControl.Status, status, StringComparison.Ordinal)) + { + s_managedControl.Status = status; + s_managedControl.Measure(Size.Infinity); + layer.ShowOverlay("DragPreview", s_managedControl, localPosition, s_managedControl.DesiredSize, false); + return; + } + layer.ShowOverlay("DragPreview", s_managedControl, localPosition, s_managedControl.DesiredSize, false); }