diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
index fa60f3d35..43ae103f1 100644
--- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml
+++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml
@@ -312,10 +312,14 @@
+ PointerPressed="TitleBar_PointerPressed"
+ PointerMoved="TitleBar_PointerMoved"
+ PointerReleased="TitleBar_PointerReleased"
+ PointerCaptureLost="TitleBar_PointerCaptureLost"/>
Close();
+ // Manual title-bar drag state. Only used for touch/pen on Windows (see TitleBar_PointerPressed),
+ // where Avalonia's BeginMoveDrag drives an OS modal loop the finger can't feed. Bound to the
+ // owning pointer so a second contact can't hijack or end an in-progress drag.
+ private IPointer? _titleBarDragPointer;
+ private Point _titleBarDragOrigin;
+ private bool _restoreThenDrag; // press began while maximized → restore on first real move
+
private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
{
- if (!e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
+ // Manual drag only on Windows + touch/pen. Mouse keeps the OS move (Aero Snap), and on
+ // macOS/Linux the native BeginMoveDrag already handles touch (and Wayland forbids self-positioning).
+ bool manualDrag = OperatingSystem.IsWindows() && e.Pointer.Type != PointerType.Mouse;
+
+ if (!manualDrag && !e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
return;
if (e.ClickCount == 2)
@@ -841,7 +852,94 @@ private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e)
return;
}
- BeginMoveDrag(e);
+ if (!manualDrag)
+ {
+ BeginMoveDrag(e);
+ return;
+ }
+
+ // Touch/pen on Windows: move the window manually (issue #4866).
+ if (_titleBarDragPointer is not null)
+ return;
+
+ _titleBarDragPointer = e.Pointer;
+ _titleBarDragOrigin = e.GetPosition(this);
+ // Dragging a maximized window restores it first (matches the native mouse gesture).
+ _restoreThenDrag = WindowState == WindowState.Maximized;
+ e.Pointer.Capture(TitleBarDragArea);
+ e.Handled = true;
+ }
+
+ private void TitleBar_PointerMoved(object? sender, PointerEventArgs e)
+ {
+ if (e.Pointer != _titleBarDragPointer)
+ return;
+
+ Vector delta = e.GetPosition(this) - _titleBarDragOrigin;
+
+ // Started on a maximized window: ignore tiny jitter (so a tap doesn't restore), then
+ // restore-and-reposition under the finger before the normal drag takes over.
+ if (_restoreThenDrag)
+ {
+ if (Math.Abs(delta.X) < 4 && Math.Abs(delta.Y) < 4)
+ return;
+ RestoreForTouchDrag(e.GetPosition(this));
+ e.Handled = true;
+ return;
+ }
+
+ if (delta.X == 0 && delta.Y == 0)
+ return;
+
+ // GetPosition is window-relative and Position is in screen pixels; scale converts between
+ // them. Closed loop: the origin stays fixed and delta is re-measured against the moved
+ // window each event, so the sub-pixel remainder is held in the geometry and re-applied
+ // rather than accumulating. Round (not truncate) to keep the residual symmetric and <0.5px.
+ double scale = RenderScaling;
+ Position += new PixelVector((int)Math.Round(delta.X * scale), (int)Math.Round(delta.Y * scale));
+ e.Handled = true;
+ }
+
+ // Restores a maximized window mid-drag and re-anchors it under the finger, matching the native
+ // mouse gesture. The finger's screen point and its horizontal fraction of the title bar are
+ // captured while still maximized; after WindowState.Normal the window is placed so that same
+ // fraction of the (now restored) width sits under the finger. On Win32 the restore is
+ // synchronous — ShowWindow sends WM_SIZE inline, so Bounds already reflects the restored size.
+ private void RestoreForTouchDrag(Point grab)
+ {
+ double fraction = Bounds.Width > 0 ? Math.Clamp(grab.X / Bounds.Width, 0, 1) : 0.5;
+ double titleY = grab.Y;
+ PixelPoint fingerScreen = this.PointToScreen(grab);
+
+ _restoreThenDrag = false;
+ WindowState = WindowState.Normal;
+
+ double scale = RenderScaling;
+ var origin = new Point(fraction * Bounds.Width, titleY);
+ Position = fingerScreen - new PixelVector(
+ (int)Math.Round(origin.X * scale),
+ (int)Math.Round(origin.Y * scale));
+ _titleBarDragOrigin = origin;
+ }
+
+ private void TitleBar_PointerReleased(object? sender, PointerReleasedEventArgs e)
+ {
+ if (e.Pointer != _titleBarDragPointer)
+ return;
+
+ _titleBarDragPointer = null;
+ _restoreThenDrag = false;
+ e.Pointer.Capture(null);
+ e.Handled = true;
+ }
+
+ private void TitleBar_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e)
+ {
+ if (e.Pointer == _titleBarDragPointer)
+ {
+ _titleBarDragPointer = null;
+ _restoreThenDrag = false;
+ }
}
private void SearchBox_KeyDown(object? sender, KeyEventArgs e)