Skip to content
248 changes: 233 additions & 15 deletions src/Dock.Avalonia/Internal/DragPreviewHelper.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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)
Expand Down Expand Up @@ -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)
{
Expand Down Expand Up @@ -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)
{
Expand All @@ -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);
}
}

Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}

Expand Down
Loading