Browse Source

Merge branch 'master' into clipboard-image

pull/12246/head
Max Katz 3 years ago
committed by GitHub
parent
commit
e32cf66abd
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/AvnView.mm
  5. 11
      native/Avalonia.Native/src/OSX/automation.mm
  6. 29
      native/Avalonia.Native/src/OSX/platformthreading.mm
  7. 2
      packages/Avalonia/Avalonia.csproj
  8. 1
      src/Avalonia.Base/Avalonia.Base.csproj
  9. 45
      src/Avalonia.Base/Media/RenderOptions.cs
  10. 2
      src/Avalonia.Base/Rendering/Composition/ICompositionTargetDebugEvents.cs
  11. 1
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.DirtyProperties.cs
  12. 22
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs
  13. 1
      src/Avalonia.Controls.ColorPicker/Avalonia.Controls.ColorPicker.csproj
  14. 1
      src/Avalonia.Controls.DataGrid/Avalonia.Controls.DataGrid.csproj
  15. 38
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  16. 1
      src/Avalonia.Controls.ItemsRepeater/Avalonia.Controls.ItemsRepeater.csproj
  17. 23
      src/Avalonia.Controls/Automation/Peers/AutomationPeer.cs
  18. 7
      src/Avalonia.Controls/Automation/Peers/ControlAutomationPeer.cs
  19. 2
      src/Avalonia.Controls/Automation/Peers/ItemsControlAutomationPeer.cs
  20. 2
      src/Avalonia.Controls/Automation/Peers/ListItemAutomationPeer.cs
  21. 17
      src/Avalonia.Controls/Automation/Peers/WindowBaseAutomationPeer.cs
  22. 33
      src/Avalonia.Controls/Automation/Provider/IEmbeddedRootProvider.cs
  23. 25
      src/Avalonia.Controls/Automation/Provider/IRootProvider.cs
  24. 1
      src/Avalonia.Controls/Avalonia.Controls.csproj
  25. 15
      src/Avalonia.Controls/ContentControl.cs
  26. 31
      src/Avalonia.Controls/ContextMenu.cs
  27. 2
      src/Avalonia.Controls/Primitives/Track.cs
  28. 4
      src/Avalonia.Controls/SplitButton/ToggleSplitButton.cs
  29. 34
      src/Avalonia.Controls/TextBox.cs
  30. 1
      src/Avalonia.Controls/TopLevel.cs
  31. 13
      src/Avalonia.Controls/TransitioningContentControl.cs
  32. 1
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  33. 1
      src/Avalonia.Desktop/Avalonia.Desktop.csproj
  34. 1
      src/Avalonia.Diagnostics/Avalonia.Diagnostics.csproj
  35. 1
      src/Avalonia.Dialogs/Avalonia.Dialogs.csproj
  36. 121
      src/Avalonia.Native/AvnAutomationPeer.cs
  37. 7
      src/Avalonia.Native/avn.idl
  38. 1
      src/Avalonia.ReactiveUI/Avalonia.ReactiveUI.csproj
  39. 3
      src/Avalonia.Remote.Protocol/Avalonia.Remote.Protocol.csproj
  40. 1
      src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj
  41. 1
      src/Avalonia.Themes.Simple/Avalonia.Themes.Simple.csproj
  42. 3
      src/Headless/Avalonia.Headless.NUnit/Avalonia.Headless.NUnit.csproj
  43. 1
      src/Headless/Avalonia.Headless.Vnc/Avalonia.Headless.Vnc.csproj
  44. 1
      src/Headless/Avalonia.Headless.XUnit/Avalonia.Headless.XUnit.csproj
  45. 1
      src/Headless/Avalonia.Headless/Avalonia.Headless.csproj
  46. 1
      src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj
  47. 1
      src/Markup/Avalonia.Markup/Avalonia.Markup.csproj
  48. 28
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  49. 75
      src/Windows/Avalonia.Win32/Automation/AutomationNode.cs
  50. 48
      src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs
  51. 1
      src/Windows/Avalonia.Win32/DataObject.cs
  52. 2
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  53. 2
      src/Windows/Avalonia.Win32/OleDropTarget.cs
  54. 3
      src/Windows/Avalonia.Win32/WindowImpl.cs
  55. 68
      tests/Avalonia.Base.UnitTests/Rendering/CompositorInvalidationClippingTests.cs
  56. 79
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  57. 28
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  58. 40
      tests/Avalonia.LeakTests/ControlTests.cs
  59. 70
      tests/Avalonia.LeakTests/DataContextTests.cs
  60. 17
      tests/Avalonia.UnitTests/CompositorTestServices.cs

1
Avalonia.sln

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

3
NuGet.Config

@ -3,8 +3,7 @@
<configuration>
<packageSources>
<clear />
<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="api.nuget.org" value="https://api.nuget.org/v3/index.json" />
<add key="skiasharp" value="https://aka.ms/skiasharp-eap/index.json" />
</packageSources>
</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/AvnView.mm

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

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

@ -73,6 +73,13 @@ private:
if (peer->IsRootProvider())
{
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 view = holder->GetNSView();
return [[AvnRootAccessibilityElement alloc] initWithPeer:peer owner:view];
@ -284,8 +291,8 @@ private:
- (id)accessibilityWindow
{
id topLevel = [self accessibilityTopLevelUIElement];
return [topLevel isKindOfClass:[NSWindow class]] ? topLevel : nil;
auto rootPeer = _peer->GetVisualRoot();
return [AvnAccessibilityElement acquire:rootPeer];
}
- (BOOL)isAccessibilityExpanded

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

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

2
packages/Avalonia/Avalonia.csproj

@ -5,7 +5,7 @@
</PropertyGroup>
<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.Build.Tasks/Avalonia.Build.Tasks.csproj">
<PrivateAssets>all</PrivateAssets>

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

@ -15,7 +15,6 @@
<Import Project="..\..\build\Base.props" />
<Import Project="..\..\build\Binding.props" />
<Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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 TextRenderingMode TextRenderingMode { get; init; }
public BitmapBlendingMode BitmapBlendingMode { get; init; }
public bool? RequiresFullOpacityHandling { get; init; }
/// <summary>
/// Gets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static BitmapInterpolationMode GetBitmapInterpolationMode(Visual visual)
{
return visual.RenderOptions.BitmapInterpolationMode;
@ -23,7 +24,7 @@ namespace Avalonia.Media
/// Sets the value of the BitmapInterpolationMode attached property for a visual.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static BitmapBlendingMode GetBitmapBlendingMode(Visual visual)
{
return visual.RenderOptions.BitmapBlendingMode;
@ -53,7 +54,7 @@ namespace Avalonia.Media
/// Gets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static EdgeMode GetEdgeMode(Visual visual)
{
return visual.RenderOptions.EdgeMode;
@ -63,7 +64,7 @@ namespace Avalonia.Media
/// Sets the value of the EdgeMode attached property for a visual.
/// </summary>
/// <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)
{
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.
/// </summary>
/// <param name="visual">The control.</param>
/// <returns>The control's left coordinate.</returns>
/// <returns>The value.</returns>
public static TextRenderingMode GetTextRenderingMode(Visual visual)
{
return visual.RenderOptions.TextRenderingMode;
@ -83,12 +84,32 @@ namespace Avalonia.Media
/// Sets the value of the TextRenderingMode attached property for a visual.
/// </summary>
/// <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)
{
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)
{
var bitmapInterpolationMode = BitmapInterpolationMode;
@ -119,12 +140,20 @@ namespace Avalonia.Media
bitmapBlendingMode = other.BitmapBlendingMode;
}
var requiresFullOpacityHandling = RequiresFullOpacityHandling;
if (requiresFullOpacityHandling == null)
{
requiresFullOpacityHandling = other.RequiresFullOpacityHandling;
}
return new RenderOptions
{
BitmapInterpolationMode = bitmapInterpolationMode,
EdgeMode = edgeMode,
TextRenderingMode = textRenderingMode,
BitmapBlendingMode = bitmapBlendingMode
BitmapBlendingMode = bitmapBlendingMode,
RequiresFullOpacityHandling = requiresFullOpacityHandling
};
}
}

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using Avalonia.Automation.Provider;
namespace Avalonia.Automation.Peers
{
@ -115,9 +116,14 @@ namespace Avalonia.Automation.Peers
/// <summary>
/// Gets the <see cref="AutomationPeer"/> that is the parent of this <see cref="AutomationPeer"/>.
/// </summary>
/// <returns></returns>
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>
/// Gets a value that indicates whether the element that is associated with this automation
/// peer currently has keyboard focus.
@ -247,6 +253,21 @@ namespace Avalonia.Automation.Peers
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()
{
return IsControlElement() && IsContentElementCore();

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

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

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

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

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

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

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

@ -1,5 +1,6 @@
using System;
using System.ComponentModel;
using System.Globalization;
using Avalonia.Automation.Provider;
using Avalonia.Controls;
using Avalonia.Input;
@ -32,7 +33,21 @@ namespace Avalonia.Automation.Peers
public AutomationPeer? GetPeerFromPoint(Point p)
{
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()

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
{
/// <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
{
/// <summary>
/// Gets the platform implementation of the TopLevel for the element.
/// </summary>
ITopLevelImpl? PlatformImpl { get; }
/// <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;
}
}

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

@ -6,7 +6,6 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Remote.Protocol\Avalonia.Remote.Protocol.csproj" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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 =
AvaloniaProperty.Register<ContentControl, VerticalAlignment>(nameof(VerticalContentAlignment));
static ContentControl()
{
ContentProperty.Changed.AddClassHandler<ContentControl>((x, e) => x.ContentChanged(e));
}
/// <summary>
/// Gets or sets the content to display.
/// </summary>
@ -100,6 +95,16 @@ namespace Avalonia.Controls
{
return RegisterContentPresenter(presenter);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ContentProperty)
{
ContentChanged(change);
}
}
/// <summary>
/// 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>
static ContextMenu()
{
ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
PlacementProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
@ -216,18 +215,23 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.ContextRequested -= ControlContextRequested;
control.AttachedToVisualTree -= ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
oldMenu._attachedControls?.Remove(control);
((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.AttachedToVisualTree += ControlOnAttachedToVisualTree;
control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
}
if (control.IsAttachedToVisualTree)
{
AttachControlToContextMenu(control);
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
@ -428,11 +432,25 @@ namespace Avalonia.Controls
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)
{
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu)
if (sender is Control { ContextMenu: { } contextMenu } control)
{
if (contextMenu._popup?.Parent == control)
{
@ -440,6 +458,7 @@ namespace Avalonia.Controls
}
contextMenu.Close();
contextMenu._attachedControls?.Remove(control);
}
}

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

@ -52,7 +52,7 @@ namespace Avalonia.Controls.Primitives
ThumbProperty.Changed.AddClassHandler<Track>((x, e) => x.ThumbChanged(e));
IncreaseButtonProperty.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()

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

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

34
src/Avalonia.Controls/TextBox.cs

@ -1879,6 +1879,37 @@ namespace Avalonia.Controls
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>
/// Raises both the <see cref="TextChanging"/> and <see cref="TextChanged"/> events.
/// </summary>
@ -2032,8 +2063,9 @@ namespace Avalonia.Controls
var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
var paragraphProperties = TextLayout.CreateTextParagraphProperties(typeface, fontSize, null, default, default, null, default, LineHeight, default);
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);

1
src/Avalonia.Controls/TopLevel.cs

@ -591,6 +591,7 @@ namespace Avalonia.Controls
Renderer.SceneInvalidated -= SceneInvalidated;
// We need to wait for the renderer to complete any in-flight operations
Renderer.Dispose();
StopRendering();
Debug.Assert(PlatformImpl != null);
// The PlatformImpl is completely invalid at this point

13
src/Avalonia.Controls/TransitioningContentControl.cs

@ -79,8 +79,12 @@ public class TransitioningContentControl : ContentControl
protected override bool RegisterContentPresenter(ContentPresenter presenter)
{
if (!base.RegisterContentPresenter(presenter) &&
presenter is ContentPresenter p &&
if (base.RegisterContentPresenter(presenter))
{
return true;
}
if (presenter is ContentPresenter p &&
p.Name == "PART_ContentPresenter2")
{
_presenter2 = p;
@ -94,12 +98,13 @@ public class TransitioningContentControl : ContentControl
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ContentProperty)
{
UpdateContent(true);
return;
}
base.OnPropertyChanged(change);
}
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.Controls\Avalonia.Controls.csproj" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
</Project>

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

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

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

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

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

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

121
src/Avalonia.Native/AvnAutomationPeer.cs

@ -22,8 +22,8 @@ namespace Avalonia.Native
{
_inner = inner;
_inner.ChildrenChanged += (_, _) => Node?.ChildrenChanged();
if (inner is WindowBaseAutomationPeer window)
window.FocusChanged += (_, _) => Node?.FocusChanged();
if (inner is IRootProvider root)
root.FocusChanged += (_, _) => Node?.FocusChanged();
}
~AvnAutomationPeer() => Node?.Dispose();
@ -39,6 +39,7 @@ namespace Avalonia.Native
public IAvnAutomationPeer? LabeledBy => Wrap(_inner.GetLabeledBy());
public IAvnString Name => _inner.GetName().ToAvnString();
public IAvnAutomationPeer? Parent => Wrap(_inner.GetParent());
public IAvnAutomationPeer? VisualRoot => Wrap(_inner.GetVisualRoot());
public int HasKeyboardFocus() => _inner.HasKeyboardFocus().AsComBool();
public int IsContentElement() => _inner.IsContentElement().AsComBool();
@ -48,6 +49,13 @@ namespace Avalonia.Native
public void SetFocus() => _inner.SetFocus();
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
{
get
@ -55,7 +63,7 @@ namespace Avalonia.Native
var peer = _inner;
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;
parent = peer.GetParent();
@ -65,26 +73,23 @@ namespace Avalonia.Native
}
}
public void SetNode(IAvnAutomationNode node)
{
if (Node is not null)
throw new InvalidOperationException("The AvnAutomationPeer already has a node.");
Node = node;
}
public int IsRootProvider() => (_inner is IRootProvider).AsComBool();
private IEmbeddedRootProvider EmbeddedRootProvider => GetProvider<IEmbeddedRootProvider>();
private IExpandCollapseProvider ExpandCollapseProvider => GetProvider<IExpandCollapseProvider>();
private IInvokeProvider InvokeProvider => GetProvider<IInvokeProvider>();
private IRangeValueProvider RangeValueProvider => GetProvider<IRangeValueProvider>();
private IRootProvider RootProvider => GetProvider<IRootProvider>();
private ISelectionItemProvider SelectionItemProvider => GetProvider<ISelectionItemProvider>();
private IToggleProvider ToggleProvider => GetProvider<IToggleProvider>();
private IValueProvider ValueProvider => GetProvider<IValueProvider>();
public IAvnWindowBase RootProvider_GetWindow()
{
var window = (WindowBase)((ControlAutomationPeer)_inner).Owner;
return ((WindowBaseImpl)window.PlatformImpl!).Native;
}
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(((IRootProvider)_inner).GetFocus());
public int IsRootProvider() => IsProvider<IRootProvider>();
public IAvnWindowBase? RootProvider_GetWindow() => (RootProvider.PlatformImpl as WindowBaseImpl)?.Native;
public IAvnAutomationPeer? RootProvider_GetFocus() => Wrap(RootProvider.GetFocus());
public IAvnAutomationPeer? RootProvider_GetPeerFromPoint(AvnPoint point)
{
var result = ((IRootProvider)_inner).GetPeerFromPoint(point.ToAvaloniaPoint());
var result = RootProvider.GetPeerFromPoint(point.ToAvaloniaPoint());
if (result is null)
return null;
@ -103,46 +108,80 @@ namespace Avalonia.Native
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.PartiallyExpanded => 1,
_ => 0,
};
public int ExpandCollapseProvider_GetShowsMenu() => ((IExpandCollapseProvider)_inner).ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ((IExpandCollapseProvider)_inner).Expand();
public void ExpandCollapseProvider_Collapse() => ((IExpandCollapseProvider)_inner).Collapse();
public int ExpandCollapseProvider_GetShowsMenu() => ExpandCollapseProvider.ShowsMenu.AsComBool();
public void ExpandCollapseProvider_Expand() => ExpandCollapseProvider.Expand();
public void ExpandCollapseProvider_Collapse() => ExpandCollapseProvider.Collapse();
public int IsInvokeProvider() => (_inner is IInvokeProvider).AsComBool();
public void InvokeProvider_Invoke() => ((IInvokeProvider)_inner).Invoke();
public int IsInvokeProvider() => IsProvider<IInvokeProvider>();
public void InvokeProvider_Invoke() => InvokeProvider.Invoke();
public int IsRangeValueProvider() => (_inner is IRangeValueProvider).AsComBool();
public double RangeValueProvider_GetValue() => ((IRangeValueProvider)_inner).Value;
public double RangeValueProvider_GetMinimum() => ((IRangeValueProvider)_inner).Minimum;
public double RangeValueProvider_GetMaximum() => ((IRangeValueProvider)_inner).Maximum;
public double RangeValueProvider_GetSmallChange() => ((IRangeValueProvider)_inner).SmallChange;
public double RangeValueProvider_GetLargeChange() => ((IRangeValueProvider)_inner).LargeChange;
public void RangeValueProvider_SetValue(double value) => ((IRangeValueProvider)_inner).SetValue(value);
public int IsRangeValueProvider() => IsProvider<IRangeValueProvider>();
public double RangeValueProvider_GetValue() => RangeValueProvider.Value;
public double RangeValueProvider_GetMinimum() => RangeValueProvider.Minimum;
public double RangeValueProvider_GetMaximum() => RangeValueProvider.Maximum;
public double RangeValueProvider_GetSmallChange() => RangeValueProvider.SmallChange;
public double RangeValueProvider_GetLargeChange() => RangeValueProvider.LargeChange;
public void RangeValueProvider_SetValue(double value) => RangeValueProvider.SetValue(value);
public int IsSelectionItemProvider() => (_inner is ISelectionItemProvider).AsComBool();
public int SelectionItemProvider_IsSelected() => ((ISelectionItemProvider)_inner).IsSelected.AsComBool();
public int IsSelectionItemProvider() => IsProvider<ISelectionItemProvider>();
public int SelectionItemProvider_IsSelected() => SelectionItemProvider.IsSelected.AsComBool();
public int IsToggleProvider() => (_inner is IToggleProvider).AsComBool();
public int ToggleProvider_GetToggleState() => (int)((IToggleProvider)_inner).ToggleState;
public void ToggleProvider_Toggle() => ((IToggleProvider)_inner).Toggle();
public int IsToggleProvider() => IsProvider<IToggleProvider>();
public int ToggleProvider_GetToggleState() => (int)ToggleProvider.ToggleState;
public void ToggleProvider_Toggle() => ToggleProvider.Toggle();
public int IsValueProvider() => (_inner is IValueProvider).AsComBool();
public IAvnString ValueProvider_GetValue() => ((IValueProvider)_inner).Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ((IValueProvider)_inner).SetValue(value);
public int IsValueProvider() => IsProvider<IValueProvider>();
public IAvnString ValueProvider_GetValue() => ValueProvider.Value.ToAvnString();
public void ValueProvider_SetValue(string value) => ValueProvider.SetValue(value);
[return: NotNullIfNotNull("peer")]
public static AvnAutomationPeer? Wrap(AutomationPeer? 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

7
src/Avalonia.Native/avn.idl

@ -921,6 +921,7 @@ interface IAvnAutomationPeer : IUnknown
IAvnAutomationPeer* GetLabeledBy();
IAvnString* GetName();
IAvnAutomationPeer* GetParent();
IAvnAutomationPeer* GetVisualRoot();
bool HasKeyboardFocus();
bool IsContentElement();
bool IsControlElement();
@ -935,7 +936,11 @@ interface IAvnAutomationPeer : IUnknown
IAvnWindowBase* RootProvider_GetWindow();
IAvnAutomationPeer* RootProvider_GetFocus();
IAvnAutomationPeer* RootProvider_GetPeerFromPoint(AvnPoint point);
bool IsEmbeddedRootProvider();
IAvnAutomationPeer* EmbeddedRootProvider_GetFocus();
IAvnAutomationPeer* EmbeddedRootProvider_GetPeerFromPoint(AvnPoint point);
bool IsExpandCollapseProvider();
bool ExpandCollapseProvider_GetIsExpanded();
bool ExpandCollapseProvider_GetShowsMenu();

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

@ -9,7 +9,6 @@
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\ReactiveUI.props" />
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\NullableEnable.props" />
<Import Project="..\..\build\TrimmingEnable.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\Compatibility\TrimmingAttributes.cs" Visible="False" />
</ItemGroup>
<Import Project="..\..\build\ApiDiff.props" />
<Import Project="..\..\build\TrimmingEnable.props" />
</Project>
</Project>

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

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

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

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

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

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

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

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

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

@ -7,7 +7,6 @@
<ProjectReference Include="..\..\..\packages\Avalonia\Avalonia.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.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.Markup\Avalonia.Markup.csproj" />
</ItemGroup>
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.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" />
</ItemGroup>
<Import Project="..\..\..\build\System.Memory.props" />
<Import Project="..\..\..\build\ApiDiff.props" />
<Import Project="..\..\..\build\NullableEnable.props" />
<Import Project="..\..\..\build\DevAnalyzers.props" />
<Import Project="..\..\..\build\TrimmingEnable.props" />

28
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@ -189,7 +189,8 @@ namespace Avalonia.Skia
var d = destRect.ToSKRect();
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.BlendMode = RenderOptions.BitmapBlendingMode.ToSKBlendMode();
@ -375,7 +376,7 @@ namespace Avalonia.Skia
{
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;
if (boxShadow.IsInset)
@ -432,7 +433,7 @@ namespace Avalonia.Skia
{
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 offsetX = (float)boxShadow.OffsetX;
@ -592,8 +593,16 @@ namespace Avalonia.Skia
{
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)
{
var rect = bounds.Value.ToSKRect();
@ -606,7 +615,6 @@ namespace Avalonia.Skia
}
else
{
_opacityStack.Push(_currentOpacity);
_currentOpacity *= opacity;
}
}
@ -616,14 +624,14 @@ namespace Avalonia.Skia
{
CheckLease();
if(_useOpacitySaveLayer)
var useOpacitySaveLayer = _useOpacitySaveLayer || RenderOptions.RequiresFullOpacityHandling == true;
if (useOpacitySaveLayer)
{
Canvas.Restore();
}
else
{
_currentOpacity = _opacityStack.Pop();
}
_currentOpacity = _opacityStack.Pop();
}
/// <inheritdoc />

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

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

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

@ -9,18 +9,14 @@ using Avalonia.Win32.Interop.Automation;
namespace Avalonia.Win32.Automation
{
[RequiresUnreferencedCode("Requires .NET COM interop")]
internal class RootAutomationNode : AutomationNode,
IRawElementProviderFragmentRoot,
IRawElementProviderAdviseEvents
internal class RootAutomationNode : AutomationNode, IRawElementProviderFragmentRoot
{
private int _raiseFocusChanged;
public RootAutomationNode(AutomationPeer peer)
: base(peer)
{
Peer = base.Peer.GetProvider<IRootProvider>() ?? throw new AvaloniaInternalException(
"Attempt to create RootAutomationNode from peer which does not implement IRootProvider.");
Peer.FocusChanged += FocusChanged;
Peer.FocusChanged += OnRootFocusChanged;
}
public override IRawElementProviderFragmentRoot? FragmentRoot => this;
@ -44,41 +40,6 @@ namespace Avalonia.Win32.Automation
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)
{
if (WindowImpl is null)
@ -101,5 +62,10 @@ namespace Avalonia.Win32.Automation
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;
public IDataObject Wrapped => _wrapped;
public DataObject(IDataObject wrapped)
{

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

@ -1509,7 +1509,7 @@ namespace Avalonia.Win32.Interop
[DllImport("user32.dll")]
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);
[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)
{
return dataObject;
return dataObject.Wrapped;
}
return new OleDataObject(pDataObj);
}

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

@ -683,9 +683,10 @@ namespace Avalonia.Win32
if (parentHwnd == IntPtr.Zero && !_windowProperties.ShowInTaskbar)
{
parentHwnd = OffscreenParentWindow.Handle;
_hiddenWindowIsParent = true;
}
_hiddenWindowIsParent = parentHwnd == OffscreenParentWindow.Handle;
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);
}
}
}

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

@ -15,6 +15,7 @@ using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
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]
public void CanUndo_CanRedo_Is_False_When_Initialized()
{
@ -1125,7 +1202,7 @@ namespace Avalonia.Controls.UnitTests
return new FuncControlTemplate<TextBox>((control, scope) =>
new ScrollViewer
{
Name = "Part_ScrollViewer",
Name = "PART_ScrollViewer",
Template = new FuncControlTemplate<ScrollViewer>(ScrollViewerTests.CreateTemplate),
Content = new TextPresenter
{

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

@ -183,6 +183,34 @@ namespace Avalonia.Controls.UnitTests
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()
{
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));
}
}
[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]
public void Standalone_ContextMenu_Is_Freed()

70
tests/Avalonia.LeakTests/DataContextTests.cs

@ -0,0 +1,70 @@
using System;
using Avalonia.Controls;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Reactive;
using Avalonia.Threading;
using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit;
using Xunit.Abstractions;
namespace Avalonia.LeakTests;
internal class ViewModelForDisposingTest
{
~ViewModelForDisposingTest() { ; }
}
[DotMemoryUnit(FailIfRunWithoutSupport = false)]
public class DataContextTests
{
public DataContextTests(ITestOutputHelper atr)
{
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact]
public void Window_DataContext_Disposed_After_Window_Close_With_Lifetime()
{
static IDisposable Run()
{
var unitTestApp = UnitTestApplication.Start(TestServices.StyledWindow);
var lifetime = new ClassicDesktopStyleApplicationLifetime();
lifetime.ShutdownMode = ShutdownMode.OnExplicitShutdown;
var window = new Window { DataContext = new ViewModelForDisposingTest() };
window.Show();
window.Close();
return Disposable.Create(lifetime, lt => lt.Shutdown())
.DisposeWith(new CompositeDisposable(lifetime, unitTestApp));
}
using var _ = Run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect();
dotMemory.Check(m => Assert.Equal(0,
m.GetObjects(o => o.Type.Is<ViewModelForDisposingTest>()).ObjectsCount));
}
[Fact]
public void Window_DataContext_Disposed_After_Window_Close_Without_Lifetime()
{
static void Run()
{
using var _ = UnitTestApplication.Start(TestServices.StyledWindow);
var window = new Window { DataContext = new ViewModelForDisposingTest() };
window.Show();
window.Close();
}
Run();
// Process all Loaded events to free control reference(s)
Dispatcher.UIThread.RunJobs(DispatcherPriority.Loaded);
GC.Collect();
dotMemory.Check(m => Assert.Equal(0,
m.GetObjects(o => o.Type.Is<ViewModelForDisposingTest>()).ObjectsCount));
}
}

17
tests/Avalonia.UnitTests/CompositorTestServices.cs

@ -89,6 +89,13 @@ public class CompositorTestServices : IDisposable
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)
=> AssertHitTest(new Point(x, y), filter, expected);
@ -110,6 +117,13 @@ public class CompositorTestServices : IDisposable
{
public List<Rect> Rects = new();
public int RenderedVisuals { get; private set; }
public void IncrementRenderedVisuals()
{
RenderedVisuals++;
}
public void RectInvalidated(Rect rc)
{
Rects.Add(rc);
@ -118,6 +132,7 @@ public class CompositorTestServices : IDisposable
public void Reset()
{
Rects.Clear();
RenderedVisuals = 0;
}
}
@ -218,4 +233,4 @@ public class DispatcherCompositorScheduler : ICompositorScheduler
{
Dispatcher.UIThread.Post(() => compositor.Commit(), DispatcherPriority.UiThreadRender);
}
}
}

Loading…
Cancel
Save