From ceabb79498e785750a7ab04ec55b5616c3badfe6 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 3 Jun 2026 11:57:29 -0400 Subject: [PATCH 1/2] Avalonia: Fix main window not draggable by touch --- src/UniGetUI.Avalonia/Views/MainWindow.axaml | 6 +- .../Views/MainWindow.axaml.cs | 59 ++++++++++++++++++- 2 files changed, 62 insertions(+), 3 deletions(-) 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 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 +851,52 @@ 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 || WindowState == WindowState.Maximized) + return; + + _titleBarDragPointer = e.Pointer; + _titleBarDragOrigin = e.GetPosition(this); + 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; + if (delta.X == 0 && delta.Y == 0) + return; + + // GetPosition is in DIPs relative to the window; Position is in screen pixels. The grab + // origin stays fixed: once the window follows, the pointer returns to it, so deltas converge. + double scale = RenderScaling; + Position += new PixelVector((int)(delta.X * scale), (int)(delta.Y * scale)); + e.Handled = true; + } + + private void TitleBar_PointerReleased(object? sender, PointerReleasedEventArgs e) + { + if (e.Pointer != _titleBarDragPointer) + return; + + _titleBarDragPointer = null; + e.Pointer.Capture(null); + e.Handled = true; + } + + private void TitleBar_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) + { + if (e.Pointer == _titleBarDragPointer) + _titleBarDragPointer = null; } private void SearchBox_KeyDown(object? sender, KeyEventArgs e) From 2f5480a4ef0599c5253bd4b87e8e745ce0e402e8 Mon Sep 17 00:00:00 2001 From: GabrielDuf Date: Wed, 3 Jun 2026 13:31:17 -0400 Subject: [PATCH 2/2] Avalonia: Refine touch title-bar drag (round + drag-to-restore) --- .../Views/MainWindow.axaml.cs | 51 +++++++++++++++++-- 1 file changed, 47 insertions(+), 4 deletions(-) diff --git a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs index bb67a322b..8d3216da5 100644 --- a/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs +++ b/src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs @@ -833,6 +833,7 @@ private void CloseButton_Click(object? sender, RoutedEventArgs e) // 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) { @@ -858,11 +859,13 @@ private void TitleBar_PointerPressed(object? sender, PointerPressedEventArgs e) } // Touch/pen on Windows: move the window manually (issue #4866). - if (_titleBarDragPointer is not null || WindowState == WindowState.Maximized) + 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; } @@ -873,22 +876,59 @@ private void TitleBar_PointerMoved(object? sender, PointerEventArgs e) 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 in DIPs relative to the window; Position is in screen pixels. The grab - // origin stays fixed: once the window follows, the pointer returns to it, so deltas converge. + // 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)(delta.X * scale), (int)(delta.Y * scale)); + 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; } @@ -896,7 +936,10 @@ private void TitleBar_PointerReleased(object? sender, PointerReleasedEventArgs e private void TitleBar_PointerCaptureLost(object? sender, PointerCaptureLostEventArgs e) { if (e.Pointer == _titleBarDragPointer) + { _titleBarDragPointer = null; + _restoreThenDrag = false; + } } private void SearchBox_KeyDown(object? sender, KeyEventArgs e)