Skip to content
Closed
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
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -304,4 +304,5 @@ Output-Performance.txt

# vscode
.vscode
.history
.history
/.copilot
14 changes: 8 additions & 6 deletions Flow.Launcher.Infrastructure/Win32Helper.cs
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@
{
var cloaked = cloak ? 1 : 0;

return PInvoke.DwmSetWindowAttribute(

Check warning on line 47 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Dwm` is not a recognized word. (unrecognized-spelling)
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_CLOAK,
&cloaked,
Expand All @@ -56,14 +56,14 @@
var backdropType = backdrop switch
{
BackdropTypes.Acrylic => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TRANSIENTWINDOW,
BackdropTypes.Mica => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_MAINWINDOW,

Check warning on line 59 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`SYSTEMBACKDROP` is not a recognized word. (unrecognized-spelling)

Check warning on line 59 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`DWMSBT` is not a recognized word. (unrecognized-spelling)
BackdropTypes.MicaAlt => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_TABBEDWINDOW,

Check warning on line 60 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`SYSTEMBACKDROP` is not a recognized word. (unrecognized-spelling)

Check warning on line 60 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`DWMSBT` is not a recognized word. (unrecognized-spelling)
_ => DWM_SYSTEMBACKDROP_TYPE.DWMSBT_AUTO

Check warning on line 61 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`SYSTEMBACKDROP` is not a recognized word. (unrecognized-spelling)

Check warning on line 61 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`DWMSBT` is not a recognized word. (unrecognized-spelling)
};

return PInvoke.DwmSetWindowAttribute(

Check warning on line 64 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`PInvoke` is not a recognized word. (unrecognized-spelling)

Check warning on line 64 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`Dwm` is not a recognized word. (unrecognized-spelling)
GetWindowHandle(window),
DWMWINDOWATTRIBUTE.DWMWA_SYSTEMBACKDROP_TYPE,

Check warning on line 66 in Flow.Launcher.Infrastructure/Win32Helper.cs

View workflow job for this annotation

GitHub Actions / Check Spelling

`DWMWA` is not a recognized word. (unrecognized-spelling)
&backdropType,
(uint)Marshal.SizeOf<int>()).Succeeded;
}
Expand Down Expand Up @@ -303,13 +303,15 @@

#region Pixel to DIP

private const double DefaultDpi = 96d;

/// <summary>
/// Transforms pixels to Device Independent Pixels used by WPF
/// Transforms pixels to Device Independent Pixels used by WPF.
/// </summary>
/// <param name="visual">current window, required to get presentation source</param>
/// <param name="unitX">horizontal position in pixels</param>
/// <param name="unitY">vertical position in pixels</param>
/// <returns>point containing device independent pixels</returns>
/// <param name="visual">Current window, required to get presentation source.</param>
/// <param name="unitX">Horizontal position in pixels.</param>
/// <param name="unitY">Vertical position in pixels.</param>
/// <returns>Point containing device independent pixels.</returns>
public static Point TransformPixelsToDIP(Visual visual, double unitX, double unitY)
{
Matrix matrix;
Expand All @@ -324,7 +326,7 @@
matrix = src.CompositionTarget.TransformFromDevice;
}

return new Point((int)(matrix.M11 * unitX), (int)(matrix.M22 * unitY));
return new Point(matrix.M11 * unitX, matrix.M22 * unitY);
}

#endregion
Expand Down
41 changes: 39 additions & 2 deletions Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -148,21 +148,40 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
public string Name { get; }

/// <summary>
/// Gets the display monitor rectangle, expressed in virtual-screen coordinates.
/// Gets the display monitor rectangle, expressed in virtual-screen coordinates and physical pixels.
/// </summary>
/// <remarks>
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
/// </remarks>
public Rect Bounds { get; }

/// <summary>
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates.
/// Gets the work area rectangle of the display monitor, expressed in virtual-screen coordinates and physical pixels.
/// </summary>
/// <remarks>
/// <note>If the monitor is not the primary display monitor, some of the rectangle's coordinates may be negative values.</note>
/// </remarks>
public Rect WorkingArea { get; }

/// <summary>
/// Transforms physical pixel coordinates on this monitor to WPF device-independent pixels.
/// </summary>
public Point TransformPixelsToDIP(double unitX, double unitY)
{
var (scaleX, scaleY) = GetDipScale();
return new Point(unitX / scaleX, unitY / scaleY);
}

/// <summary>
/// Transforms a physical pixel rectangle on this monitor to WPF device-independent pixels.
/// </summary>
public Rect TransformPixelsToDIP(Rect rect)
{
var topLeft = TransformPixelsToDIP(rect.Left, rect.Top);
var bottomRight = TransformPixelsToDIP(rect.Right, rect.Bottom);
return new Rect(topLeft, bottomRight);
}

/// <summary>
/// Gets if the monitor is the primary display monitor.
/// </summary>
Expand All @@ -171,6 +190,24 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
/// <inheritdoc />
public override string ToString() => $"{Name} {Bounds.Width}x{Bounds.Height}";

private (double ScaleX, double ScaleY) GetDipScale()
{
if (GetDpiForMonitor(_monitor, MonitorDpiType.EffectiveDpi, out var dpiX, out var dpiY) == 0 && dpiX != 0 && dpiY != 0)
{
return (dpiX / 96, dpiY / 96);
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1: DPI scale uses integer division, truncating non-100%/200% scaling and producing incorrect DIP coordinates.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher.Plugin/SharedModels/MonitorInfo.cs, line 197:

<comment>DPI scale uses integer division, truncating non-100%/200% scaling and producing incorrect DIP coordinates.</comment>

<file context>
@@ -171,6 +190,24 @@ internal unsafe MonitorInfo(HMONITOR monitor, RECT* rect)
+    {
+        if (GetDpiForMonitor(_monitor, MonitorDpiType.EffectiveDpi, out var dpiX, out var dpiY) == 0 && dpiX != 0 && dpiY != 0)
+        {
+            return (dpiX / 96, dpiY / 96);
+        }
+
</file context>
Suggested change
return (dpiX / 96, dpiY / 96);
return (dpiX / 96d, dpiY / 96d);
Fix with Cubic

}

return (1d, 1d);
}

[DllImport("Shcore.dll")]
private static extern int GetDpiForMonitor(HMONITOR hmonitor, MonitorDpiType dpiType, out uint dpiX, out uint dpiY);

private enum MonitorDpiType
{
EffectiveDpi = 0,
}

private static unsafe bool GetMonitorInfo(HMONITOR hMonitor, ref MONITORINFOEXW lpmi)
{
fixed (MONITORINFOEXW* lpmiLocal = &lpmi)
Expand Down
89 changes: 49 additions & 40 deletions Flow.Launcher/MainWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -395,8 +395,7 @@ private void OnLocationChanged(object sender, EventArgs e)

if (IsLoaded)
{
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
SaveWindowPositionAndDisplayMetrics();
}
}

Expand All @@ -407,8 +406,7 @@ private async void OnDeactivated(object sender, EventArgs e)
return;
}

_settings.WindowLeft = Left;
_settings.WindowTop = Top;
SaveWindowPositionAndDisplayMetrics();

_viewModel.ClockPanelOpacity = 0.0;
_viewModel.SearchIconOpacity = 0.0;
Expand Down Expand Up @@ -537,7 +535,7 @@ private void OnMouseDown(object sender, MouseButtonEventArgs e)
// Current monitor information
var screen = MonitorInfo.GetNearestDisplayMonitor(new WindowInteropHelper(this).Handle);
var workingArea = screen.WorkingArea;
var screenLeftTop = Win32Helper.TransformPixelsToDIP(this, workingArea.X, workingArea.Y);
var screenLeftTop = screen.TransformPixelsToDIP(workingArea.X, workingArea.Y);

// Switch to Normal state
WindowState = WindowState.Normal;
Expand Down Expand Up @@ -897,10 +895,8 @@ void InitializePositionInner()
{
var previousScreenWidth = _settings.PreviousScreenWidth;
var previousScreenHeight = _settings.PreviousScreenHeight;
GetDpi(out var previousDpiX, out var previousDpiY);

_settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth;
_settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight;
var previousDpiX = _settings.PreviousDpiX;
var previousDpiY = _settings.PreviousDpiY;
GetDpi(out var currentDpiX, out var currentDpiY);

if (previousScreenWidth != 0 && previousScreenHeight != 0 &&
Expand All @@ -910,11 +906,13 @@ void InitializePositionInner()
previousDpiX != currentDpiX || previousDpiY != currentDpiY))
{
AdjustPositionForResolutionChange();
return;
}

Left = _settings.WindowLeft;
Top = _settings.WindowTop;
else
{
Left = _settings.WindowLeft;
Top = _settings.WindowTop;
}
SaveCurrentDisplayMetrics(currentDpiX, currentDpiY);
}
else
{
Expand All @@ -938,12 +936,11 @@ void InitializePositionInner()
Top = VerticalTop(screen);
break;
case SearchWindowAligns.Custom:
var customLeft = Win32Helper.TransformPixelsToDIP(this,
screen.WorkingArea.X + _settings.CustomWindowLeft, 0);
var customTop = Win32Helper.TransformPixelsToDIP(this, 0,
var customPosition = screen.TransformPixelsToDIP(
screen.WorkingArea.X + _settings.CustomWindowLeft,
screen.WorkingArea.Y + _settings.CustomWindowTop);
Left = customLeft.X;
Top = customTop.Y;
Left = customPosition.X;
Top = customPosition.Y;
break;
}
}
Expand All @@ -954,19 +951,15 @@ private void AdjustPositionForResolutionChange()
{
var screenWidth = SystemParameters.VirtualScreenWidth;
var screenHeight = SystemParameters.VirtualScreenHeight;
GetDpi(out var currentDpiX, out var currentDpiY);

var previousLeft = _settings.WindowLeft;
var previousTop = _settings.WindowTop;
GetDpi(out var previousDpiX, out var previousDpiY);

var widthRatio = screenWidth / _settings.PreviousScreenWidth;
var heightRatio = screenHeight / _settings.PreviousScreenHeight;
var dpiXRatio = currentDpiX / previousDpiX;
var dpiYRatio = currentDpiY / previousDpiY;

var newLeft = previousLeft * widthRatio * dpiXRatio;
var newTop = previousTop * heightRatio * dpiYRatio;
var newLeft = previousLeft * widthRatio;
var newTop = previousTop * heightRatio;

var screenLeft = SystemParameters.VirtualScreenLeft;
var screenTop = SystemParameters.VirtualScreenTop;
Expand Down Expand Up @@ -994,6 +987,22 @@ private void GetDpi(out double dpiX, out double dpiY)
}
}

private void SaveWindowPositionAndDisplayMetrics()
{
_settings.WindowLeft = Left;
_settings.WindowTop = Top;
GetDpi(out var dpiX, out var dpiY);
SaveCurrentDisplayMetrics(dpiX, dpiY);
}

private void SaveCurrentDisplayMetrics(double dpiX, double dpiY)
{
_settings.PreviousScreenWidth = SystemParameters.VirtualScreenWidth;
_settings.PreviousScreenHeight = SystemParameters.VirtualScreenHeight;
_settings.PreviousDpiX = dpiX;
_settings.PreviousDpiY = dpiY;
}

private MonitorInfo SelectedScreen()
{
MonitorInfo screen;
Expand Down Expand Up @@ -1025,38 +1034,38 @@ private MonitorInfo SelectedScreen()

private double HorizonCenter(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0);
var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}

private double VerticalCenter(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y);
var dip2 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Height);
var top = (dip2.Y - QueryTextBox.ActualHeight) / 4 + dip1.Y;
return top;
}

private double HorizonRight(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0);
var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0);
var left = (dip1.X + dip2.X - ActualWidth) - 10;
return left;
}

private double HorizonLeft(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0);
var left = dip1.X + 10;
return left;
}

private double VerticalTop(MonitorInfo screen)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y);
var top = dip1.Y + 10;
return top;
}
Expand Down Expand Up @@ -1185,7 +1194,7 @@ private void WindowAnimation()
iconsb.Children.Add(IconMotion);
iconsb.Children.Add(IconOpacity);

_settings.WindowLeft = Left;
SaveWindowPositionAndDisplayMetrics();
_isArrowKeyPressed = false;

clocksb.Begin(ClockPanel);
Expand Down Expand Up @@ -1449,23 +1458,23 @@ private void InitializeDialogJumpPosition()
var result = Win32Helper.GetWindowRect(_viewModel.DialogWindowHandle, out var window);
if (!result) return;

var monitor = MonitorInfo.GetNearestDisplayMonitor(_viewModel.DialogWindowHandle);
var dipWindow = monitor.TransformPixelsToDIP(window);

// Move window below the bottom of the dialog and keep it center
Top = VerticalBottom(window);
Left = HorizonCenter(window);
Top = VerticalBottom(dipWindow);
Left = HorizonCenter(dipWindow);
}

private double HorizonCenter(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, window.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, window.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
var left = (window.Width - ActualWidth) / 2 + window.X;
return left;
}

private double VerticalBottom(Rect window)
{
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, window.Bottom);
return dip1.Y;
return window.Bottom;
}

#endregion
Expand Down
34 changes: 15 additions & 19 deletions Flow.Launcher/SettingWindow.xaml.cs
Original file line number Diff line number Diff line change
Expand Up @@ -179,8 +179,8 @@ private void SetWindowPosition(double top, double left)
// Ensure window does not exceed screen boundaries
top = Math.Max(top, SystemParameters.VirtualScreenTop);
left = Math.Max(left, SystemParameters.VirtualScreenLeft);
top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight);
left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth);
top = Math.Min(top, SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - ActualHeight);
left = Math.Min(left, SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - ActualWidth);

Top = top;
Left = left;
Expand All @@ -191,39 +191,35 @@ private void AdjustWindowPosition(ref double top, ref double left)
// Adjust window position if it exceeds screen boundaries
top = Math.Max(top, SystemParameters.VirtualScreenTop);
left = Math.Max(left, SystemParameters.VirtualScreenLeft);
top = Math.Min(top, SystemParameters.VirtualScreenHeight - ActualHeight);
left = Math.Min(left, SystemParameters.VirtualScreenWidth - ActualWidth);
top = Math.Min(top, SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight - ActualHeight);
left = Math.Min(left, SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth - ActualWidth);
}

private static bool IsPositionValid(double top, double left)
{
foreach (var screen in MonitorInfo.GetDisplayMonitors())
{
var workingArea = screen.WorkingArea;

if (left >= workingArea.Left && left < workingArea.Right &&
top >= workingArea.Top && top < workingArea.Bottom)
{
return true;
}
}
return false;
// Use SystemParameters (DIP units) to match the coordinate system of Window.Top/Left.
// MonitorInfo.WorkingArea uses physical pixels which can differ from DIP units when DPI
// scaling is active, leading to incorrect results on high-DPI or mixed-DPI setups.
return left >= SystemParameters.VirtualScreenLeft &&
left < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth &&
top >= SystemParameters.VirtualScreenTop &&
top < SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight;
Comment on lines +203 to +206
Copy link
Copy Markdown
Contributor

@cubic-dev-ai cubic-dev-ai bot Apr 7, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2: IsPositionValid now validates against the virtual-screen bounding box, which can mark coordinates in non-visible gaps between monitors as valid. That can still restore Settings off-screen on irregular multi-monitor layouts.

Prompt for AI agents
Check if this issue is valid — if so, understand the root cause and fix it. At Flow.Launcher/SettingWindow.xaml.cs, line 203:

<comment>`IsPositionValid` now validates against the virtual-screen bounding box, which can mark coordinates in non-visible gaps between monitors as valid. That can still restore Settings off-screen on irregular multi-monitor layouts.</comment>

<file context>
@@ -191,39 +191,35 @@ private void AdjustWindowPosition(ref double top, ref double left)
+        // Use SystemParameters (DIP units) to match the coordinate system of Window.Top/Left.
+        // MonitorInfo.WorkingArea uses physical pixels which can differ from DIP units when DPI
+        // scaling is active, leading to incorrect results on high-DPI or mixed-DPI setups.
+        return left >= SystemParameters.VirtualScreenLeft &&
+               left < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth &&
+               top >= SystemParameters.VirtualScreenTop &&
</file context>
Suggested change
return left >= SystemParameters.VirtualScreenLeft &&
left < SystemParameters.VirtualScreenLeft + SystemParameters.VirtualScreenWidth &&
top >= SystemParameters.VirtualScreenTop &&
top < SystemParameters.VirtualScreenTop + SystemParameters.VirtualScreenHeight;
foreach (var screen in MonitorInfo.GetDisplayMonitors())
{
var origin = screen.TransformPixelsToDIP(screen.WorkingArea.X, screen.WorkingArea.Y);
var size = screen.TransformPixelsToDIP(screen.WorkingArea.Width, screen.WorkingArea.Height);
if (left >= origin.X && left < origin.X + size.X &&
top >= origin.Y && top < origin.Y + size.Y)
{
return true;
}
}
return false;
Fix with Cubic

}

private double WindowLeft()
{
var screen = MonitorInfo.GetCursorDisplayMonitor();
var dip1 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.X, 0);
var dip2 = Win32Helper.TransformPixelsToDIP(this, screen.WorkingArea.Width, 0);
var dip1 = screen.TransformPixelsToDIP(screen.WorkingArea.X, 0);
var dip2 = screen.TransformPixelsToDIP(screen.WorkingArea.Width, 0);
var left = (dip2.X - ActualWidth) / 2 + dip1.X;
return left;
}

private double WindowTop()
{
var screen = MonitorInfo.GetCursorDisplayMonitor();
var dip1 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Y);
var dip2 = Win32Helper.TransformPixelsToDIP(this, 0, screen.WorkingArea.Height);
var dip1 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Y);
var dip2 = screen.TransformPixelsToDIP(0, screen.WorkingArea.Height);
var top = (dip2.Y - ActualHeight) / 2 + dip1.Y - 20;
return top;
}
Expand Down
Loading