Browse Source

Merge branch 'master' into refactor/bindings

pull/13970/head
Steven Kirk 3 years ago
parent
commit
9751a330c3
  1. 1
      .gitignore
  2. 2
      .nuke/build.schema.json
  3. 13
      Avalonia.sln
  4. 150
      nukebuild/ApiDiffHelper.cs
  5. 16
      nukebuild/Build.cs
  6. 3
      nukebuild/_build.csproj
  7. 22
      packages/Avalonia/AvaloniaBuildTasks.targets
  8. 2
      readme.md
  9. 8
      src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs
  10. 90
      src/Avalonia.Base/Media/Pen.cs
  11. 2
      src/Avalonia.Base/Styling/ControlTheme.cs
  12. 2
      src/Avalonia.Base/Styling/Style.cs
  13. 10
      src/Avalonia.Base/Styling/StyleBase.cs
  14. 12
      src/Avalonia.Controls.DataGrid/Themes/Fluent.xaml
  15. 13
      src/Avalonia.Controls/ContextMenu.cs
  16. 7
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  17. 115
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  18. 63
      src/Avalonia.Controls/Shapes/Shape.cs
  19. 34
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  20. 2
      src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj
  21. 15
      src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs
  22. 4
      src/Avalonia.Native/WindowImplBase.cs
  23. 8
      src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs
  24. 33
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  25. 1
      src/iOS/Avalonia.iOS/Avalonia.iOS.csproj
  26. 57
      src/iOS/Avalonia.iOS/DispatcherImpl.cs
  27. 25
      src/iOS/Avalonia.iOS/Interop.cs
  28. 8
      src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs
  29. 188
      src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs
  30. 83
      tests/Avalonia.Base.UnitTests/Media/PenTests.cs
  31. 39
      tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs
  32. 26
      tests/Avalonia.Controls.UnitTests/EnumerableExtensions.cs
  33. 189
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  34. 95
      tests/Avalonia.LeakTests/TransitionTests.cs
  35. 19
      tests/Avalonia.UnitTests/NotifyingBase.cs

1
.gitignore

@ -217,3 +217,4 @@ node_modules
src/Browser/Avalonia.Browser.Blazor/webapp/package-lock.json
src/Browser/Avalonia.Browser.Blazor/wwwroot
src/Browser/Avalonia.Browser/wwwroot
api/diff

2
.nuke/build.schema.json

@ -83,6 +83,7 @@
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"OutputApiDiff",
"Package",
"RunCoreLibsTests",
"RunHtmlPreviewerTests",
@ -117,6 +118,7 @@
"CreateIntermediateNugetPackages",
"CreateNugetPackages",
"GenerateCppHeaders",
"OutputApiDiff",
"Package",
"RunCoreLibsTests",
"RunHtmlPreviewerTests",

13
Avalonia.sln

@ -236,6 +236,19 @@ EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{176582E8-46AF-416A-85C1-13A5C6744497}"
ProjectSection(SolutionItems) = preProject
.editorconfig = .editorconfig
azure-pipelines.yml = azure-pipelines.yml
azure-pipelines-integrationtests.yml = azure-pipelines-integrationtests.yml
CODE_OF_CONDUCT.md = CODE_OF_CONDUCT.md
CONTRIBUTING.md = CONTRIBUTING.md
Directory.Build.props = Directory.Build.props
Directory.Build.targets = Directory.Build.targets
dirs.proj = dirs.proj
global.json = global.json
licence.md = licence.md
NOTICE.md = NOTICE.md
NuGet.Config = NuGet.Config
readme.md = readme.md
Settings.StyleCop = Settings.StyleCop
EndProjectSection
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Controls.ItemsRepeater", "src\Avalonia.Controls.ItemsRepeater\Avalonia.Controls.ItemsRepeater.csproj", "{EE0F0DD4-A70D-472B-BD5D-B7D32D0E9386}"

150
nukebuild/ApiDiffValidation.cs → nukebuild/ApiDiffHelper.cs

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.IO.Compression;
using System.Linq;
@ -10,9 +11,95 @@ using System.Threading.Tasks;
using Nuke.Common.Tooling;
using static Serilog.Log;
public static class ApiDiffValidation
public static class ApiDiffHelper
{
private static readonly HttpClient s_httpClient = new();
static readonly HttpClient s_httpClient = new();
public static async Task GetDiff(
Tool apiDiffTool, string outputFolder,
string packagePath, string baselineVersion)
{
await using var baselineStream = await DownloadBaselinePackage(packagePath, baselineVersion);
if (baselineStream == null)
return;
if (!Directory.Exists(outputFolder))
{
Directory.CreateDirectory(outputFolder!);
}
using (var target = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.Read), ZipArchiveMode.Read))
using (var baseline = new ZipArchive(baselineStream, ZipArchiveMode.Read))
using (Helpers.UseTempDir(out var tempFolder))
{
var targetDlls = GetDlls(target);
var baselineDlls = GetDlls(baseline);
var pairs = new List<(string baseline, string target)>();
var packageId = GetPackageId(packagePath);
// Don't use Path.Combine with these left and right tool parameters.
// Microsoft.DotNet.ApiCompat.Tool is stupid and treats '/' and '\' as different assemblies in suppression files.
// So, always use Unix '/'
foreach (var baselineDll in baselineDlls)
{
var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);
var targetTfm = baselineDll.target;
if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm)).newTfm is {} newTfm)
{
targetTfm = newTfm;
}
var targetDll = targetDlls.FirstOrDefault(e =>
e.target.StartsWith(targetTfm) && e.entry.Name == baselineDll.entry.Name);
if (targetDll?.entry is null)
{
throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
}
var targetDllPath = await ExtractDll("target", targetDll, tempFolder);
pairs.Add((baselineDllPath, targetDllPath));
}
await Task.WhenAll(pairs.Select(p => Task.Run(() =>
{
var baselineApi = p.baseline + ".api.cs";
var targetApi = p.target + ".api.cs";
var resultDiff = p.target + ".api.diff.cs";
GenerateApiListing(apiDiffTool, p.baseline, baselineApi, tempFolder);
GenerateApiListing(apiDiffTool, p.target, targetApi, tempFolder);
var args = $"""-c core.autocrlf=false diff --no-index --minimal """;
args += """--ignore-matching-lines="^\[assembly: System.Reflection.AssemblyVersionAttribute" """;
args += $""" --output {resultDiff} {baselineApi} {targetApi}""";
using (var gitProcess = new Process())
{
gitProcess.StartInfo = new ProcessStartInfo
{
CreateNoWindow = true,
RedirectStandardError = false,
RedirectStandardOutput = false,
FileName = "git",
Arguments = args,
WorkingDirectory = tempFolder
};
gitProcess.Start();
gitProcess.WaitForExit();
}
var resultFile = new FileInfo(Path.Combine(tempFolder, resultDiff));
if (resultFile.Length > 0)
{
resultFile.CopyTo(Path.Combine(outputFolder, Path.GetFileName(resultDiff)), true);
}
})));
}
}
private static readonly (string oldTfm, string newTfm)[] s_tfmRedirects = new[]
{
@ -25,12 +112,6 @@ public static class ApiDiffValidation
Tool apiCompatTool, string packagePath, string baselineVersion,
string suppressionFilesFolder, bool updateSuppressionFile)
{
if (baselineVersion is null)
{
throw new InvalidOperationException(
"Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
}
if (!Directory.Exists(suppressionFilesFolder))
{
Directory.CreateDirectory(suppressionFilesFolder!);
@ -58,13 +139,7 @@ public static class ApiDiffValidation
// So, always use Unix '/'
foreach (var baselineDll in baselineDlls)
{
var baselineDllPath = $"baseline/{baselineDll.target}/{baselineDll.entry.Name}";
var baselineDllRealPath = Path.Combine(tempFolder, baselineDllPath);
Directory.CreateDirectory(Path.GetDirectoryName(baselineDllRealPath)!);
await using (var baselineDllFile = File.Create(baselineDllRealPath))
{
await baselineDll.entry.Open().CopyToAsync(baselineDllFile);
}
var baselineDllPath = await ExtractDll("baseline", baselineDll, tempFolder);
var targetTfm = baselineDll.target;
if (s_tfmRedirects.FirstOrDefault(t => baselineDll.target.StartsWith(t.oldTfm)).newTfm is {} newTfm)
@ -79,13 +154,7 @@ public static class ApiDiffValidation
throw new InvalidOperationException($"Some assemblies are missing in the new package {packageId}: {baselineDll.entry.Name} for {baselineDll.target}");
}
var targetDllPath = $"target/{targetDll.target}/{targetDll.entry.Name}";
var targetDllRealPath = Path.Combine(tempFolder, targetDllPath);
Directory.CreateDirectory(Path.GetDirectoryName(targetDllRealPath)!);
await using (var targetDllFile = File.Create(targetDllRealPath))
{
await targetDll.entry.Open().CopyToAsync(targetDllFile);
}
var targetDllPath = await ExtractDll("target", targetDll, tempFolder);
left.Add(baselineDllPath);
right.Add(targetDllPath);
@ -116,7 +185,9 @@ public static class ApiDiffValidation
}
}
private static IReadOnlyCollection<(string target, ZipArchiveEntry entry)> GetDlls(ZipArchive archive)
record DllEntry(string target, ZipArchiveEntry entry);
static IReadOnlyCollection<DllEntry> GetDlls(ZipArchive archive)
{
return archive.Entries
.Where(e => Path.GetExtension(e.FullName) == ".dll"
@ -130,12 +201,18 @@ public static class ApiDiffValidation
)
.GroupBy(e => (e.target, e.entry.Name))
.Select(g => g.MaxBy(e => e.isRef))
.Select(e => (e.target, e.entry))
.Select(e => new DllEntry(e.target, e.entry))
.ToArray();
}
static async Task<Stream> DownloadBaselinePackage(string packagePath, string baselineVersion)
{
if (baselineVersion is null)
{
throw new InvalidOperationException(
"Build \"api-baseline\" parameter must be set when running Nuke CreatePackages");
}
/*
Gets package name from versions like:
Avalonia.0.10.0-preview1
@ -167,6 +244,31 @@ public static class ApiDiffValidation
}
}
static async Task<string> ExtractDll(string basePath, DllEntry dllEntry, string targetFolder)
{
var dllPath = $"{basePath}/{dllEntry.target}/{dllEntry.entry.Name}";
var dllRealPath = Path.Combine(targetFolder, dllPath);
Directory.CreateDirectory(Path.GetDirectoryName(dllRealPath)!);
await using (var dllFile = File.Create(dllRealPath))
{
await dllEntry.entry.Open().CopyToAsync(dllFile);
}
return dllPath;
}
static void GenerateApiListing(Tool apiDiffTool, string inputFile, string outputFile, string workingDif)
{
var args = $""" --assembly={inputFile} --output-path={outputFile} --include-assembly-attributes=true""";
var result = apiDiffTool(args, workingDif)
.Where(t => t.Type == OutputType.Err).ToArray();
if (result.Any())
{
throw new AggregateException($"GetApi tool failed task has failed",
result.Select(r => new Exception(r.Text)));
}
}
static string GetPackageId(string packagePath)
{
return Regex.Replace(

16
nukebuild/Build.cs

@ -18,6 +18,7 @@ using static Nuke.Common.Tools.Xunit.XunitTasks;
using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
using static Serilog.Log;
using MicroCom.CodeGenerator;
using Nuke.Common.IO;
/*
Before editing this file, install support plugin for your IDE,
@ -33,6 +34,9 @@ partial class Build : NukeBuild
[PackageExecutable("Microsoft.DotNet.ApiCompat.Tool", "Microsoft.DotNet.ApiCompat.Tool.dll", Framework = "net6.0")]
Tool ApiCompatTool;
[PackageExecutable("Microsoft.DotNet.GenAPI.Tool", "Microsoft.DotNet.GenAPI.Tool.dll", Framework = "net8.0")]
Tool ApiGenTool;
protected override void OnBuildInitialized()
{
@ -283,11 +287,21 @@ partial class Build : NukeBuild
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffValidation.ValidatePackage(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.ValidatePackage(
ApiCompatTool, nugetPackage, Parameters.ApiValidationBaseline,
Parameters.ApiValidationSuppressionFiles, Parameters.UpdateApiValidationSuppression)));
});
Target OutputApiDiff => _ => _
.DependsOn(CreateNugetPackages)
.Executes(async () =>
{
await Task.WhenAll(
Directory.GetFiles(Parameters.NugetRoot, "*.nupkg").Select(nugetPackage => ApiDiffHelper.GetDiff(
ApiGenTool, RootDirectory / "api" / "diff",
nugetPackage, Parameters.ApiValidationBaseline)));
});
Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)

3
nukebuild/_build.csproj

@ -7,6 +7,8 @@
<NoWarn>$(NoWarn);CS0649;CS0169;SYSLIB0011</NoWarn>
<NukeTelemetryVersion>1</NukeTelemetryVersion>
<TargetFramework>net7.0</TargetFramework>
<!-- Necessary for Microsoft.DotNet.GenAPI.Tool -->
<RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet8-transport/nuget/v3/index.json</RestoreAdditionalProjectSources>
</PropertyGroup>
<Import Project="..\build\JetBrains.dotMemoryUnit.props" />
@ -24,6 +26,7 @@
</PackageReference>
<PackageDownload Include="Microsoft.DotNet.ApiCompat.Tool" Version="[7.0.305]" />
<PackageDownload Include="Microsoft.DotNet.GenAPI.Tool" Version="[8.0.101-servicing.23580.12]" />
</ItemGroup>
<ItemGroup>

22
packages/Avalonia/AvaloniaBuildTasks.targets

@ -131,6 +131,7 @@
<AvaloniaXamlIlVerifyIl Condition="'$(AvaloniaXamlIlVerifyIl)' == ''">false</AvaloniaXamlIlVerifyIl>
<AvaloniaXamlIlDebuggerLaunch Condition="'$(AvaloniaXamlIlDebuggerLaunch)' == ''">false</AvaloniaXamlIlDebuggerLaunch>
<AvaloniaXamlVerboseExceptions Condition="'$(AvaloniaXamlVerboseExceptions)' == ''">false</AvaloniaXamlVerboseExceptions>
<_AvaloniaHasCompiledXaml>true</_AvaloniaHasCompiledXaml>
</PropertyGroup>
<WriteLinesToFile
Condition="'$(_AvaloniaForceInternalMSBuild)' != 'true'"
@ -195,4 +196,25 @@
<Exec Command="dotnet exec --runtimeconfig &quot;$(APreviewerRuntimeConfigPath)&quot; --depsfile &quot;$(APreviewerDepsJsonPath)&quot; &quot;$(AvaloniaPreviewerNetCoreToolPath)&quot; --method html --html-url $(APreviewerUrl) --transport $(APreviewTransportUrl) &quot;$(APreviewExecutable)&quot;"/>
</Target>
<!--
Deletes the target ref assembly before the CopyRefAssembly task (in target CopyFilesToOutputDirectory) tries to access it.
CopyRefAssembly reads the ref assembly's MVID from the .mvid PE section to avoid copying if necessary.
However, Cecil doesn't preserve that PE section: this results in a warning.
By deleting the file beforehand, we're preventing the warning.
There are no changes in behavior since CopyRefAssembly always copy the file if it couldn't read the MVID.
-->
<Target
Name="AvaloniaDeleteRefAssemblyBeforeOutputCopy"
BeforeTargets="CopyFilesToOutputDirectory"
Condition="
'$(_AvaloniaHasCompiledXaml)' == 'true' and
'$(TargetRefPath)' != '' and
'$(ProduceReferenceAssembly)' == 'true' and
('$(CopyBuildOutputToOutputDirectory)' == '' or '$(CopyBuildOutputToOutputDirectory)' == 'true') and
'$(SkipCopyBuildProduct)' != 'true'">
<Delete Files="$(TargetRefPath)" Condition="Exists('$(TargetRefPath)')" />
</Target>
</Project>

2
readme.md

@ -8,7 +8,7 @@
## 📖 About
[Avalonia](https://avaloniaui.net) is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of platforms such as Windows, macOS, Linux, iOS, Android and WebAssembly. Avalonia is mature and production ready and is used by companies, including [Schneider Electric](https://avaloniaui.net/showcase#se), [Unity](https://avaloniaui.net/showcase#unity), [JetBrains](https://avaloniaui.net/showcase#rider) and [Github](https://avaloniaui.net/showcase#github).
[Avalonia](https://avaloniaui.net) is a cross-platform UI framework for dotnet, providing a flexible styling system and supporting a wide range of platforms such as Windows, macOS, Linux, iOS, Android and WebAssembly. Avalonia is mature and production ready and is used by companies, including [Schneider Electric](https://avaloniaui.net/showcase#se), [Unity](https://avaloniaui.net/showcase#unity), [JetBrains](https://avaloniaui.net/showcase#rider) and [GitHub](https://avaloniaui.net/showcase#github).
Considered by many to be the spiritual successor to WPF, Avalonia UI provides a familiar, modern development experience for XAML developers creating cross-platform applications. While Avalonia UI is [similar to WPF](https://docs.avaloniaui.net/docs/next/get-started/wpf/), it isn't a 1:1 copy, and you'll find plenty of improvements.

8
src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs

@ -1,12 +1,10 @@
using System;
using System.Collections.Generic;
using System.Runtime.Versioning;
using System.Threading;
using Android.App;
using Android.Content;
using Android.Graphics;
using Android.Graphics.Drawables;
using Android.OS;
using Android.Runtime;
using Android.Text;
using Android.Views;
@ -27,10 +25,8 @@ using Avalonia.OpenGL.Egl;
using Avalonia.OpenGL.Surfaces;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Java.Lang;
using static System.Net.Mime.MediaTypeNames;
using ClipboardManager = Android.Content.ClipboardManager;
namespace Avalonia.Android.Platform.SkiaPlatform
@ -76,6 +72,8 @@ namespace Avalonia.Android.Platform.SkiaPlatform
_transparencyLevel = WindowTransparencyLevel.None;
_systemNavigationManager = new AndroidSystemNavigationManagerImpl(avaloniaView.Context as IActivityNavigationService);
Surfaces = new object[] { _gl, _framebuffer, Handle };
}
public virtual Point GetAvaloniaPointFromEvent(MotionEvent e, int pointerIndex) =>
@ -107,7 +105,7 @@ namespace Avalonia.Android.Platform.SkiaPlatform
public IPlatformHandle Handle => _view;
public IEnumerable<object> Surfaces => new object[] { _gl, _framebuffer, Handle };
public IEnumerable<object> Surfaces { get; }
public Compositor Compositor => AndroidPlatform.Compositor;

90
src/Avalonia.Base/Media/Pen.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
using Avalonia.Media.Immutable;
using Avalonia.Rendering.Composition;
using Avalonia.Rendering.Composition.Drawing;
@ -56,8 +59,7 @@ namespace Avalonia.Media
/// Initializes a new instance of the <see cref="Pen"/> class.
/// </summary>
public Pen()
{
}
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
@ -75,8 +77,7 @@ namespace Avalonia.Media
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0) : this(new SolidColorBrush(color), thickness, dashStyle, lineCap, lineJoin, miterLimit)
{
}
{ }
/// <summary>
/// Initializes a new instance of the <see cref="Pen"/> class.
@ -178,6 +179,69 @@ namespace Avalonia.Media
MiterLimit);
}
/// <summary>
/// Smart reuse and update pen properties.
/// </summary>
/// <param name="pen">Old pen to modify.</param>
/// <param name="brush">The brush used to draw.</param>
/// <param name="thickness">The stroke thickness.</param>
/// <param name="strokeDashArray">The stroke dask array.</param>
/// <param name="strokeDaskOffset">The stroke dask offset.</param>
/// <param name="lineCap">The line cap.</param>
/// <param name="lineJoin">The line join.</param>
/// <param name="miterLimit">The miter limit.</param>
/// <returns>If a new instance was created and visual invalidation required.</returns>
internal static bool TryModifyOrCreate(ref IPen? pen,
IBrush? brush,
double thickness,
IList<double>? strokeDashArray = null,
double strokeDaskOffset = default,
PenLineCap lineCap = PenLineCap.Flat,
PenLineJoin lineJoin = PenLineJoin.Miter,
double miterLimit = 10.0)
{
var previousPen = pen;
if (brush is null)
{
pen = null;
return previousPen is not null;
}
IDashStyle? dashStyle = null;
if (strokeDashArray is { Count: > 0 })
{
// strokeDashArray can be IList (instead of AvaloniaList) in future
// So, if it supports notification - create a mutable DashStyle
dashStyle = strokeDashArray is INotifyCollectionChanged
? new DashStyle(strokeDashArray, strokeDaskOffset)
: new ImmutableDashStyle(strokeDashArray, strokeDaskOffset);
}
if (brush is IImmutableBrush immutableBrush && dashStyle is null or ImmutableDashStyle)
{
pen = new ImmutablePen(
immutableBrush,
thickness,
(ImmutableDashStyle?)dashStyle,
lineCap,
lineJoin,
miterLimit);
return true;
}
var mutablePen = previousPen as Pen ?? new Pen();
mutablePen.Brush = brush;
mutablePen.Thickness = thickness;
mutablePen.LineCap = lineCap;
mutablePen.LineJoin = lineJoin;
mutablePen.DashStyle = dashStyle;
mutablePen.MiterLimit = miterLimit;
pen = mutablePen;
return !Equals(previousPen, pen);
}
void RegisterForSerialization()
{
_resource.RegisterForInvalidationOnAllCompositors(this);
@ -186,21 +250,21 @@ namespace Avalonia.Media
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
RegisterForSerialization();
if (change.Property == BrushProperty)
if (change.Property == BrushProperty)
_resource.ProcessPropertyChangeNotification(change);
if(change.Property == DashStyleProperty)
if (change.Property == DashStyleProperty)
UpdateDashStyleSubscription();
base.OnPropertyChanged(change);
}
void UpdateDashStyleSubscription()
{
var newValue = _resource.IsAttached ? DashStyle as DashStyle : null;
if(ReferenceEquals(_subscribedToDashes, newValue))
if (ReferenceEquals(_subscribedToDashes, newValue))
return;
if (_subscribedToDashes != null && _weakSubscriber != null)
@ -221,9 +285,9 @@ namespace Avalonia.Media
_subscribedToDashes = newValue;
}
}
private CompositorResourceHolder<ServerCompositionSimplePen> _resource;
IPen ICompositionRenderResource<IPen>.GetForCompositor(Compositor c) => _resource.GetForCompositor(c);
void ICompositionRenderResource.AddRefOnCompositor(Compositor c)

2
src/Avalonia.Base/Styling/ControlTheme.cs

@ -48,7 +48,7 @@ namespace Avalonia.Styling
if (HasSettersOrAnimations && TargetType.IsAssignableFrom(StyledElement.GetStyleKey(target)))
{
Attach(target, null, type);
Attach(target, null, type, true);
return SelectorMatchResult.AlwaysThisType;
}

2
src/Avalonia.Base/Styling/Style.cs

@ -74,7 +74,7 @@ namespace Avalonia.Styling
if (match.IsMatch)
{
Attach(target, match.Activator, type);
Attach(target, match.Activator, type, Selector is not OrSelector);
}
result = match.Result;

10
src/Avalonia.Base/Styling/StyleBase.cs

@ -92,20 +92,24 @@ namespace Avalonia.Styling
return false;
}
internal ValueFrame Attach(StyledElement target, IStyleActivator? activator, FrameType type)
internal ValueFrame Attach(
StyledElement target,
IStyleActivator? activator,
FrameType type,
bool canShareInstance)
{
if (target is not AvaloniaObject ao)
throw new InvalidOperationException("Styles can only be applied to AvaloniaObjects.");
StyleInstance instance;
if (_sharedInstance is not null)
if (_sharedInstance is not null && canShareInstance)
{
instance = _sharedInstance;
}
else
{
var canShareInstance = activator is null;
canShareInstance &= activator is null;
instance = new StyleInstance(this, activator, type);

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

@ -15,6 +15,10 @@
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
@ -34,6 +38,10 @@
<SolidColorBrush x:Key="DataGridRowGroupHeaderHoveredBackgroundBrush" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowHoveredBackgroundColor" Color="{DynamicResource SystemListLowColor}" />
<SolidColorBrush x:Key="DataGridRowInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualPrimaryBrush" Color="{DynamicResource SystemBaseHighColor}" />
<SolidColorBrush x:Key="DataGridCellFocusVisualSecondaryBrush" Color="{DynamicResource SystemAltMediumColor}" />
<SolidColorBrush x:Key="DataGridCellInvalidBrush" Color="{DynamicResource SystemErrorTextColor}" />
@ -53,13 +61,9 @@
<StreamGeometry x:Key="DataGridRowGroupHeaderIconOpenedPath">M109 486 19 576 1024 1581 2029 576 1939 486 1024 1401z</StreamGeometry>
<StaticResource x:Key="DataGridRowBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<SolidColorBrush x:Key="DataGridRowSelectedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedUnfocusedBackgroundOpacity" ResourceKey="ListAccentLowOpacity" />
<SolidColorBrush x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundBrush" Color="{DynamicResource SystemAccentColor}" />
<StaticResource x:Key="DataGridRowSelectedHoveredUnfocusedBackgroundOpacity" ResourceKey="ListAccentMediumOpacity" />
<StaticResource x:Key="DataGridCellBackgroundBrush" ResourceKey="SystemControlTransparentBrush" />
<StaticResource x:Key="DataGridCurrencyVisualPrimaryBrush" ResourceKey="SystemControlTransparentBrush" />

13
src/Avalonia.Controls/ContextMenu.cs

@ -265,7 +265,7 @@ namespace Avalonia.Controls
}
control ??= _attachedControls![0];
Open(control, PlacementTarget ?? control, false);
Open(control, PlacementTarget ?? control, Placement);
}
/// <summary>
@ -303,7 +303,7 @@ namespace Avalonia.Controls
remove => _popupHostChangedHandler -= value;
}
private void Open(Control control, Control placementTarget, bool requestedByPointer)
private void Open(Control control, Control placementTarget, PlacementMode placement)
{
if (IsOpen)
{
@ -330,9 +330,7 @@ namespace Avalonia.Controls
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Placement = !requestedByPointer && Placement == PlacementMode.Pointer
? PlacementMode.Bottom
: Placement;
_popup.Placement = placement;
//Position of the line below is really important.
//All styles are being applied only when control has logical parent.
@ -420,7 +418,10 @@ namespace Avalonia.Controls
&& !contextMenu.CancelOpening())
{
var requestedByPointer = e.TryGetPosition(null, out _);
contextMenu.Open(control, e.Source as Control ?? control, requestedByPointer);
contextMenu.Open(
control,
e.Source as Control ?? control,
requestedByPointer ? contextMenu.Placement : PlacementMode.Bottom);
e.Handled = true;
}
}

7
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -169,7 +169,12 @@ namespace Avalonia.Controls.Presenters
/// </summary>
static ContentPresenter()
{
AffectsRender<ContentPresenter>(BackgroundProperty, BorderBrushProperty, BorderThicknessProperty, CornerRadiusProperty);
AffectsRender<ContentPresenter>(
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty,
BoxShadowProperty);
AffectsArrange<ContentPresenter>(HorizontalContentAlignmentProperty, VerticalContentAlignmentProperty);
AffectsMeasure<ContentPresenter>(BorderThicknessProperty, PaddingProperty);
}

115
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -1,6 +1,5 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
@ -8,7 +7,6 @@ using System.Linq;
using Avalonia.Controls.Selection;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Threading;
@ -187,14 +185,21 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public int SelectedIndex
{
get =>
get
{
// When a Begin/EndInit/DataContext update is in place we return the value to be
// updated here, even though it's not yet active and the property changed notification
// has not yet been raised. If we don't do this then the old value will be written back
// to the source when two-way bound, and the update value will be lost.
_updateState?.SelectedIndex.HasValue == true ?
_updateState.SelectedIndex.Value :
Selection.SelectedIndex;
if (_updateState is not null)
{
return _updateState.SelectedIndex.HasValue ?
_updateState.SelectedIndex.Value :
TryGetExistingSelection()?.SelectedIndex ?? -1;
}
return Selection.SelectedIndex;
}
set
{
if (_updateState is object)
@ -213,11 +218,18 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public object? SelectedItem
{
get =>
// See SelectedIndex setter for more information.
_updateState?.SelectedItem.HasValue == true ?
_updateState.SelectedItem.Value :
Selection.SelectedItem;
get
{
// See SelectedIndex getter for more information.
if (_updateState is not null)
{
return _updateState.SelectedItem.HasValue ?
_updateState.SelectedItem.Value :
TryGetExistingSelection()?.SelectedItem;
}
return Selection.SelectedItem;
}
set
{
if (_updateState is object)
@ -270,6 +282,7 @@ namespace Avalonia.Controls.Primitives
{
return _updateState.SelectedItems.Value;
}
else if (Selection is InternalSelectionModel ism)
{
var result = ism.WritableSelectedItems;
@ -456,10 +469,8 @@ namespace Avalonia.Controls.Primitives
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (Selection?.AnchorIndex is int index)
{
AutoScrollToSelectedItemIfNecessary(index);
}
AutoScrollToSelectedItemIfNecessary(GetAnchorIndex());
}
/// <inheritdoc />
@ -470,10 +481,8 @@ namespace Avalonia.Controls.Primitives
void ExecuteScrollWhenLayoutUpdated(object? sender, EventArgs e)
{
LayoutUpdated -= ExecuteScrollWhenLayoutUpdated;
if (Selection?.AnchorIndex is int index)
{
AutoScrollToSelectedItemIfNecessary(index);
}
AutoScrollToSelectedItemIfNecessary(GetAnchorIndex());
}
if (AutoScrollToSelectedItem)
@ -482,6 +491,15 @@ namespace Avalonia.Controls.Primitives
}
}
internal int GetAnchorIndex()
{
var selection = _updateState is not null ? TryGetExistingSelection() : Selection;
return selection?.AnchorIndex ?? -1;
}
private ISelectionModel? TryGetExistingSelection()
=> _updateState?.Selection.HasValue == true ? _updateState.Selection.Value : _selection;
protected internal override void PrepareContainerForItemOverride(Control container, object? item, int index)
{
// Ensure that the selection model is created at this point so that accessing it in
@ -634,10 +652,7 @@ namespace Avalonia.Controls.Primitives
if (change.Property == AutoScrollToSelectedItemProperty)
{
if (Selection?.AnchorIndex is int index)
{
AutoScrollToSelectedItemIfNecessary(index);
}
AutoScrollToSelectedItemIfNecessary(GetAnchorIndex());
}
else if (change.Property == SelectionModeProperty && _selection is object)
{
@ -671,7 +686,7 @@ namespace Avalonia.Controls.Primitives
return;
}
var value = change.GetNewValue<IBinding>();
var value = change.GetNewValue<IBinding?>();
if (value is null)
{
// Clearing SelectedValueBinding makes the SelectedValue the item itself
@ -921,11 +936,10 @@ namespace Avalonia.Controls.Primitives
if (e.PropertyName == nameof(ISelectionModel.AnchorIndex))
{
_hasScrolledToSelectedItem = false;
if (Selection?.AnchorIndex is int index)
{
KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(index));
AutoScrollToSelectedItemIfNecessary(index);
}
var anchorIndex = GetAnchorIndex();
KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(anchorIndex));
AutoScrollToSelectedItemIfNecessary(anchorIndex);
}
else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex)
{
@ -1279,9 +1293,17 @@ namespace Avalonia.Controls.Primitives
state.SelectedItem = item;
}
// SelectedIndex vs SelectedItem:
// - If only one has a value, use it
// - If both have a value, prefer the one having a "non-empty" value, e.g. not -1 nor null
// - If both have a "non-empty" value, prefer the index
if (state.SelectedIndex.HasValue)
{
SelectedIndex = state.SelectedIndex.Value;
var selectedIndex = state.SelectedIndex.Value;
if (selectedIndex >= 0 || !state.SelectedItem.HasValue)
SelectedIndex = selectedIndex;
else
SelectedItem = state.SelectedItem.Value;
}
else if (state.SelectedItem.HasValue)
{
@ -1338,39 +1360,12 @@ namespace Avalonia.Controls.Primitives
// - Both the old and new SelectionModels have the incorrect Source
private class UpdateState
{
private Optional<int> _selectedIndex;
private Optional<object?> _selectedItem;
private Optional<object?> _selectedValue;
public int UpdateCount { get; set; }
public Optional<ISelectionModel> Selection { get; set; }
public Optional<IList?> SelectedItems { get; set; }
public Optional<int> SelectedIndex
{
get => _selectedIndex;
set
{
_selectedIndex = value;
_selectedItem = default;
}
}
public Optional<object?> SelectedItem
{
get => _selectedItem;
set
{
_selectedItem = value;
_selectedIndex = default;
}
}
public Optional<object?> SelectedValue
{
get => _selectedValue;
set => _selectedValue = value;
}
public Optional<int> SelectedIndex { get; set; }
public Optional<object?> SelectedItem { get; set; }
public Optional<object?> SelectedValue { get; set; }
}
/// <summary>

63
src/Avalonia.Controls/Shapes/Shape.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -62,14 +63,7 @@ namespace Avalonia.Controls.Shapes
private Matrix _transform = Matrix.Identity;
private Geometry? _definingGeometry;
private Geometry? _renderedGeometry;
static Shape()
{
AffectsMeasure<Shape>(StretchProperty, StrokeThicknessProperty);
AffectsRender<Shape>(FillProperty, StrokeProperty, StrokeDashArrayProperty, StrokeDashOffsetProperty,
StrokeThicknessProperty, StrokeLineCapProperty, StrokeJoinProperty);
}
private IPen? _strokePen;
/// <summary>
/// Gets a value that represents the <see cref="Geometry"/> of the shape.
@ -199,30 +193,7 @@ namespace Avalonia.Controls.Shapes
if (geometry != null)
{
var stroke = Stroke;
ImmutablePen? pen = null;
if (stroke != null)
{
var strokeDashArray = StrokeDashArray;
ImmutableDashStyle? dashStyle = null;
if (strokeDashArray != null && strokeDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(strokeDashArray, StrokeDashOffset);
}
pen = new ImmutablePen(
stroke.ToImmutable(),
StrokeThickness,
dashStyle,
StrokeLineCap,
StrokeJoin);
}
context.DrawGeometry(Fill, pen, geometry);
context.DrawGeometry(Fill, _strokePen, geometry);
}
}
@ -266,6 +237,34 @@ namespace Avalonia.Controls.Shapes
InvalidateMeasure();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == StrokeProperty
|| change.Property == StrokeThicknessProperty
|| change.Property == StrokeDashArrayProperty
|| change.Property == StrokeDashOffsetProperty
|| change.Property == StrokeLineCapProperty
|| change.Property == StrokeJoinProperty)
{
if (change.Property == StrokeProperty
|| change.Property == StrokeThicknessProperty)
{
InvalidateMeasure();
}
if (!Pen.TryModifyOrCreate(ref _strokePen, Stroke, StrokeThickness, StrokeDashArray, StrokeDashOffset, StrokeLineCap, StrokeJoin))
{
InvalidateVisual();
}
}
else if (change.Property == FillProperty)
{
InvalidateVisual();
}
}
protected override Size MeasureOverride(Size availableSize)
{
if (DefiningGeometry is null)

34
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -17,6 +17,7 @@ namespace Avalonia.Controls.Utils
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private bool _initialized;
private IPen? _cachedPen;
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
@ -87,22 +88,17 @@ namespace Avalonia.Controls.Utils
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush? background, IBrush? borderBrush, BoxShadows boxShadows, double borderDashOffset = 0,
PenLineCap borderLineCap = PenLineCap.Flat, PenLineJoin borderLineJoin = PenLineJoin.Miter,
AvaloniaList<double>? borderDashArray = null)
IBrush? background, IBrush? borderBrush, BoxShadows boxShadows)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows, borderDashOffset, borderLineCap, borderLineJoin,
borderDashArray);
RenderCore(context, background, borderBrush, boxShadows);
}
void RenderCore(DrawingContext context, IBrush? background, IBrush? borderBrush, BoxShadows boxShadows,
double borderDashOffset, PenLineCap borderLineCap, PenLineJoin borderLineJoin,
AvaloniaList<double>? borderDashArray)
void RenderCore(DrawingContext context, IBrush? background, IBrush? borderBrush, BoxShadows boxShadows)
{
if (_useComplexRendering)
{
@ -121,26 +117,8 @@ namespace Avalonia.Controls.Utils
else
{
var borderThickness = _borderThickness.Top;
IPen? pen = null;
ImmutableDashStyle? dashStyle = null;
if (borderDashArray != null && borderDashArray.Count > 0)
{
dashStyle = new ImmutableDashStyle(borderDashArray, borderDashOffset);
}
if (borderBrush != null && borderThickness > 0)
{
pen = new ImmutablePen(
borderBrush.ToImmutable(),
borderThickness,
dashStyle,
borderLineCap,
borderLineJoin);
}
Pen.TryModifyOrCreate(ref _cachedPen, borderBrush, borderThickness);
var rect = new Rect(_size);
if (!MathUtilities.IsZero(borderThickness))
@ -148,7 +126,7 @@ namespace Avalonia.Controls.Utils
var rrect = new RoundedRect(rect, _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
context.DrawRectangle(background, pen, rrect, boxShadows);
context.DrawRectangle(background, _cachedPen, rrect, boxShadows);
}
}

2
src/Avalonia.FreeDesktop/Avalonia.FreeDesktop.csproj

@ -13,7 +13,7 @@
<ItemGroup>
<PackageReference Include="Tmds.DBus.Protocol" Version="0.15.0" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.11" PrivateAssets="All" />
<PackageReference Include="Tmds.DBus.SourceGenerator" Version="0.0.13" PrivateAssets="All" />
</ItemGroup>
<ItemGroup>

15
src/Avalonia.FreeDesktop/DBusIme/DBusTextInputMethodBase.cs

@ -179,12 +179,23 @@ namespace Avalonia.FreeDesktop.DBusIme
_disposables.Add(d);
}
public void Dispose()
public async void Dispose()
{
foreach(var d in _disposables)
d.Dispose();
_disposables.Clear();
_ = DisconnectAsync();
if (!IsConnected)
return;
try
{
await DisconnectAsync();
}
catch (Exception ex)
{
Logger.TryGet(LogEventLevel.Error, "IME")
?.Log(this, "Error while destroying the context:\n" + ex);
}
_currentName = null;
}

4
src/Avalonia.Native/WindowImplBase.cs

@ -10,10 +10,8 @@ using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Platform.Storage;
using Avalonia.Rendering;
using Avalonia.Rendering.Composition;
using Avalonia.Threading;
@ -58,7 +56,6 @@ namespace Avalonia.Native
private readonly IKeyboardDevice _keyboard;
private readonly ICursorFactory _cursorFactory;
private Size _savedLogicalSize;
private Size _lastRenderedLogicalSize;
private double _savedScaling;
private NativeControlHostImpl _nativeControlHost;
private IStorageProvider _storageProvider;
@ -172,7 +169,6 @@ namespace Avalonia.Native
if (_parent._native != null && _target != null)
{
cb(_parent._native);
_parent._lastRenderedLogicalSize = _parent._savedLogicalSize;
}
}
}, (int)w, (int)h, new Vector(dpi, dpi));

8
src/Avalonia.OpenGL/Controls/OpenGlControlBase.cs

@ -220,15 +220,15 @@ namespace Avalonia.OpenGL.Controls
[Obsolete("Use RequestNextFrameRendering()"), EditorBrowsable(EditorBrowsableState.Never)]
// ReSharper disable once MemberCanBeProtected.Global
public new void InvalidateVisual() => RequestNextFrameRendering();
public new void InvalidateVisual() => RequestNextFrameRendering();
public void RequestNextFrameRendering()
{
if ((_initialization == null || _initialization is { Status: TaskStatus.RanToCompletion }) &&
!_updateQueued)
!_updateQueued && _compositor != null)
{
_updateQueued = true;
_compositor?.RequestCompositionUpdate(_update);
_compositor.RequestCompositionUpdate(_update);
}
}

33
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -81,15 +81,11 @@
SharedSizeGroup="MenuItemChevron" />
</Grid.ColumnDefinitions>
<Viewbox Name="PART_IconPresenter"
Margin="{DynamicResource MenuIconPresenterMargin}"
StretchDirection="DownOnly"
HorizontalAlignment="Center"
VerticalAlignment="Center"
IsVisible="False"
Width="16" Height="16">
<ContentPresenter Content="{TemplateBinding Icon}"/>
</Viewbox>
<ContentControl x:Name="PART_IconPresenter"
Theme="{StaticResource FluentMenuItemIconTheme}"
Content="{TemplateBinding Icon}"
IsVisible="False"
Margin="{DynamicResource MenuIconPresenterMargin}" />
<ContentPresenter Name="PART_HeaderPresenter"
Content="{TemplateBinding Header}"
@ -143,7 +139,7 @@
</ControlTemplate>
</Setter>
<Style Selector="^:icon /template/ Viewbox#PART_IconPresenter">
<Style Selector="^:icon /template/ ContentControl#PART_IconPresenter">
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="^:selected">
@ -210,4 +206,21 @@
<Setter Property="Padding" Value="{DynamicResource HorizontalMenuFlyoutItemThemePaddingNarrow}" />
<Setter Property="Margin" Value="{DynamicResource HorizontalMenuFlyoutItemMargin}" />
</ControlTheme>
<ControlTheme x:Key="FluentMenuItemIconTheme"
TargetType="ContentControl">
<Setter Property="Width"
Value="16" />
<Setter Property="Height"
Value="16" />
<Setter Property="Template">
<ControlTemplate>
<Viewbox
StretchDirection="DownOnly"
HorizontalAlignment="Center"
VerticalAlignment="Center">
<ContentPresenter x:Name="PART_ContentPresenter" Content="{TemplateBinding Content}" />
</Viewbox>
</ControlTemplate>
</Setter>
</ControlTheme>
</ResourceDictionary>

1
src/iOS/Avalonia.iOS/Avalonia.iOS.csproj

@ -3,6 +3,7 @@
<TargetFramework>net7.0-ios16.0</TargetFramework>
<SupportedOSPlatformVersion>13.0</SupportedOSPlatformVersion>
<MSBuildEnableWorkloadResolver>true</MSBuildEnableWorkloadResolver>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.Base\Avalonia.Base.csproj" />

57
src/iOS/Avalonia.iOS/DispatcherImpl.cs

@ -2,12 +2,11 @@
using System;
using System.Diagnostics;
using System.Runtime.InteropServices;
using System.Threading;
using Avalonia.Threading;
using CoreFoundation;
using Foundation;
using ObjCRuntime;
using CFIndex = System.IntPtr;
namespace Avalonia.iOS;
@ -20,30 +19,28 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
internal static readonly DispatcherImpl Instance = new();
private readonly Stopwatch _clock = Stopwatch.StartNew();
private readonly Action _checkSignaledAction;
private readonly Action _wakeUpLoopAction;
private readonly object _sync = new();
private readonly IntPtr _timer;
private readonly IntPtr _mainLoop;
private readonly IntPtr _mainQueue;
private Thread? _loopThread;
private bool _backgroundProcessingRequested, _signaled;
private DispatcherImpl()
private unsafe DispatcherImpl()
{
_checkSignaledAction = CheckSignaled;
_wakeUpLoopAction = () =>
{
// This is needed to wakeup the loop if we are called from inside of BeforeWait hook
};
_mainLoop = Interop.CFRunLoopGetMain();
_mainQueue = DispatchQueue.MainQueue.Handle.Handle;
var observer = Interop.CFRunLoopObserverCreate(IntPtr.Zero,
Interop.CFOptionFlags.kCFRunLoopAfterWaiting | Interop.CFOptionFlags.kCFRunLoopBeforeSources |
Interop.CFOptionFlags.kCFRunLoopBeforeWaiting,
true, 0, ObserverCallback, IntPtr.Zero);
Interop.CFRunLoopAddObserver(Interop.CFRunLoopGetMain(), observer, Interop.kCFRunLoopCommonModes);
1, 0, &ObserverCallback, IntPtr.Zero);
Interop.CFRunLoopAddObserver(_mainLoop, observer, Interop.kCFRunLoopDefaultMode);
_timer = Interop.CFRunLoopTimerCreate(IntPtr.Zero,
Interop.CFAbsoluteTimeGetCurrent() + DistantFutureInterval,
DistantFutureInterval, 0, 0, TimerCallback, IntPtr.Zero);
Interop.CFRunLoopAddTimer(Interop.CFRunLoopGetMain(), _timer, Interop.kCFRunLoopCommonModes);
DistantFutureInterval, 0, 0, &TimerCallback, IntPtr.Zero);
Interop.CFRunLoopAddTimer(_mainLoop, _timer, Interop.kCFRunLoopDefaultMode);
}
public event Action? Signaled;
@ -63,16 +60,16 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
}
}
public void Signal()
public unsafe void Signal()
{
lock (this)
lock (_sync)
{
if (_signaled)
return;
_signaled = true;
DispatchQueue.MainQueue.DispatchAsync(_checkSignaledAction);
CFRunLoop.Main.WakeUp();
Interop.dispatch_async_f(_mainQueue, IntPtr.Zero, &CheckSignaled);
Interop.CFRunLoopWakeUp(_mainLoop);
}
}
@ -86,18 +83,18 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
public long Now => _clock.ElapsedMilliseconds;
public void RequestBackgroundProcessing()
public unsafe void RequestBackgroundProcessing()
{
if (_backgroundProcessingRequested)
return;
_backgroundProcessingRequested = true;
DispatchQueue.MainQueue.DispatchAsync(_wakeUpLoopAction);
Interop.dispatch_async_f(_mainQueue, IntPtr.Zero, &WakeUpCallback);
}
private void CheckSignaled()
{
bool signaled;
lock (this)
lock (_sync)
{
signaled = _signaled;
_signaled = false;
@ -109,13 +106,25 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
}
}
[MonoPInvokeCallback(typeof(Interop.CFRunLoopObserverCallback))]
[UnmanagedCallersOnly]
private static void CheckSignaled(IntPtr context)
{
Instance.CheckSignaled();
}
[UnmanagedCallersOnly]
private static void WakeUpCallback(IntPtr context)
{
}
[UnmanagedCallersOnly]
private static void ObserverCallback(IntPtr observer, Interop.CFOptionFlags activity, IntPtr info)
{
if (activity == Interop.CFOptionFlags.kCFRunLoopBeforeWaiting)
{
bool triggerProcessing;
lock (Instance)
lock (Instance._sync)
{
triggerProcessing = Instance._backgroundProcessingRequested;
Instance._backgroundProcessingRequested = false;
@ -127,7 +136,7 @@ internal class DispatcherImpl : IDispatcherImplWithExplicitBackgroundProcessing
Instance.CheckSignaled();
}
[MonoPInvokeCallback(typeof(Interop.CFRunLoopTimerCallback))]
[UnmanagedCallersOnly]
private static void TimerCallback(IntPtr timer, IntPtr info)
{
Instance.Timer?.Invoke();

25
src/iOS/Avalonia.iOS/Interop.cs

@ -7,11 +7,11 @@ using ObjCRuntime;
namespace Avalonia.iOS;
// TODO: use LibraryImport in NET7
internal class Interop
internal unsafe class Interop
{
internal const string CoreFoundationLibrary = "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation";
internal static NativeHandle kCFRunLoopCommonModes = CFString.CreateNative("kCFRunLoopCommonModes");
internal const string libcLibrary = "/usr/lib/libc.dylib";
internal static NativeHandle kCFRunLoopDefaultMode = CFString.CreateNative("kCFRunLoopDefaultMode");
[Flags]
internal enum CFOptionFlags : ulong
@ -20,26 +20,29 @@ internal class Interop
kCFRunLoopAfterWaiting = (1UL << 6),
kCFRunLoopBeforeWaiting = (1UL << 5)
}
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void CFRunLoopObserverCallback(IntPtr observer, CFOptionFlags activity, IntPtr info);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
internal delegate void CFRunLoopTimerCallback(IntPtr timer, IntPtr info);
[DllImport(libcLibrary)]
internal static extern void dispatch_async_f(IntPtr queue, IntPtr context, delegate* unmanaged<IntPtr, void> dispatch);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopGetMain();
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopGetCurrent();
[DllImport (CoreFoundationLibrary)]
internal static extern void CFRunLoopWakeUp(IntPtr rl);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopObserverCreate(IntPtr allocator, CFOptionFlags activities,
bool repeats, int index, CFRunLoopObserverCallback callout, IntPtr context);
int repeats, int index, delegate* unmanaged<IntPtr, CFOptionFlags, IntPtr, void> callout, IntPtr context);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopAddObserver(IntPtr loop, IntPtr observer, IntPtr mode);
[DllImport(CoreFoundationLibrary)]
internal static extern IntPtr CFRunLoopTimerCreate(IntPtr allocator, double firstDate, double interval,
CFOptionFlags flags, int order, CFRunLoopTimerCallback callout, IntPtr context);
CFOptionFlags flags, int order, delegate* unmanaged<IntPtr, IntPtr, void> callout, IntPtr context);
[DllImport(CoreFoundationLibrary)]
internal static extern void CFRunLoopTimerSetTolerance(IntPtr timer, double tolerance);

8
src/iOS/Avalonia.iOS/Storage/IOSSecurityScopedStream.cs

@ -13,13 +13,15 @@ internal sealed class IOSSecurityScopedStream : Stream
private readonly UIDocument _document;
private readonly FileStream _stream;
private readonly NSUrl _url;
private readonly NSUrl _securityScopedAncestorUrl;
internal IOSSecurityScopedStream(NSUrl url, FileAccess access)
internal IOSSecurityScopedStream(NSUrl url, NSUrl securityScopedAncestorUrl, FileAccess access)
{
_document = new UIDocument(url);
var path = _document.FileUrl.Path!;
_url = url;
_url.StartAccessingSecurityScopedResource();
_securityScopedAncestorUrl = securityScopedAncestorUrl;
_securityScopedAncestorUrl.StartAccessingSecurityScopedResource();
_stream = File.Open(path, FileMode.Open, access);
}
@ -60,7 +62,7 @@ internal sealed class IOSSecurityScopedStream : Stream
{
_stream.Dispose();
_document.Dispose();
_url.StopAccessingSecurityScopedResource();
_securityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
}

188
src/iOS/Avalonia.iOS/Storage/IOSStorageItem.cs

@ -17,9 +17,10 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
{
private readonly string _filePath;
protected IOSStorageItem(NSUrl url)
protected IOSStorageItem(NSUrl url, NSUrl? securityScopedAncestorUrl = null)
{
Url = url ?? throw new ArgumentNullException(nameof(url));
SecurityScopedAncestorUrl = securityScopedAncestorUrl ?? url;
using (var doc = new UIDocument(url))
{
@ -32,6 +33,11 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
}
internal NSUrl Url { get; }
// Calling StartAccessingSecurityScopedResource on items retrieved from, or created in a folder
// fails, because only folders directly opened via StorageProvider.OpenFolderPickerAsync have
// security-scoped NSUrls. This property stores and exposes that ancestor's Url, so we can have
// recursive access to an opened folder.
internal NSUrl SecurityScopedAncestorUrl { get; }
internal string FilePath => _filePath;
public bool CanBookmark => true;
@ -57,39 +63,59 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
public Task<IStorageFolder?> GetParentAsync()
{
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent()));
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(Url.RemoveLastPathComponent(), SecurityScopedAncestorUrl));
}
public Task DeleteAsync()
{
return NSFileManager.DefaultManager.Remove(Url, out var error)
? Task.CompletedTask
: Task.FromException(new NSErrorException(error));
try
{
SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
return NSFileManager.DefaultManager.Remove(Url, out var error)
? Task.CompletedTask
: Task.FromException(new NSErrorException(error));
}
finally
{
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
public Task<IStorageItem?> MoveAsync(IStorageFolder destination)
public async Task<IStorageItem?> MoveAsync(IStorageFolder destination)
{
if (destination is not IOSStorageFolder folder)
{
throw new InvalidOperationException("Destination folder must be initialized the StorageProvider API.");
}
var isDir = this is IStorageFolder;
var newPath = new NSUrl(System.IO.Path.Combine(folder.FilePath, Name), isDir);
if (NSFileManager.DefaultManager.Move(folder.Url, newPath, out var error))
try
{
return isDir
? Task.FromResult<IStorageItem?>(new IOSStorageFolder(newPath))
: Task.FromResult<IStorageItem?>(new IOSStorageFile(newPath));
}
SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
folder.SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
if (error is not null)
var isDir = this is IStorageFolder;
var newPath = new NSUrl(System.IO.Path.Combine(folder.FilePath, Name), isDir);
if (NSFileManager.DefaultManager.Move(Url, newPath, out var error))
{
return isDir
? new IOSStorageFolder(newPath)
: new IOSStorageFile(newPath);
}
if (error is not null)
{
throw new NSErrorException(error);
}
return null;
}
finally
{
throw new NSErrorException(error);
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
folder.SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
return Task.FromResult<IStorageItem?>(null);
}
public Task ReleaseBookmarkAsync()
@ -102,7 +128,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
{
try
{
if (!Url.StartAccessingSecurityScopedResource())
if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource())
{
return Task.FromResult<string?>(null);
}
@ -120,7 +146,7 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
}
finally
{
Url.StopAccessingSecurityScopedResource();
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
@ -131,89 +157,121 @@ internal abstract class IOSStorageItem : IStorageBookmarkItem
internal sealed class IOSStorageFile : IOSStorageItem, IStorageBookmarkFile
{
public IOSStorageFile(NSUrl url) : base(url)
public IOSStorageFile(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl)
{
}
public Task<Stream> OpenReadAsync()
{
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Read));
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Read));
}
public Task<Stream> OpenWriteAsync()
{
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, FileAccess.Write));
return Task.FromResult<Stream>(new IOSSecurityScopedStream(Url, SecurityScopedAncestorUrl, FileAccess.Write));
}
}
internal sealed class IOSStorageFolder : IOSStorageItem, IStorageBookmarkFolder
{
public IOSStorageFolder(NSUrl url) : base(url)
public IOSStorageFolder(NSUrl url, NSUrl? securityScopedAncestorUrl = null) : base(url, securityScopedAncestorUrl)
{
}
public async IAsyncEnumerable<IStorageItem> GetItemsAsync()
{
// TODO: find out if it can be lazily enumerated.
var tcs = new TaskCompletionSource<IReadOnlyList<IStorageItem>>();
try
{
SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
new NSFileCoordinator().CoordinateRead(Url,
NSFileCoordinatorReadingOptions.WithoutChanges,
out var error,
uri =>
{
var content = NSFileManager.DefaultManager.GetDirectoryContent(uri, null, NSDirectoryEnumerationOptions.None, out var error);
if (error is not null)
{
tcs.TrySetException(new NSErrorException(error));
}
else
// TODO: find out if it can be lazily enumerated.
var tcs = new TaskCompletionSource<IReadOnlyList<IStorageItem>>();
new NSFileCoordinator().CoordinateRead(Url,
NSFileCoordinatorReadingOptions.WithoutChanges,
out var error,
uri =>
{
var items = content
.Select(u => u.HasDirectoryPath ? (IStorageItem)new IOSStorageFolder(u) : new IOSStorageFile(u))
.ToArray();
tcs.TrySetResult(items);
}
});
var content = NSFileManager.DefaultManager.GetDirectoryContent(uri, null, NSDirectoryEnumerationOptions.None, out var error);
if (error is not null)
{
tcs.TrySetException(new NSErrorException(error));
}
else
{
var items = content
.Select(u => u.HasDirectoryPath ?
(IStorageItem)new IOSStorageFolder(u, SecurityScopedAncestorUrl) :
new IOSStorageFile(u, SecurityScopedAncestorUrl))
.ToArray();
tcs.TrySetResult(items);
}
});
if (error is not null)
{
throw new NSErrorException(error);
}
if (error is not null)
{
throw new NSErrorException(error);
}
var items = await tcs.Task;
foreach (var item in items)
var items = await tcs.Task;
foreach (var item in items)
{
yield return item;
}
}
finally
{
yield return item;
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
public Task<IStorageFile?> CreateFileAsync(string name)
{
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateFile(path, null, attributes))
try
{
return Task.FromResult<IStorageFile?>(new IOSStorageFile(new NSUrl(path, false)));
}
if (!SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource())
{
return Task.FromResult<IStorageFile?>(null);
}
return Task.FromResult<IStorageFile?>(null);
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateFile(path, new NSData(), attributes))
{
return Task.FromResult<IStorageFile?>(new IOSStorageFile(new NSUrl(path, false), SecurityScopedAncestorUrl));
}
return Task.FromResult<IStorageFile?>(null);
}
finally
{
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
}
public Task<IStorageFolder?> CreateFolderAsync(string name)
{
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error))
try
{
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(new NSUrl(path, true)));
}
SecurityScopedAncestorUrl.StartAccessingSecurityScopedResource();
if (error is not null)
var path = System.IO.Path.Combine(FilePath, name);
NSFileAttributes? attributes = null;
if (NSFileManager.DefaultManager.CreateDirectory(path, false, attributes, out var error))
{
return Task.FromResult<IStorageFolder?>(new IOSStorageFolder(new NSUrl(path, true), SecurityScopedAncestorUrl));
}
if (error is not null)
{
throw new NSErrorException(error);
}
return Task.FromResult<IStorageFolder?>(null);
}
finally
{
throw new NSErrorException(error);
SecurityScopedAncestorUrl.StopAccessingSecurityScopedResource();
}
return Task.FromResult<IStorageFolder?>(null);
}
}

83
tests/Avalonia.Base.UnitTests/Media/PenTests.cs

@ -1,4 +1,5 @@
using System;
#nullable enable
using System;
using Avalonia.Collections;
using Avalonia.Media;
using Avalonia.Media.Immutable;
@ -116,5 +117,85 @@ namespace Avalonia.Base.UnitTests.Media
Assert.True(Equals(target1, target2));
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Exists_And_Assign_Null_When_Brush_Is_Null()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, null, 2);
Assert.True(result);
Assert.Null(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_False_When_Previous_Not_Exists_And_Assign_Null_When_Brush_Is_Null()
{
IPen? target = null;
var result = Pen.TryModifyOrCreate(ref target, null, 2);
Assert.False(result);
Assert.Null(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Immutable_And_Assign_Mutable_When_Brush_Is_Mutable()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, new SolidColorBrush(Colors.Blue), 2);
Assert.True(result);
Assert.IsType<Pen>(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_True_When_Previous_Immutable_And_Assign_Immutable_When_Brush_Is_Immutable()
{
IPen? target = new ImmutablePen(
brush: new ImmutableSolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
var result = Pen.TryModifyOrCreate(ref target, new ImmutableSolidColorBrush(Colors.Blue), 2);
Assert.True(result);
Assert.IsType<ImmutablePen>(target);
}
[Fact]
public void TryModifyOrCreate_Should_Return_False_When_Previous_Mutable_And_Modify_Mutable_When_Brush_Is_Mutable()
{
var oldPen = new Pen(
brush: new SolidColorBrush(Colors.Red),
thickness: 2,
dashStyle: new ImmutableDashStyle(new[] { 0.1, 0.2 }, 5),
lineCap: PenLineCap.Round,
lineJoin: PenLineJoin.Round,
miterLimit: 21);
IPen? target = oldPen;
var result = Pen.TryModifyOrCreate(ref target, new SolidColorBrush(Colors.Blue), 2);
Assert.False(result);
Assert.Same(oldPen, target);
}
}
}

39
tests/Avalonia.Base.UnitTests/Styling/StyleTests.cs

@ -1029,6 +1029,28 @@ namespace Avalonia.Base.UnitTests.Styling
Assert.Equal(Brushes.Blue, border.Background);
}
[Fact]
public void Should_Not_Share_Instance_When_Or_Selector_Is_Present()
{
// Issue #13910
Style style = new Style(x => Selectors.Or(x.OfType<Class1>(), x.OfType<Class2>().Class("bar")))
{
Setters =
{
new Setter(Class1.FooProperty, "Foo"),
},
};
var target1 = new Class1 { Classes = { "foo" } };
var target2 = new Class2();
StyleHelpers.TryAttach(style, target1);
StyleHelpers.TryAttach(style, target2);
Assert.Equal("Foo", target1.Foo);
Assert.Equal("foodefault", target2.Foo);
}
private class Class1 : Control
{
public static readonly StyledProperty<string> FooProperty =
@ -1063,5 +1085,22 @@ namespace Avalonia.Base.UnitTests.Styling
throw new NotImplementedException();
}
}
private class Class2 : Control
{
public static readonly StyledProperty<string> FooProperty =
Class1.FooProperty.AddOwner<Class2>();
public string Foo
{
get { return GetValue(FooProperty); }
set { SetValue(FooProperty, value); }
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
}
}
}
}

26
tests/Avalonia.Controls.UnitTests/EnumerableExtensions.cs

@ -1,8 +1,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.Controls.UnitTests
{
@ -16,5 +14,29 @@ namespace Avalonia.Controls.UnitTests
yield return i;
}
}
public static IEnumerable<T[]> Permutations<T>(this IEnumerable<T> source)
{
var sourceArray = source.ToArray();
var results = new List<T[]>();
Permute(sourceArray, 0, sourceArray.Length - 1);
return results;
void Permute(T[] elements, int depth, int maxDepth)
{
if (depth == maxDepth)
{
results.Add(elements.ToArray());
return;
}
for (var i = depth; i <= maxDepth; i++)
{
(elements[depth], elements[i]) = (elements[i], elements[depth]);
Permute(elements, depth + 1, maxDepth);
(elements[depth], elements[i]) = (elements[i], elements[depth]);
}
}
}
}
}

189
tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs

@ -5,7 +5,6 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Disposables;
using System.Threading.Tasks;
using Avalonia.Collections;
using Avalonia.Controls.Presenters;
@ -18,7 +17,6 @@ using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Markup.Data;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.Threading;
using Avalonia.UnitTests;
@ -2294,6 +2292,134 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
[Theory]
[MemberData(nameof(GetSelectionFieldPermutationParameters))]
public void SelectedItem_And_Selection_Properties_Work_In_Any_Order_When_Initializing(SelectionField[] fields)
=> TestSelectionFields(vm => vm.SelectedItem = vm.Items[2], fields);
[Theory]
[MemberData(nameof(GetSelectionFieldPermutationParameters))]
public void SelectedIndex_And_Selection_Properties_Work_In_Any_Order_When_Initializing(SelectionField[] fields)
=> TestSelectionFields(vm => vm.SelectedIndex = 2, fields);
[Theory]
[MemberData(nameof(GetSelectionFieldPermutationParameters))]
public void SelectedValue_And_Selection_Properties_Work_In_Any_Order_When_Initializing(SelectionField[] fields)
=> TestSelectionFields(vm => vm.SelectedValue = 12, fields);
private void TestSelectionFields(Action<FullSelectionViewModel> setItem2, SelectionField[] fields)
{
using var _ = Start();
var vm = new FullSelectionViewModel
{
Items =
{
new ItemModel { Id = 10, Name = "Item0" },
new ItemModel { Id = 11, Name = "Item1" },
new ItemModel { Id = 12, Name = "Item2" },
new ItemModel { Id = 13, Name = "Item3" }
}
};
setItem2(vm);
var root = new TestRoot
{
Width = 100,
Height = 100
};
// Match the Begin/EndInit sequence emitted by the XAML compiler
root.BeginInit();
var target = new ListBox();
target.BeginInit();
root.Child = target;
target.DataContext = vm;
foreach (var field in fields)
{
switch (field)
{
case SelectionField.ItemsSource:
target.Bind(ItemsControl.ItemsSourceProperty, new Binding(nameof(FullSelectionViewModel.Items)));
break;
case SelectionField.SelectedItem:
target.Bind(SelectingItemsControl.SelectedItemProperty, new Binding(nameof(FullSelectionViewModel.SelectedItem)));
break;
case SelectionField.SelectedIndex:
target.Bind(SelectingItemsControl.SelectedIndexProperty, new Binding(nameof(FullSelectionViewModel.SelectedIndex)));
break;
case SelectionField.SelectedValue:
target.Bind(SelectingItemsControl.SelectedValueProperty, new Binding(nameof(FullSelectionViewModel.SelectedValue)));
break;
case SelectionField.SelectedValueBinding:
target.SelectedValueBinding = new Binding(nameof(ItemModel.Id));
break;
default:
throw new InvalidOperationException($"Unkown field {field}");
}
}
target.EndInit();
root.EndInit();
Assert.Equal(vm.Items[2], target.SelectedItem);
Assert.Equal(2, target.SelectedIndex);
Assert.Equal(12, target.SelectedValue);
Assert.Equal(vm.Items[2], vm.SelectedItem);
Assert.Equal(2, vm.SelectedIndex);
Assert.Equal(12, vm.SelectedValue);
}
[Fact]
public void SelectedItem_Can_Access_Selection_DuringInit()
{
using var _ = Start();
var target = new ListBox();
target.BeginInit();
var item = new ItemModel();
target.Selection = new SelectionModel<ItemModel> {
SelectedItem = item
};
Assert.Equal(item, target.SelectedItem);
}
[Fact]
public void SelectedIndex_Can_Access_Selection_DuringInit()
{
using var _ = Start();
var target = new ListBox();
target.BeginInit();
target.Selection = new SelectionModel<ItemModel> {
SelectedIndex = 42
};
Assert.Equal(42, target.SelectedIndex);
}
[Fact]
public void AnchorIndex_Can_Access_Selection_DuringInit()
{
using var _ = Start();
var target = new ListBox();
target.BeginInit();
target.Selection = new SelectionModel<ItemModel> {
AnchorIndex = 42
};
Assert.Equal(42, target.GetAnchorIndex());
}
private static IDisposable Start()
{
return UnitTestApplication.Start(TestServices.StyledWindow);
@ -2336,6 +2462,9 @@ namespace Avalonia.Controls.UnitTests.Primitives
}.RegisterInNameScope(scope));
}
public static IEnumerable<object[]> GetSelectionFieldPermutationParameters()
=> Enum.GetValues<SelectionField>().Permutations().Select(fields => new object[] { fields });
private class Item : Control, ISelectable
{
public string Value { get; set; }
@ -2467,5 +2596,61 @@ namespace Avalonia.Controls.UnitTests.Primitives
public event NotifyCollectionChangedEventHandler CollectionChanged;
}
#nullable enable
private sealed class FullSelectionViewModel : NotifyingBase
{
private ItemModel? _selectedItem;
private int _selectedIndex = -1;
private int? _selectedValue;
public ObservableCollection<ItemModel> Items { get; } = new();
public ItemModel? SelectedItem
{
get => _selectedItem;
set => SetField(ref _selectedItem, value);
}
public int SelectedIndex
{
get => _selectedIndex;
set => SetField(ref _selectedIndex, value);
}
public int? SelectedValue
{
get => _selectedValue;
set => SetField(ref _selectedValue, value);
}
}
private sealed class ItemModel : NotifyingBase
{
private int _id;
private string? _name;
public int Id
{
get => _id;
set => SetField(ref _id, value);
}
public string? Name
{
get => _name;
set => SetField(ref _name, value);
}
}
public enum SelectionField
{
ItemsSource,
SelectedItem,
SelectedIndex,
SelectedValue,
SelectedValueBinding
}
}
}

95
tests/Avalonia.LeakTests/TransitionTests.cs

@ -1,6 +1,8 @@
using System;
using Avalonia.Animation;
using Avalonia.Controls;
using Avalonia.Data;
using Avalonia.Styling;
using Avalonia.UnitTests;
using JetBrains.dotMemoryUnit;
using Xunit;
@ -16,18 +18,18 @@ namespace Avalonia.LeakTests
DotMemoryUnitTestOutput.SetOutputMethod(atr.WriteLine);
}
[Fact(Skip = "TODO: Fix this leak")]
[Fact]
public void Transition_On_StyledProperty_Is_Freed()
{
var clock = new MockGlobalClock();
using (UnitTestApplication.Start(new TestServices(globalClock: clock)))
using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock)))
{
Func<Border> run = () =>
{
var border = new Border
{
Transitions =
Transitions = new Transitions()
{
new DoubleTransition
{
@ -36,6 +38,9 @@ namespace Avalonia.LeakTests
}
}
};
var window = new Window();
window.Content = border;
window.Show();
border.Opacity = 0;
@ -47,6 +52,9 @@ namespace Avalonia.LeakTests
clock.Pulse(TimeSpan.FromSeconds(1));
Assert.Equal(0, border.Opacity);
window.Close();
return border;
};
@ -56,5 +64,86 @@ namespace Avalonia.LeakTests
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<TransitionInstance>()).ObjectsCount));
}
}
[Fact]
public void Shared_Transition_Collection_Is_Not_Leaking()
{
var clock = new MockGlobalClock();
using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock)))
{
// Our themes do share transition collections, so we need to test this scenario well.
var sharedTransitions = new Transitions
{
new TransformOperationsTransition
{
Property = Visual.RenderTransformProperty, Duration = TimeSpan.FromSeconds(0.750)
}
};
var controlTheme = new ControlTheme(typeof(Button))
{
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
};
Func<Window> run = () =>
{
var button = new Button() { Theme = controlTheme };
var window = new Window();
window.Content = button;
window.Show();
window.Content = null;
window.Close();
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
}
}
[Fact]
public void Lazily_Created_Control_Should_Not_Leak_Transitions()
{
var clock = new MockGlobalClock();
using (UnitTestApplication.Start(TestServices.StyledWindow.With(globalClock: clock)))
{
var sharedTransitions = new Transitions
{
new TransformOperationsTransition
{
Property = Visual.RenderTransformProperty, Duration = TimeSpan.FromSeconds(0.750)
}
};
var controlTheme = new ControlTheme(typeof(Button))
{
BasedOn = Application.Current?.Resources[typeof(Button)] as ControlTheme,
Setters = { new Setter(Animatable.TransitionsProperty, sharedTransitions) }
};
Func<Window> run = () =>
{
var window = new Window();
window.Show();
window.Content = new UserControl
{
Content = new Button() { Theme = controlTheme },
// When invisible, Button won't be attached to the visual tree
IsVisible = false
};
window.Content = null;
window.Close();
return window;
};
var result = run();
dotMemory.Check(memory =>
Assert.Equal(0, memory.GetObjects(where => where.Type.Is<Button>()).ObjectsCount));
}
}
}
}

19
tests/Avalonia.UnitTests/NotifyingBase.cs

@ -1,3 +1,6 @@
#nullable enable
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Runtime.CompilerServices;
@ -6,9 +9,9 @@ namespace Avalonia.UnitTests
{
public class NotifyingBase : INotifyPropertyChanged
{
private PropertyChangedEventHandler _propertyChanged;
private PropertyChangedEventHandler? _propertyChanged;
public event PropertyChangedEventHandler PropertyChanged
public event PropertyChangedEventHandler? PropertyChanged
{
add
{
@ -32,9 +35,19 @@ namespace Avalonia.UnitTests
private set;
}
public void RaisePropertyChanged([CallerMemberName] string propertyName = null)
public void RaisePropertyChanged([CallerMemberName] string? propertyName = null)
{
_propertyChanged?.Invoke(this, new PropertyChangedEventArgs(propertyName));
}
protected bool SetField<T>(ref T field, T value, [CallerMemberName] string? propertyName = null)
{
if (EqualityComparer<T>.Default.Equals(field, value))
return false;
field = value;
RaisePropertyChanged(propertyName);
return true;
}
}
}

Loading…
Cancel
Save