Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion src/UniGetUI.Avalonia/Views/MainWindow.axaml
Original file line number Diff line number Diff line change
Expand Up @@ -312,10 +312,14 @@

<!-- Drag area — sits behind everything so interactive controls still work -->
<Border Grid.Column="0" Grid.ColumnSpan="3"
x:Name="TitleBarDragArea"
Background="{DynamicResource MicaPageBackground}"
automation:AutomationProperties.AccessibilityView="Raw"
IsHitTestVisible="True"
PointerPressed="TitleBar_PointerPressed"/>
PointerPressed="TitleBar_PointerPressed"
PointerMoved="TitleBar_PointerMoved"
PointerReleased="TitleBar_PointerReleased"
PointerCaptureLost="TitleBar_PointerCaptureLost"/>

<!-- Left: hamburger + app name -->
<StackPanel x:Name="HamburgerPanel"
Expand Down
102 changes: 100 additions & 2 deletions src/UniGetUI.Avalonia/Views/MainWindow.axaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -828,9 +828,20 @@ public struct MONITORINFO
private void CloseButton_Click(object? sender, RoutedEventArgs e)
=> 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)
Expand All @@ -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;
Comment thread
GabrielDuf marked this conversation as resolved.

_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;
Comment thread
GabrielDuf marked this conversation as resolved.
}

// 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)
Expand Down
Loading