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)