Browse Source

Merge branch 'master' into fix-12663

pull/12667/head
Benedikt Stebner 3 years ago
committed by GitHub
parent
commit
4b9d2a4866
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Avalonia.sln
  2. 3
      NuGet.Config
  3. 13
      build/ApiDiff.props
  4. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  5. 4
      native/Avalonia.Native/src/OSX/AvnView.mm
  6. 84
      native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm
  7. 11
      native/Avalonia.Native/src/OSX/automation.mm
  8. 1
      native/Avalonia.Native/src/OSX/common.h
  9. 11
      native/Avalonia.Native/src/OSX/main.mm
  10. 29
      native/Avalonia.Native/src/OSX/platformthreading.mm
  11. 2
      packages/Avalonia/Avalonia.csproj
  12. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  13. 45
      src/Avalonia.Base/Media/RenderOptions.cs
  14. 7
      src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs
  15. 2
      src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs
  16. 2
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  17. 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  18. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  19. 31
      src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs
  20. 1
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  21. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  22. 38
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  23. 1
      src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
  24. 23
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  25. 7
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  26. 2
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  27. 2
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  28. 17
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  29. 33
      src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
  30. 25
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  31. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  32. 15
      src/Avalonia.Controls/ContentControl.cs
  33. 31
      src/Avalonia.Controls/ContextMenu.cs
  34. 26
      src/Avalonia.Controls/Documents/Inline.cs
  35. 28
      src/Avalonia.Controls/ItemsControl.cs
  36. 11
      src/Avalonia.Controls/Primitives/AccessText.cs
  37. 2
      src/Avalonia.Controls/Primitives/Track.cs
  38. 5
      src/Avalonia.Controls/Shapes/Polygon.cs
  39. 5
      src/Avalonia.Controls/Shapes/Polyline.cs
  40. 4
      src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
  41. 2
      src/Avalonia.Controls/TextBlock.cs
  42. 34
      src/Avalonia.Controls/TextBox.cs
  43. 13
      src/Avalonia.Controls/TransitioningContentControl.cs
  44. 1
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  45. 1
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  46. 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  47. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  48. 2
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  49. 58
      src/Avalonia.Native/AvaloniaNativeRenderTimer.cs
  50. 121
      src/Avalonia.Native/AvnAutomationPeer.cs
  51. 17
      src/Avalonia.Native/avn.idl
  52. 1
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  53. 3
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  54. 1
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  55. 1
      src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
  56. 3
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  57. 1
      src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  58. 1
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  59. 1
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  60. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  61. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  62. 55
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  63. 151
      src/Skia/Avalonia.Skia/GeometryImpl.cs
  64. 20
      src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs
  65. 19
      src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs
  66. 75
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  67. 48
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  68. 1
      src/Windows/Avalonia.Win32/DataObject.cs
  69. 2
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  70. 2
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  71. 3
      src/Windows/Avalonia.Win32/WindowImpl.cs
  72. 68
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
  73. 152
      tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs
  74. 21
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  75. 79
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  76. 28
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  77. 40
      tests/Avalonia.LeakTests/ControlTests.cs
  78. 43
      tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs
  79. 17
      tests/Avalonia.UnitTests/CompositorTestServices.cs

1
Avalonia.sln

@ -91,7 +91,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s
EndProject EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}"
ProjectSection(SolutionItems) = preProject ProjectSection(SolutionItems) = preProject
build\ApiDiff.props = build\ApiDiff.props
build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props build\AvaloniaPublicKey.props = build\AvaloniaPublicKey.props
build\Base.props = build\Base.props build\Base.props = build\Base.props
build\Binding.props = build\Binding.props build\Binding.props = build\Binding.props

3
NuGet.Config

@ -3,8 +3,7 @@
<configuration> <configuration>
<packageSources> <packageSources>
<clear /> <clear />
<add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" /> <add key="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="dotnet-eng" value="https://nuget.avaloniaui.net/repository/avalonia-devdeps/index.json" protocolVersion="3" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" /> <add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
</packageSources> </packageSources>
</configuration> </configuration>

13
build/ApiDiff.props

@ -1,13 +0,0 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<ApiContractPackageVersion>0.10.0</ApiContractPackageVersion>
<NugetPackageName Condition="'$(PackageId)' != ''">$(PackageId)</NugetPackageName>
<NugetPackageName Condition="'$(PackageId)' == ''">Avalonia</NugetPackageName>
<RunApiCompat Condition="'$(TargetFramework)' == 'net6.0'">false</RunApiCompat>
</PropertyGroup>
<ItemGroup>
<PackageDownload Include="$(NugetPackageName)" Version="[$(ApiContractPackageVersion)]" />
<PackageReference Include="Microsoft.DotNet.ApiCompat" Version="5.0.0-beta.20372.2" PrivateAssets="All" />
<ResolvedMatchingContract Include="$(NuGetPackageRoot)\$(NugetPackageName.ToLowerInvariant())\$(ApiContractPackageVersion)\lib\$(TargetFramework)\$(AssemblyName).dll" />
</ItemGroup>
</Project>

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -59,6 +59,7 @@
BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; }; BC11A5BE2608D58F0017BAD0 /* automation.h in Headers */ = {isa = PBXBuildFile; fileRef = BC11A5BC2608D58F0017BAD0 /* automation.h */; };
BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; }; BC11A5BF2608D58F0017BAD0 /* automation.mm in Sources */ = {isa = PBXBuildFile; fileRef = BC11A5BD2608D58F0017BAD0 /* automation.mm */; };
ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; }; ED3791C42862E1F40080BD62 /* UniformTypeIdentifiers.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */; };
ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */ = {isa = PBXBuildFile; fileRef = ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */; };
EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; }; EDF8CDCD2964CB01001EE34F /* PlatformSettings.mm in Sources */ = {isa = PBXBuildFile; fileRef = EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */; };
/* End PBXBuildFile section */ /* End PBXBuildFile section */
@ -122,6 +123,7 @@
BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; }; BC11A5BC2608D58F0017BAD0 /* automation.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = automation.h; sourceTree = "<group>"; };
BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; }; BC11A5BD2608D58F0017BAD0 /* automation.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = automation.mm; sourceTree = "<group>"; };
ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; }; ED3791C32862E1F40080BD62 /* UniformTypeIdentifiers.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = UniformTypeIdentifiers.framework; path = System/Library/Frameworks/UniformTypeIdentifiers.framework; sourceTree = SDKROOT; };
ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformRenderTimer.mm; sourceTree = "<group>"; };
EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = "<group>"; }; EDF8CDCC2964CB01001EE34F /* PlatformSettings.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = PlatformSettings.mm; sourceTree = "<group>"; };
/* End PBXFileReference section */ /* End PBXFileReference section */
@ -162,6 +164,7 @@
AB7A61E62147C814003C5833 = { AB7A61E62147C814003C5833 = {
isa = PBXGroup; isa = PBXGroup;
children = ( children = (
ED754D252A97306B0078B4DF /* PlatformRenderTimer.mm */,
855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */, 855EDC9E28C6546F00807998 /* PlatformBehaviorInhibition.mm */,
8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */, 8D2F3511292F6AAE007FCF54 /* AvnTextInputMethodDelegate.h */,
8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */, 8D300D68292E1E5D00320C49 /* AvnTextInputMethod.mm */,
@ -333,6 +336,7 @@
18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */, 18391068E48EF96E3DB5FDAB /* ResizeScope.mm in Sources */,
18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */, 18391D4EB311BC7EF8B8C0A6 /* AvnView.mm in Sources */,
18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */, 18391AA7E0BBA74D184C5734 /* AutoFitContentView.mm in Sources */,
ED754D262A97306B0078B4DF /* PlatformRenderTimer.mm in Sources */,
1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */, 1839151F32D1BB1AB51A7BB6 /* AvnPanelWindow.mm in Sources */,
18391AC16726CBC45856233B /* AvnWindow.mm in Sources */, 18391AC16726CBC45856233B /* AvnWindow.mm in Sources */,
18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */, 18391D8CD1756DC858DC1A09 /* PopupImpl.mm in Sources */,

4
native/Avalonia.Native/src/OSX/AvnView.mm

@ -538,10 +538,10 @@
{ {
_lastKeyHandled = false; _lastKeyHandled = false;
[[self inputContext] handleEvent:event]; [self keyboardEvent:event withType:KeyDown];
if(!_lastKeyHandled){ if(!_lastKeyHandled){
[self keyboardEvent:event withType:KeyDown]; [[self inputContext] handleEvent:event];
} }
} }

84
native/Avalonia.Native/src/OSX/PlatformRenderTimer.mm

@ -0,0 +1,84 @@
#include "common.h"
class PlatformRenderTimer : public ComSingleObject<IAvnPlatformRenderTimer, &IID_IAvnPlatformRenderTimer>
{
private:
ComPtr<IAvnActionCallback> _callback;
CVDisplayLinkRef _displayLink;
public:
FORWARD_IUNKNOWN()
virtual HRESULT RegisterTick (
IAvnActionCallback* callback) override
{
START_COM_CALL;
@autoreleasepool
{
if (_displayLink != nil)
{
return E_UNEXPECTED;
}
_callback = callback;
auto result = CVDisplayLinkCreateWithActiveCGDisplays(&_displayLink);
if (result != 0)
{
return E_FAIL;
}
result = CVDisplayLinkSetOutputCallback(_displayLink, OnTick, this);
if (result != 0)
{
return E_FAIL;
}
}
return S_OK;
}
virtual void Start () override
{
START_COM_CALL;
@autoreleasepool
{
if (CVDisplayLinkIsRunning(_displayLink) == false) {
CVDisplayLinkStart(_displayLink);
}
}
}
virtual void Stop () override
{
START_COM_CALL;
@autoreleasepool
{
if (CVDisplayLinkIsRunning(_displayLink) == true) {
CVDisplayLinkStop(_displayLink);
}
}
}
virtual bool RunsInBackground () override
{
START_COM_CALL;
@autoreleasepool
{
return true;
}
}
static CVReturn OnTick(CVDisplayLinkRef displayLink, const CVTimeStamp *inNow, const CVTimeStamp *inOutputTime, CVOptionFlags flagsIn, CVOptionFlags *flagsOut, void *displayLinkContext)
{
PlatformRenderTimer *object = (PlatformRenderTimer *)displayLinkContext;
object->_callback->Run();
return kCVReturnSuccess;
}
};
extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer()
{
return new PlatformRenderTimer();
}

11
native/Avalonia.Native/src/OSX/automation.mm

@ -73,6 +73,13 @@ private:
if (peer->IsRootProvider()) if (peer->IsRootProvider())
{ {
auto window = peer->RootProvider_GetWindow(); auto window = peer->RootProvider_GetWindow();
if (window == nullptr)
{
NSLog(@"IRootProvider.PlatformImpl returned null or a non-WindowBaseImpl.");
return nil;
}
auto holder = dynamic_cast<INSWindowHolder*>(window); auto holder = dynamic_cast<INSWindowHolder*>(window);
auto view = holder->GetNSView(); auto view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view]; return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@ -284,8 +291,8 @@ private:
- (id)accessibilityWindow - (id)accessibilityWindow
{ {
id topLevel = [self accessibilityTopLevelUIElement]; auto rootPeer = _peer->GetVisualRoot();
return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil; return [AvnAccessibilityElement acquire:rootPeer];
} }
- (BOOL)isAccessibilityExpanded - (BOOL)isAccessibilityExpanded

1
native/Avalonia.Native/src/OSX/common.h

@ -32,6 +32,7 @@ extern IAvnApplicationCommands* CreateApplicationCommands();
extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition(); extern IAvnPlatformBehaviorInhibition* CreatePlatformBehaviorInhibition();
extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent); extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);
extern IAvnPlatformSettings* CreatePlatformSettings(); extern IAvnPlatformSettings* CreatePlatformSettings();
extern IAvnPlatformRenderTimer* CreatePlatformRenderTimer();
extern void SetAppMenu(IAvnMenu *menu); extern void SetAppMenu(IAvnMenu *menu);
extern void SetServicesMenu (IAvnMenu* menu); extern void SetServicesMenu (IAvnMenu* menu);
extern IAvnMenu* GetAppMenu (); extern IAvnMenu* GetAppMenu ();

11
native/Avalonia.Native/src/OSX/main.mm

@ -446,6 +446,17 @@ public:
return S_OK; return S_OK;
} }
} }
virtual HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv) override
{
START_COM_CALL;
@autoreleasepool
{
*ppv = ::CreatePlatformRenderTimer();
return S_OK;
}
}
}; };
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative() extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

29
native/Avalonia.Native/src/OSX/platformthreading.mm

@ -17,7 +17,6 @@ public:
Cancelled = true; Cancelled = true;
if(Running) if(Running)
{ {
Running = false;
if(![NSThread isMainThread]) if(![NSThread isMainThread])
{ {
AddRef(); AddRef();
@ -28,22 +27,22 @@ public:
}); });
return; return;
}; };
Running = false;
if(IsApp) if(IsApp)
[NSApp stop:nil]; [NSApp stop:nil];
else
{ // Wakeup the event loop
// Wakeup the event loop NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined
NSEvent* event = [NSEvent otherEventWithType:NSEventTypeApplicationDefined location:NSMakePoint(0, 0)
location:NSMakePoint(0, 0) modifierFlags:0
modifierFlags:0 timestamp:0
timestamp:0 windowNumber:0
windowNumber:0 context:nil
context:nil subtype:0
subtype:0 data1:0
data1:0 data2:0];
data2:0]; [NSApp postEvent:event atStart:YES];
[NSApp postEvent:event atStart:YES];
}
} }
}; };
}; };

2
packages/Avalonia/Avalonia.csproj

@ -5,7 +5,7 @@
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>
<PackageReference Include="Avalonia.BuildServices" Version="0.0.28" /> <PackageReference Include="Avalonia.BuildServices" Version="0.0.29" />
<ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" /> <ProjectReference Include="../../src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj" />
<ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj"> <ProjectReference Include="../../src/Avalonia.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets> <PrivateAssets>all</PrivateAssets>

1
src/Avalonia.Base/Avalonia.Base.csproj

@ -15,7 +15,6 @@
<Import Project="..\..\build\Base.props" /> <Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" /> <Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\System.Memory.props" /> <Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

45
src/Avalonia.Base/Media/RenderOptions.cs

@ -8,12 +8,13 @@ namespace Avalonia.Media
public EdgeMode EdgeMode { get; init; } public EdgeMode EdgeMode { get; init; }
public TextRenderingMode TextRenderingMode { get; init; } public TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; } public BitmapBlendingMode BitmapBlendingMode { get; init; }
public bool? RequiresFullOpacityHandling { get; init; }
/// <summary> /// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a visual. /// Gets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual) public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{ {
return visual.RenderOptions.BitmapInterpolationMode; return visual.RenderOptions.BitmapInterpolationMode;
@ -23,7 +24,7 @@ namespace Avalonia.Media
/// Sets the value of the BitmapInterpolationMode attached property for a visual. /// Sets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value) public static void SetBitmapInterpolationMode(Visual visual, BitmapInterpolationMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value }; visual.RenderOptions = visual.RenderOptions with { BitmapInterpolationMode = value };
@ -33,7 +34,7 @@ namespace Avalonia.Media
/// Gets the value of the BitmapBlendingMode attached property for a visual. /// Gets the value of the BitmapBlendingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual) public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{ {
return visual.RenderOptions.BitmapBlendingMode; return visual.RenderOptions.BitmapBlendingMode;
@ -53,7 +54,7 @@ namespace Avalonia.Media
/// Gets the value of the EdgeMode attached property for a visual. /// Gets the value of the EdgeMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static EdgeMode GetEdgeMode(Visual visual) public static EdgeMode GetEdgeMode(Visual visual)
{ {
return visual.RenderOptions.EdgeMode; return visual.RenderOptions.EdgeMode;
@ -63,7 +64,7 @@ namespace Avalonia.Media
/// Sets the value of the EdgeMode attached property for a visual. /// Sets the value of the EdgeMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetEdgeMode(Visual visual, EdgeMode value) public static void SetEdgeMode(Visual visual, EdgeMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { EdgeMode = value }; visual.RenderOptions = visual.RenderOptions with { EdgeMode = value };
@ -73,7 +74,7 @@ namespace Avalonia.Media
/// Gets the value of the TextRenderingMode attached property for a visual. /// Gets the value of the TextRenderingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns> /// <returns>The value.</returns>
public static TextRenderingMode GetTextRenderingMode(Visual visual) public static TextRenderingMode GetTextRenderingMode(Visual visual)
{ {
return visual.RenderOptions.TextRenderingMode; return visual.RenderOptions.TextRenderingMode;
@ -83,12 +84,32 @@ namespace Avalonia.Media
/// Sets the value of the TextRenderingMode attached property for a visual. /// Sets the value of the TextRenderingMode attached property for a visual.
/// </summary> /// </summary>
/// <param name="visual">The control.</param> /// <param name="visual">The control.</param>
/// <param name="value">The left value.</param> /// <param name="value">The value.</param>
public static void SetTextRenderingMode(Visual visual, TextRenderingMode value) public static void SetTextRenderingMode(Visual visual, TextRenderingMode value)
{ {
visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value }; visual.RenderOptions = visual.RenderOptions with { TextRenderingMode = value };
} }
/// <summary>
/// Gets the value of the RequiresFullOpacityHandling attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The value.</returns>
public static bool? GetRequiresFullOpacityHandling(Visual visual)
{
return visual.RenderOptions.RequiresFullOpacityHandling;
}
/// <summary>
/// Sets the value of the RequiresFullOpacityHandling attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <param name="value">The value.</param>
public static void SetRequiresFullOpacityHandling(Visual visual, bool? value)
{
visual.RenderOptions = visual.RenderOptions with { RequiresFullOpacityHandling = value };
}
public RenderOptions MergeWith(RenderOptions other) public RenderOptions MergeWith(RenderOptions other)
{ {
var bitmapInterpolationMode = BitmapInterpolationMode; var bitmapInterpolationMode = BitmapInterpolationMode;
@ -119,12 +140,20 @@ namespace Avalonia.Media
bitmapBlendingMode = other.BitmapBlendingMode; bitmapBlendingMode = other.BitmapBlendingMode;
} }
var requiresFullOpacityHandling = RequiresFullOpacityHandling;
if (requiresFullOpacityHandling == null)
{
requiresFullOpacityHandling = other.RequiresFullOpacityHandling;
}
return new RenderOptions return new RenderOptions
{ {
BitmapInterpolationMode = bitmapInterpolationMode, BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode, EdgeMode = edgeMode,
TextRenderingMode = textRenderingMode, TextRenderingMode = textRenderingMode,
BitmapBlendingMode = bitmapBlendingMode BitmapBlendingMode = bitmapBlendingMode,
RequiresFullOpacityHandling = requiresFullOpacityHandling
}; };
} }
} }

7
src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs

@ -1278,14 +1278,11 @@ namespace Avalonia.Media.TextFormatting
var start = GetParagraphOffsetX(width, widthIncludingWhitespace); var start = GetParagraphOffsetX(width, widthIncludingWhitespace);
var overhangLeading = Math.Max(0, bounds.Left - start); var overhangLeading = Math.Max(0, bounds.Left - start);
var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace); var overhangTrailing = Math.Max(0, bounds.Width - widthIncludingWhitespace);
var hasOverflowed = overhangLeading + widthIncludingWhitespace + overhangTrailing > _paragraphWidth; var hasOverflowed = width > _paragraphWidth;
if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight))
{ {
if (lineHeight > height) height = lineHeight;
{
height = lineHeight;
}
} }
return new TextLineMetrics return new TextLineMetrics

2
src/Avalonia.Base/Rendering/Composition/Drawing/Nodes/RenderDataGeometryNode.cs

@ -25,5 +25,5 @@ class RenderDataGeometryNode : RenderDataBrushAndPenNode
context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!); context.Context.DrawGeometry(ServerBrush, ServerPen, Geometry!);
} }
public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen).CalculateBoundsWithLineCaps(ServerPen) ?? default; public override Rect? Bounds => Geometry?.GetRenderBounds(ServerPen) ?? default;
} }

2
src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs

@ -2,5 +2,7 @@ namespace Avalonia.Rendering.Composition;
internal interface ICompositionTargetDebugEvents internal interface ICompositionTargetDebugEvents
{ {
int RenderedVisuals { get; }
void IncrementRenderedVisuals();
void RectInvalidated(Rect rc); void RectInvalidated(Rect rc);
} }

1
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs

@ -38,6 +38,7 @@ partial class ServerCompositionVisual
CompositionVisualChangedFields.Size CompositionVisualChangedFields.Size
| CompositionVisualChangedFields.SizeAnimated | CompositionVisualChangedFields.SizeAnimated
| CompositionVisualChangedFields.ClipToBounds | CompositionVisualChangedFields.ClipToBounds
| CompositionVisualChangedFields.Clip
| CompositionVisualChangedFields.ClipToBoundsAnimated; | CompositionVisualChangedFields.ClipToBoundsAnimated;
partial void OnFieldsDeserialized(CompositionVisualChangedFields changed) partial void OnFieldsDeserialized(CompositionVisualChangedFields changed)

22
src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs

@ -38,6 +38,7 @@ namespace Avalonia.Rendering.Composition.Server
return; return;
Root!.RenderedVisuals++; Root!.RenderedVisuals++;
Root!.DebugEvents?.IncrementRenderedVisuals();
var boundsRect = new Rect(new Size(Size.X, Size.Y)); var boundsRect = new Rect(new Size(Size.X, Size.Y));
@ -182,11 +183,24 @@ namespace Avalonia.Rendering.Composition.Server
if (_clipSizeDirty || positionChanged) if (_clipSizeDirty || positionChanged)
{ {
_transformedClipBounds = ClipToBounds Rect? transformedVisualBounds = null;
? new Rect(new Size(Size.X, Size.Y)) Rect? transformedClipBounds = null;
.TransformToAABB(GlobalTransformMatrix)
: null;
if (ClipToBounds)
transformedVisualBounds = new Rect(new Size(Size.X, Size.Y)).TransformToAABB(GlobalTransformMatrix);
if (Clip != null)
transformedClipBounds = Clip.Bounds.TransformToAABB(GlobalTransformMatrix);
if (transformedVisualBounds != null && transformedClipBounds != null)
_transformedClipBounds = transformedVisualBounds.Value.Intersect(transformedClipBounds.Value);
else if (transformedVisualBounds != null)
_transformedClipBounds = transformedVisualBounds;
else if (transformedClipBounds != null)
_transformedClipBounds = transformedClipBounds;
else
_transformedClipBounds = null;
_clipSizeDirty = false; _clipSizeDirty = false;
} }

31
src/Avalonia.Base/Rendering/SceneGraph/GeometryBoundsHelper.cs

@ -1,31 +0,0 @@
using System;
using Avalonia.Media;
using Avalonia.Utilities;
namespace Avalonia.Rendering.SceneGraph;
internal static class GeometryBoundsHelper
{
/// <summary>
/// Calculates the bounds of a given geometry with respect to the pens <see cref="IPen.LineCap"/>
/// </summary>
/// <param name="originalBounds">The calculated bounds without <see cref="IPen.LineCap"/>s</param>
/// <param name="pen">The pen with information about the <see cref="IPen.LineCap"/>s</param>
/// <returns></returns>
public static Rect CalculateBoundsWithLineCaps(this Rect originalBounds, IPen? pen)
{
if (pen is null || MathUtilities.IsZero(pen.Thickness)) return originalBounds;
switch (pen.LineCap)
{
case PenLineCap.Flat:
return originalBounds;
case PenLineCap.Round:
return originalBounds.Inflate(pen.Thickness / 2);
case PenLineCap.Square:
return originalBounds.Inflate(pen.Thickness);
default:
throw new ArgumentOutOfRangeException();
}
}
}

1
src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj

@ -17,7 +17,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

1
src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj

@ -14,7 +14,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<ItemGroup Label="InternalsVisibleTo"> <ItemGroup Label="InternalsVisibleTo">

38
src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml

@ -170,26 +170,28 @@
CornerRadius="{TemplateBinding CornerRadius}"> CornerRadius="{TemplateBinding CornerRadius}">
<Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto"> <Grid Name="PART_ColumnHeaderRoot" ColumnDefinitions="*,Auto">
<Grid Margin="{TemplateBinding Padding}" <Panel Margin="{TemplateBinding Padding}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}"> VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<Grid.ColumnDefinitions> <Grid>
<ColumnDefinition Width="*" /> <Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto" MinWidth="{DynamicResource DataGridSortIconMinWidth}" /> <ColumnDefinition Width="*" />
</Grid.ColumnDefinitions> <ColumnDefinition Width="Auto" MinWidth="{DynamicResource DataGridSortIconMinWidth}" />
</Grid.ColumnDefinitions>
<ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" /> <ContentPresenter Content="{TemplateBinding Content}"
ContentTemplate="{TemplateBinding ContentTemplate}" />
<Path Name="SortIcon"
IsVisible="False" <Path Name="SortIcon"
Grid.Column="1" IsVisible="False"
Height="12" Grid.Column="1"
HorizontalAlignment="Center" Height="12"
VerticalAlignment="Center" HorizontalAlignment="Center"
Fill="{TemplateBinding Foreground}" VerticalAlignment="Center"
Stretch="Uniform" /> Fill="{TemplateBinding Foreground}"
</Grid> Stretch="Uniform" />
</Grid>
</Panel>
<Rectangle Name="VerticalSeparator" <Rectangle Name="VerticalSeparator"
Grid.Column="1" Grid.Column="1"

1
src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj

@ -13,7 +13,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<!--<Import Project="..\..\build\ApiDiff.props" />-->
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

23
src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs

@ -1,5 +1,6 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Automation.Provider;
namespace Avalonia.Automation.Peers namespace Avalonia.Automation.Peers
{ {
@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers
/// <summary> /// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>. /// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
/// </summary> /// </summary>
/// <returns></returns>
public AutomationPeer? GetParent() => GetParentCore(); public AutomationPeer? GetParent() => GetParentCore();
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the root of this <see cref="AutomationPeer"/>'s
/// visual tree.
/// </summary>
public AutomationPeer? GetVisualRoot() => GetVisualRootCore();
/// <summary> /// <summary>
/// Gets a value that indicates whether the element that is associated with this automation /// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus. /// peer currently has keyboard focus.
@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers
return GetAutomationControlTypeCore(); return GetAutomationControlTypeCore();
} }
protected virtual AutomationPeer? GetVisualRootCore()
{
var peer = this;
var parent = peer.GetParent();
while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{
peer = parent;
parent = peer.GetParent();
}
return peer;
}
protected virtual bool IsContentElementOverrideCore() protected virtual bool IsContentElementOverrideCore()
{ {
return IsControlElement() && IsContentElementCore(); return IsControlElement() && IsContentElementCore();

7
src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs

@ -120,6 +120,13 @@ namespace Avalonia.Automation.Peers
return _parent; return _parent;
} }
protected override AutomationPeer? GetVisualRootCore()
{
if (Owner.GetVisualRoot() is Control c)
return CreatePeerForElement(c);
return null;
}
/// <summary> /// <summary>
/// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>. /// Invalidates the peer's children and causes a re-read from <see cref="GetChildrenCore"/>.
/// </summary> /// </summary>

2
src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs

@ -28,7 +28,7 @@ namespace Avalonia.Automation.Peers
if (!_searchedForScrollable) if (!_searchedForScrollable)
{ {
if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable) if (Owner.GetValue(ListBox.ScrollProperty) is Control scrollable)
_scroller = GetOrCreate(scrollable) as IScrollProvider; _scroller = GetOrCreate(scrollable).GetProvider<IScrollProvider>();
_searchedForScrollable = true; _searchedForScrollable = true;
} }

2
src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs

@ -22,7 +22,7 @@ namespace Avalonia.Automation.Peers
if (Owner.Parent is Control parent) if (Owner.Parent is Control parent)
{ {
var parentPeer = GetOrCreate(parent); var parentPeer = GetOrCreate(parent);
return parentPeer as ISelectionProvider; return parentPeer.GetProvider<ISelectionProvider>();
} }
return null; return null;

17
src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs

@ -1,5 +1,6 @@
using System; using System;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization;
using Avalonia.Automation.Provider; using Avalonia.Automation.Provider;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Input; using Avalonia.Input;
@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers
public AutomationPeer? GetPeerFromPoint(Point p) public AutomationPeer? GetPeerFromPoint(Point p)
{ {
var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true); var hit = Owner.GetVisualAt(p)?.FindAncestorOfType<Control>(includeSelf: true);
return hit is object ? GetOrCreate(hit) : null;
if (hit is null)
return null;
var peer = GetOrCreate(hit);
while (peer != this && peer.GetProvider<IEmbeddedRootProvider>() is { } embedded)
{
var embeddedHit = embedded.GetPeerFromPoint(p);
if (embeddedHit is null)
break;
peer = embeddedHit;
}
return peer;
} }
protected void StartTrackingFocus() protected void StartTrackingFocus()

33
src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Automation.Peers;
namespace Avalonia.Automation.Provider
{
/// <summary>
/// Exposure methods and properties to support UI Automation client access to the root of an
/// automation tree hosted by another UI framework.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and can be used
/// to embed an automation tree from a 3rd party UI framework that wishes to use Avalonia's
/// automation support.
/// </remarks>
public interface IEmbeddedRootProvider
{
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged;
}
}

25
src/Avalonia.Controls/Automation/Provider/IRootProvider.cs

@ -4,11 +4,36 @@ using Avalonia.Platform;
namespace Avalonia.Automation.Provider namespace Avalonia.Automation.Provider
{ {
/// <summary>
/// Exposes methods and properties to support UI Automation client access to the root of an
/// automation tree.
/// </summary>
/// <remarks>
/// This interface is implemented by the <see cref="AutomationPeer"/> class, and should only
/// be implemented on true root elements, such as Windows. To embed an automation tree, use
/// <see cref="IEmbeddedRootProvider"/> instead.
/// </remarks>
public interface IRootProvider public interface IRootProvider
{ {
/// <summary>
/// Gets the platform implementation of the TopLevel for the element.
/// </summary>
ITopLevelImpl? PlatformImpl { get; } ITopLevelImpl? PlatformImpl { get; }
/// <summary>
/// Gets the currently focused element.
/// </summary>
AutomationPeer? GetFocus(); AutomationPeer? GetFocus();
/// <summary>
/// Gets the element at the specified point, expressed in top-level coordinates.
/// </summary>
/// <param name="p">The point.</param>
AutomationPeer? GetPeerFromPoint(Point p); AutomationPeer? GetPeerFromPoint(Point p);
/// <summary>
/// Raised by the automation peer when the focus changes.
/// </summary>
event EventHandler? FocusChanged; event EventHandler? FocusChanged;
} }
} }

1
src/Avalonia.Controls/Avalonia.Controls.csproj

@ -6,7 +6,6 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" /> <ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

15
src/Avalonia.Controls/ContentControl.cs

@ -40,11 +40,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty = public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
AvaloniaProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment)); AvaloniaProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
static ContentControl()
{
ContentProperty.Changed.AddClassHandler<ContentControl>((x, e) => x.ContentChanged(e));
}
/// <summary> /// <summary>
/// Gets or sets the content to display. /// Gets or sets the content to display.
/// </summary> /// </summary>
@ -100,6 +95,16 @@ namespace Avalonia.Controls
{ {
return RegisterContentPresenter(presenter); return RegisterContentPresenter(presenter);
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ContentProperty)
{
ContentChanged(change);
}
}
/// <summary> /// <summary>
/// Called when an <see cref="ContentPresenter"/> is registered with the control. /// Called when an <see cref="ContentPresenter"/> is registered with the control.

31
src/Avalonia.Controls/ContextMenu.cs

@ -114,7 +114,6 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
static ContextMenu() static ContextMenu()
{ {
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer); PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged); ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control); AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
@ -216,18 +215,23 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu) if (e.OldValue is ContextMenu oldMenu)
{ {
control.ContextRequested -= ControlContextRequested; control.ContextRequested -= ControlContextRequested;
control.AttachedToVisualTree -= ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree -= ControlDetachedFromVisualTree; control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
oldMenu._attachedControls?.Remove(control); oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null); ((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
} }
if (e.NewValue is ContextMenu newMenu) if (e.NewValue is ContextMenu)
{ {
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.ContextRequested += ControlContextRequested; control.ContextRequested += ControlContextRequested;
control.AttachedToVisualTree += ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree += ControlDetachedFromVisualTree; control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
} }
if (control.IsAttachedToVisualTree)
{
AttachControlToContextMenu(control);
}
} }
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -428,11 +432,25 @@ namespace Avalonia.Controls
e.Handled = true; e.Handled = true;
} }
} }
private static void ControlOnAttachedToVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
AttachControlToContextMenu(sender);
}
private static void AttachControlToContextMenu(object? sender)
{
if (sender is Control { ContextMenu: { } contextMenu } control)
{
contextMenu._attachedControls ??= new List<Control>();
contextMenu._attachedControls.Add(control);
}
}
private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e) private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{ {
if (sender is Control control if (sender is Control { ContextMenu: { } contextMenu } control)
&& control.ContextMenu is ContextMenu contextMenu)
{ {
if (contextMenu._popup?.Parent == control) if (contextMenu._popup?.Parent == control)
{ {
@ -440,6 +458,7 @@ namespace Avalonia.Controls
} }
contextMenu.Close(); contextMenu.Close();
contextMenu._attachedControls?.Remove(control);
} }
} }

26
src/Avalonia.Controls/Documents/Inline.cs

@ -10,12 +10,14 @@ namespace Avalonia.Controls.Documents
/// </summary> /// </summary>
public abstract class Inline : TextElement public abstract class Inline : TextElement
{ {
// TODO12: change the field type to an AttachedProperty for consistency (breaking change)
/// <summary> /// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property. /// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary> /// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<Inline, TextDecorationCollection?>( AvaloniaProperty.RegisterAttached<Inline, Inline, TextDecorationCollection?>(
nameof(TextDecorations)); nameof(TextDecorations),
inherits: true);
/// <summary> /// <summary>
/// AvaloniaProperty for <see cref="BaselineAlignment" /> property. /// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
@ -43,7 +45,27 @@ namespace Avalonia.Controls.Documents
get { return GetValue(BaselineAlignmentProperty); } get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); } set { SetValue(BaselineAlignmentProperty, value); }
} }
/// <summary>
/// Gets the value of the attached <see cref="TextDecorationsProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <returns>The font style.</returns>
public static TextDecorationCollection? GetTextDecorations(Control control)
{
return control.GetValue(TextDecorationsProperty);
}
/// <summary>
/// Sets the value of the attached <see cref="TextDecorationsProperty"/> on a control.
/// </summary>
/// <param name="control">The control.</param>
/// <param name="value">The property value to set.</param>
public static void SetTextDecorations(Control control, TextDecorationCollection? value)
{
control.SetValue(TextDecorationsProperty, value);
}
internal abstract void BuildTextRun(IList<TextRun> textRuns); internal abstract void BuildTextRun(IList<TextRun> textRuns);
internal abstract void AppendText(StringBuilder stringBuilder); internal abstract void AppendText(StringBuilder stringBuilder);

28
src/Avalonia.Controls/ItemsControl.cs

@ -67,6 +67,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty = public static readonly StyledProperty<IBinding?> DisplayMemberBindingProperty =
AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding)); AvaloniaProperty.Register<ItemsControl, IBinding?>(nameof(DisplayMemberBinding));
private static readonly AttachedProperty<ControlTheme?> AppliedItemContainerTheme =
AvaloniaProperty.RegisterAttached<ItemsControl, Control, ControlTheme?>("AppliedItemContainerTheme");
/// <summary> /// <summary>
/// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item. /// Gets or sets the <see cref="IBinding"/> to use for binding to the display member of each item.
/// </summary> /// </summary>
@ -663,13 +666,26 @@ namespace Avalonia.Controls
internal void PrepareItemContainer(Control container, object? item, int index) internal void PrepareItemContainer(Control container, object? item, int index)
{ {
var itemContainerTheme = ItemContainerTheme; // If the container has no theme set, or we've already applied our ItemContainerTheme
// (and it hasn't changed since) then we're in control of the container's Theme and may
if (itemContainerTheme is not null && // need to update it.
!container.IsSet(ThemeProperty) && if (!container.IsSet(ThemeProperty) || container.GetValue(AppliedItemContainerTheme) == container.Theme)
StyledElement.GetStyleKey(container) == itemContainerTheme.TargetType)
{ {
container.Theme = itemContainerTheme; var itemContainerTheme = ItemContainerTheme;
if (itemContainerTheme?.TargetType?.IsAssignableFrom(GetStyleKey(container)) == true)
{
// We have an ItemContainerTheme and it matches the container. Set the Theme
// property, and mark the container as having had ItemContainerTheme applied.
container.SetCurrentValue(ThemeProperty, itemContainerTheme);
container.SetValue(AppliedItemContainerTheme, itemContainerTheme);
}
else
{
// Otherwise clear the theme and the AppliedItemContainerTheme property.
container.ClearValue(ThemeProperty);
container.ClearValue(AppliedItemContainerTheme);
}
} }
if (item is not Control) if (item is not Control)

11
src/Avalonia.Controls/Primitives/AccessText.cs

@ -3,6 +3,7 @@ using Avalonia.Input;
using Avalonia.Reactive; using Avalonia.Reactive;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using System;
namespace Avalonia.Controls.Primitives namespace Avalonia.Controls.Primitives
{ {
@ -68,11 +69,15 @@ namespace Avalonia.Controls.Primitives
if (underscore != -1 && ShowAccessKey) if (underscore != -1 && ShowAccessKey)
{ {
var rect = TextLayout!.HitTestTextPosition(underscore); var rect = TextLayout!.HitTestTextPosition(underscore);
var offset = new Vector(0, -1.5);
var x1 = Math.Round(rect.Left, MidpointRounding.AwayFromZero);
var x2 = Math.Round(rect.Right, MidpointRounding.AwayFromZero);
var y = Math.Round(rect.Bottom, MidpointRounding.AwayFromZero) - 1.5;
context.DrawLine( context.DrawLine(
new Pen(Foreground, 1), new Pen(Foreground, 1),
rect.BottomLeft + offset, new Point(x1, y),
rect.BottomRight + offset); new Point(x2, y));
} }
} }

2
src/Avalonia.Controls/Primitives/Track.cs

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e)); ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e));
IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); IncreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e)); DecreaseButtonProperty.Changed.AddClassHandler<Track>((x, e) => x.ButtonChanged(e));
AffectsArrange<Track>(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty); AffectsArrange<Track>(IsDirectionReversedProperty, MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
} }
public Track() public Track()

5
src/Avalonia.Controls/Shapes/Polygon.cs

@ -1,5 +1,6 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Data;
namespace Avalonia.Controls.Shapes namespace Avalonia.Controls.Shapes
{ {
@ -15,9 +16,9 @@ namespace Avalonia.Controls.Shapes
public Polygon() public Polygon()
{ {
Points = new Points(); SetValue(PointsProperty, new Points(), BindingPriority.Template);
} }
public IList<Point> Points public IList<Point> Points
{ {
get => GetValue(PointsProperty); get => GetValue(PointsProperty);

5
src/Avalonia.Controls/Shapes/Polyline.cs

@ -1,9 +1,10 @@
using System.Collections.Generic; using System.Collections.Generic;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Data;
namespace Avalonia.Controls.Shapes namespace Avalonia.Controls.Shapes
{ {
public class Polyline: Shape public class Polyline : Shape
{ {
public static readonly StyledProperty<IList<Point>> PointsProperty = public static readonly StyledProperty<IList<Point>> PointsProperty =
AvaloniaProperty.Register<Polyline, IList<Point>>("Points"); AvaloniaProperty.Register<Polyline, IList<Point>>("Points");
@ -16,7 +17,7 @@ namespace Avalonia.Controls.Shapes
public Polyline() public Polyline()
{ {
Points = new Points(); SetValue(PointsProperty, new Points(), BindingPriority.Template);
} }
public IList<Point> Points public IList<Point> Points

4
src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs

@ -2,6 +2,7 @@
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Styling; using Avalonia.Styling;
@ -36,8 +37,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="IsChecked"/> property. /// Defines the <see cref="IsChecked"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsCheckedProperty = public static readonly StyledProperty<bool> IsCheckedProperty =
AvaloniaProperty.Register<ToggleSplitButton, bool>( AvaloniaProperty.Register<ToggleSplitButton, bool>(nameof(IsChecked), false, defaultBindingMode: BindingMode.TwoWay);
nameof(IsChecked));
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="ToggleSplitButton"/> class. /// Initializes a new instance of the <see cref="ToggleSplitButton"/> class.

2
src/Avalonia.Controls/TextBlock.cs

@ -135,7 +135,7 @@ namespace Avalonia.Controls
/// Defines the <see cref="TextDecorations"/> property. /// Defines the <see cref="TextDecorations"/> property.
/// </summary> /// </summary>
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations)); Inline.TextDecorationsProperty.AddOwner<TextBlock>();
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.

34
src/Avalonia.Controls/TextBox.cs

@ -1879,6 +1879,37 @@ namespace Avalonia.Controls
return text.Substring(start, end - start); return text.Substring(start, end - start);
} }
/// <summary>
/// Returns the sum of any vertical whitespace added between the <see cref="ScrollViewer"/> and <see cref="TextPresenter"/> in the control template.
/// </summary>
/// <returns>The total vertical whitespace.</returns>
private double GetVerticalSpaceBetweenScrollViewerAndPresenter()
{
var verticalSpace = 0.0;
if (_presenter != null)
{
Visual? visual = _presenter;
while ((visual != null) && (visual != this))
{
if (visual == _scrollViewer)
{
// ScrollViewer is a stopping point and should only include the Padding
verticalSpace += _scrollViewer.Padding.Top + _scrollViewer.Padding.Bottom;
break;
}
var margin = visual.GetValue<Thickness>(Layoutable.MarginProperty);
var padding = visual.GetValue<Thickness>(Decorator.PaddingProperty);
verticalSpace += margin.Top + padding.Top + padding.Bottom + margin.Bottom;
visual = visual.VisualParent;
}
}
return verticalSpace;
}
/// <summary> /// <summary>
/// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events. /// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events.
/// </summary> /// </summary>
@ -2032,8 +2063,9 @@ namespace Avalonia.Controls
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default); var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties); var textLayout = new TextLayout(new MaxLinesTextSource(MaxLines), paragraphProperties);
var verticalSpace = GetVerticalSpaceBetweenScrollViewerAndPresenter();
maxHeight = Math.Ceiling(textLayout.Height); maxHeight = Math.Ceiling(textLayout.Height + verticalSpace);
} }
_scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight); _scrollViewer.SetCurrentValue(MaxHeightProperty, maxHeight);

13
src/Avalonia.Controls/TransitioningContentControl.cs

@ -79,8 +79,12 @@ public class TransitioningContentControl : ContentControl
protected override bool RegisterContentPresenter(ContentPresenter presenter) protected override bool RegisterContentPresenter(ContentPresenter presenter)
{ {
if (!base.RegisterContentPresenter(presenter) && if (base.RegisterContentPresenter(presenter))
presenter is ContentPresenter p && {
return true;
}
if (presenter is ContentPresenter p &&
p.Name == "PART_ContentPresenter2") p.Name == "PART_ContentPresenter2")
{ {
_presenter2 = p; _presenter2 = p;
@ -94,12 +98,13 @@ public class TransitioningContentControl : ContentControl
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {
base.OnPropertyChanged(change);
if (change.Property == ContentProperty) if (change.Property == ContentProperty)
{ {
UpdateContent(true); UpdateContent(true);
return;
} }
base.OnPropertyChanged(change);
} }
private void UpdateContent(bool withTransition) private void UpdateContent(bool withTransition)

1
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -19,5 +19,4 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\Avalonia.Controls\Avalonia.Controls.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
</Project> </Project>

1
src/Avalonia.Desktop/Avalonia.Desktop.csproj

@ -12,7 +12,6 @@
<ProjectReference Include="../Avalonia.X11/Avalonia.X11.csproj" /> <ProjectReference Include="../Avalonia.X11/Avalonia.X11.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj

@ -24,7 +24,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\EmbedXaml.props" /> <Import Project="..\..\build\EmbedXaml.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

1
src/Avalonia.Dialogs/Avalonia.Dialogs.csproj

@ -19,7 +19,6 @@
<InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" /> <InternalsVisibleTo Include="ControlCatalog, PublicKey=$(AvaloniaPublicKey)" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
</Project> </Project>

2
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -108,7 +108,7 @@ namespace Avalonia.Native
.Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings())) .Bind<IPlatformSettings>().ToConstant(new NativePlatformSettings(_factory.CreatePlatformSettings()))
.Bind<IWindowingPlatform>().ToConstant(this) .Bind<IWindowingPlatform>().ToConstant(this)
.Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard())) .Bind<IClipboard>().ToConstant(new ClipboardImpl(_factory.CreateClipboard()))
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60)) .Bind<IRenderTimer>().ToConstant(new AvaloniaNativeRenderTimer(_factory.CreatePlatformRenderTimer()))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider()) .Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory)) .Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
.Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform) .Bind<IPlatformLifetimeEventsImpl>().ToConstant(applicationPlatform)

58
src/Avalonia.Native/AvaloniaNativeRenderTimer.cs

@ -0,0 +1,58 @@
using System;
using System.Diagnostics;
using Avalonia.Native.Interop;
using Avalonia.Rendering;
#nullable enable
namespace Avalonia.Native;
internal sealed class AvaloniaNativeRenderTimer : NativeCallbackBase, IRenderTimer, IAvnActionCallback
{
private readonly IAvnPlatformRenderTimer _platformRenderTimer;
private readonly Stopwatch _stopwatch;
private Action<TimeSpan>? _tick;
private int _subscriberCount;
private bool registered;
public AvaloniaNativeRenderTimer(IAvnPlatformRenderTimer platformRenderTimer)
{
_platformRenderTimer = platformRenderTimer;
_stopwatch = Stopwatch.StartNew();
}
public event Action<TimeSpan> Tick
{
add
{
_tick += value;
if (!registered)
{
registered = true;
_platformRenderTimer.RegisterTick(this);
}
if (_subscriberCount++ == 0)
{
_platformRenderTimer.Start();
}
}
remove
{
if (--_subscriberCount == 0)
{
_platformRenderTimer.Stop();
}
_tick -= value;
}
}
public bool RunsInBackground => _platformRenderTimer.RunsInBackground().FromComBool();
public void Run()
{
_tick?.Invoke(_stopwatch.Elapsed);
}
}

121
src/Avalonia.Native/AvnAutomationPeer.cs

@ -22,8 +22,8 @@ namespace Avalonia.Native
{ {
_inner = inner; _inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged(); _inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
if (inner is WindowBaseAutomationPeer window) if (inner is IRootProvider root)
window.FocusChanged += (_, _) => Node?.FocusChanged(); root.FocusChanged += (_, _) => Node?.FocusChanged();
} }
~AvnAutomationPeer() => Node?.Dispose(); ~AvnAutomationPeer() => Node?.Dispose();
@ -39,6 +39,7 @@ namespace Avalonia.Native
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy()); public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString(); public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent()); public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool(); public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool(); public int IsContentElement() => _inner.IsContentElement().AsComBool();
@ -48,6 +49,13 @@ namespace Avalonia.Native
public void SetFocus() => _inner.SetFocus(); public void SetFocus() => _inner.SetFocus();
public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool(); public int ShowContextMenu() => _inner.ShowContextMenu().AsComBool();
public void SetNode(IAvnAutomationNode node)
{
if (Node is not null)
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
Node = node;
}
public IAvnAutomationPeer? RootPeer public IAvnAutomationPeer? RootPeer
{ {
get get
@ -55,7 +63,7 @@ namespace Avalonia.Native
var peer = _inner; var peer = _inner;
var parent = peer.GetParent(); var parent = peer.GetParent();
while (peer is not IRootProvider && parent is not null) while (peer.GetProvider<IRootProvider>() is null && parent is not null)
{ {
peer = parent; peer = parent;
parent = peer.GetParent(); parent = peer.GetParent();
@ -65,26 +73,23 @@ namespace Avalonia.Native
} }
} }
public void SetNode(IAvnAutomationNode node) private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider<IEmbeddedRootProvider>();
{ private IExpandCollapseProvider ExpandCollapseProvider => GetProvider<IExpandCollapseProvider>();
if (Node is not null) private IInvokeProvider InvokeProvider => GetProvider<IInvokeProvider>();
throw new InvalidOperationException("The AvnAutomationPeer already has a node."); private IRangeValueProvider RangeValueProvider => GetProvider<IRangeValueProvider>();
Node = node; private IRootProvider RootProvider => GetProvider<IRootProvider>();
} private ISelectionItemProvider SelectionItemProvider => GetProvider<ISelectionItemProvider>();
private IToggleProvider ToggleProvider => GetProvider<IToggleProvider>();
public int IsRootProvider() => (_inner is IRootProvider).AsComBool(); private IValueProvider ValueProvider => GetProvider<IValueProvider>();
public IAvnWindowBase RootProvider_GetWindow() public int IsRootProvider() => IsProvider<IRootProvider>();
{
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner; public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native;
return ((WindowBaseImpl)window.PlatformImpl!).Native; public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus());
}
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point) public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
{ {
var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint()); var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null) if (result is null)
return null; return null;
@ -103,46 +108,80 @@ namespace Avalonia.Native
return Wrap(result); return Wrap(result);
} }
public int IsExpandCollapseProvider() => (_inner is IExpandCollapseProvider).AsComBool();
public int ExpandCollapseProvider_GetIsExpanded() => ((IExpandCollapseProvider)_inner).ExpandCollapseState switch public int IsEmbeddedRootProvider() => IsProvider<IEmbeddedRootProvider>();
public IAvnAutomationPeer? EmbeddedRootProvider_GetFocus() => Wrap(EmbeddedRootProvider.GetFocus());
public IAvnAutomationPeer? EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point)
{
var result = EmbeddedRootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
// The OSX accessibility APIs expect non-ignored elements when hit-testing.
while (!result.IsControlElement())
{
var parent = result.GetParent();
if (parent is not null)
result = parent;
else
break;
}
return Wrap(result);
}
public int IsExpandCollapseProvider() => IsProvider<IExpandCollapseProvider>();
public int ExpandCollapseProvider_GetIsExpanded() => ExpandCollapseProvider.ExpandCollapseState switch
{ {
ExpandCollapseState.Expanded => 1, ExpandCollapseState.Expanded => 1,
ExpandCollapseState.PartiallyExpanded => 1, ExpandCollapseState.PartiallyExpanded => 1,
_ => 0, _ => 0,
}; };
public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool(); public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand(); public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand();
public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse(); public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse();
public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool(); public int IsInvokeProvider() => IsProvider<IInvokeProvider>();
public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke(); public void InvokeProvider_Invoke() => InvokeProvider.Invoke();
public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool(); public int IsRangeValueProvider() => IsProvider<IRangeValueProvider>();
public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value; public double RangeValueProvider_GetValue() => RangeValueProvider.Value;
public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum; public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum;
public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum; public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum;
public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange; public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange;
public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange; public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange;
public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value); public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value);
public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool(); public int IsSelectionItemProvider() => IsProvider<ISelectionItemProvider>();
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool(); public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool(); public int IsToggleProvider() => IsProvider<IToggleProvider>();
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState; public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle(); public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
public int IsValueProvider() => (_inner is IValueProvider).AsComBool(); public int IsValueProvider() => IsProvider<IValueProvider>();
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString(); public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value); public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value);
[return: NotNullIfNotNull("peer")] [return: NotNullIfNotNull("peer")]
public static AvnAutomationPeer? Wrap(AutomationPeer? peer) public static AvnAutomationPeer? Wrap(AutomationPeer? peer)
{ {
return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer)); return peer is null ? null : s_wrappers.GetValue(peer, x => new(peer));
} }
private T GetProvider<T>()
{
return _inner.GetProvider<T>() ?? throw new InvalidOperationException(
$"The peer {_inner} does not implement {typeof(T)}.");
}
private int IsProvider<T>() => (_inner.GetProvider<T>() is not null).AsComBool();
} }
internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray internal class AvnAutomationPeerArray : NativeCallbackBase, IAvnAutomationPeerArray

17
src/Avalonia.Native/avn.idl

@ -507,6 +507,7 @@ interface IAvaloniaNativeFactory : IUnknown
HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv); HRESULT CreateApplicationCommands(IAvnApplicationCommands** ppv);
HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv); HRESULT CreatePlatformSettings(IAvnPlatformSettings** ppv);
HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv); HRESULT CreatePlatformBehaviorInhibition(IAvnPlatformBehaviorInhibition** ppv);
HRESULT CreatePlatformRenderTimer(IAvnPlatformRenderTimer** ppv);
} }
[uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)] [uuid(233e094f-9b9f-44a3-9a6e-6948bbdd9fb1)]
@ -921,6 +922,7 @@ interface IAvnAutomationPeer : IUnknown
IAvnAutomationPeer* GetLabeledBy(); IAvnAutomationPeer* GetLabeledBy();
IAvnString* GetName(); IAvnString* GetName();
IAvnAutomationPeer* GetParent(); IAvnAutomationPeer* GetParent();
IAvnAutomationPeer* GetVisualRoot();
bool HasKeyboardFocus(); bool HasKeyboardFocus();
bool IsContentElement(); bool IsContentElement();
bool IsControlElement(); bool IsControlElement();
@ -935,7 +937,11 @@ interface IAvnAutomationPeer : IUnknown
IAvnWindowBase* RootProvider_GetWindow(); IAvnWindowBase* RootProvider_GetWindow();
IAvnAutomationPeer* RootProvider_GetFocus(); IAvnAutomationPeer* RootProvider_GetFocus();
IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point); IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point);
bool IsEmbeddedRootProvider();
IAvnAutomationPeer* EmbeddedRootProvider_GetFocus();
IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point);
bool IsExpandCollapseProvider(); bool IsExpandCollapseProvider();
bool ExpandCollapseProvider_GetIsExpanded(); bool ExpandCollapseProvider_GetIsExpanded();
bool ExpandCollapseProvider_GetShowsMenu(); bool ExpandCollapseProvider_GetShowsMenu();
@ -994,3 +1000,12 @@ interface IAvnPlatformBehaviorInhibition : IUnknown
{ {
void SetInhibitAppSleep(bool inhibitAppSleep, char* reason); void SetInhibitAppSleep(bool inhibitAppSleep, char* reason);
} }
[uuid(22edf20d-5803-2d3f-9247-b4842e5e9322)]
interface IAvnPlatformRenderTimer : IUnknown
{
HRESULT RegisterTick(IAvnActionCallback* callback);
void Start();
void Stop();
bool RunsInBackground();
}

1
src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj

@ -9,7 +9,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\Rx.props" /> <Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" /> <Import Project="..\..\build\ReactiveUI.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />

3
src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj

@ -10,6 +10,5 @@
<Compile Include="..\Avalonia.Base\Input\Key.cs" /> <Compile Include="..\Avalonia.Base\Input\Key.cs" />
<Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" /> <Compile Include="..\Avalonia.Base\Compatibility\TrimmingAttributes.cs" Visible="False" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
</Project> </Project>

1
src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj

@ -11,7 +11,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

1
src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj

@ -11,7 +11,6 @@
</ItemGroup> </ItemGroup>
<Import Project="..\..\build\NullableEnable.props" /> <Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\BuildTargets.targets" /> <Import Project="..\..\build\BuildTargets.targets" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" /> <Import Project="..\..\build\TrimmingEnable.props" />
<Import Project="..\..\build\DevAnalyzers.props" /> <Import Project="..\..\build\DevAnalyzers.props" />
</Project> </Project>

3
src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj

@ -12,8 +12,7 @@
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> <ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj

@ -10,7 +10,6 @@
<PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" /> <PackageReference Include="Quamotion.RemoteViewing" Version="1.1.21" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj

@ -13,7 +13,6 @@
<ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" /> <ProjectReference Include="..\Avalonia.Headless\Avalonia.Headless.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
</Project> </Project>

1
src/Headless/Avalonia.Headless/Avalonia.Headless.csproj

@ -7,7 +7,6 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" /> <ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj

@ -65,7 +65,6 @@
<ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" /> <ProjectReference Include="..\..\Avalonia.Controls\Avalonia.Controls.csproj" />
<ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj" /> <ProjectReference Include="..\Avalonia.Markup\Avalonia.Markup.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />

1
src/Markup/Avalonia.Markup/Avalonia.Markup.csproj

@ -13,7 +13,6 @@
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" /> <ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />
</ItemGroup> </ItemGroup>
<Import Project="..\..\..\build\System.Memory.props" /> <Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\NullableEnable.props" /> <Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" /> <Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" /> <Import Project="..\..\..\build\TrimmingEnable.props" />

55
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -189,7 +189,8 @@ namespace Avalonia.Skia
var d = destRect.ToSKRect(); var d = destRect.ToSKRect();
var paint = SKPaintCache.Shared.Get(); var paint = SKPaintCache.Shared.Get();
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * (_useOpacitySaveLayer ? 1 : _currentOpacity)));
paint.Color = new SKColor(255, 255, 255, (byte)(255 * opacity * _currentOpacity));
paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality(); paint.FilterQuality = RenderOptions.BitmapInterpolationMode.ToSKFilterQuality();
paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode(); paint.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
@ -375,7 +376,7 @@ namespace Avalonia.Skia
{ {
if (boxShadow != default && !boxShadow.IsInset) if (boxShadow != default && !boxShadow.IsInset)
{ {
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{ {
var spread = (float)boxShadow.Spread; var spread = (float)boxShadow.Spread;
if (boxShadow.IsInset) if (boxShadow.IsInset)
@ -432,7 +433,7 @@ namespace Avalonia.Skia
{ {
if (boxShadow != default && boxShadow.IsInset) if (boxShadow != default && boxShadow.IsInset)
{ {
using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _useOpacitySaveLayer ? 1 : _currentOpacity)) using (var shadow = BoxShadowFilter.Create(_boxShadowPaint, boxShadow, _currentOpacity))
{ {
var spread = (float)boxShadow.Spread; var spread = (float)boxShadow.Spread;
var offsetX = (float)boxShadow.OffsetX; var offsetX = (float)boxShadow.OffsetX;
@ -592,8 +593,16 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
if(_useOpacitySaveLayer) _opacityStack.Push(_currentOpacity);
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{ {
opacity = _currentOpacity * opacity; //Take current multiplied opacity
_currentOpacity = 1; //Opacity is applied via layering
if (bounds.HasValue) if (bounds.HasValue)
{ {
var rect = bounds.Value.ToSKRect(); var rect = bounds.Value.ToSKRect();
@ -606,7 +615,6 @@ namespace Avalonia.Skia
} }
else else
{ {
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity; _currentOpacity *= opacity;
} }
} }
@ -616,14 +624,14 @@ namespace Avalonia.Skia
{ {
CheckLease(); CheckLease();
if(_useOpacitySaveLayer) var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{ {
Canvas.Restore(); Canvas.Restore();
} }
else
{ _currentOpacity = _opacityStack.Pop();
_currentOpacity = _opacityStack.Pop();
}
} }
/// <inheritdoc /> /// <inheritdoc />
@ -1239,31 +1247,8 @@ namespace Avalonia.Skia
// https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots // https://docs.microsoft.com/en-us/xamarin/xamarin-forms/user-interface/graphics/skiasharp/paths/dots
// TODO: Still something is off, dashes are now present, but don't look the same as D2D ones. // TODO: Still something is off, dashes are now present, but don't look the same as D2D ones.
switch (pen.LineCap) paint.StrokeCap = pen.LineCap.ToSKStrokeCap();
{ paint.StrokeJoin = pen.LineJoin.ToSKStrokeJoin();
case PenLineCap.Round:
paint.StrokeCap = SKStrokeCap.Round;
break;
case PenLineCap.Square:
paint.StrokeCap = SKStrokeCap.Square;
break;
default:
paint.StrokeCap = SKStrokeCap.Butt;
break;
}
switch (pen.LineJoin)
{
case PenLineJoin.Miter:
paint.StrokeJoin = SKStrokeJoin.Miter;
break;
case PenLineJoin.Round:
paint.StrokeJoin = SKStrokeJoin.Round;
break;
default:
paint.StrokeJoin = SKStrokeJoin.Bevel;
break;
}
paint.StrokeMiter = (float) pen.MiterLimit; paint.StrokeMiter = (float) pen.MiterLimit;

151
src/Skia/Avalonia.Skia/GeometryImpl.cs

@ -43,46 +43,11 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public bool StrokeContains(IPen? pen, Point point) public bool StrokeContains(IPen? pen, Point point)
{ {
// Skia requires to compute stroke path to check for point containment. _pathCache.UpdateIfNeeded(StrokePath, pen);
// Due to that we are caching using stroke width.
// Usually this function is being called with same stroke width per path, so this saves a lot of Skia traffic.
var strokeWidth = (float)(pen?.Thickness ?? 0); return PathContainsCore(_pathCache.ExpandedPath, point);
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return PathContainsCore(_pathCache.CachedStrokePath, point);
}
/// <summary>
/// Update path cache for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width.</param>
private void UpdatePathCache(float strokeWidth)
{
var strokePath = new SKPath();
// For stroke widths close to 0 simply use empty path. Render bounds are cached from fill path.
if (Math.Abs(strokeWidth) < float.Epsilon)
{
_pathCache.Cache(strokePath, strokeWidth, Bounds);
}
else
{
var paint = SKPaintCache.Shared.Get();
paint.IsStroke = true;
paint.StrokeWidth = strokeWidth;
paint.GetFillPath(StrokePath, strokePath);
SKPaintCache.Shared.ReturnReset(paint);
_pathCache.Cache(strokePath, strokeWidth, strokePath.TightBounds.ToAvaloniaRect());
}
} }
/// <summary> /// <summary>
/// Check Skia path if it contains a point. /// Check Skia path if it contains a point.
/// </summary> /// </summary>
@ -106,14 +71,8 @@ namespace Avalonia.Skia
/// <inheritdoc /> /// <inheritdoc />
public Rect GetRenderBounds(IPen? pen) public Rect GetRenderBounds(IPen? pen)
{ {
var strokeWidth = (float)(pen?.Thickness ?? 0); _pathCache.UpdateIfNeeded(StrokePath, pen);
return _pathCache.RenderBounds;
if (!_pathCache.HasCacheFor(strokeWidth))
{
UpdatePathCache(strokeWidth);
}
return _pathCache.CachedGeometryRenderBounds;
} }
/// <inheritdoc /> /// <inheritdoc />
@ -180,66 +139,70 @@ namespace Avalonia.Skia
/// </summary> /// </summary>
protected void InvalidateCaches() protected void InvalidateCaches()
{ {
_pathCache.Invalidate(); _pathCache.Dispose();
_pathCache = default;
} }
private struct PathCache private struct PathCache
{ {
private float _cachedStrokeWidth; private double _width, _miterLimit;
private PenLineCap _cap;
/// <summary> private PenLineJoin _join;
/// Tolerance for two stroke widths to be deemed equal private SKPath? _path, _cachedFor;
/// </summary> private Rect? _renderBounds;
public const float Tolerance = float.Epsilon; private static readonly SKPath s_emptyPath = new();
/// <summary>
/// Cached contour path. public Rect RenderBounds => _renderBounds ??= (_path ?? _cachedFor ?? s_emptyPath).Bounds.ToAvaloniaRect();
/// </summary> public SKPath ExpandedPath => _path ?? s_emptyPath;
public SKPath? CachedStrokePath { get; private set; }
public void UpdateIfNeeded(SKPath? strokePath, IPen? pen)
/// <summary>
/// Cached geometry render bounds.
/// </summary>
public Rect CachedGeometryRenderBounds { get; private set; }
/// <summary>
/// Is cached valid for given stroke width.
/// </summary>
/// <param name="strokeWidth">Stroke width to check.</param>
/// <returns>True, if CachedStrokePath can be used for given stroke width.</returns>
public bool HasCacheFor(float strokeWidth)
{ {
return CachedStrokePath != null && Math.Abs(_cachedStrokeWidth - strokeWidth) < Tolerance; var strokeWidth = pen?.Thickness ?? 0;
} var miterLimit = pen?.MiterLimit ?? 0;
var cap = pen?.LineCap ?? default;
/// <summary> var join = pen?.LineJoin ?? default;
/// Cache path for given stroke width. Takes ownership of a passed path.
/// </summary> if (_cachedFor == strokePath
/// <param name="path">Path to cache.</param> && _path != null
/// <param name="strokeWidth">Stroke width to cache.</param> && cap == _cap
/// <param name="geometryRenderBounds">Render bounds to use.</param> && join == _join
public void Cache(SKPath path, float strokeWidth, Rect geometryRenderBounds) && Math.Abs(_width - strokeWidth) < float.Epsilon
{ && (join != PenLineJoin.Miter || Math.Abs(_miterLimit - miterLimit) > float.Epsilon))
if (CachedStrokePath != path) // We are up to date
return;
_renderBounds = null;
_cachedFor = strokePath;
_width = strokeWidth;
_cap = cap;
_join = join;
_miterLimit = miterLimit;
if (strokePath == null || Math.Abs(strokeWidth) < float.Epsilon)
{ {
CachedStrokePath?.Dispose(); _path = null;
return;
} }
CachedStrokePath = path; var paint = SKPaintCache.Shared.Get();
CachedGeometryRenderBounds = geometryRenderBounds; paint.IsStroke = true;
_cachedStrokeWidth = strokeWidth; paint.StrokeWidth = (float)_width;
paint.StrokeCap = cap.ToSKStrokeCap();
paint.StrokeJoin = join.ToSKStrokeJoin();
paint.StrokeMiter = (float)miterLimit;
_path = new SKPath();
paint.GetFillPath(strokePath, _path);
SKPaintCache.Shared.ReturnReset(paint);
} }
/// <summary> public void Dispose()
/// Invalidate cache state.
/// </summary>
public void Invalidate()
{ {
CachedStrokePath?.Dispose(); _path?.Dispose();
CachedStrokePath = null; _path = null;
CachedGeometryRenderBounds = default;
_cachedStrokeWidth = default;
} }
} }
} }
} }

20
src/Skia/Avalonia.Skia/SkiaSharpExtensions.cs

@ -196,6 +196,26 @@ namespace Avalonia.Skia
} }
} }
public static SKStrokeCap ToSKStrokeCap(this PenLineCap cap)
{
return cap switch
{
PenLineCap.Round => SKStrokeCap.Round,
PenLineCap.Square => SKStrokeCap.Square,
_ => SKStrokeCap.Butt
};
}
public static SKStrokeJoin ToSKStrokeJoin(this PenLineJoin join)
{
return join switch
{
PenLineJoin.Bevel => SKStrokeJoin.Bevel,
PenLineJoin.Round => SKStrokeJoin.Round,
_ => SKStrokeJoin.Miter
};
}
public static TextAlignment ToAvalonia(this SKTextAlign a) public static TextAlignment ToAvalonia(this SKTextAlign a)
{ {
switch (a) switch (a)

19
src/Windows/Avalonia.Direct2D1/Media/GeometryImpl.cs

@ -1,6 +1,10 @@
using System;
using Avalonia.Logging; using Avalonia.Logging;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using SharpDX.Direct2D1; using SharpDX.Direct2D1;
using Geometry = SharpDX.Direct2D1.Geometry;
using PathGeometry = SharpDX.Direct2D1.PathGeometry;
namespace Avalonia.Direct2D1.Media namespace Avalonia.Direct2D1.Media
{ {
@ -27,7 +31,20 @@ namespace Avalonia.Direct2D1.Media
/// <inheritdoc/> /// <inheritdoc/>
public Rect GetRenderBounds(Avalonia.Media.IPen pen) public Rect GetRenderBounds(Avalonia.Media.IPen pen)
{ {
return Geometry.GetWidenedBounds((float)(pen?.Thickness ?? 0)).ToAvalonia(); if (pen == null || Math.Abs(pen.Thickness) < float.Epsilon)
return Geometry.GetBounds().ToAvalonia();
var originalBounds = Geometry.GetWidenedBounds((float)pen.Thickness).ToAvalonia();
switch (pen.LineCap)
{
case PenLineCap.Flat:
return originalBounds;
case PenLineCap.Round:
return originalBounds.Inflate(pen.Thickness / 2);
case PenLineCap.Square:
return originalBounds.Inflate(pen.Thickness);
default:
throw new ArgumentOutOfRangeException();
}
} }
/// <inheritdoc/> /// <inheritdoc/>

75
src/Windows/Avalonia.Win32/Automation/AutomationNode.cs

@ -54,25 +54,11 @@ namespace Avalonia.Win32.Automation
_runtimeId = new int[] { 3, GetHashCode() }; _runtimeId = new int[] { 3, GetHashCode() };
Peer = peer; Peer = peer;
s_nodes.Add(peer, this); s_nodes.Add(peer, this);
peer.ChildrenChanged += Peer_ChildrenChanged; peer.ChildrenChanged += OnPeerChildrenChanged;
peer.PropertyChanged += Peer_PropertyChanged; peer.PropertyChanged += OnPeerPropertyChanged;
}
private void Peer_ChildrenChanged(object? sender, EventArgs e)
{
ChildrenChanged();
}
private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
{ embeddedRoot.FocusChanged += OnEmbeddedRootFocusChanged;
if (s_propertyMap.TryGetValue(e.Property, out var id))
{
UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
}
} }
public AutomationPeer Peer { get; protected set; } public AutomationPeer Peer { get; protected set; }
@ -89,21 +75,12 @@ namespace Avalonia.Win32.Automation
public virtual IRawElementProviderFragmentRoot? FragmentRoot public virtual IRawElementProviderFragmentRoot? FragmentRoot
{ {
get => InvokeSync(() => GetRoot()) as IRawElementProviderFragmentRoot; get => InvokeSync(() => GetRoot());
} }
public virtual IRawElementProviderSimple? HostRawElementProvider => null; public virtual IRawElementProviderSimple? HostRawElementProvider => null;
public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider; public ProviderOptions ProviderOptions => ProviderOptions.ServerSideProvider;
public void ChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
[return: MarshalAs(UnmanagedType.IUnknown)] [return: MarshalAs(UnmanagedType.IUnknown)]
public virtual object? GetPatternProvider(int patternId) public virtual object? GetPatternProvider(int patternId)
{ {
@ -250,21 +227,49 @@ namespace Avalonia.Win32.Automation
throw new NotSupportedException(); throw new NotSupportedException();
} }
protected void RaiseChildrenChanged()
{
UiaCoreProviderApi.UiaRaiseStructureChangedEvent(
this,
StructureChangeType.ChildrenInvalidated,
null,
0);
}
private AutomationNode? GetRoot() protected void RaiseFocusChanged(AutomationNode? focused)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
private RootAutomationNode? GetRoot()
{ {
Dispatcher.UIThread.VerifyAccess(); Dispatcher.UIThread.VerifyAccess();
return GetOrCreate(Peer.GetVisualRoot()) as RootAutomationNode;
}
var peer = Peer; private void OnPeerChildrenChanged(object? sender, EventArgs e)
var parent = peer.GetParent(); {
RaiseChildrenChanged();
}
while (peer.GetProvider<AAP.IRootProvider>() is null && parent is object) private void OnPeerPropertyChanged(object? sender, AutomationPropertyChangedEventArgs e)
{
if (s_propertyMap.TryGetValue(e.Property, out var id))
{ {
peer = parent; UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(
parent = peer.GetParent(); this,
(int)id,
e.OldValue as IConvertible,
e.NewValue as IConvertible);
} }
}
return peer is object ? GetOrCreate(peer) : null; private void OnEmbeddedRootFocusChanged(object? sender, EventArgs e)
{
if (Peer.GetProvider<AAP.IEmbeddedRootProvider>() is { } embeddedRoot)
RaiseFocusChanged(GetOrCreate(embeddedRoot.GetFocus()));
} }
private static AutomationNode Create(AutomationPeer peer) private static AutomationNode Create(AutomationPeer peer)

48
src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs

@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation;
namespace Avalonia.Win32.Automation namespace Avalonia.Win32.Automation
{ {
[RequiresUnreferencedCode("Requires .NET COM interop")] [RequiresUnreferencedCode("Requires .NET COM interop")]
internal class RootAutomationNode : AutomationNode, internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot
IRawElementProviderFragmentRoot,
IRawElementProviderAdviseEvents
{ {
private int _raiseFocusChanged;
public RootAutomationNode(AutomationPeer peer) public RootAutomationNode(AutomationPeer peer)
: base(peer) : base(peer)
{ {
Peer = base.Peer.GetProvider<IRootProvider>() ?? throw new AvaloniaInternalException( Peer = base.Peer.GetProvider<IRootProvider>() ?? throw new AvaloniaInternalException(
"Attempt to create RootAutomationNode from peer which does not implement IRootProvider."); "Attempt to create RootAutomationNode from peer which does not implement IRootProvider.");
Peer.FocusChanged += FocusChanged; Peer.FocusChanged += OnRootFocusChanged;
} }
public override IRawElementProviderFragmentRoot? FragmentRoot => this; public override IRawElementProviderFragmentRoot? FragmentRoot => this;
@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation
return GetOrCreate(focus); return GetOrCreate(focus);
} }
void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
++_raiseFocusChanged;
break;
}
}
void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties)
{
switch ((UiaEventId)eventId)
{
case UiaEventId.AutomationFocusChanged:
--_raiseFocusChanged;
break;
}
}
protected void RaiseFocusChanged(AutomationNode? focused)
{
if (_raiseFocusChanged > 0)
{
UiaCoreProviderApi.UiaRaiseAutomationEvent(
focused,
(int)UiaEventId.AutomationFocusChanged);
}
}
public void FocusChanged(object? sender, EventArgs e)
{
RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
}
public Rect ToScreen(Rect rect) public Rect ToScreen(Rect rect)
{ {
if (WindowImpl is null) if (WindowImpl is null)
@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation
return result; return result;
} }
} }
private void OnRootFocusChanged(object? sender, EventArgs e)
{
RaiseFocusChanged(GetOrCreate(Peer.GetFocus()));
}
} }
} }

1
src/Windows/Avalonia.Win32/DataObject.cs

@ -103,6 +103,7 @@ namespace Avalonia.Win32
private IDataObject _wrapped; private IDataObject _wrapped;
public IDataObject Wrapped => _wrapped;
public DataObject(IDataObject wrapped) public DataObject(IDataObject wrapped)
{ {

2
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -1509,7 +1509,7 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")] [DllImport("user32.dll")]
public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem); public static extern IntPtr SetClipboardData(ClipboardFormat uFormat, IntPtr hMem);
[DllImport("ole32.dll", PreserveSig = false)] [DllImport("ole32.dll", PreserveSig = true)]
public static extern int OleGetClipboard(out IntPtr dataObject); public static extern int OleGetClipboard(out IntPtr dataObject);
[DllImport("ole32.dll", PreserveSig = true)] [DllImport("ole32.dll", PreserveSig = true)]

2
src/Windows/Avalonia.Win32/OleDropTarget.cs

@ -217,7 +217,7 @@ namespace Avalonia.Win32
if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject) if (MicroComRuntime.TryUnwrapManagedObject(pDataObj) is DataObject dataObject)
{ {
return dataObject; return dataObject.Wrapped;
} }
return new OleDataObject(pDataObj); return new OleDataObject(pDataObj);
} }

3
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -683,9 +683,10 @@ namespace Avalonia.Win32
if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar) if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
{ {
parentHwnd = OffscreenParentWindow.Handle; parentHwnd = OffscreenParentWindow.Handle;
_hiddenWindowIsParent = true;
} }
_hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle;
SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd); SetWindowLongPtr(_hwnd, (int)WindowLongParam.GWL_HWNDPARENT, parentHwnd);
} }

68
tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs

@ -0,0 +1,68 @@
using Avalonia.Controls;
using Avalonia.Media;
using Xunit;
namespace Avalonia.Base.UnitTests.Rendering;
/// <summary>
/// Test class that verifies how clipping influences rendering in the compositor
/// </summary>
public class CompositorInvalidationClippingTests : CompositorTestsBase
{
[Fact]
// Test case: When the ClipToBounds is false, all visuals should be rendered
public void Siblings_Should_Be_Rendered_On_Invalidate_Without_ClipToBounds()
{
AssertRenderedVisuals(clipToBounds: false, clipGeometry: false, expectedRenderedVisualsCount: 4);
}
[Fact]
// Test case: When the ClipToBounds is true, only visuals within the clipped boundary should be rendered
public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_ClipToBounds()
{
AssertRenderedVisuals(clipToBounds: true, clipGeometry: false, expectedRenderedVisualsCount: 3);
}
[Fact]
// Test case: When the Clip is used, only visuals within the clip geometry should be rendered
public void Siblings_Should_Not_Be_Rendered_On_Invalidate_With_Clip()
{
AssertRenderedVisuals(clipToBounds: false, clipGeometry: true, expectedRenderedVisualsCount: 3);
}
private void AssertRenderedVisuals(bool clipToBounds, bool clipGeometry, int expectedRenderedVisualsCount)
{
using (var s = new CompositorCanvas())
{
//#1 visual is top level
//#2 visual is s.Canvas
//#3 visual is border1
s.Canvas.Children.Add(new Border()
{
[Canvas.LeftProperty] = 0, [Canvas.TopProperty] = 0,
Width = 20, Height = 10,
Background = Brushes.Red,
ClipToBounds = clipToBounds,
Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
});
//#4 visual is border2
s.Canvas.Children.Add(new Border()
{
[Canvas.LeftProperty] = 30, [Canvas.TopProperty] = 50,
Width = 20, Height = 10,
Background = Brushes.Red,
ClipToBounds = clipToBounds,
Clip = clipGeometry ? new RectangleGeometry(new Rect(new Size(20, 10))) : null
});
s.RunJobs();
s.Events.Reset();
//invalidate border1
s.Canvas.Children[0].IsVisible = false;
s.RunJobs();
s.AssertRenderedVisuals(expectedRenderedVisualsCount);
}
}
}

152
tests/Avalonia.Controls.UnitTests/ItemsControlTests.cs

@ -14,6 +14,7 @@ using Avalonia.Input;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Styling; using Avalonia.Styling;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -128,6 +129,155 @@ namespace Avalonia.Controls.UnitTests
Assert.Same(container.Theme, theme); Assert.Same(container.Theme, theme);
} }
[Fact]
public void Container_Should_Have_Theme_Set_To_ItemContainerTheme_With_Base_TargetType()
{
using var app = Start();
var theme = new ControlTheme { TargetType = typeof(Control) };
var target = CreateTarget(
itemsSource: new[] { "Foo" },
itemContainerTheme: theme);
var container = GetContainer(target);
Assert.Same(container.Theme, theme);
}
[Fact]
public void ItemContainerTheme_Can_Be_Changed()
{
using var app = Start();
var theme1 = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
};
var theme2 = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
};
var target = CreateTarget(
itemsSource: new[] { "Foo" },
itemContainerTheme: theme1);
var container = GetContainer(target);
Assert.Same(container.Theme, theme1);
Assert.Equal(container.Background, Brushes.Red);
target.ItemContainerTheme = theme2;
container = GetContainer(target);
Assert.Same(container.Theme, theme2);
Assert.Equal(container.Background, Brushes.Green);
}
[Fact]
public void ItemContainerTheme_Can_Be_Changed_Virtualizing()
{
using var app = Start();
var theme1 = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
};
var theme2 = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
};
var itemsPanel = new FuncTemplate<Panel?>(() => new VirtualizingStackPanel());
var target = CreateTarget(
itemsSource: new[] { "Foo" },
itemContainerTheme: theme1,
itemsPanel: itemsPanel);
var container = GetContainer(target);
Assert.Same(container.Theme, theme1);
Assert.Equal(container.Background, Brushes.Red);
target.ItemContainerTheme = theme2;
Layout(target);
container = GetContainer(target);
Assert.Same(container.Theme, theme2);
Assert.Equal(container.Background, Brushes.Green);
}
[Fact]
public void ItemContainerTheme_Can_Be_Cleared()
{
using var app = Start();
var theme = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
};
var target = CreateTarget(
itemsSource: new[] { "Foo" },
itemContainerTheme: theme);
var container = GetContainer(target);
Assert.Same(container.Theme, theme);
Assert.Equal(container.Background, Brushes.Red);
target.ItemContainerTheme = null;
container = GetContainer(target);
Assert.Null(container.Theme);
Assert.Null(container.Background);
}
[Fact]
public void ItemContainerTheme_Should_Not_Override_LocalValue_Theme()
{
using var app = Start();
var theme1 = new ControlTheme
{
TargetType = typeof(ContentPresenter),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Red) }
};
var theme2 = new ControlTheme
{
TargetType = typeof(Control),
Setters = { new Setter(ContentPresenter.BackgroundProperty, Brushes.Green) }
};
var items = new object[]
{
new ContentPresenter(),
new ContentPresenter
{
Theme = theme2
},
};
var target = CreateTarget(
itemsSource: items,
itemContainerTheme: theme1);
Assert.Same(theme1, GetContainer(target, 0).Theme);
Assert.Same(theme2, GetContainer(target, 1).Theme);
target.ItemContainerTheme = null;
Assert.Null(GetContainer(target, 0).Theme);
Assert.Same(theme2, GetContainer(target, 1).Theme);
}
[Fact] [Fact]
public void Container_Should_Have_LogicalParent_Set_To_ItemsControl() public void Container_Should_Have_LogicalParent_Set_To_ItemsControl()
{ {
@ -851,6 +1001,7 @@ namespace Avalonia.Controls.UnitTests
IList? itemsSource = null, IList? itemsSource = null,
ControlTheme? itemContainerTheme = null, ControlTheme? itemContainerTheme = null,
IDataTemplate? itemTemplate = null, IDataTemplate? itemTemplate = null,
ITemplate<Panel?>? itemsPanel = null,
IEnumerable<IDataTemplate>? dataTemplates = null, IEnumerable<IDataTemplate>? dataTemplates = null,
bool performLayout = true) bool performLayout = true)
{ {
@ -861,6 +1012,7 @@ namespace Avalonia.Controls.UnitTests
itemsSource: itemsSource, itemsSource: itemsSource,
itemContainerTheme: itemContainerTheme, itemContainerTheme: itemContainerTheme,
itemTemplate: itemTemplate, itemTemplate: itemTemplate,
itemsPanel: itemsPanel,
dataTemplates: dataTemplates, dataTemplates: dataTemplates,
performLayout: performLayout); performLayout: performLayout);
} }

21
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -202,5 +202,26 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(0, target.Inlines.Count); Assert.Equal(0, target.Inlines.Count);
} }
} }
[Fact]
public void Setting_TextDecorations_Should_Update_Inlines()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new TextBlock();
target.Inlines.Add(new Run("Hello World"));
Assert.Equal(1, target.Inlines.Count);
Assert.Null(target.Inlines[0].TextDecorations);
var underline = TextDecorations.Underline;
target.TextDecorations = underline;
Assert.Equal(underline, target.Inlines[0].TextDecorations);
}
}
} }
} }

79
tests/Avalonia.Controls.UnitTests/TextBoxTests.cs

@ -15,6 +15,7 @@ using Avalonia.Platform;
using Avalonia.Rendering; using Avalonia.Rendering;
using Avalonia.Rendering.Composition; using Avalonia.Rendering.Composition;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq; using Moq;
using Xunit; using Xunit;
@ -916,6 +917,82 @@ namespace Avalonia.Controls.UnitTests
} }
} }
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MaxLines_Sets_ScrollViewer_MaxHeight(int maxLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MaxLines = maxLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
Assert.Equal(new Thickness(0), textPresenter.Margin); // Test assumes no margin on TextPresenter
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal(maxLines * target.LineHeight, scrollViewer.MaxHeight);
}
}
[Theory]
[InlineData(1)]
[InlineData(2)]
[InlineData(3)]
public void MaxLines_Sets_ScrollViewer_MaxHeight_With_TextPresenter_Margin(int maxLines)
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
MaxLines = maxLines,
// Define explicit whole number line height for predictable calculations
LineHeight = 20
};
var impl = CreateMockTopLevelImpl();
var topLevel = new TestTopLevel(impl.Object)
{
Template = CreateTopLevelTemplate(),
Content = target
};
topLevel.ApplyTemplate();
topLevel.LayoutManager.ExecuteInitialLayoutPass();
var textPresenter = target.FindDescendantOfType<TextPresenter>();
Assert.Equal("PART_TextPresenter", textPresenter.Name);
var textPresenterMargin = new Thickness(horizontal: 0, vertical: 3);
textPresenter.Margin = textPresenterMargin;
target.InvalidateMeasure();
target.Measure(Size.Infinity);
var scrollViewer = target.FindDescendantOfType<ScrollViewer>();
Assert.Equal("PART_ScrollViewer", scrollViewer.Name);
Assert.Equal((maxLines * target.LineHeight) + textPresenterMargin.Top + textPresenterMargin.Bottom, scrollViewer.MaxHeight);
}
}
[Fact] [Fact]
public void CanUndo_CanRedo_Is_False_When_Initialized() public void CanUndo_CanRedo_Is_False_When_Initialized()
{ {
@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate<TextBox>((control, scope) => return new FuncControlTemplate<TextBox>((control, scope) =>
new ScrollViewer new ScrollViewer
{ {
Name = "Part_ScrollViewer", Name = "PART_ScrollViewer",
Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate), Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
Content = new TextPresenter Content = new TextPresenter
{ {

28
tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs

@ -183,6 +183,34 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal("bar", presenter2.Content); Assert.Equal("bar", presenter2.Content);
} }
[Fact]
public void Logical_Children_Should_Not_Be_Duplicated()
{
using var app = Start();
var (target, transition) = CreateTarget("");
target.PageTransition = null;
var childControl = new Control();
target.Content = childControl;
Assert.Equal(1, target.LogicalChildren.Count);
Assert.Equal(target.LogicalChildren[0], childControl);
}
[Fact]
public void First_Presenter_Should_Register_TCC_As_His_Host()
{
using var app = Start();
var (target, transition) = CreateTarget("");
target.PageTransition = null;
var childControl = new Control();
target.Presenter!.Content = childControl;
Assert.Equal(1, target.LogicalChildren.Count);
Assert.Equal(target.LogicalChildren[0], childControl);
}
private static IDisposable Start() private static IDisposable Start()
{ {
return UnitTestApplication.Start( return UnitTestApplication.Start(

40
tests/Avalonia.LeakTests/ControlTests.cs

@ -583,6 +583,46 @@ namespace Avalonia.LeakTests
Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount)); Assert.Equal(initialMenuItemCount, memory.GetObjects(where => where.Type.Is<MenuItem>()).ObjectsCount));
} }
} }
[Fact]
public void Attached_Control_From_ContextMenu_Is_Freed()
{
using (Start())
{
var contextMenu = new ContextMenu();
Func<Window> run = () =>
{
var window = new Window
{
Content = new TextBlock
{
ContextMenu = contextMenu
}
};
window.Show();
// Do a layout and make sure that TextBlock gets added to visual tree.
window.LayoutManager.ExecuteInitialLayoutPass();
Assert.IsType<TextBlock>(window.Presenter.Child);
// Clear the content and ensure the TextBlock is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
};
var result = run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TextBlock>()).ObjectsCount));
}
}
[Fact] [Fact]
public void Standalone_ContextMenu_Is_Freed() public void Standalone_ContextMenu_Is_Freed()

43
tests/Avalonia.Skia.UnitTests/RenderBoundsTests.cs

@ -0,0 +1,43 @@
using System;
using Avalonia.Controls.Shapes;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Skia.UnitTests
{
public class RenderBoundsTests
{
[Theory,
InlineData("M10 20 L 20 10 L 30 20", PenLineCap.Round, PenLineJoin.Miter, 2, 10,
8.585786819458008, 8.585786819458008, 22.828428268432617, 12.828428268432617),
InlineData("M10 10 L 20 10", PenLineCap.Round, PenLineJoin.Miter,2, 10,
9,9,12,2),
InlineData("M10 10 L 20 15 L 10 20", PenLineCap.Flat, PenLineJoin.Miter, 2, 20,
9.552786827087402, 9.105572700500488, 12.683281898498535, 11.788853645324707)
]
public void RenderBoundsAreCorrectlyCalculated(string path, PenLineCap cap, PenLineJoin join, double thickness, double miterLimit, double x, double y, double width, double height)
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface())))
{
var geo = PathGeometry.Parse(path);
var pen = new Pen(Brushes.Black, thickness, null, cap, join, miterLimit);
var bounds = geo.GetRenderBounds(pen);
var tolerance = 0.001;
if (
Math.Abs(bounds.X - x) > tolerance
|| Math.Abs(bounds.Y - y) > tolerance
|| Math.Abs(bounds.Width - width) > tolerance
|| Math.Abs(bounds.Height - height) > tolerance)
Assert.Fail($"Expected {x}:{y}:{width}:{height}, got {bounds}");
Assert.Equal(new Rect(x, y, width, height), bounds);
}
}
}
}

17
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable
Events.Rects.Clear(); Events.Rects.Clear();
} }
public void AssertRenderedVisuals(int renderVisuals)
{
RunJobs();
Assert.Equal(Events.RenderedVisuals, renderVisuals);
Events.Rects.Clear();
}
public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected) public void AssertHitTest(double x, double y, Func<Visual, bool> filter, params object[] expected)
=> AssertHitTest(new Point(x, y), filter, expected); => AssertHitTest(new Point(x, y), filter, expected);
@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{ {
public List<Rect> Rects = new(); public List<Rect> Rects = new();
public int RenderedVisuals { get; private set; }
public void IncrementRenderedVisuals()
{
RenderedVisuals++;
}
public void RectInvalidated(Rect rc) public void RectInvalidated(Rect rc)
{ {
Rects.Add(rc); Rects.Add(rc);
@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
public void Reset() public void Reset()
{ {
Rects.Clear(); Rects.Clear();
RenderedVisuals = 0;
} }
} }
@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
{ {
Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender); Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
} }
} }

Loading…
Cancel
Save