/*
* Copyright (c) 2025 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Microsoft.UI;
using Microsoft.UI.Windowing;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Media;
using ProtonVPN.Client.Common.Interop;
using ProtonVPN.Client.Common.Models;
using ProtonVPN.Client.Common.UI.Controls.Custom;
using ProtonVPN.Client.Common.UI.Windowing;
using ProtonVPN.Client.Core.Helpers;
using ProtonVPN.Common.Core.Helpers;
using Windows.Foundation;
using Windows.Graphics;
using Windows.Storage;
using Windows.Storage.Pickers;
using WinUIEx;
using static Vanara.PInvoke.Shell32;
using Icon = System.Drawing.Icon;
using ProtonVPN.Client.Core.Bases;
namespace ProtonVPN.Client.Core.Extensions;
public static class WindowExtensions
{
private static readonly Lazy _taskbar = new(() =>
{
try
{
ITaskbarList3 taskbar = new();
taskbar.HrInit();
return taskbar;
}
catch
{
return null;
}
});
public static void ApplyTheme(this Window window, ElementTheme theme)
{
window.AppWindow.TitleBar.BackgroundColor = Colors.Transparent;
window.AppWindow.TitleBar.InactiveBackgroundColor = Colors.Transparent;
window.AppWindow.TitleBar.ForegroundColor = ResourceHelper.GetColor("TextNormColor", theme);
window.AppWindow.TitleBar.InactiveForegroundColor = ResourceHelper.GetColor("TextHintColor", theme);
window.AppWindow.TitleBar.ButtonBackgroundColor = ResourceHelper.GetColor("InteractionDefaultEmptyColor", theme);
window.AppWindow.TitleBar.ButtonHoverBackgroundColor = ResourceHelper.GetColor("InteractionDefaultHoverColor", theme);
window.AppWindow.TitleBar.ButtonPressedBackgroundColor = ResourceHelper.GetColor("InteractionDefaultActiveColor", theme);
window.AppWindow.TitleBar.ButtonInactiveBackgroundColor = ResourceHelper.GetColor("InteractionDefaultEmptyColor", theme);
window.AppWindow.TitleBar.ButtonForegroundColor = ResourceHelper.GetColor("TextNormColor", theme);
window.AppWindow.TitleBar.ButtonHoverForegroundColor = ResourceHelper.GetColor("TextNormColor", theme);
window.AppWindow.TitleBar.ButtonPressedForegroundColor = ResourceHelper.GetColor("TextNormColor", theme);
window.AppWindow.TitleBar.ButtonInactiveForegroundColor = ResourceHelper.GetColor("TextWeakColor", theme);
if (window.Content is FrameworkElement element)
{
element.RequestedTheme = theme;
}
}
public static void ApplyFlowDirection(this Window window, FlowDirection flowDirection)
{
ExtendedWindowStyle windowStyle = window.GetExtendedWindowStyle();
if (flowDirection == FlowDirection.RightToLeft)
{
windowStyle |= ExtendedWindowStyle.LayoutRtl;
}
else
{
windowStyle &= ~ExtendedWindowStyle.LayoutRtl;
}
window.SetExtendedWindowStyle(windowStyle);
if (window.Content is FrameworkElement element)
{
element.FlowDirection = flowDirection;
}
}
public static void SetDragArea(this Window window, double width, double height)
{
double scaleAdjustment = window.GetDpiForWindow() / 96.0;
// Scale the dimensions
int scaledWidth = (int)(width * scaleAdjustment);
int scaledHeight = (int)(height * scaleAdjustment);
RectInt32 dragRect = new()
{
X = 0,
Y = 0,
Width = scaledWidth,
Height = scaledHeight
};
window.AppWindow.TitleBar.SetDragRectangles([dragRect]);
}
public static Point GetRelativePosition(this Window window, UIElement element)
{
FrameworkElement? root = window.Content as FrameworkElement;
if (root == null || element == null)
{
return new Point(0, 0);
}
GeneralTransform transform = element.TransformToVisual(root);
return transform.TransformPoint(new Point(0, 0));
}
public static void SetDragArea(this Window window, double width, double height, RectInt32 gap)
{
double scaleAdjustment = window.GetDpiForWindow() / 96.0;
// Scale the dimensions
int scaledWidth = (int)(width * scaleAdjustment);
int scaledHeight = (int)(height * scaleAdjustment);
// Scale the gap
RectInt32 scaledGap = new()
{
X = (int)(gap.X * scaleAdjustment),
Y = (int)(gap.Y * scaleAdjustment),
Width = (int)(gap.Width * scaleAdjustment),
Height = (int)(gap.Height * scaleAdjustment)
};
List dragRects = new();
// Left area
if (scaledGap.X > 0)
{
dragRects.Add(new RectInt32
{
X = 0,
Y = 0,
Width = scaledGap.X,
Height = scaledHeight
});
}
// Right area
if (scaledGap.X + scaledGap.Width < scaledWidth)
{
dragRects.Add(new RectInt32
{
X = scaledGap.X + scaledGap.Width,
Y = 0,
Width = scaledWidth - (scaledGap.X + scaledGap.Width),
Height = scaledHeight
});
}
// Top area (if needed to leave a gap above the button)
if (scaledGap.Y > 0)
{
dragRects.Add(new RectInt32
{
X = scaledGap.X,
Y = 0,
Width = scaledGap.Width,
Height = scaledGap.Y
});
}
// Bottom area (if needed to leave a gap below the button)
if (scaledGap.Y + scaledGap.Height < scaledHeight)
{
dragRects.Add(new RectInt32
{
X = scaledGap.X,
Y = scaledGap.Y + scaledGap.Height,
Width = scaledGap.Width,
Height = scaledHeight - (scaledGap.Y + scaledGap.Height)
});
}
window.AppWindow.TitleBar.SetDragRectangles(dragRects.ToArray());
}
public static void MoveAndResize(this BaseWindow window, WindowPositionParameters parameters)
{
bool isPositionSpecified =
parameters.XPosition is not null &&
parameters.YPosition is not null;
PointInt32 cursorPosition = MonitorCalculator.GetCursorPosition();
DisplayArea displayArea = isPositionSpecified
? DisplayArea.GetFromRect(parameters.ToRect(), DisplayAreaFallback.Nearest)
: DisplayArea.GetFromPoint(cursorPosition, DisplayAreaFallback.Primary);
RectInt32 workArea = displayArea.WorkArea;
// Get display area DPI
uint dpi = displayArea.GetDpi();
// Ensure the window size is within the work area limits (size is calculated in DIPs)
double windowWidth = parameters.Width.Clamp(window.MinWidth, workArea.Width.ToDips(dpi));
double windowHeight = parameters.Height.Clamp(window.MinHeight, workArea.Height.ToDips(dpi));
double windowPositionX;
double windowPositionY;
if (isPositionSpecified)
{
// Ensure the window position is within the work area bounds (position is calculated in pixels)
windowPositionX = parameters.XPosition!.Value.Clamp(workArea.X, workArea.X + workArea.Width - windowWidth.ToPixels(dpi));
windowPositionY = parameters.YPosition!.Value.Clamp(workArea.Y, workArea.Y + workArea.Height - windowHeight.ToPixels(dpi));
}
else
{
// No position specified, center the window on the current monitor (position is calculated in pixels)
windowPositionX = workArea.X + ((workArea.Width - windowWidth.ToPixels(dpi)) / 2);
windowPositionY = workArea.Y + ((workArea.Height - windowHeight.ToPixels(dpi)) / 2);
}
window.MoveAndResize(
x: windowPositionX,
y: windowPositionY,
width: windowWidth,
height: windowHeight);
}
public static void MoveNearTray(this BaseWindow window, double width, double height, double margin)
{
RectInt32? trayRect = MonitorCalculator.GetTrayRect();
PointInt32 cursorPosition = MonitorCalculator.GetCursorPosition();
// Determine the display area for the system tray.
// 1. Ideally use the tray rectangle if available.
// 2. If not available, for Windows 11 or higher, the system tray should always be on the primary display.
// 3. for Windows 10, use the display area where the cursor is located.
DisplayArea displayArea = trayRect.HasValue
? DisplayArea.GetFromRect(trayRect.Value, DisplayAreaFallback.Primary)
: OSVersion.IsWindows11OrHigher()
? DisplayArea.Primary
: DisplayArea.GetFromPoint(cursorPosition, DisplayAreaFallback.Primary);
RectInt32 workArea = displayArea.WorkArea;
// Get display area DPI and taskbard edge
uint dpi = displayArea.GetDpi();
TaskbarEdge taskbarEdge = MonitorCalculator.GetTaskbarEdge();
// Ensure the window size is within the work area limits (size is calculated in DIPs)
double windowWidth = width.Clamp(window.MinWidth, workArea.Width.ToDips(dpi) - (2 * margin));
double windowHeight = height.Clamp(window.MinHeight, workArea.Height.ToDips(dpi) - (2 * margin));
// Calculate the position based on the taskbar edge (position is calculated in pixels)
double windowPositionX = taskbarEdge switch
{
TaskbarEdge.Left => workArea.X + margin, // Dock to the left
_ => workArea.X + workArea.Width - windowWidth.ToPixels(dpi) - margin // Dock to the right
};
double windowPositionY = taskbarEdge switch
{
TaskbarEdge.Top => workArea.Y + margin, // Dock to the top
_ => workArea.Y + workArea.Height - windowHeight.ToPixels(dpi) - margin // Dock to the bottom
};
window.MoveAndResize(
x: windowPositionX,
y: windowPositionY,
width: windowWidth,
height: windowHeight);
}
public static async Task PickSingleFileAsync(this Window window, string filterName, string[] filterFileExtensions)
{
try
{
FileOpenPicker picker = window.CreateOpenFilePicker();
// Note: Filter name (eg. Image Files) is not supported by the FileOpenPicker
foreach (string fileExtension in filterFileExtensions)
{
picker.FileTypeFilter.Add(fileExtension);
}
StorageFile file = await picker.PickSingleFileAsync();
return file?.Path ?? string.Empty;
}
catch (Exception)
{
// The method above fails when the app run in elevated mode. Use Win32 API instead.
return RuntimeHelper.PickSingleFile(window, filterName, filterFileExtensions);
}
}
public static double GetTitleBarOpacity(this Window window)
{
return window is IFocusAware focusAware && focusAware.IsFocused()
? OpacityConstants.TITLE_BAR_FOCUSED
: OpacityConstants.TITLE_BAR_UNFOCUSED;
}
public static XamlRoot GetXamlRoot(this Window window)
{
return window.Content?.XamlRoot
?? throw new InvalidOperationException("Cannot proceed, XamlRoot is undefined.");
}
public static void CenterOnMainWindowMonitor(this Window window, Window mainWindow)
{
// Get the monitor area where the main window is located
RectInt32 monitorArea = DisplayArea.GetFromWindowId(mainWindow.AppWindow.Id, DisplayAreaFallback.Nearest).WorkArea;
window.Move(monitorArea.X, monitorArea.Y);
window.CenterOnScreen();
}
public static void SetBadge(this Window window, Icon? badgeIcon)
{
try
{
if (IsTaskbarBadgeSupported())
{
IntPtr hWnd = WinRT.Interop.WindowNative.GetWindowHandle(window);
_taskbar.Value?.SetOverlayIcon(hWnd, badgeIcon?.Handle ?? IntPtr.Zero, string.Empty);
}
}
catch { }
}
public static void ClearBadge(this Window window)
{
window.SetBadge(null);
}
private static bool IsTaskbarBadgeSupported()
{
try
{
return OSVersion.IsOrHigherThan(OSVersion.TaskbarBadgeMinimumWindowsVersion)
&& _taskbar.Value != null;
}
catch (Exception)
{
return false;
}
}
private static RectInt32 ToRect(this WindowPositionParameters parameters)
{
return new RectInt32(
Convert.ToInt32(parameters.XPosition ?? 0),
Convert.ToInt32(parameters.YPosition ?? 0),
Convert.ToInt32(parameters.Width),
Convert.ToInt32(parameters.Height));
}
private static double Clamp(this double value, double min, double max)
{
try
{
return min >= max ? max : Math.Clamp(value, min, max);
}
catch (Exception)
{
return Math.Min(min, max);
}
}
public static class OpacityConstants
{
public const double TITLE_BAR_FOCUSED = 1.0;
public const double TITLE_BAR_UNFOCUSED = 0.6;
}
/// Wraps IsShownInSwitchers setter because an Exception is thrown when Explorer.exe is not running
public static void TrySetIsShownInSwitchers(this WindowEx window, bool value)
{
try
{
window.IsShownInSwitchers = value;
}
catch { }
}
}