Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add WindowCornerHintsProperty #17354

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 1 commit
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
5 changes: 5 additions & 0 deletions src/Avalonia.Controls/Platform/IWin32OptionsTopLevelImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,5 +21,10 @@ public interface IWin32OptionsTopLevelImpl : ITopLevelImpl
/// Gets or sets a custom callback for the window's WndProc
/// </summary>
public CustomWndProcHookCallback? WndProcHookCallback { get; set; }

/// <summary>
/// Sets hints that configure the shape of window corners.
/// </summary>
void SetWindowCornerHints(Win32WindowCornerHints hints);
}
}
30 changes: 30 additions & 0 deletions src/Avalonia.Controls/Platform/Win32CornerHints.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
using System;

namespace Avalonia.Platform;

/// <summary>
/// Specifies hints for window corner appearance.
/// </summary>
[Flags]
public enum Win32WindowCornerHints
Comment on lines +8 to +9
Copy link
Contributor

Choose a reason for hiding this comment

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

Why is this flags? And plural naming?

Reading the docs I don't think multiple values can be set at once. So it's isn't treated as flags.

https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwmwindowattribute
https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference

Perhaps the name should be Win32WindowCornerPreference as well. Although, I think it's supported only on Windows 11+ so calling it "Hints" might be appropriate as its ignored on platforms that don't support it.

{
/// <summary>
/// Default Avalonia behavior.
/// </summary>
NoHint,

/// <summary>
/// The platform's default corner style.
/// </summary>
PlatformDefault,
Comment on lines +14 to +19
Copy link
Contributor

Choose a reason for hiding this comment

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

NoHint is exactly the same as PlatformDefault. We shouldn't have two values here. These should be combined into a Default value. Default means no hint is given so the platform defaults will be used.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the current behavior:

if (_isClientAreaExtended && WindowState != WindowState.FullScreen)
{
var margins = UpdateExtendMargins();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
}
else
{
var margins = new MARGINS();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);
_offScreenMargin = new Thickness();
_extendedMargins = new Thickness();
unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
}

So it is DWMWCP_ROUND when _isClientAreaExtended is true and window state is not full screen. Otherwise it uses DWMWCP_DEFAULT.

This is because DWMWCP_DEFAULT with client area extended results in not rounded corners.

This is also why I've split the enum into NoHint (aka avalonia default) and Default (aka platform default). However, I could agree than maybe Default aka "Platform Default" is not useful in practice. Avalonia default is good for 99.9% programmers and for the small percentage the choice between "rounded" and "not rounded" it probably enough.

Copy link
Contributor

Choose a reason for hiding this comment

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

Reading, I think we need the following values: Default, Rounded, RoundedSmall, NotRounded (Alternative: Round, RoundSmall, DoNotRound following upstream API)


/// <summary>
/// Rounded corners for the window.
/// </summary>
Rounded,

/// <summary>
/// Prevents corners from being rounded.
/// </summary>
DoNotRound
Copy link
Contributor

Choose a reason for hiding this comment

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

I'm not a fan of this name. I think the whole enum should go away but "NotRounded" is probably better.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That's right, changed it for consistency, thanks!

}
19 changes: 18 additions & 1 deletion src/Avalonia.Controls/Platform/Win32Properties.cs
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ namespace Avalonia.Controls
/// <summary>
/// Set of Win32 specific properties and events that allow deeper customization of the application per platform.
/// </summary>
public static class Win32Properties
public class Win32Properties
{
public static readonly AttachedProperty<Win32WindowCornerHints> WindowCornerHintProperty =
AvaloniaProperty.RegisterAttached<Win32Properties, Window, Win32WindowCornerHints>("WindowCornerHint");

Copy link
Contributor

Choose a reason for hiding this comment

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

Why can't we just make this a 3-state bool value? This property is ONLY controlling a window corner radius. This means we need to control whether it is ON or OFF. Then it's probably useful to have a "default" condition as well.

bool? could communicate:

  • null : Platform default
  • true : Round corners
  • false : Don't round corners

Will there ever be a reason to have more values?

Copy link
Member

Choose a reason for hiding this comment

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

If it's a windows specific property, as it is now, it should mirror windows API.
If we assume it shouldn't be windows specific one day, it can either be made a 3-state bool later. But it's unlikely to be supported on wider range of platforms.

Copy link
Contributor

Choose a reason for hiding this comment

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

If it's a windows specific property, as it is now, it should mirror windows API.

Good point. That's the right way to look at it I think so your probably right.

I also notice there are more values supported: https://learn.microsoft.com/en-us/windows/win32/api/dwmapi/ne-dwmapi-dwm_window_corner_preference. Namely RoundSmall. So an enum is the way to go.

public static void SetWindowCornerHint(Window obj, Win32WindowCornerHints value) => obj.SetValue(WindowCornerHintProperty, value);
public static Win32WindowCornerHints GetWindowCornerHint(Window obj) => obj.GetValue(WindowCornerHintProperty);

public delegate (uint style, uint exStyle) CustomWindowStylesCallback(uint style, uint exStyle);
public delegate IntPtr CustomWndProcHookCallback(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool handled);

Expand Down Expand Up @@ -70,5 +76,16 @@ public static void RemoveWndProcHookCallback(TopLevel topLevel, CustomWndProcHoo
toplevelImpl.WndProcHookCallback -= callback;
}
}

static Win32Properties()
{
WindowCornerHintProperty.Changed.AddClassHandler<Window>((window, e) =>
{
if (window.PlatformImpl is IWin32OptionsTopLevelImpl toplevelImpl)
{
toplevelImpl.SetWindowCornerHints(e.GetNewValue<Win32WindowCornerHints>());
}
});
}
}
}
47 changes: 35 additions & 12 deletions src/Windows/Avalonia.Win32/WindowImpl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ internal partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindow
private POINT _maxTrackSize;
private WindowImpl? _parent;
private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default;
private Win32WindowCornerHints _cornerHints;
private bool _isCloseRequested;
private bool _shown;
private bool _hiddenWindowIsParent;
Expand Down Expand Up @@ -1133,6 +1134,30 @@ private MARGINS UpdateExtendMargins()
return margins;
}

private static DwmWindowCornerPreference HintsToCornerPreference(Win32WindowCornerHints cornerHints, DwmWindowCornerPreference noHintDefault)
{
return cornerHints switch
{
Win32WindowCornerHints.NoHint => noHintDefault,
Win32WindowCornerHints.PlatformDefault => DwmWindowCornerPreference.DWMWCP_DEFAULT,
Win32WindowCornerHints.Rounded => DwmWindowCornerPreference.DWMWCP_ROUND,
Win32WindowCornerHints.DoNotRound => DwmWindowCornerPreference.DWMWCP_DONOTROUND,
_ => throw new ArgumentOutOfRangeException(nameof(cornerHints), cornerHints, null)
};
}

private void UpdateWindowCornerPreference()
{
int cornerPreference = (int)HintsToCornerPreference(_cornerHints,
_isClientAreaExtended && WindowState != WindowState.FullScreen ?
DwmWindowCornerPreference.DWMWCP_ROUND :
DwmWindowCornerPreference.DWMWCP_DEFAULT);
unsafe
{
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
}

private void ExtendClientArea()
{
if (!_shown)
Expand All @@ -1151,12 +1176,6 @@ private void ExtendClientArea()
{
var margins = UpdateExtendMargins();
DwmExtendFrameIntoClientArea(_hwnd, ref margins);

unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_ROUND;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
}
else
{
Expand All @@ -1165,14 +1184,10 @@ private void ExtendClientArea()

_offScreenMargin = new Thickness();
_extendedMargins = new Thickness();

unsafe
{
int cornerPreference = (int)DwmWindowCornerPreference.DWMWCP_DEFAULT;
DwmSetWindowAttribute(_hwnd, (int)DwmWindowAttribute.DWMWA_WINDOW_CORNER_PREFERENCE, &cornerPreference, sizeof(int));
}
}

UpdateWindowCornerPreference();

if (!_isClientAreaExtended || (_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.SystemChrome) &&
!_extendChromeHints.HasAllFlags(ExtendClientAreaChromeHints.PreferSystemChrome)))
{
Expand Down Expand Up @@ -1592,6 +1607,14 @@ public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight)
ExtendClientArea();
}

/// <inheritdoc/>
public void SetWindowCornerHints(Win32WindowCornerHints hints)
{
_cornerHints = hints;

UpdateWindowCornerPreference();
}

/// <inheritdoc/>
public void GetWindowsZOrder(Span<Window> windows, Span<long> zOrder)
{
Expand Down
Loading