Browse Source

Merge branch 'master' into feature/style-add-remove

pull/3647/head
Steven Kirk 7 years ago
committed by GitHub
parent
commit
9acaa40887
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/BindingDemo.v3.ncrunchproject
  2. 5
      .ncrunch/RenderDemo.v3.ncrunchproject
  3. 5
      .ncrunch/VirtualizationDemo.v3.ncrunchproject
  4. 14
      azure-pipelines.yml
  5. 3
      global.json
  6. 8
      native/Avalonia.Native/inc/avalonia-native.h
  7. 81
      native/Avalonia.Native/src/OSX/window.mm
  8. 166
      nukebuild/Build.cs
  9. 21
      nukebuild/BuildParameters.cs
  10. 8
      nukebuild/Shims.cs
  11. 8
      nukebuild/_build.csproj
  12. 6
      packages/Avalonia/Avalonia.csproj
  13. 2
      packages/Avalonia/Avalonia.props
  14. 35
      readme.md
  15. 2
      samples/BindingDemo/BindingDemo.csproj
  16. 2
      samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj
  17. 11
      samples/ControlCatalog/MainView.xaml
  18. 14
      samples/ControlCatalog/MainView.xaml.cs
  19. 6
      samples/ControlCatalog/MainWindow.xaml
  20. 14
      samples/ControlCatalog/MainWindow.xaml.cs
  21. 19
      samples/ControlCatalog/Pages/ImagePage.xaml
  22. 33
      samples/ControlCatalog/Pages/ImagePage.xaml.cs
  23. 4
      samples/ControlCatalog/Pages/MenuPage.xaml
  24. 2
      samples/PlatformSanityChecks/PlatformSanityChecks.csproj
  25. 2
      samples/Previewer/Previewer.csproj
  26. 2
      samples/RemoteDemo/RemoteDemo.csproj
  27. 2
      samples/RenderDemo/RenderDemo.csproj
  28. 2
      samples/VirtualizationDemo/VirtualizationDemo.csproj
  29. 6
      src/Avalonia.Base/AvaloniaObject.cs
  30. 88
      src/Avalonia.Base/AvaloniaPropertyRegistry.cs
  31. 2
      src/Avalonia.Controls/Calendar/DatePicker.cs
  32. 3
      src/Avalonia.Controls/ComboBox.cs
  33. 195
      src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs
  34. 47
      src/Avalonia.Controls/MenuItem.cs
  35. 2
      src/Avalonia.Controls/Platform/IWindowImpl.cs
  36. 11
      src/Avalonia.Controls/Primitives/AccessText.cs
  37. 2
      src/Avalonia.Controls/Primitives/Popup.cs
  38. 60
      src/Avalonia.Controls/TextBlock.cs
  39. 41
      src/Avalonia.Controls/TextBox.cs
  40. 96
      src/Avalonia.Controls/Window.cs
  41. 2
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  42. 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  43. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  44. 2
      src/Avalonia.Diagnostics/Diagnostics/DevTools.cs
  45. 44
      src/Avalonia.Input/KeyGesture.cs
  46. 6
      src/Avalonia.Interactivity/Avalonia.Interactivity.csproj
  47. 200
      src/Avalonia.Interactivity/EventRoute.cs
  48. 20
      src/Avalonia.Interactivity/EventSubscription.cs
  49. 13
      src/Avalonia.Interactivity/IInteractive.cs
  50. 293
      src/Avalonia.Interactivity/Interactive.cs
  51. 30
      src/Avalonia.Interactivity/InteractiveExtensions.cs
  52. 20
      src/Avalonia.Interactivity/RoutedEvent.cs
  53. 8
      src/Avalonia.Interactivity/RoutedEventArgs.cs
  54. 6
      src/Avalonia.Interactivity/RoutedEventRegistry.cs
  55. 4
      src/Avalonia.Native/WindowImpl.cs
  56. 9
      src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs
  57. 35
      src/Avalonia.Themes.Default/MenuItem.xaml
  58. 1
      src/Avalonia.Themes.Default/NativeMenuBar.xaml
  59. 96
      src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs
  60. 27
      src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs
  61. 16
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs
  62. 1
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  63. 10
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  64. 16
      src/Avalonia.X11/X11Window.cs
  65. 10
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  66. 20
      src/Windows/Avalonia.Win32/WindowImpl.cs
  67. 2
      src/iOS/Avalonia.iOS/EmbeddableImpl.cs
  68. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  69. 2
      tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj
  70. 2
      tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj
  71. 2
      tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj
  72. 11
      tests/Avalonia.Benchmarks/NullGlyphRun.cs
  73. 4
      tests/Avalonia.Benchmarks/NullRenderingPlatform.cs
  74. 2
      tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj
  75. 2
      tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj
  76. 43
      tests/Avalonia.Controls.UnitTests/TextBoxTests.cs
  77. 2
      tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj
  78. 2
      tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj
  79. 6
      tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs
  80. 2
      tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj
  81. 2
      tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj
  82. 2
      tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj
  83. 31
      tests/Avalonia.Input.UnitTests/KeyGestureTests.cs
  84. 3
      tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj
  85. 25
      tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs
  86. 2
      tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj
  87. 2
      tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj
  88. 2
      tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj
  89. 2
      tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj
  90. 2
      tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj
  91. 2
      tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj
  92. 17
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  93. 2
      tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj
  94. 2
      tests/Avalonia.UnitTests/MouseTestHelper.cs
  95. 2
      tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj

5
.ncrunch/BindingDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/RenderDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/VirtualizationDemo.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

14
azure-pipelines.yml

@ -14,7 +14,7 @@ jobs:
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2
displayName: 'Run Nuke'
inputs:
@ -35,16 +35,16 @@ jobs:
vmImage: 'macOS-10.14'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 3.0.x'
displayName: 'Use .NET Core SDK 3.1.101'
inputs:
packageType: sdk
version: 3.0.x
version: 3.1.101
- task: UseDotNet@2
displayName: 'Use .NET Core Runtime 2.1.x'
displayName: 'Use .NET Core Runtime 3.1.1'
inputs:
packageType: runtime
version: 2.1.x
version: 3.1.1
- task: CmdLine@2
displayName: 'Install Mono 5.18'
@ -74,7 +74,7 @@ jobs:
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2
displayName: 'Run Nuke'
@ -116,7 +116,7 @@ jobs:
displayName: 'Install Nuke'
inputs:
script: |
dotnet tool install --global Nuke.GlobalTool --version 0.12.3
dotnet tool install --global Nuke.GlobalTool --version 0.24.0
- task: CmdLine@2
displayName: 'Run Nuke'

3
global.json

@ -1,4 +1,7 @@
{
"sdk": {
"version": "3.1.101"
},
"msbuild-sdks": {
"Microsoft.Build.Traversal": "1.0.43",
"MSBuild.Sdk.Extras": "2.0.46",

8
native/Avalonia.Native/inc/avalonia-native.h

@ -25,6 +25,12 @@ struct IAvnGlSurfaceRenderingSession;
struct IAvnAppMenu;
struct IAvnAppMenuItem;
enum SystemDecorations {
SystemDecorationsNone = 0,
SystemDecorationsBorderOnly = 1,
SystemDecorationsFull = 2,
};
struct AvnSize
{
double Width, Height;
@ -236,7 +242,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
{
virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
virtual HRESULT SetCanResize(bool value) = 0;
virtual HRESULT SetHasDecorations(bool value) = 0;
virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
virtual HRESULT SetTitle (void* utf8Title) = 0;
virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
virtual HRESULT SetWindowState(AvnWindowState state) = 0;

81
native/Avalonia.Native/src/OSX/window.mm

@ -115,7 +115,6 @@ public:
[NSApp activateIgnoringOtherApps:YES];
[Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
return S_OK;
}
@ -411,7 +410,7 @@ class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, pub
{
private:
bool _canResize = true;
bool _hasDecorations = true;
SystemDecorations _hasDecorations = SystemDecorationsFull;
CGRect _lastUndecoratedFrame;
AvnWindowState _lastWindowState;
@ -476,23 +475,26 @@ private:
bool IsZoomed ()
{
return _hasDecorations ? [Window isZoomed] : UndecoratedIsMaximized();
return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
}
void DoZoom()
{
if (_hasDecorations)
switch (_hasDecorations)
{
[Window performZoom:Window];
}
else
{
if (!UndecoratedIsMaximized())
{
_lastUndecoratedFrame = [Window frame];
}
[Window zoom:Window];
case SystemDecorationsNone:
if (!UndecoratedIsMaximized())
{
_lastUndecoratedFrame = [Window frame];
}
[Window zoom:Window];
break;
case SystemDecorationsBorderOnly:
case SystemDecorationsFull:
[Window performZoom:Window];
break;
}
}
@ -506,13 +508,35 @@ private:
}
}
virtual HRESULT SetHasDecorations(bool value) override
virtual HRESULT SetHasDecorations(SystemDecorations value) override
{
@autoreleasepool
{
_hasDecorations = value;
UpdateStyle();
switch (_hasDecorations)
{
case SystemDecorationsNone:
[Window setHasShadow:NO];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
break;
case SystemDecorationsBorderOnly:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleHidden];
[Window setTitlebarAppearsTransparent:YES];
break;
case SystemDecorationsFull:
[Window setHasShadow:YES];
[Window setTitleVisibility:NSWindowTitleVisible];
[Window setTitlebarAppearsTransparent:NO];
[Window setTitle:_lastTitle];
break;
}
return S_OK;
}
}
@ -523,7 +547,6 @@ private:
{
_lastTitle = [NSString stringWithUTF8String:(const char*)utf8title];
[Window setTitle:_lastTitle];
[Window setTitleVisibility:NSWindowTitleVisible];
return S_OK;
}
@ -645,10 +668,26 @@ protected:
virtual NSWindowStyleMask GetStyle() override
{
unsigned long s = NSWindowStyleMaskBorderless;
if(_hasDecorations)
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable;
if(_canResize)
s = s | NSWindowStyleMaskResizable;
switch (_hasDecorations)
{
case SystemDecorationsNone:
break;
case SystemDecorationsBorderOnly:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
break;
case SystemDecorationsFull:
s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
if(_canResize)
{
s = s | NSWindowStyleMaskResizable;
}
break;
}
return s;
}
};

166
nukebuild/Build.cs

@ -13,6 +13,7 @@ using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
@ -26,11 +27,13 @@ using static Nuke.Common.Tools.VSWhere.VSWhereTasks;
running and debugging a particular target (optionally without deps) would be way easier
ReSharper/Rider - https://plugins.jetbrains.com/plugin/10803-nuke-support
VSCode - https://marketplace.visualstudio.com/items?itemName=nuke.support
*/
partial class Build : NukeBuild
{
[Solution("Avalonia.sln")] readonly Solution Solution;
static Lazy<string> MsBuildExe = new Lazy<string>(() =>
{
if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
@ -54,7 +57,7 @@ partial class Build : NukeBuild
protected override void OnBuildInitialized()
{
Parameters = new BuildParameters(this);
Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.",
Information("Building version {0} of Avalonia ({1}) using version {2} of Nuke.",
Parameters.Version,
Parameters.Configuration,
typeof(NukeBuild).Assembly.GetName().Version.ToString());
@ -93,29 +96,24 @@ partial class Build : NukeBuild
string projectFile,
Configure<MSBuildSettings> configurator = null)
{
return MSBuild(projectFile, c =>
{
return MSBuild(c => c
.SetProjectFile(projectFile)
// This is required for VS2019 image on Azure Pipelines
if (Parameters.IsRunningOnWindows && Parameters.IsRunningOnAzure)
{
var javaSdk = Environment.GetEnvironmentVariable("JAVA_HOME_8_X64");
if (javaSdk != null)
c = c.AddProperty("JavaSdkDirectory", javaSdk);
}
c = c.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", "true")
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal);
c = configurator?.Invoke(c) ?? c;
return c;
});
.When(Parameters.IsRunningOnWindows &&
Parameters.IsRunningOnAzure, c => c
.AddProperty("JavaSdkDirectory", GetVariable<string>("JAVA_HOME_8_X64")))
.AddProperty("PackageVersion", Parameters.Version)
.AddProperty("iOSRoslynPathHackRequired", true)
.SetToolPath(MsBuildExe.Value)
.SetConfiguration(Parameters.Configuration)
.SetVerbosity(MSBuildVerbosity.Minimal)
.Apply(configurator));
}
Target Clean => _ => _.Executes(() =>
{
DeleteDirectories(Parameters.BuildDirs);
EnsureCleanDirectories(Parameters.BuildDirs);
Parameters.BuildDirs.ForEach(DeleteDirectory);
Parameters.BuildDirs.ForEach(EnsureCleanDirectory);
EnsureCleanDirectory(Parameters.ArtifactsDir);
EnsureCleanDirectory(Parameters.NugetIntermediateRoot);
EnsureCleanDirectory(Parameters.NugetRoot);
@ -134,97 +132,84 @@ partial class Build : NukeBuild
);
else
DotNetBuild(Parameters.MSBuildSolution, c => c
DotNetBuild(c => c
.SetProjectFile(Parameters.MSBuildSolution)
.AddProperty("PackageVersion", Parameters.Version)
.SetConfiguration(Parameters.Configuration)
);
});
void RunCoreTest(string project)
void RunCoreTest(string projectName)
{
if(!project.EndsWith(".csproj"))
project = System.IO.Path.Combine(project, System.IO.Path.GetFileName(project)+".csproj");
Information("Running tests from " + project);
XDocument xdoc;
using (var s = File.OpenRead(project))
xdoc = XDocument.Load(s);
List<string> frameworks = null;
var targets = xdoc.Root.Descendants("TargetFrameworks").FirstOrDefault();
if (targets != null)
frameworks = targets.Value.Split(';').Where(f => !string.IsNullOrWhiteSpace(f)).ToList();
else
frameworks = new List<string> {xdoc.Root.Descendants("TargetFramework").First().Value};
foreach(var fw in frameworks)
Information($"Running tests from {projectName}");
var project = Solution.GetProject(projectName).NotNull("project != null");
foreach (var fw in project.GetTargetFrameworks())
{
if (fw.StartsWith("net4")
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
&& RuntimeInformation.IsOSPlatform(OSPlatform.Linux)
&& Environment.GetEnvironmentVariable("FORCE_LINUX_TESTS") != "1")
{
Information($"Skipping {fw} tests on Linux - https://github.com/mono/mono/issues/13969");
Information($"Skipping {projectName} ({fw}) tests on Linux - https://github.com/mono/mono/issues/13969");
continue;
}
Information("Running for " + fw);
DotNetTest(c =>
{
c = c
.SetProjectFile(project)
.SetConfiguration(Parameters.Configuration)
.SetFramework(fw)
.EnableNoBuild()
.EnableNoRestore();
// NOTE: I can see that we could maybe add another extension method "Switch" or "If" to make this more convenient
if (Parameters.PublishTestResults)
c = c.SetLogger("trx").SetResultsDirectory(Parameters.TestResultsRoot);
return c;
});
Information($"Running for {projectName} ({fw}) ...");
DotNetTest(c => c
.SetProjectFile(project)
.SetConfiguration(Parameters.Configuration)
.SetFramework(fw)
.EnableNoBuild()
.EnableNoRestore()
.When(Parameters.PublishTestResults, c => c
.SetLogger("trx")
.SetResultsDirectory(Parameters.TestResultsRoot)));
}
}
Target RunCoreLibsTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests)
.OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile)
.Executes(() =>
{
RunCoreTest("./tests/Avalonia.Animation.UnitTests");
RunCoreTest("./tests/Avalonia.Base.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.UnitTests");
RunCoreTest("./tests/Avalonia.Controls.DataGrid.UnitTests");
RunCoreTest("./tests/Avalonia.Input.UnitTests");
RunCoreTest("./tests/Avalonia.Interactivity.UnitTests");
RunCoreTest("./tests/Avalonia.Layout.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.UnitTests");
RunCoreTest("./tests/Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("./tests/Avalonia.Styling.UnitTests");
RunCoreTest("./tests/Avalonia.Visuals.UnitTests");
RunCoreTest("./tests/Avalonia.Skia.UnitTests");
RunCoreTest("./tests/Avalonia.ReactiveUI.UnitTests");
RunCoreTest("Avalonia.Animation.UnitTests");
RunCoreTest("Avalonia.Base.UnitTests");
RunCoreTest("Avalonia.Controls.UnitTests");
RunCoreTest("Avalonia.Controls.DataGrid.UnitTests");
RunCoreTest("Avalonia.Input.UnitTests");
RunCoreTest("Avalonia.Interactivity.UnitTests");
RunCoreTest("Avalonia.Layout.UnitTests");
RunCoreTest("Avalonia.Markup.UnitTests");
RunCoreTest("Avalonia.Markup.Xaml.UnitTests");
RunCoreTest("Avalonia.Styling.UnitTests");
RunCoreTest("Avalonia.Visuals.UnitTests");
RunCoreTest("Avalonia.Skia.UnitTests");
RunCoreTest("Avalonia.ReactiveUI.UnitTests");
});
Target RunRenderTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests)
.OnlyWhenStatic(() => !Parameters.SkipTests)
.DependsOn(Compile)
.Executes(() =>
{
RunCoreTest("./tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj");
RunCoreTest("Avalonia.Skia.RenderTests");
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
RunCoreTest("./tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj");
RunCoreTest("Avalonia.Direct2D1.RenderTests");
});
Target RunDesignerTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile)
.Executes(() =>
{
RunCoreTest("./tests/Avalonia.DesignerSupport.Tests");
RunCoreTest("Avalonia.DesignerSupport.Tests");
});
[PackageExecutable("JetBrains.dotMemoryUnit", "dotMemoryUnit.exe")] readonly Tool DotMemoryUnit;
Target RunLeakTests => _ => _
.OnlyWhen(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.OnlyWhenStatic(() => !Parameters.SkipTests && Parameters.IsRunningOnWindows)
.DependsOn(Compile)
.Executes(() =>
{
@ -235,7 +220,7 @@ partial class Build : NukeBuild
});
Target ZipFiles => _ => _
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
.After(CreateNugetPackages, Compile, RunCoreLibsTests, Package)
.Executes(() =>
{
var data = Parameters;
@ -259,9 +244,10 @@ partial class Build : NukeBuild
MsBuildCommon(Parameters.MSBuildSolution, c => c
.AddTargets("Pack"));
else
DotNetPack(Parameters.MSBuildSolution, c =>
c.SetConfiguration(Parameters.Configuration)
.AddProperty("PackageVersion", Parameters.Version));
DotNetPack(c => c
.SetProject(Parameters.MSBuildSolution)
.SetConfiguration(Parameters.Configuration)
.AddProperty("PackageVersion", Parameters.Version));
});
Target CreateNugetPackages => _ => _
@ -274,32 +260,40 @@ partial class Build : NukeBuild
new NumergeNukeLogger()))
throw new Exception("Package merge failed");
});
Target RunTests => _ => _
.DependsOn(RunCoreLibsTests)
.DependsOn(RunRenderTests)
.DependsOn(RunDesignerTests)
.DependsOn(RunLeakTests);
Target Package => _ => _
.DependsOn(RunTests)
.DependsOn(CreateNugetPackages);
Target CiAzureLinux => _ => _
.DependsOn(RunTests);
Target CiAzureOSX => _ => _
.DependsOn(Package)
.DependsOn(ZipFiles);
Target CiAzureWindows => _ => _
.DependsOn(Package)
.DependsOn(ZipFiles);
public static int Main() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? Execute<Build>(x => x.Package)
: Execute<Build>(x => x.RunTests);
}
public static class ToolSettingsExtensions
{
public static T Apply<T>(this T settings, Configure<T> configurator)
{
return configurator != null ? configurator(settings) : settings;
}
}

21
nukebuild/BuildParameters.cs

@ -4,24 +4,21 @@ using System.Linq;
using System.Runtime.InteropServices;
using System.Xml.Linq;
using Nuke.Common;
using Nuke.Common.BuildServers;
using Nuke.Common.Execution;
using Nuke.Common.CI.AzurePipelines;
using Nuke.Common.IO;
using static Nuke.Common.IO.FileSystemTasks;
using static Nuke.Common.IO.PathConstruction;
using static Nuke.Common.Tools.MSBuild.MSBuildTasks;
public partial class Build
{
[Parameter("configuration")]
public string Configuration { get; set; }
[Parameter("skip-tests")]
public bool SkipTests { get; set; }
[Parameter("force-nuget-version")]
public string ForceNugetVersion { get; set; }
public class BuildParameters
{
public string Configuration { get; }
@ -79,15 +76,15 @@ public partial class Build
IsRunningOnUnix = Environment.OSVersion.Platform == PlatformID.Unix ||
Environment.OSVersion.Platform == PlatformID.MacOSX;
IsRunningOnWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
IsRunningOnAzure = Host == HostType.TeamServices ||
IsRunningOnAzure = Host == HostType.AzurePipelines ||
Environment.GetEnvironmentVariable("LOGNAME") == "vsts";
if (IsRunningOnAzure)
{
RepositoryName = TeamServices.Instance.RepositoryUri;
RepositoryBranch = TeamServices.Instance.SourceBranch;
IsPullRequest = TeamServices.Instance.PullRequestId.HasValue;
IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, TeamServices.Instance.RepositoryUri);
RepositoryName = AzurePipelines.Instance.RepositoryUri;
RepositoryBranch = AzurePipelines.Instance.SourceBranch;
IsPullRequest = AzurePipelines.Instance.PullRequestId.HasValue;
IsMainRepo = StringComparer.OrdinalIgnoreCase.Equals(MainRepo, AzurePipelines.Instance.RepositoryUri);
}
IsMainRepo =
StringComparer.OrdinalIgnoreCase.Equals(MainRepo,

8
nukebuild/Shims.cs

@ -19,9 +19,9 @@ public partial class Build
Logger.Info(info, args);
}
private void Zip(PathConstruction.AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable());
private void Zip(AbsolutePath target, params string[] paths) => Zip(target, paths.AsEnumerable());
private void Zip(PathConstruction.AbsolutePath target, IEnumerable<string> paths)
private void Zip(AbsolutePath target, IEnumerable<string> paths)
{
var targetPath = target.ToString();
bool finished = false, atLeastOneFileAdded = false;
@ -38,7 +38,7 @@ public partial class Build
fileStream.CopyTo(entryStream);
atLeastOneFileAdded = true;
}
foreach (var path in paths)
{
if (Directory.Exists(path))
@ -64,7 +64,7 @@ public partial class Build
finished = true;
}
finally
finally
{
try
{

8
nukebuild/_build.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<AppendTargetFrameworkToOutputPath>false</AppendTargetFrameworkToOutputPath>
<RootNamespace></RootNamespace>
<IsPackable>False</IsPackable>
@ -10,7 +10,7 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Nuke.Common" Version="0.12.3" />
<PackageReference Include="Nuke.Common" Version="0.24.0" />
<PackageReference Include="xunit.runner.console" Version="2.3.1" />
<PackageReference Include="JetBrains.dotMemoryUnit" Version="3.0.20171219.105559" />
<PackageReference Include="vswhere" Version="2.6.7" Condition=" '$(OS)' == 'Windows_NT' " />
@ -20,11 +20,11 @@
<NukeMetadata Include="**\*.json" Exclude="bin\**;obj\**" />
<NukeExternalFiles Include="**\*.*.ext" Exclude="bin\**;obj\**" />
<None Remove="*.csproj.DotSettings;*.ref.*.txt" />
<!-- Common build related files -->
<None Include="..\build.ps1" />
<None Include="..\build.sh" />
<None Include="..\.nuke" />
<None Include="..\.nuke" />
<None Include="..\global.json" Condition="Exists('..\global.json')" />
<None Include="..\nuget.config" Condition="Exists('..\nuget.config')" />
<None Include="..\Jenkinsfile" Condition="Exists('..\Jenkinsfile')" />

6
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net461;netcoreapp3.1</TargetFrameworks>
<PackageId>Avalonia</PackageId>
</PropertyGroup>
@ -20,8 +20,8 @@
Platform=$(Platform)" />
<ItemGroup>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp2.0/designer</PackagePath>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp3.1/designer</PackagePath>
<Visible>false</Visible>
<BuildAction>None</BuildAction>
</_PackageFiles>

2
packages/Avalonia/Avalonia.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
<AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>

35
readme.md

@ -8,25 +8,21 @@
## About
**Avalonia** is a WPF/UWP-inspired cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS and with experimental support for Android and iOS.
**Avalonia** is a cross-platform XAML-based UI framework providing a flexible styling system and supporting a wide range of Operating Systems such as Windows (.NET Framework, .NET Core), Linux (via Xorg), macOS.
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and [breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) as we continue along into this project's development. To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
**Avalonia** is ready for **General-Purpose Desktop App Development**. However, there may be some bugs and breaking changes as we continue along into this project's development.
| Control catalog | Desktop platforms | Mobile platforms |
|---|---|---|
| <a href='https://youtu.be/wHcB3sGLVYg'><img width='300' src='http://avaloniaui.net/images/screen.png'></a> | <a href='https://www.youtube.com/watch?t=28&v=c_AB_XSILp0' target='_blank'><img width='300' src='http://avaloniaui.net/images/avalonia-video.png'></a> | <a href='https://www.youtube.com/watch?v=NJ9-hnmUbBM' target='_blank'><img width='300' src='https://i.ytimg.com/vi/NJ9-hnmUbBM/hqdefault.jpg'></a> |
To see the status of some of our features, please see our [Roadmap here](https://github.com/AvaloniaUI/Avalonia/issues/2239).
[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is curated list of awesome Avalonia UI tools, libraries, projects and resources.
You can also see what [breaking changes](https://github.com/AvaloniaUI/Avalonia/issues/3538) we have planned and what our [past breaking changes](https://github.com/AvaloniaUI/Avalonia/wiki/Breaking-Changes) have been.
## Getting Started
Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started. After installing it, open "New Project" dialog in Visual Studio, choose "Avalonia" in "Visual C#" section, select "Avalonia .NET Core Application" and press OK (<a href="http://avaloniaui.net/docs/quickstart/images/new-project-dialog.png">screenshot</a>). Now you can write code and markup that will work on multiple platforms!
[Awesome Avalonia](https://github.com/AvaloniaCommunity/awesome-avalonia) is community-curated list of awesome Avalonia UI tools, libraries, projects and resources. Go and see what people are building with Avalonia!
For those without Visual Studio, a starter guide for .NET Core CLI can be found [here](http://avaloniaui.net/docs/quickstart/create-new-project#net-core).
## Getting Started
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.
The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project).
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: [stable(ish)](https://www.nuget.org/packages/Avalonia/)
Avalonia is delivered via <b>NuGet</b> package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/
Use these commands in the Package Manager console to install Avalonia manually:
```
@ -34,18 +30,17 @@ Install-Package Avalonia
Install-Package Avalonia.Desktop
```
## Bleeding Edge Builds
## JetBrains Rider
or use nightly build feeds as described here:
https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed
If you need to develop Avalonia app with JetBrains Rider, go and *vote* on [this issue](https://youtrack.jetbrains.com/issue/RIDER-39247) in their tracker. JetBrains won't do things without their users telling them that they want the feature, so only **YOU** can make it happen.
## Documentation
## Bleeding Edge Builds
You can take a look at the [getting started page](http://avaloniaui.net/docs/quickstart/) for an overview of how to get started but probably the best thing to do for now is to already know a little bit about WPF/Silverlight/UWP/XAML and ask questions in our [Gitter room](https://gitter.im/AvaloniaUI/Avalonia).
We also have a [nightly build](https://github.com/AvaloniaUI/Avalonia/wiki/Using-nightly-build-feed) which tracks the current state of master. Although these packages are less stable than the release on NuGet.org, you'll get all the latest features and bugfixes right away and many of our users actually prefer this feed!
There's also a high-level [architecture document](http://avaloniaui.net/architecture/project-structure) that is currently a little bit out of date, and I've also started writing blog posts on Avalonia at http://grokys.github.io/.
## Documentation
Contributions for our docs are always welcome!
Documentation can be found on our website at http://avaloniaui.net/docs/. We also have a [tutorial](http://avaloniaui.net/docs/tutorial/) over there for newcomers.
## Building and Using
@ -60,14 +55,12 @@ Please read the [contribution guidelines](http://avaloniaui.net/contributing/con
This project exists thanks to all the people who contribute. [[Contribute](http://avaloniaui.net/contributing/contributing)].
<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
### Backers
Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
<a href="https://opencollective.com/Avalonia#backers" target="_blank"><img src="https://opencollective.com/Avalonia/backers.svg?width=890"></a>
### Sponsors
Support this project by becoming a sponsor. Your logo will show up here with a link to your website. [[Become a sponsor](https://opencollective.com/Avalonia#sponsor)]

2
samples/BindingDemo/BindingDemo.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

2
samples/ControlCatalog.NetCore/ControlCatalog.NetCore.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<TargetLatestRuntimePatch>true</TargetLatestRuntimePatch>
</PropertyGroup>

11
samples/ControlCatalog/MainView.xaml

@ -59,10 +59,17 @@
<TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
<TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
<TabControl.Tag>
<ComboBox x:Name="Themes" SelectedIndex="0" Width="100" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
<ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
<ComboBoxItem>No Decorations</ComboBoxItem>
<ComboBoxItem>Border Only</ComboBoxItem>
<ComboBoxItem>Full Decorations</ComboBoxItem>
</ComboBox>
<ComboBox x:Name="Themes" SelectedIndex="0">
<ComboBoxItem>Light</ComboBoxItem>
<ComboBoxItem>Dark</ComboBoxItem>
</ComboBox>
</ComboBox>
</StackPanel>
</TabControl.Tag>
</TabControl>
</Grid>

14
samples/ControlCatalog/MainView.xaml.cs

@ -56,6 +56,20 @@ namespace ControlCatalog
}
};
Styles.Add(light);
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectionChanged += (sender, e) =>
{
Window window = (Window)VisualRoot;
window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex;
};
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var decorations = this.Find<ComboBox>("Decorations");
decorations.SelectedIndex = (int)((Window)VisualRoot).SystemDecorations;
}
}
}

6
samples/ControlCatalog/MainWindow.xaml

@ -14,7 +14,7 @@
<NativeMenuItem Header="File">
<NativeMenuItem.Menu>
<NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked"/>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent">
<NativeMenuItem.Menu>
@ -22,7 +22,9 @@
</NativeMenuItem.Menu>
</NativeMenuItem>
<NativeMenuItemSeperator/>
<NativeMenuItem Header="Quit Avalonia" Clicked="OnCloseClicked" Gesture="CMD+Q"/>
<NativeMenuItem Header="{x:Static local:MainWindow.MenuQuitHeader}"
Gesture="{x:Static local:MainWindow.MenuQuitGesture}"
Clicked="OnCloseClicked" />
</NativeMenu>
</NativeMenuItem.Menu>
</NativeMenuItem>

14
samples/ControlCatalog/MainWindow.xaml.cs

@ -1,13 +1,11 @@
using System;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Notifications;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Threading;
using ControlCatalog.ViewModels;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
namespace ControlCatalog
{
@ -35,6 +33,12 @@ namespace ControlCatalog
mainMenu.AttachedToVisualTree += MenuAttached;
}
public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";
public static KeyGesture MenuQuitGesture => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ?
new KeyGesture(Key.Q, KeyModifiers.Meta) :
new KeyGesture(Key.F4, KeyModifiers.Alt);
public void MenuAttached(object sender, VisualTreeAttachmentEventArgs e)
{
if (NativeMenu.GetIsNativeMenuExported(this) && sender is Menu mainMenu)

19
samples/ControlCatalog/Pages/ImagePage.xaml

@ -7,7 +7,7 @@
<TextBlock Classes="h2">Displays an image</TextBlock>
</StackPanel>
<Grid ColumnDefinitions="*,*" RowDefinitions="Auto,*" Margin="64">
<Grid ColumnDefinitions="*,*,*" RowDefinitions="Auto,*" Margin="64">
<DockPanel Grid.Column="0" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Bitmap</TextBlock>
@ -22,6 +22,23 @@
</DockPanel>
<DockPanel Grid.Column="1" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Crop</TextBlock>
<ComboBox Name="bitmapCrop" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="BitmapCropChanged">
<ComboBoxItem>None</ComboBoxItem>
<ComboBoxItem>Center</ComboBoxItem>
<ComboBoxItem>TopLeft</ComboBoxItem>
<ComboBoxItem>TopRight</ComboBoxItem>
<ComboBoxItem>BottomLeft</ComboBoxItem>
<ComboBoxItem>BottomRight</ComboBoxItem>
</ComboBox>
<Image Name="croppedImage">
<Image.Source>
<CroppedBitmap Source="/Assets/delicate-arch-896885_640.jpg" SourceRect="0 0 320 240"/>
</Image.Source>
</Image>
</DockPanel>
<DockPanel Grid.Column="2" Grid.Row="1" Margin="16">
<TextBlock DockPanel.Dock="Top" Classes="h3" Margin="0 8">Drawing</TextBlock>
<ComboBox Name="drawingStretch" DockPanel.Dock="Top" SelectedIndex="2" SelectionChanged="DrawingStretchChanged">
<ComboBoxItem>None</ComboBoxItem>

33
samples/ControlCatalog/Pages/ImagePage.xaml.cs

@ -1,6 +1,10 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Media.Imaging;
using Avalonia.Platform;
namespace ControlCatalog.Pages
{
@ -8,12 +12,14 @@ namespace ControlCatalog.Pages
{
private readonly Image _bitmapImage;
private readonly Image _drawingImage;
private readonly Image _croppedImage;
public ImagePage()
{
InitializeComponent();
_bitmapImage = this.FindControl<Image>("bitmapImage");
_drawingImage = this.FindControl<Image>("drawingImage");
_croppedImage = this.FindControl<Image>("croppedImage");
}
private void InitializeComponent()
@ -38,5 +44,32 @@ namespace ControlCatalog.Pages
_drawingImage.Stretch = (Stretch)comboxBox.SelectedIndex;
}
}
public void BitmapCropChanged(object sender, SelectionChangedEventArgs e)
{
if (_croppedImage != null)
{
var comboxBox = (ComboBox)sender;
var croppedBitmap = _croppedImage.Source as CroppedBitmap;
croppedBitmap.SourceRect = GetCropRect(comboxBox.SelectedIndex);
}
}
private PixelRect GetCropRect(int index)
{
var bitmapWidth = 640;
var bitmapHeight = 426;
var cropSize = new PixelSize(320, 240);
return index switch
{
1 => new PixelRect(new PixelPoint((bitmapWidth - cropSize.Width) / 2, (bitmapHeight - cropSize.Width) / 2), cropSize),
2 => new PixelRect(new PixelPoint(0, 0), cropSize),
3 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, 0), cropSize),
4 => new PixelRect(new PixelPoint(0, bitmapHeight - cropSize.Height), cropSize),
5 => new PixelRect(new PixelPoint(bitmapWidth - cropSize.Width, bitmapHeight - cropSize.Height), cropSize),
_ => PixelRect.Empty
};
}
}
}

4
samples/ControlCatalog/Pages/MenuPage.xaml

@ -16,13 +16,13 @@
<TextBlock Classes="h3" Margin="4 8">Defined in XAML</TextBlock>
<Menu>
<MenuItem Header="_First">
<MenuItem Header="Standard _Menu Item"/>
<MenuItem Header="Standard _Menu Item" InputGesture="Ctrl+A"/>
<Separator/>
<MenuItem Header="Menu with _Submenu">
<MenuItem Header="Submenu _1"/>
<MenuItem Header="Submenu _2"/>
</MenuItem>
<MenuItem Header="Menu Item with _Icon">
<MenuItem Header="Menu Item with _Icon" InputGesture="Ctrl+Shift+B">
<MenuItem.Icon>
<Image Source="/Assets/github_icon.png"/>
</MenuItem.Icon>

2
samples/PlatformSanityChecks/PlatformSanityChecks.csproj

@ -2,7 +2,7 @@
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>

2
samples/Previewer/Previewer.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">

2
samples/RemoteDemo/RemoteDemo.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\ControlCatalog\ControlCatalog.csproj" />

2
samples/RenderDemo/RenderDemo.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

2
samples/VirtualizationDemo/VirtualizationDemo.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>netcoreapp2.0;net461</TargetFrameworks>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />

6
src/Avalonia.Base/AvaloniaObject.cs

@ -80,8 +80,12 @@ namespace Avalonia
_inheritanceParent?.RemoveInheritanceChild(this);
_inheritanceParent = value;
foreach (var property in AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType()))
var properties = AvaloniaPropertyRegistry.Instance.GetRegisteredInherited(GetType());
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{
var property = properties[i];
if (valuestore?.IsSet(property) == true)
{
// If local value set there can be no change.

88
src/Avalonia.Base/AvaloniaPropertyRegistry.cs

@ -3,9 +3,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Data;
namespace Avalonia
{
@ -28,8 +26,6 @@ namespace Avalonia
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _directCache =
new Dictionary<Type, List<AvaloniaProperty>>();
private readonly Dictionary<Type, List<PropertyInitializationData>> _initializedCache =
new Dictionary<Type, List<PropertyInitializationData>>();
private readonly Dictionary<Type, List<AvaloniaProperty>> _inheritedCache =
new Dictionary<Type, List<AvaloniaProperty>>();
@ -49,7 +45,7 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(Type type)
public IReadOnlyList<AvaloniaProperty> GetRegistered(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
@ -83,7 +79,7 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredAttached(Type type)
public IReadOnlyList<AvaloniaProperty> GetRegisteredAttached(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
@ -114,7 +110,7 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredDirect(Type type)
public IReadOnlyList<AvaloniaProperty> GetRegisteredDirect(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
@ -145,7 +141,7 @@ namespace Avalonia
/// </summary>
/// <param name="type">The type.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegisteredInherited(Type type)
public IReadOnlyList<AvaloniaProperty> GetRegisteredInherited(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
@ -157,16 +153,27 @@ namespace Avalonia
result = new List<AvaloniaProperty>();
var visited = new HashSet<AvaloniaProperty>();
foreach (var property in GetRegistered(type))
var registered = GetRegistered(type);
var registeredCount = registered.Count;
for (var i = 0; i < registeredCount; i++)
{
var property = registered[i];
if (property.Inherits)
{
result.Add(property);
visited.Add(property);
}
}
foreach (var property in GetRegisteredAttached(type))
var registeredAttached = GetRegisteredAttached(type);
var registeredAttachedCount = registeredAttached.Count;
for (var i = 0; i < registeredAttachedCount; i++)
{
var property = registeredAttached[i];
if (property.Inherits)
{
if (!visited.Contains(property))
@ -185,7 +192,7 @@ namespace Avalonia
/// </summary>
/// <param name="o">The object.</param>
/// <returns>A collection of <see cref="AvaloniaProperty"/> definitions.</returns>
public IEnumerable<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
public IReadOnlyList<AvaloniaProperty> GetRegistered(IAvaloniaObject o)
{
Contract.Requires<ArgumentNullException>(o != null);
@ -229,8 +236,13 @@ namespace Avalonia
throw new InvalidOperationException("Attached properties not supported.");
}
foreach (AvaloniaProperty x in GetRegistered(type))
var registered = GetRegistered(type);
var registeredCount = registered.Count;
for (var i = 0; i < registeredCount; i++)
{
AvaloniaProperty x = registered[i];
if (x.Name == name)
{
return x;
@ -276,8 +288,13 @@ namespace Avalonia
return property;
}
foreach (var p in GetRegisteredDirect(o.GetType()))
var registeredDirect = GetRegisteredDirect(o.GetType());
var registeredDirectCount = registeredDirect.Count;
for (var i = 0; i < registeredDirectCount; i++)
{
var p = registeredDirect[i];
if (p == property)
{
return (DirectPropertyBase<T>)p;
@ -308,8 +325,23 @@ namespace Avalonia
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(property != null);
return Instance.GetRegistered(type).Any(x => x == property) ||
Instance.GetRegisteredAttached(type).Any(x => x == property);
static bool ContainsProperty(IReadOnlyList<AvaloniaProperty> properties, AvaloniaProperty property)
{
var propertiesCount = properties.Count;
for (var i = 0; i < propertiesCount; i++)
{
if (properties[i] == property)
{
return true;
}
}
return false;
}
return ContainsProperty(Instance.GetRegistered(type), property) ||
ContainsProperty(Instance.GetRegisteredAttached(type), property);
}
/// <summary>
@ -374,7 +406,6 @@ namespace Avalonia
}
_registeredCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
@ -411,32 +442,7 @@ namespace Avalonia
}
_attachedCache.Clear();
_initializedCache.Clear();
_inheritedCache.Clear();
}
private readonly struct PropertyInitializationData
{
public AvaloniaProperty Property { get; }
public object Value { get; }
public bool IsDirect { get; }
public IDirectPropertyAccessor DirectAccessor { get; }
public PropertyInitializationData(AvaloniaProperty property, IDirectPropertyAccessor directAccessor)
{
Property = property;
Value = null;
IsDirect = true;
DirectAccessor = directAccessor;
}
public PropertyInitializationData(AvaloniaProperty property, IStyledPropertyAccessor styledAccessor, Type type)
{
Property = property;
Value = styledAccessor.GetDefaultValue(type);
IsDirect = false;
DirectAccessor = null;
}
}
}
}

2
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -476,7 +476,7 @@ namespace Avalonia.Controls
{
_dropDownButton.Click += DropDownButton_Click;
_buttonPointerPressedSubscription =
_dropDownButton.AddHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
_dropDownButton.AddDisposableHandler(PointerPressedEvent, DropDownButton_PointerPressed, handledEventsToo: true);
}
if (_textBox != null)

3
src/Avalonia.Controls/ComboBox.cs

@ -9,6 +9,7 @@ using Avalonia.Controls.Primitives;
using Avalonia.Controls.Shapes;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.VisualTree;
@ -265,7 +266,7 @@ namespace Avalonia.Controls
var toplevel = this.GetVisualRoot() as TopLevel;
if (toplevel != null)
{
_subscriptionsOnOpen = toplevel.AddHandler(PointerWheelChangedEvent, (s, ev) =>
_subscriptionsOnOpen = toplevel.AddDisposableHandler(PointerWheelChangedEvent, (s, ev) =>
{
//eat wheel scroll event outside dropdown popup while it's open
if (IsDropDownOpen && (ev.Source as IVisual).GetVisualRoot() == toplevel)

195
src/Avalonia.Controls/Converters/PlatformKeyGestureConverter.cs

@ -0,0 +1,195 @@
using System;
using System.Globalization;
using System.Runtime.InteropServices;
using System.Text;
using Avalonia.Data.Converters;
using Avalonia.Input;
namespace Avalonia.Controls.Converters
{
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
public class PlatformKeyGestureConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is null)
{
return null;
}
else if (value is KeyGesture gesture && targetType == typeof(string))
{
return ToPlatformString(gesture);
}
else
{
throw new NotSupportedException();
}
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
/// <summary>
/// Converts a <see cref="KeyGesture"/> to a string, formatting it according to the current
/// platform's style guidelines.
/// </summary>
/// <param name="gesture">The gesture.</param>
/// <returns>The gesture formatted according to the current platform.</returns>
public static string ToPlatformString(KeyGesture gesture)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return ToString(gesture, "Win");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
return ToString(gesture, "Super");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
{
return ToOSXString(gesture);
}
else
{
return gesture.ToString();
}
}
private static string ToString(KeyGesture gesture, string meta)
{
var s = new StringBuilder();
static void Plus(StringBuilder s)
{
if (s.Length > 0)
{
s.Append("+");
}
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
Plus(s);
s.Append(meta);
}
Plus(s);
s.Append(ToString(gesture.Key));
return s.ToString();
}
private static string ToOSXString(KeyGesture gesture)
{
var s = new StringBuilder();
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append('⌃');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
s.Append('⌥');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
s.Append('⇧');
}
if (gesture.KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
s.Append('⌘');
}
s.Append(ToOSXString(gesture.Key));
return s.ToString();
}
private static string ToString(Key key)
{
return key switch
{
Key.Add => "+",
Key.Back => "Backspace",
Key.D0 => "0",
Key.D1 => "1",
Key.D2 => "2",
Key.D3 => "3",
Key.D4 => "4",
Key.D5 => "5",
Key.D6 => "6",
Key.D7 => "7",
Key.D8 => "8",
Key.D9 => "9",
Key.Decimal => ".",
Key.Divide => "/",
Key.Down => "Down Arrow",
Key.Left => "Left Arrow",
Key.Multiply => "*",
Key.OemBackslash => "\\",
Key.OemCloseBrackets => "]",
Key.OemComma => ",",
Key.OemMinus => "-",
Key.OemOpenBrackets => "[",
Key.OemPeriod=> ".",
Key.OemPipe => "|",
Key.OemPlus => "+",
Key.OemQuestion => "/",
Key.OemQuotes => "\"",
Key.OemSemicolon => ";",
Key.OemTilde => "`",
Key.Right => "Right Arrow",
Key.Separator => "/",
Key.Subtract => "-",
Key.Up => "Up Arrow",
_ => key.ToString(),
};
}
private static string ToOSXString(Key key)
{
return key switch
{
Key.Back => "⌫",
Key.Down => "↓",
Key.End => "↘",
Key.Escape => "⎋",
Key.Home => "↖",
Key.Left => "←",
Key.Return => "↩",
Key.PageDown => "⇞",
Key.PageUp => "⇟",
Key.Right => "→",
Key.Space => "␣",
Key.Tab => "⇥",
Key.Up => "↑",
_ => ToString(key),
};
}
}
}

47
src/Avalonia.Controls/MenuItem.cs

@ -13,6 +13,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -48,6 +49,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<object> IconProperty =
AvaloniaProperty.Register<MenuItem, object>(nameof(Icon));
/// <summary>
/// Defines the <see cref="InputGesture"/> property.
/// </summary>
public static readonly StyledProperty<KeyGesture> InputGestureProperty =
AvaloniaProperty.Register<MenuItem, KeyGesture>(nameof(InputGesture));
/// <summary>
/// Defines the <see cref="IsSelected"/> property.
/// </summary>
@ -93,6 +100,7 @@ namespace Avalonia.Controls
private ICommand _command;
private bool _commandCanExecute = true;
private Popup _popup;
private IDisposable _gridHack;
/// <summary>
/// Initializes static members of the <see cref="MenuItem"/> class.
@ -194,6 +202,19 @@ namespace Avalonia.Controls
set { SetValue(IconProperty, value); }
}
/// <summary>
/// Gets or sets the input gesture that will be displayed in the menu item.
/// </summary>
/// <remarks>
/// Setting this property does not cause the input gesture to be handled by the menu item,
/// it simply displays the gesture text in the menu.
/// </remarks>
public KeyGesture InputGesture
{
get { return GetValue(InputGestureProperty); }
set { SetValue(InputGestureProperty, value); }
}
/// <summary>
/// Gets or sets a value indicating whether the <see cref="MenuItem"/> is currently selected.
/// </summary>
@ -304,6 +325,32 @@ namespace Avalonia.Controls
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
_gridHack?.Dispose();
_gridHack = null;
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
if (this.GetVisualParent() is IControl parent)
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
_gridHack = Bind(
DefinitionBase.PrivateSharedSizeScopeProperty,
parent.GetBindingObservable(DefinitionBase.PrivateSharedSizeScopeProperty));
}
}
/// <summary>

2
src/Avalonia.Controls/Platform/IWindowImpl.cs

@ -36,7 +36,7 @@ namespace Avalonia.Platform
/// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary>
void SetSystemDecorations(bool enabled);
void SetSystemDecorations(SystemDecorations enabled);
/// <summary>
/// Sets the icon of this window.

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

@ -160,17 +160,6 @@ namespace Avalonia.Controls.Primitives
return base.CreateTextLayout(constraint, StripAccessKey(text));
}
/// <summary>
/// Measures the control.
/// </summary>
/// <param name="availableSize">The available size for the control.</param>
/// <returns>The desired size.</returns>
protected override Size MeasureOverride(Size availableSize)
{
var result = base.MeasureOverride(availableSize);
return result.WithHeight(result.Height + 1);
}
/// <inheritdoc/>
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{

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

@ -279,7 +279,7 @@ namespace Avalonia.Controls.Primitives
}
}
DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(topLevel.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel));
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick));

60
src/Avalonia.Controls/TextBlock.cs

@ -20,6 +20,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<IBrush> BackgroundProperty =
Border.BackgroundProperty.AddOwner<TextBlock>();
/// <summary>
/// Defines the <see cref="Padding"/> property.
/// </summary>
public static readonly StyledProperty<Thickness> PaddingProperty =
Decorator.PaddingProperty.AddOwner<TextBlock>();
// TODO: Define these attached properties elsewhere (e.g. on a Text class) and AddOwner
// them into TextBlock.
@ -29,7 +35,7 @@ namespace Avalonia.Controls
public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
AvaloniaProperty.RegisterAttached<TextBlock, Control, FontFamily>(
nameof(FontFamily),
defaultValue: FontFamily.Default,
defaultValue: FontFamily.Default,
inherits: true);
/// <summary>
@ -110,20 +116,31 @@ namespace Avalonia.Controls
static TextBlock()
{
ClipToBoundsProperty.OverrideDefaultValue<TextBlock>(true);
AffectsRender<TextBlock>(
BackgroundProperty,
ForegroundProperty,
FontWeightProperty,
FontSizeProperty,
FontStyleProperty);
BackgroundProperty, ForegroundProperty, FontSizeProperty,
FontWeightProperty, FontStyleProperty, TextWrappingProperty,
TextTrimmingProperty, TextAlignmentProperty, FontFamilyProperty,
TextDecorationsProperty, TextProperty, PaddingProperty);
AffectsMeasure<TextBlock>(
FontSizeProperty, FontWeightProperty, FontStyleProperty,
FontFamilyProperty, TextTrimmingProperty, TextProperty,
PaddingProperty);
Observable.Merge(
TextProperty.Changed,
ForegroundProperty.Changed,
TextAlignmentProperty.Changed,
TextWrappingProperty.Changed,
TextTrimmingProperty.Changed,
FontSizeProperty.Changed,
FontStyleProperty.Changed,
FontWeightProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.OnTextPropertiesChanged());
FontWeightProperty.Changed,
FontFamilyProperty.Changed,
TextDecorationsProperty.Changed,
PaddingProperty.Changed
).AddClassHandler<TextBlock>((x, _) => x.InvalidateTextLayout());
}
/// <summary>
@ -145,6 +162,15 @@ namespace Avalonia.Controls
}
}
/// <summary>
/// Gets or sets the padding to place around the <see cref="Text"/>.
/// </summary>
public Thickness Padding
{
get { return GetValue(PaddingProperty); }
set { SetValue(PaddingProperty, value); }
}
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
@ -363,7 +389,9 @@ namespace Avalonia.Controls
context.FillRectangle(background, new Rect(Bounds.Size));
}
TextLayout?.Draw(context.PlatformImpl, new Point());
var padding = Padding;
TextLayout?.Draw(context.PlatformImpl, new Point(padding.Left, padding.Top));
}
/// <summary>
@ -412,6 +440,10 @@ namespace Avalonia.Controls
return new Size();
}
var padding = Padding;
availableSize = availableSize.Deflate(padding);
if (_constraint != availableSize)
{
InvalidateTextLayout();
@ -419,19 +451,17 @@ namespace Avalonia.Controls
_constraint = availableSize;
return TextLayout?.Bounds.Size ?? Size.Empty;
var measuredSize = TextLayout?.Bounds.Size ?? Size.Empty;
return measuredSize.Inflate(padding);
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
InvalidateTextLayout();
InvalidateMeasure();
}
private void OnTextPropertiesChanged()
{
InvalidateTextLayout();
InvalidateMeasure();
}
}

41
src/Avalonia.Controls/TextBox.cs

@ -275,6 +275,22 @@ namespace Avalonia.Controls
}
}
public string SelectedText
{
get { return GetSelection(); }
set
{
if (value == null)
{
return;
}
_undoRedoHelper.Snapshot();
HandleTextInput(value);
_undoRedoHelper.Snapshot();
}
}
/// <summary>
/// Gets or sets the horizontal alignment of the content within the control.
/// </summary>
@ -683,12 +699,12 @@ namespace Avalonia.Controls
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);
var text = Text;
if (text != null && e.MouseButton == MouseButton.Left)
if (text != null && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(_presenter);
var index = CaretIndex = _presenter.GetCaretIndex(point);
switch (e.ClickCount)
{
case 1:
@ -714,7 +730,8 @@ namespace Avalonia.Controls
protected override void OnPointerMoved(PointerEventArgs e)
{
if (_presenter != null && e.Pointer.Captured == _presenter)
// selection should not change during pointer move if the user right clicks
if (_presenter != null && e.Pointer.Captured == _presenter && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(_presenter);
@ -727,6 +744,22 @@ namespace Avalonia.Controls
{
if (_presenter != null && e.Pointer.Captured == _presenter)
{
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(_presenter);
var caretIndex = _presenter.GetCaretIndex(point);
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
CaretIndex = SelectionEnd = SelectionStart = caretIndex;
}
}
e.Pointer.Capture(null);
}
}

96
src/Avalonia.Controls/Window.cs

@ -2,19 +2,19 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading.Tasks;
using Avalonia.Controls.Platform;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Styling;
using System.Collections.Generic;
using System.Linq;
using JetBrains.Annotations;
using System.ComponentModel;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -45,6 +45,27 @@ namespace Avalonia.Controls
WidthAndHeight = 3,
}
/// <summary>
/// Determines system decorations (title bar, border, etc) for a <see cref="Window"/>
/// </summary>
public enum SystemDecorations
{
/// <summary>
/// No decorations
/// </summary>
None = 0,
/// <summary>
/// Window border without titlebar
/// </summary>
BorderOnly = 1,
/// <summary>
/// Fully decorated (default)
/// </summary>
Full = 2
}
/// <summary>
/// A top-level window.
/// </summary>
@ -59,8 +80,18 @@ namespace Avalonia.Controls
/// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary>
public static readonly StyledProperty<bool> HasSystemDecorationsProperty =
AvaloniaProperty.Register<Window, bool>(nameof(HasSystemDecorations), true);
[Obsolete("Use SystemDecorationsProperty instead")]
public static readonly DirectProperty<Window, bool> HasSystemDecorationsProperty =
AvaloniaProperty.RegisterDirect<Window, bool>(
nameof(HasSystemDecorations),
o => o.HasSystemDecorations,
(o, v) => o.HasSystemDecorations = v);
/// <summary>
/// Defines the <see cref="SystemDecorations"/> property.
/// </summary>
public static readonly StyledProperty<SystemDecorations> SystemDecorationsProperty =
AvaloniaProperty.Register<Window, SystemDecorations>(nameof(SystemDecorations), SystemDecorations.Full);
/// <summary>
/// Enables or disables the taskbar icon
@ -124,9 +155,6 @@ namespace Avalonia.Controls
{
BackgroundProperty.OverrideDefaultValue(typeof(Window), Brushes.White);
TitleProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetTitle((string)e.NewValue));
HasSystemDecorationsProperty.Changed.AddClassHandler<Window>(
(s, e) => s.PlatformImpl?.SetSystemDecorations((bool)e.NewValue));
ShowInTaskbarProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.ShowTaskbarIcon((bool)e.NewValue));
IconProperty.Changed.AddClassHandler<Window>((s, e) => s.PlatformImpl?.SetIcon(((WindowIcon)e.NewValue)?.PlatformImpl));
@ -135,12 +163,11 @@ namespace Avalonia.Controls
WindowStateProperty.Changed.AddClassHandler<Window>(
(w, e) => { if (w.PlatformImpl != null) w.PlatformImpl.WindowState = (WindowState)e.NewValue; });
MinWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size((double)e.NewValue, w.MinHeight), new Size(w.MaxWidth, w.MaxHeight)));
MinHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, (double)e.NewValue), new Size(w.MaxWidth, w.MaxHeight)));
MaxWidthProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size((double)e.NewValue, w.MaxHeight)));
MaxHeightProperty.Changed.AddClassHandler<Window>((w, e) => w.PlatformImpl?.SetMinMaxSize(new Size(w.MinWidth, w.MinHeight), new Size(w.MaxWidth, (double)e.NewValue)));
}
/// <summary>
@ -191,11 +218,30 @@ namespace Avalonia.Controls
/// <summary>
/// Enables or disables system window decorations (title bar, buttons, etc)
/// </summary>
///
[Obsolete("Use SystemDecorations instead")]
public bool HasSystemDecorations
{
get { return GetValue(HasSystemDecorationsProperty); }
set { SetValue(HasSystemDecorationsProperty, value); }
get => SystemDecorations == SystemDecorations.Full;
set
{
var oldValue = HasSystemDecorations;
if (oldValue != value)
{
SystemDecorations = value ? SystemDecorations.Full : SystemDecorations.None;
RaisePropertyChanged(HasSystemDecorationsProperty, oldValue, value);
}
}
}
/// <summary>
/// Sets the system decorations (title bar, border, etc)
/// </summary>
///
public SystemDecorations SystemDecorations
{
get { return GetValue(SystemDecorationsProperty); }
set { SetValue(SystemDecorationsProperty, value); }
}
/// <summary>
@ -584,5 +630,27 @@ namespace Avalonia.Controls
/// <see cref="Closing"/> event needs to be raised.
/// </remarks>
protected virtual void OnClosing(CancelEventArgs e) => Closing?.Invoke(this, e);
protected override void OnPropertyChanged<T>(
AvaloniaProperty<T> property,
Optional<T> oldValue,
BindingValue<T> newValue,
BindingPriority priority)
{
if (property == SystemDecorationsProperty)
{
var typedNewValue = newValue.GetValueOrDefault<SystemDecorations>();
PlatformImpl?.SetSystemDecorations(typedNewValue);
var o = oldValue.GetValueOrDefault<SystemDecorations>() == SystemDecorations.Full;
var n = typedNewValue == SystemDecorations.Full;
if (o != n)
{
RaisePropertyChanged(HasSystemDecorationsProperty, o, n);
}
}
}
}
}

2
src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs

@ -96,7 +96,7 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetSystemDecorations(bool enabled)
public void SetSystemDecorations(SystemDecorations enabled)
{
}

2
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@ -110,7 +110,7 @@ namespace Avalonia.DesignerSupport.Remote
{
}
public void SetSystemDecorations(bool enabled)
public void SetSystemDecorations(SystemDecorations enabled)
{
}

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

2
src/Avalonia.Diagnostics/Diagnostics/DevTools.cs

@ -22,7 +22,7 @@ namespace Avalonia.Diagnostics
}
}
return root.AddHandler(
return root.AddDisposableHandler(
InputElement.KeyDownEvent,
PreviewKeyDown,
RoutingStrategies.Tunnel);

44
src/Avalonia.Input/KeyGesture.cs

@ -1,9 +1,11 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using System.Text;
namespace Avalonia.Input
{
@ -108,19 +110,43 @@ namespace Avalonia.Input
public override string ToString()
{
var parts = new List<string>();
var s = new StringBuilder();
foreach (var flag in Enum.GetValues(typeof(KeyModifiers)).Cast<KeyModifiers>())
static void Plus(StringBuilder s)
{
if (KeyModifiers.HasFlag(flag) && flag != KeyModifiers.None)
if (s.Length > 0)
{
parts.Add(flag.ToString());
s.Append("+");
}
}
parts.Add(Key.ToString());
if (KeyModifiers.HasFlagCustom(KeyModifiers.Control))
{
s.Append("Ctrl");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Shift))
{
Plus(s);
s.Append("Shift");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Alt))
{
Plus(s);
s.Append("Alt");
}
if (KeyModifiers.HasFlagCustom(KeyModifiers.Meta))
{
Plus(s);
s.Append("Cmd");
}
Plus(s);
s.Append(Key);
return string.Join(" + ", parts);
return s.ToString();
}
public bool Matches(KeyEventArgs keyEvent) => ResolveNumPadOperationKey(keyEvent.Key) == Key && keyEvent.KeyModifiers == KeyModifiers;
@ -141,7 +167,9 @@ namespace Avalonia.Input
return KeyModifiers.Control;
}
if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase))
if (modifier.Equals("cmd".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
modifier.Equals("win".AsSpan(), StringComparison.OrdinalIgnoreCase) ||
modifier.Equals("⌘".AsSpan(), StringComparison.OrdinalIgnoreCase))
{
return KeyModifiers.Meta;
}

6
src/Avalonia.Interactivity/Avalonia.Interactivity.csproj

@ -1,6 +1,8 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<Nullable>Enable</Nullable>
<WarningsAsErrors>CS8600;CS8602;CS8603</WarningsAsErrors>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />
@ -9,6 +11,4 @@
<ProjectReference Include="..\Avalonia.Visuals\Avalonia.Visuals.csproj" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
</Project>
</Project>

200
src/Avalonia.Interactivity/EventRoute.cs

@ -0,0 +1,200 @@
using System;
using Avalonia.Collections.Pooled;
namespace Avalonia.Interactivity
{
/// <summary>
/// Holds the route for a routed event and supports raising an event on that route.
/// </summary>
public class EventRoute : IDisposable
{
private readonly RoutedEvent _event;
private PooledList<RouteItem>? _route;
/// <summary>
/// Initializes a new instance of the <see cref="RoutedEvent"/> class.
/// </summary>
/// <param name="e">The routed event to be raised.</param>
public EventRoute(RoutedEvent e)
{
e = e ?? throw new ArgumentNullException(nameof(e));
_event = e;
_route = null;
}
/// <summary>
/// Gets a value indicating whether the route has any handlers.
/// </summary>
public bool HasHandlers => _route?.Count > 0;
/// <summary>
/// Adds a handler to the route.
/// </summary>
/// <param name="target">The target on which the event should be raised.</param>
/// <param name="handler">The handler for the event.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">
/// If true the handler will be raised even when the routed event is marked as handled.
/// </param>
/// <param name="adapter">
/// An optional adapter which if supplied, will be called with <paramref name="handler"/>
/// and the parameters for the event. This adapter can be used to avoid calling
/// `DynamicInvoke` on the handler.
/// </param>
public void Add(
IInteractive target,
Delegate handler,
RoutingStrategies routes,
bool handledEventsToo = false,
Action<Delegate, object, RoutedEventArgs>? adapter = null)
{
target = target ?? throw new ArgumentNullException(nameof(target));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
_route ??= new PooledList<RouteItem>(16);
_route.Add(new RouteItem(target, handler, adapter, routes, handledEventsToo));
}
/// <summary>
/// Adds a class handler to the route.
/// </summary>
/// <param name="target">The target on which the event should be raised.</param>
public void AddClassHandler(IInteractive target)
{
target = target ?? throw new ArgumentNullException(nameof(target));
_route ??= new PooledList<RouteItem>(16);
_route.Add(new RouteItem(target, null, null, 0, false));
}
/// <summary>
/// Raises an event along the route.
/// </summary>
/// <param name="source">The event source.</param>
/// <param name="e">The event args.</param>
public void RaiseEvent(IInteractive source, RoutedEventArgs e)
{
source = source ?? throw new ArgumentNullException(nameof(source));
e = e ?? throw new ArgumentNullException(nameof(e));
e.Source = source;
if (_event.RoutingStrategies == RoutingStrategies.Direct)
{
e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
else
{
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
{
e.Route = RoutingStrategies.Tunnel;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
if (_event.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble))
{
e.Route = RoutingStrategies.Bubble;
RaiseEventImpl(e);
_event.InvokeRouteFinished(e);
}
}
}
/// <summary>
/// Disposes of the event route.
/// </summary>
public void Dispose()
{
_route?.Dispose();
_route = null;
}
private void RaiseEventImpl(RoutedEventArgs e)
{
if (_route is null)
{
return;
}
if (e.Source is null)
{
throw new ArgumentException("Event source may not be null", nameof(e));
}
IInteractive? lastTarget = null;
var start = 0;
var end = _route.Count;
var step = 1;
if (e.Route == RoutingStrategies.Tunnel)
{
start = end - 1;
step = end = -1;
}
for (var i = start; i != end; i += step)
{
var entry = _route[i];
// If we've got to a new control then call any RoutedEvent.Raised listeners.
if (entry.Target != lastTarget)
{
if (!e.Handled)
{
_event.InvokeRaised(entry.Target, e);
}
// If this is a direct event and we've already raised events then we're finished.
if (e.Route == RoutingStrategies.Direct && lastTarget is object)
{
return;
}
lastTarget = entry.Target;
}
// Raise the event handler.
if (entry.Handler is object &&
entry.Routes.HasFlagCustom(e.Route) &&
(!e.Handled || entry.HandledEventsToo))
{
if (entry.Adapter is object)
{
entry.Adapter(entry.Handler, entry.Target, e);
}
else
{
entry.Handler.DynamicInvoke(entry.Target, e);
}
}
}
}
private readonly struct RouteItem
{
public RouteItem(
IInteractive target,
Delegate? handler,
Action<Delegate, object, RoutedEventArgs>? adapter,
RoutingStrategies routes,
bool handledEventsToo)
{
Target = target;
Handler = handler;
Adapter = adapter;
Routes = routes;
HandledEventsToo = handledEventsToo;
}
public IInteractive Target { get; }
public Delegate? Handler { get; }
public Action<Delegate, object, RoutedEventArgs>? Adapter { get; }
public RoutingStrategies Routes { get; }
public bool HandledEventsToo { get; }
}
}
}

20
src/Avalonia.Interactivity/EventSubscription.cs

@ -1,20 +0,0 @@
// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
namespace Avalonia.Interactivity
{
internal delegate void HandlerInvokeSignature(Delegate baseHandler, object sender, RoutedEventArgs args);
internal class EventSubscription
{
public HandlerInvokeSignature InvokeAdapter { get; set; }
public Delegate Handler { get; set; }
public RoutingStrategies Routes { get; set; }
public bool AlsoIfHandled { get; set; }
}
}

13
src/Avalonia.Interactivity/IInteractive.cs

@ -13,7 +13,7 @@ namespace Avalonia.Interactivity
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive InteractiveParent { get; }
IInteractive? InteractiveParent { get; }
/// <summary>
/// Adds a handler for the specified routed event.
@ -23,7 +23,7 @@ namespace Avalonia.Interactivity
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
IDisposable AddHandler(
void AddHandler(
RoutedEvent routedEvent,
Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@ -38,7 +38,7 @@ namespace Avalonia.Interactivity
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
IDisposable AddHandler<TEventArgs>(
void AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
@ -60,6 +60,13 @@ namespace Avalonia.Interactivity
void RemoveHandler<TEventArgs>(RoutedEvent<TEventArgs> routedEvent, EventHandler<TEventArgs> handler)
where TEventArgs : RoutedEventArgs;
/// <summary>
/// Adds the object's handlers for a routed event to an event route.
/// </summary>
/// <param name="routedEvent">The event.</param>
/// <param name="route">The event route.</param>
void AddToEventRoute(RoutedEvent routedEvent, EventRoute route);
/// <summary>
/// Raises a routed event.
/// </summary>

293
src/Avalonia.Interactivity/Interactive.cs

@ -3,8 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Avalonia.Layout;
using Avalonia.VisualTree;
@ -15,16 +13,12 @@ namespace Avalonia.Interactivity
/// </summary>
public class Interactive : Layoutable, IInteractive
{
private Dictionary<RoutedEvent, List<EventSubscription>> _eventHandlers;
private static readonly Dictionary<Type, HandlerInvokeSignature> s_invokeHandlerCache = new Dictionary<Type, HandlerInvokeSignature>();
private Dictionary<RoutedEvent, List<EventSubscription>>? _eventHandlers;
/// <summary>
/// Gets the interactive parent of the object for bubbling and tunneling events.
/// </summary>
IInteractive IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
private Dictionary<RoutedEvent, List<EventSubscription>> EventHandlers => _eventHandlers ?? (_eventHandlers = new Dictionary<RoutedEvent, List<EventSubscription>>());
IInteractive? IInteractive.InteractiveParent => ((IVisual)this).VisualParent as IInteractive;
/// <summary>
/// Adds a handler for the specified routed event.
@ -33,24 +27,18 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
public IDisposable AddHandler(
public void AddHandler(
RoutedEvent routedEvent,
Delegate handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false)
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
var subscription = new EventSubscription
{
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
var subscription = new EventSubscription(handler, routes, handledEventsToo);
return AddEventSubscription(routedEvent, subscription);
AddEventSubscription(routedEvent, subscription);
}
/// <summary>
@ -61,44 +49,26 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
public IDisposable AddHandler<TEventArgs>(
public void AddHandler<TEventArgs>(
RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
// EventHandler delegate is not covariant, this forces us to create small wrapper
// that will cast our type erased instance and invoke it.
Type eventArgsType = routedEvent.EventArgsType;
if (!s_invokeHandlerCache.TryGetValue(eventArgsType, out var invokeAdapter))
static void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
{
void InvokeAdapter(Delegate baseHandler, object sender, RoutedEventArgs args)
{
var typedHandler = (EventHandler<TEventArgs>)baseHandler;
var typedArgs = (TEventArgs)args;
var typedHandler = (EventHandler<TEventArgs>)baseHandler;
var typedArgs = (TEventArgs)args;
typedHandler(sender, typedArgs);
}
invokeAdapter = InvokeAdapter;
s_invokeHandlerCache.Add(eventArgsType, invokeAdapter);
typedHandler(sender, typedArgs);
}
var subscription = new EventSubscription
{
InvokeAdapter = invokeAdapter,
Handler = handler,
Routes = routes,
AlsoIfHandled = handledEventsToo,
};
var subscription = new EventSubscription(handler, routes, handledEventsToo, (baseHandler, sender, args) => InvokeAdapter(baseHandler, sender, args));
return AddEventSubscription(routedEvent, subscription);
AddEventSubscription(routedEvent, subscription);
}
/// <summary>
@ -108,14 +78,19 @@ namespace Avalonia.Interactivity
/// <param name="handler">The handler.</param>
public void RemoveHandler(RoutedEvent routedEvent, Delegate handler)
{
Contract.Requires<ArgumentNullException>(routedEvent != null);
Contract.Requires<ArgumentNullException>(handler != null);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
handler = handler ?? throw new ArgumentNullException(nameof(handler));
List<EventSubscription> subscriptions = null;
if (_eventHandlers?.TryGetValue(routedEvent, out subscriptions) == true)
if (_eventHandlers is object &&
_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
subscriptions.RemoveAll(x => x.Handler == handler);
for (var i = subscriptions.Count - 1; i >= 0; i--)
{
if (subscriptions[i].Handler == handler)
{
subscriptions.RemoveAt(i);
}
}
}
}
@ -137,191 +112,115 @@ namespace Avalonia.Interactivity
/// <param name="e">The event args.</param>
public void RaiseEvent(RoutedEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Source = e.Source ?? this;
if (e.RoutedEvent.RoutingStrategies == RoutingStrategies.Direct)
{
e.Route = RoutingStrategies.Direct;
RaiseEventImpl(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
e = e ?? throw new ArgumentNullException(nameof(e));
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Tunnel) != 0)
if (e.RoutedEvent == null)
{
TunnelEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
throw new ArgumentException("Cannot raise an event whose RoutedEvent is null.");
}
if ((e.RoutedEvent.RoutingStrategies & RoutingStrategies.Bubble) != 0)
{
BubbleEvent(e);
e.RoutedEvent.InvokeRouteFinished(e);
}
using var route = BuildEventRoute(e.RoutedEvent);
route.RaiseEvent(this, e);
}
/// <summary>
/// Bubbles an event.
/// </summary>
/// <param name="e">The event args.</param>
private void BubbleEvent(RoutedEventArgs e)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Bubble;
var traverser = HierarchyTraverser<RaiseEventTraverse, NopTraverse>.Create(e);
traverser.Traverse(this);
}
/// <summary>
/// Tunnels an event.
/// </summary>
/// <param name="e">The event args.</param>
private void TunnelEvent(RoutedEventArgs e)
void IInteractive.AddToEventRoute(RoutedEvent routedEvent, EventRoute route)
{
Contract.Requires<ArgumentNullException>(e != null);
e.Route = RoutingStrategies.Tunnel;
var traverser = HierarchyTraverser<NopTraverse, RaiseEventTraverse>.Create(e);
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
route = route ?? throw new ArgumentNullException(nameof(route));
traverser.Traverse(this);
if (_eventHandlers != null &&
_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
foreach (var sub in subscriptions)
{
route.Add(this, sub.Handler, sub.Routes, sub.HandledEventsToo, sub.InvokeAdapter);
}
}
}
/// <summary>
/// Carries out the actual invocation of an event on this object.
/// Builds an event route for a routed event.
/// </summary>
/// <param name="e">The event args.</param>
private void RaiseEventImpl(RoutedEventArgs e)
/// <param name="e">The routed event.</param>
/// <returns>An <see cref="EventRoute"/> describing the route.</returns>
/// <remarks>
/// Usually, calling <see cref="RaiseEvent(RoutedEventArgs)"/> is sufficent to raise a routed
/// event, however there are situations in which the construction of the event args is expensive
/// and should be avoided if there are no handlers for an event. In these cases you can call
/// this method to build the event route and check the <see cref="EventRoute.HasHandlers"/>
/// property to see if there are any handlers registered on the route. If there are, call
/// <see cref="EventRoute.RaiseEvent(IInteractive, RoutedEventArgs)"/> to raise the event.
/// </remarks>
protected EventRoute BuildEventRoute(RoutedEvent e)
{
Contract.Requires<ArgumentNullException>(e != null);
e = e ?? throw new ArgumentNullException(nameof(e));
e.RoutedEvent.InvokeRaised(this, e);
var result = new EventRoute(e);
var hasClassHandlers = e.HasRaisedSubscriptions;
List<EventSubscription> subscriptions = null;
if (_eventHandlers?.TryGetValue(e.RoutedEvent, out subscriptions) == true)
if (e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Bubble) ||
e.RoutingStrategies.HasFlagCustom(RoutingStrategies.Tunnel))
{
foreach (var sub in subscriptions.ToList())
{
bool correctRoute = (e.Route & sub.Routes) != 0;
bool notFinished = !e.Handled || sub.AlsoIfHandled;
IInteractive? element = this;
if (correctRoute && notFinished)
while (element != null)
{
if (hasClassHandlers)
{
if (sub.InvokeAdapter != null)
{
sub.InvokeAdapter(sub.Handler, this, e);
}
else
{
sub.Handler.DynamicInvoke(this, e);
}
result.AddClassHandler(element);
}
element.AddToEventRoute(e, result);
element = element.InteractiveParent;
}
}
}
private List<EventSubscription> GetEventSubscriptions(RoutedEvent routedEvent)
{
if (!EventHandlers.TryGetValue(routedEvent, out var subscriptions))
else
{
subscriptions = new List<EventSubscription>();
EventHandlers.Add(routedEvent, subscriptions);
}
return subscriptions;
}
private IDisposable AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{
List<EventSubscription> subscriptions = GetEventSubscriptions(routedEvent);
subscriptions.Add(subscription);
return new UnsubscribeDisposable(subscriptions, subscription);
}
private sealed class UnsubscribeDisposable : IDisposable
{
private readonly List<EventSubscription> _subscriptions;
private readonly EventSubscription _subscription;
if (hasClassHandlers)
{
result.AddClassHandler(this);
}
public UnsubscribeDisposable(List<EventSubscription> subscriptions, EventSubscription subscription)
{
_subscriptions = subscriptions;
_subscription = subscription;
((IInteractive)this).AddToEventRoute(e, result);
}
public void Dispose()
{
_subscriptions.Remove(_subscription);
}
return result;
}
private interface ITraverse
private void AddEventSubscription(RoutedEvent routedEvent, EventSubscription subscription)
{
void Execute(IInteractive target, RoutedEventArgs e);
}
_eventHandlers ??= new Dictionary<RoutedEvent, List<EventSubscription>>();
private struct NopTraverse : ITraverse
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(IInteractive target, RoutedEventArgs e)
if (!_eventHandlers.TryGetValue(routedEvent, out var subscriptions))
{
subscriptions = new List<EventSubscription>();
_eventHandlers.Add(routedEvent, subscriptions);
}
}
private struct RaiseEventTraverse : ITraverse
{
[MethodImpl(MethodImplOptions.AggressiveInlining)]
public void Execute(IInteractive target, RoutedEventArgs e)
{
((Interactive)target).RaiseEventImpl(e);
}
subscriptions.Add(subscription);
}
/// <summary>
/// Traverses interactive hierarchy allowing for raising events.
/// </summary>
/// <typeparam name="TPreTraverse">Called before parent is traversed.</typeparam>
/// <typeparam name="TPostTraverse">Called after parent has been traversed.</typeparam>
private struct HierarchyTraverser<TPreTraverse, TPostTraverse>
where TPreTraverse : struct, ITraverse
where TPostTraverse : struct, ITraverse
private readonly struct EventSubscription
{
private TPreTraverse _preTraverse;
private TPostTraverse _postTraverse;
private readonly RoutedEventArgs _args;
private HierarchyTraverser(TPreTraverse preTraverse, TPostTraverse postTraverse, RoutedEventArgs args)
{
_preTraverse = preTraverse;
_postTraverse = postTraverse;
_args = args;
}
public static HierarchyTraverser<TPreTraverse, TPostTraverse> Create(RoutedEventArgs args)
{
return new HierarchyTraverser<TPreTraverse, TPostTraverse>(new TPreTraverse(), new TPostTraverse(), args);
public EventSubscription(
Delegate handler,
RoutingStrategies routes,
bool handledEventsToo,
Action<Delegate, object, RoutedEventArgs>? invokeAdapter = null)
{
Handler = handler;
Routes = routes;
HandledEventsToo = handledEventsToo;
InvokeAdapter = invokeAdapter;
}
public void Traverse(IInteractive target)
{
_preTraverse.Execute(target, _args);
public Action<Delegate, object, RoutedEventArgs>? InvokeAdapter { get; }
IInteractive parent = target.InteractiveParent;
public Delegate Handler { get; }
if (parent != null)
{
Traverse(parent);
}
public RoutingStrategies Routes { get; }
_postTraverse.Execute(target, _args);
}
public bool HandledEventsToo { get; }
}
}
}

30
src/Avalonia.Interactivity/InteractiveExtensions.cs

@ -2,8 +2,7 @@
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
namespace Avalonia.Interactivity
@ -13,6 +12,28 @@ namespace Avalonia.Interactivity
/// </summary>
public static class InteractiveExtensions
{
/// <summary>
/// Adds a handler for the specified routed event and returns a disposable that can terminate the event subscription.
/// </summary>
/// <typeparam name="TEventArgs">The type of the event's args.</typeparam>
/// <param name="o">Target for adding given event handler.</param>
/// <param name="routedEvent">The routed event.</param>
/// <param name="handler">The handler.</param>
/// <param name="routes">The routing strategies to listen to.</param>
/// <param name="handledEventsToo">Whether handled events should also be listened for.</param>
/// <returns>A disposable that terminates the event subscription.</returns>
public static IDisposable AddDisposableHandler<TEventArgs>(this IInteractive o, RoutedEvent<TEventArgs> routedEvent,
EventHandler<TEventArgs> handler,
RoutingStrategies routes = RoutingStrategies.Direct | RoutingStrategies.Bubble,
bool handledEventsToo = false) where TEventArgs : RoutedEventArgs
{
o.AddHandler(routedEvent, handler, routes, handledEventsToo);
return Disposable.Create(
(instance: o, handler, routedEvent),
state => state.instance.RemoveHandler(state.routedEvent, state.handler));
}
/// <summary>
/// Gets an observable for a <see cref="RoutedEvent{TEventArgs}"/>.
/// </summary>
@ -30,7 +51,10 @@ namespace Avalonia.Interactivity
bool handledEventsToo = false)
where TEventArgs : RoutedEventArgs
{
return Observable.Create<TEventArgs>(x => o.AddHandler(
o = o ?? throw new ArgumentNullException(nameof(o));
routedEvent = routedEvent ?? throw new ArgumentNullException(nameof(routedEvent));
return Observable.Create<TEventArgs>(x => o.AddDisposableHandler(
routedEvent,
(_, e) => x.OnNext(e),
routes,

20
src/Avalonia.Interactivity/RoutedEvent.cs

@ -25,10 +25,14 @@ namespace Avalonia.Interactivity
Type eventArgsType,
Type ownerType)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(eventArgsType != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
Contract.Requires<InvalidCastException>(typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType));
name = name ?? throw new ArgumentNullException(nameof(name));
eventArgsType = eventArgsType ?? throw new ArgumentNullException(nameof(name));
ownerType = ownerType ?? throw new ArgumentNullException(nameof(name));
if (!typeof(RoutedEventArgs).IsAssignableFrom(eventArgsType))
{
throw new InvalidCastException("eventArgsType must be derived from RoutedEventArgs.");
}
EventArgsType = eventArgsType;
Name = name;
@ -44,6 +48,8 @@ namespace Avalonia.Interactivity
public RoutingStrategies RoutingStrategies { get; }
public bool HasRaisedSubscriptions => _raised.HasObservers;
public IObservable<(object, RoutedEventArgs)> Raised => _raised;
public IObservable<RoutedEventArgs> RouteFinished => _routeFinished;
@ -52,7 +58,7 @@ namespace Avalonia.Interactivity
RoutingStrategies routingStrategy)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(name != null);
name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, typeof(TOwner));
RoutedEventRegistry.Instance.Register(typeof(TOwner), routedEvent);
@ -65,7 +71,7 @@ namespace Avalonia.Interactivity
Type ownerType)
where TEventArgs : RoutedEventArgs
{
Contract.Requires<ArgumentNullException>(name != null);
name = name ?? throw new ArgumentNullException(nameof(name));
var routedEvent = new RoutedEvent<TEventArgs>(name, routingStrategy, ownerType);
RoutedEventRegistry.Instance.Register(ownerType, routedEvent);
@ -108,8 +114,6 @@ namespace Avalonia.Interactivity
public RoutedEvent(string name, RoutingStrategies routingStrategies, Type ownerType)
: base(name, routingStrategies, typeof(TEventArgs), ownerType)
{
Contract.Requires<ArgumentNullException>(name != null);
Contract.Requires<ArgumentNullException>(ownerType != null);
}
[Obsolete("Use overload taking Action<TTarget, TEventArgs>.")]

8
src/Avalonia.Interactivity/RoutedEventArgs.cs

@ -11,12 +11,12 @@ namespace Avalonia.Interactivity
{
}
public RoutedEventArgs(RoutedEvent routedEvent)
public RoutedEventArgs(RoutedEvent? routedEvent)
{
RoutedEvent = routedEvent;
}
public RoutedEventArgs(RoutedEvent routedEvent, IInteractive source)
public RoutedEventArgs(RoutedEvent? routedEvent, IInteractive? source)
{
RoutedEvent = routedEvent;
Source = source;
@ -24,10 +24,10 @@ namespace Avalonia.Interactivity
public bool Handled { get; set; }
public RoutedEvent RoutedEvent { get; set; }
public RoutedEvent? RoutedEvent { get; set; }
public RoutingStrategies Route { get; set; }
public IInteractive Source { get; set; }
public IInteractive? Source { get; set; }
}
}

6
src/Avalonia.Interactivity/RoutedEventRegistry.cs

@ -32,8 +32,8 @@ namespace Avalonia.Interactivity
/// </remarks>
public void Register(Type type, RoutedEvent @event)
{
Contract.Requires<ArgumentNullException>(type != null);
Contract.Requires<ArgumentNullException>(@event != null);
type = type ?? throw new ArgumentNullException(nameof(type));
@event = @event ?? throw new ArgumentNullException(nameof(@event));
if (!_registeredRoutedEvents.TryGetValue(type, out var list))
{
@ -66,7 +66,7 @@ namespace Avalonia.Interactivity
/// <returns>All routed events registered with the provided type.</returns>
public IReadOnlyList<RoutedEvent> GetRegistered(Type type)
{
Contract.Requires<ArgumentNullException>(type != null);
type = type ?? throw new ArgumentNullException(nameof(type));
if (_registeredRoutedEvents.TryGetValue(type, out var events))
{

4
src/Avalonia.Native/WindowImpl.cs

@ -68,9 +68,9 @@ namespace Avalonia.Native
_native.CanResize = value;
}
public void SetSystemDecorations(bool enabled)
public void SetSystemDecorations(Controls.SystemDecorations enabled)
{
_native.HasDecorations = enabled;
_native.HasDecorations = (Interop.SystemDecorations)enabled;
}
public void SetTitleBarColor (Avalonia.Media.Color color)

9
src/Avalonia.Styling/Styling/Activators/StyleClassActivator.cs

@ -14,6 +14,7 @@ namespace Avalonia.Styling.Activators
{
private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes;
private NotifyCollectionChangedEventHandler? _classesChangedHandler;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match)
{
@ -21,6 +22,9 @@ namespace Avalonia.Styling.Activators
_match = match;
}
private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
_classesChangedHandler ??= ClassesChanged;
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{
int remainingMatches = toMatch.Count;
@ -51,16 +55,15 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0;
}
protected override void Initialize()
{
PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChanged;
_classes.CollectionChanged += ClassesChangedHandler;
}
protected override void Deinitialize()
{
_classes.CollectionChanged -= ClassesChanged;
_classes.CollectionChanged -= ClassesChangedHandler;
}
private void ClassesChanged(object sender, NotifyCollectionChangedEventArgs e)

35
src/Avalonia.Themes.Default/MenuItem.xaml

@ -1,6 +1,10 @@
<Styles xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:conv="clr-namespace:Avalonia.Controls.Converters;assembly=Avalonia.Controls"
xmlns:sys="clr-namespace:System;assembly=netstandard">
<Styles.Resources>
<conv:PlatformKeyGestureConverter x:Key="KeyGestureConverter"/>
</Styles.Resources>
<Style Selector="MenuItem">
<Setter Property="Background" Value="Transparent"/>
<Setter Property="BorderThickness" Value="1"/>
@ -11,7 +15,14 @@
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}">
<Grid ColumnDefinitions="20,5,*,20">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="20"/>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto" SharedSizeGroup="MenuItemIGT"/>
<ColumnDefinition Width="20"/>
</Grid.ColumnDefinitions>
<ContentPresenter Name="icon"
Content="{TemplateBinding Icon}"
Width="16"
@ -36,12 +47,16 @@
</DataTemplate>
</ContentPresenter.DataTemplates>
</ContentPresenter>
<TextBlock x:Name="PART_InputGestureText"
Grid.Column="3"
Text="{TemplateBinding InputGesture, Converter={StaticResource KeyGestureConverter}}"
VerticalAlignment="Center"/>
<Path Name="rightArrow"
Data="M0,0L4,3.5 0,7z"
Fill="{DynamicResource ThemeForegroundBrush}"
Margin="10,0,0,0"
VerticalAlignment="Center"
Grid.Column="3"/>
Grid.Column="4"/>
<Popup Name="PART_Popup"
PlacementMode="Right"
StaysOpen="True"
@ -54,7 +69,7 @@
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="4 2"/>
Grid.IsSharedSizeScope="True"/>
</ScrollViewer>
</Border>
</Popup>
@ -100,10 +115,10 @@
BorderThickness="{TemplateBinding BorderThickness}">
<ScrollViewer>
<ItemsPresenter Name="PART_ItemsPresenter"
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Margin="2"/>
Items="{TemplateBinding Items}"
ItemsPanel="{TemplateBinding ItemsPanel}"
ItemTemplate="{TemplateBinding ItemTemplate}"
Grid.IsSharedSizeScope="True"/>
</ScrollViewer>
</Border>
</Popup>
@ -113,6 +128,10 @@
</Setter>
</Style>
<Style Selector="MenuItem /template/ ItemsPresenter#PART_ItemsPresenter">
<Setter Property="Margin" Value="2"/>
</Style>
<Style Selector="MenuItem:selected /template/ Border#root">
<Setter Property="Background" Value="{DynamicResource ThemeAccentBrush4}"/>
<Setter Property="BorderBrush" Value="{DynamicResource ThemeAccentBrush}"/>

1
src/Avalonia.Themes.Default/NativeMenuBar.xaml

@ -13,6 +13,7 @@
<Menu.Styles>
<Style Selector="MenuItem">
<Setter Property="Header" Value="{Binding Header}"/>
<Setter Property="InputGesture" Value="{Binding Gesture}"/>
<Setter Property="Items" Value="{Binding Menu.Items}"/>
<Setter Property="Command" Value="{Binding Command}"/>
<Setter Property="CommandParameter" Value="{Binding CommandParameter}"/>

96
src/Avalonia.Visuals/Media/Imaging/CroppedBitmap.cs

@ -0,0 +1,96 @@
using System;
using Avalonia.Visuals.Media.Imaging;
namespace Avalonia.Media.Imaging
{
/// <summary>
/// Crops a Bitmap.
/// </summary>
public class CroppedBitmap : AvaloniaObject, IImage, IAffectsRender, IDisposable
{
/// <summary>
/// Defines the <see cref="Source"/> property.
/// </summary>
public static readonly StyledProperty<IImage> SourceProperty =
AvaloniaProperty.Register<CroppedBitmap, IImage>(nameof(Source));
/// <summary>
/// Defines the <see cref="SourceRect"/> property.
/// </summary>
public static readonly StyledProperty<PixelRect> SourceRectProperty =
AvaloniaProperty.Register<CroppedBitmap, PixelRect>(nameof(SourceRect));
public event EventHandler Invalidated;
static CroppedBitmap()
{
SourceRectProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceRectChanged(e));
SourceProperty.Changed.AddClassHandler<CroppedBitmap>((x, e) => x.SourceChanged(e));
}
/// <summary>
/// Gets or sets the source for the bitmap.
/// </summary>
public IImage Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets the rectangular area that the bitmap is cropped to.
/// </summary>
public PixelRect SourceRect
{
get => GetValue(SourceRectProperty);
set => SetValue(SourceRectProperty, value);
}
public CroppedBitmap()
{
Source = null;
SourceRect = default;
}
public CroppedBitmap(IImage source, PixelRect sourceRect)
{
Source = source;
SourceRect = sourceRect;
}
private void SourceChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue == null)
return;
if (!(e.NewValue is IBitmap))
throw new ArgumentException("Only IBitmap supported as source");
Invalidated?.Invoke(this, e);
}
private void SourceRectChanged(AvaloniaPropertyChangedEventArgs e) => Invalidated?.Invoke(this, e);
public virtual void Dispose()
{
(Source as IBitmap)?.Dispose();
}
public Size Size {
get
{
if (Source == null)
return Size.Empty;
if (SourceRect.IsEmpty)
return Source.Size;
return SourceRect.Size.ToSizeWithDpi((Source as IBitmap).Dpi);
}
}
public void Draw(DrawingContext context, Rect sourceRect, Rect destRect, BitmapInterpolationMode bitmapInterpolationMode)
{
if (Source == null)
return;
var topLeft = SourceRect.TopLeft.ToPointWithDpi((Source as IBitmap).Dpi);
Source.Draw(context, sourceRect.Translate(new Vector(topLeft.X, topLeft.Y)), destRect, bitmapInterpolationMode);
}
}
}

27
src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs

@ -201,18 +201,17 @@ namespace Avalonia.Media.TextFormatting
var availableWidth = paragraphWidth;
var currentWidth = 0.0;
var runIndex = 0;
var length = 0;
while (runIndex < textRuns.Count)
{
var currentRun = textRuns[runIndex];
currentWidth += currentRun.GlyphRun.Bounds.Width;
if (currentWidth > availableWidth)
if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth)
{
var measuredLength = MeasureText(currentRun, paragraphWidth);
var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth);
if (measuredLength < text.End)
if (measuredLength < currentRun.Text.Length)
{
var currentBreakPosition = -1;
@ -241,15 +240,19 @@ namespace Avalonia.Media.TextFormatting
}
}
var splitResult = SplitTextRuns(textRuns, measuredLength);
length += measuredLength;
var splitResult = SplitTextRuns(textRuns, length);
var textLineMetrics =
TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment);
return new SimpleTextLine(text.Take(measuredLength), splitResult.First, textLineMetrics);
return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics);
}
availableWidth -= currentRun.GlyphRun.Bounds.Width;
currentWidth += currentRun.GlyphRun.Bounds.Width;
length += currentRun.GlyphRun.Characters.Length;
runIndex++;
}
@ -281,12 +284,18 @@ namespace Avalonia.Media.TextFormatting
if (measuredWidth + advance > availableWidth)
{
index--;
break;
}
measuredWidth += advance;
}
if(index < 0)
{
return 0;
}
var cluster = textRun.GlyphRun.GlyphClusters[index];
var characterHit = textRun.GlyphRun.FindNearestCharacterHit(cluster, out _);
@ -355,7 +364,7 @@ namespace Avalonia.Media.TextFormatting
continue;
}
var firstCount = currentRun.GlyphRun.Characters.Length > 1 ? i + 1 : i;
var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i;
var first = new ShapedTextRun[firstCount];

16
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -233,6 +233,12 @@ namespace Avalonia.Media.TextFormatting
var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties);
if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight)
{
currentPosition = _text.Length;
break;
}
UpdateBounds(textLine, ref left, ref right, ref bottom);
textLines.Add(textLine);
@ -253,17 +259,17 @@ namespace Avalonia.Media.TextFormatting
{
var emptyTextLine = CreateEmptyTextLine(currentPosition);
if (!double.IsPositiveInfinity(MaxHeight) && bottom + emptyTextLine.LineMetrics.Size.Height > MaxHeight)
{
break;
}
UpdateBounds(emptyTextLine, ref left, ref right, ref bottom);
textLines.Add(emptyTextLine);
break;
}
if (!double.IsPositiveInfinity(MaxHeight) && MaxHeight < Bounds.Height)
{
break;
}
}
Bounds = new Rect(left, 0, right, bottom);

1
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@ -8,6 +8,7 @@ using Avalonia.Metadata;
[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Imaging")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]

10
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@ -355,15 +355,21 @@ namespace Avalonia.Rendering
node.BeginRender(context, isLayerRoot);
foreach (var operation in node.DrawOperations)
var drawOperations = node.DrawOperations;
var drawOperationsCount = drawOperations.Count;
for (int i = 0; i < drawOperationsCount; i++)
{
var operation = drawOperations[i];
_currentDraw = operation;
operation.Item.Render(context);
_currentDraw = null;
}
foreach (var child in node.Children)
var children = node.Children;
var childrenCount = children.Count;
for (int i = 0; i < childrenCount; i++)
{
var child = children[i];
Render(context, (VisualNode)child, layer, clipBounds);
}

16
src/Avalonia.X11/X11Window.cs

@ -173,6 +173,7 @@ namespace Avalonia.X11
Surfaces = surfaces.ToArray();
UpdateMotifHints();
UpdateSizeHints(null);
_xic = XCreateIC(_x11.Xim, XNames.XNInputStyle, XIMProperties.XIMPreeditNothing | XIMProperties.XIMStatusNothing,
XNames.XNClientWindow, _handle, IntPtr.Zero);
XFlush(_x11.Display);
@ -219,12 +220,16 @@ namespace Avalonia.X11
var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
if (_popup || !_systemDecorations)
if (_popup || _systemDecorations == SystemDecorations.None)
{
decorations = 0;
}
else if (_systemDecorations == SystemDecorations.BorderOnly)
{
decorations = MotifDecorations.Border;
}
if (!_canResize)
if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
{
functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@ -247,7 +252,7 @@ namespace Avalonia.X11
var min = _minMaxSize.minSize;
var max = _minMaxSize.maxSize;
if (!_canResize)
if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
max = min = _realSize;
if (preResize.HasValue)
@ -621,7 +626,7 @@ namespace Avalonia.X11
return rv;
}
private bool _systemDecorations = true;
private SystemDecorations _systemDecorations = SystemDecorations.Full;
private bool _canResize = true;
private const int MaxWindowDimension = 100000;
@ -777,10 +782,11 @@ namespace Avalonia.X11
(int)(point.X * Scaling + Position.X),
(int)(point.Y * Scaling + Position.Y));
public void SetSystemDecorations(bool enabled)
public void SetSystemDecorations(SystemDecorations enabled)
{
_systemDecorations = enabled;
UpdateMotifHints();
UpdateSizeHints(null);
}

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

@ -1298,7 +1298,17 @@ namespace Avalonia.Win32.Interop
[DllImport("ole32.dll", CharSet = CharSet.Auto, ExactSpelling = true, PreserveSig = false)]
internal static extern void DoDragDrop(IOleDataObject dataObject, IDropSource dropSource, int allowedEffects, out int finalEffect);
[DllImport("dwmapi.dll")]
public static extern int DwmExtendFrameIntoClientArea(IntPtr hwnd, ref MARGINS margins);
[StructLayout(LayoutKind.Sequential)]
internal struct MARGINS
{
public int cxLeftWidth;
public int cxRightWidth;
public int cyTopHeight;
public int cyBottomHeight;
}
public enum MONITOR
{

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

@ -34,7 +34,7 @@ namespace Avalonia.Win32
private IInputRoot _owner;
private ManagedDeferredRendererLock _rendererLock = new ManagedDeferredRendererLock();
private bool _trackingMouse;
private bool _decorated = true;
private SystemDecorations _decorated = SystemDecorations.Full;
private bool _resizable = true;
private bool _topmost = false;
private bool _taskbarIcon = true;
@ -97,7 +97,7 @@ namespace Avalonia.Win32
{
get
{
if (_decorated)
if (_decorated == SystemDecorations.Full)
{
var style = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_STYLE);
var exStyle = UnmanagedMethods.GetWindowLong(_hwnd, (int)UnmanagedMethods.WindowLongParam.GWL_EXSTYLE);
@ -281,7 +281,7 @@ namespace Avalonia.Win32
UnmanagedMethods.ShowWindow(_hwnd, UnmanagedMethods.ShowWindowCommand.Hide);
}
public void SetSystemDecorations(bool value)
public void SetSystemDecorations(SystemDecorations value)
{
if (value == _decorated)
{
@ -464,7 +464,7 @@ namespace Avalonia.Win32
return IntPtr.Zero;
case WindowsMessage.WM_NCCALCSIZE:
if (ToInt32(wParam) == 1 && !_decorated)
if (ToInt32(wParam) == 1 && _decorated != SystemDecorations.Full)
{
return IntPtr.Zero;
}
@ -682,14 +682,14 @@ namespace Avalonia.Win32
break;
case WindowsMessage.WM_NCPAINT:
if (!_decorated)
if (_decorated != SystemDecorations.Full)
{
return IntPtr.Zero;
}
break;
case WindowsMessage.WM_NCACTIVATE:
if (!_decorated)
if (_decorated != SystemDecorations.Full)
{
return new IntPtr(1);
}
@ -1001,7 +1001,7 @@ namespace Avalonia.Win32
style |= WindowStyles.WS_OVERLAPPEDWINDOW;
if (!_decorated)
if (_decorated != SystemDecorations.Full)
{
style ^= (WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU);
}
@ -1011,6 +1011,10 @@ namespace Avalonia.Win32
style ^= (WindowStyles.WS_SIZEFRAME);
}
MARGINS margins = new MARGINS();
margins.cyBottomHeight = _decorated == SystemDecorations.BorderOnly ? 1 : 0;
UnmanagedMethods.DwmExtendFrameIntoClientArea(_hwnd, ref margins);
GetClientRect(_hwnd, out var oldClientRect);
var oldClientRectOrigin = new UnmanagedMethods.POINT();
ClientToScreen(_hwnd, ref oldClientRectOrigin);
@ -1024,7 +1028,7 @@ namespace Avalonia.Win32
if (oldDecorated != _decorated)
{
var newRect = oldClientRect;
if (_decorated)
if (_decorated == SystemDecorations.Full)
AdjustWindowRectEx(ref newRect, (uint)style, false,
GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE));
SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,

2
src/iOS/Avalonia.iOS/EmbeddableImpl.cs

@ -20,7 +20,7 @@ namespace Avalonia.iOS
return Disposable.Empty;
}
public void SetSystemDecorations(bool enabled)
public void SetSystemDecorations(SystemDecorations enabled)
{
}

2
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />

2
tests/Avalonia.Animation.UnitTests/Avalonia.Animation.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

2
tests/Avalonia.Base.UnitTests/Avalonia.Base.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

2
tests/Avalonia.Benchmarks/Avalonia.Benchmarks.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<OutputType>Exe</OutputType>
<IsPackable>false</IsPackable>
</PropertyGroup>

11
tests/Avalonia.Benchmarks/NullGlyphRun.cs

@ -0,0 +1,11 @@
using Avalonia.Platform;
namespace Avalonia.Benchmarks
{
internal class NullGlyphRun : IGlyphRunImpl
{
public void Dispose()
{
}
}
}

4
tests/Avalonia.Benchmarks/NullRenderingPlatform.cs

@ -72,7 +72,9 @@ namespace Avalonia.Benchmarks
public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
{
throw new NotImplementedException();
width = default;
return new NullGlyphRun();
}
}
}

2
tests/Avalonia.Controls.DataGrid.UnitTests/Avalonia.Controls.DataGrid.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>

2
tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<LangVersion>latest</LangVersion>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>

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

@ -385,6 +385,49 @@ namespace Avalonia.Controls.UnitTests
Assert.True(target.SelectionEnd <= "123".Length);
}
}
[Fact]
public void SelectedText_Changes_OnSelectionChange()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123456789"
};
Assert.True(target.SelectedText == "");
target.SelectionStart = 2;
target.SelectionEnd = 4;
Assert.True(target.SelectedText == "23");
}
}
[Fact]
public void SelectedText_EditsText()
{
using (UnitTestApplication.Start(Services))
{
var target = new TextBox
{
Template = CreateTemplate(),
Text = "0123"
};
target.SelectedText = "AA";
Assert.True(target.Text == "AA0123");
target.SelectionStart = 1;
target.SelectionEnd = 3;
target.SelectedText = "BB";
Assert.True(target.Text == "ABB123");
}
}
[Fact]
public void CoerceCaretIndex_Doesnt_Cause_Exception_with_malformed_line_ending()
{

2
tests/Avalonia.DesignerSupport.TestApp/Avalonia.DesignerSupport.TestApp.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
<IsPackable>false</IsPackable>
</PropertyGroup>
<ItemGroup>

2
tests/Avalonia.DesignerSupport.Tests/Avalonia.DesignerSupport.Tests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />

6
tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs

@ -18,7 +18,7 @@ namespace Avalonia.DesignerSupport.Tests
{
public class DesignerSupportTests
{
private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp2.0/Avalonia.Designer.HostApp.dll";
private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp3.1/Avalonia.Designer.HostApp.dll";
private readonly Xunit.Abstractions.ITestOutputHelper outputHelper;
public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper)
@ -28,12 +28,12 @@ namespace Avalonia.DesignerSupport.Tests
[SkippableTheory,
InlineData(
@"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp2.0/",
@"..\..\..\..\..\tests/Avalonia.DesignerSupport.TestApp/bin/$BUILD/netcoreapp3.1/",
"Avalonia.DesignerSupport.TestApp",
"Avalonia.DesignerSupport.TestApp.dll",
@"..\..\..\..\..\tests\Avalonia.DesignerSupport.TestApp\MainWindow.xaml"),
InlineData(
@"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp2.0\",
@"..\..\..\..\..\samples\ControlCatalog.NetCore\bin\$BUILD\netcoreapp3.1\",
"ControlCatalog.NetCore",
"ControlCatalog.dll",
@"..\..\..\..\..\samples\ControlCatalog\MainWindow.xaml")]

2
tests/Avalonia.Direct2D1.RenderTests/Avalonia.Direct2D1.RenderTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Avalonia.RenderTests\**\*.cs" />

2
tests/Avalonia.Direct2D1.UnitTests/Avalonia.Direct2D1.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

2
tests/Avalonia.Input.UnitTests/Avalonia.Input.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

31
tests/Avalonia.Input.UnitTests/KeyGestureTests.cs

@ -1,19 +1,31 @@
using System.Collections.Generic;
using System.Collections.Generic;
using Xunit;
namespace Avalonia.Input.UnitTests
{
public class KeyGestureTests
{
public static readonly IEnumerable<object[]> SampleData = new object[][]
public static readonly IEnumerable<object[]> ParseData = new object[][]
{
new object[]{"Ctrl+A", new KeyGesture(Key.A, InputModifiers.Control)},
new object[]{" \tShift\t+Alt +B", new KeyGesture(Key.B, InputModifiers.Shift | InputModifiers.Alt) },
new object[]{"Control++", new KeyGesture(Key.OemPlus, InputModifiers.Control) }
new object[]{"Ctrl+A", new KeyGesture(Key.A, KeyModifiers.Control)},
new object[]{" \tShift\t+Alt +B", new KeyGesture(Key.B, KeyModifiers.Shift | KeyModifiers.Alt) },
new object[]{"Control++", new KeyGesture(Key.OemPlus, KeyModifiers.Control) },
new object[]{ "Shift+⌘+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
new object[]{ "Shift+Cmd+A", new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift) },
};
public static readonly IEnumerable<object[]> ToStringData = new object[][]
{
new object[]{new KeyGesture(Key.A), "A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control), "Ctrl+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Shift), "Ctrl+Shift+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Alt | KeyModifiers.Shift), "Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Control | KeyModifiers.Alt | KeyModifiers.Shift), "Ctrl+Shift+Alt+A"},
new object[]{new KeyGesture(Key.A, KeyModifiers.Meta | KeyModifiers.Shift), "Shift+Cmd+A"},
};
[Theory]
[MemberData(nameof(SampleData))]
[MemberData(nameof(ParseData))]
public void Key_Gesture_Is_Able_To_Parse_Sample_Data(string text, KeyGesture gesture)
{
Assert.Equal(gesture, KeyGesture.Parse(text));
@ -32,5 +44,12 @@ namespace Avalonia.Input.UnitTests
Key = pressedKey
}));
}
[Theory]
[MemberData(nameof(ToStringData))]
public void ToString_Produces_Correct_Results(KeyGesture gesture, string expected)
{
Assert.Equal(expected, gesture.ToString());
}
}
}

3
tests/Avalonia.Interactivity.UnitTests/Avalonia.Interactivity.UnitTests.csproj

@ -1,7 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
<LangVersion>latest</LangVersion>

25
tests/Avalonia.Interactivity.UnitTests/InteractiveTests.cs

@ -4,7 +4,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Interactivity;
using Avalonia.VisualTree;
using Xunit;
@ -358,6 +357,29 @@ namespace Avalonia.Interactivity.UnitTests
Assert.Equal(1, called);
}
[Fact]
public void Removing_Control_In_Handler_Should_Not_Stop_Event()
{
// Issue #3176
var ev = new RoutedEvent("test", RoutingStrategies.Bubble, typeof(RoutedEventArgs), typeof(TestInteractive));
var invoked = new List<string>();
EventHandler<RoutedEventArgs> handler = (s, e) => invoked.Add(((TestInteractive)s).Name);
var parent = CreateTree(ev, handler, RoutingStrategies.Bubble | RoutingStrategies.Tunnel);
var target = (IInteractive)parent.GetVisualChildren().Single();
EventHandler<RoutedEventArgs> removeHandler = (s, e) =>
{
parent.Children = Array.Empty<IVisual>();
};
target.AddHandler(ev, removeHandler);
var args = new RoutedEventArgs(ev, target);
target.RaiseEvent(args);
Assert.Equal(new[] { "3", "2b", "1" }, invoked);
}
private TestInteractive CreateTree(
RoutedEvent ev,
EventHandler<RoutedEventArgs> handler,
@ -414,6 +436,7 @@ namespace Avalonia.Interactivity.UnitTests
set
{
VisualChildren.Clear();
VisualChildren.AddRange(value.Cast<Visual>());
}
}

2
tests/Avalonia.Layout.UnitTests/Avalonia.Layout.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<OutputType>Library</OutputType>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />

2
tests/Avalonia.Markup.UnitTests/Avalonia.Markup.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

2
tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

2
tests/Avalonia.ReactiveUI.UnitTests/Avalonia.ReactiveUI.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netcoreapp2.0</TargetFramework>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

2
tests/Avalonia.Skia.RenderTests/Avalonia.Skia.RenderTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
<DefineConstants>AVALONIA_SKIA;AVALONIA_SKIA_SKIP_FAIL</DefineConstants>
</PropertyGroup>
<ItemGroup>

2
tests/Avalonia.Skia.UnitTests/Avalonia.Skia.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1</TargetFrameworks>
</PropertyGroup>
<Import Project="..\..\build\UnitTests.NetCore.targets" />
<Import Project="..\..\build\Moq.props" />

17
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@ -406,9 +406,13 @@ namespace Avalonia.Skia.UnitTests
}
[Theory]
[InlineData("abcde\r\n")]
[InlineData("abcde\n\r")]
public void Should_Break_With_BreakChar_Pair(string text)
[InlineData("abcde\r\n", 7)] // Carriage Return + Line Feed
[InlineData("abcde\n\r", 7)] // This isn't valid but we somehow have to support it.
[InlineData("abcde\u000A", 6)] // Line Feed
[InlineData("abcde\u000B", 6)] // Vertical Tab
[InlineData("abcde\u000C", 6)] // Form Feed
[InlineData("abcde\u000D", 6)] // Carriage Return
public void Should_Break_With_BreakChar(string text, int expectedLength)
{
using (Start())
{
@ -422,11 +426,14 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal(1, layout.TextLines[0].TextRuns.Count);
Assert.Equal(7, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length);
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]);
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
if(expectedLength == 7)
{
Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]);
}
}
}

2
tests/Avalonia.Styling.UnitTests/Avalonia.Styling.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<NoWarn>CS0067</NoWarn>
<IsTestProject>true</IsTestProject>

2
tests/Avalonia.UnitTests/MouseTestHelper.cs

@ -56,7 +56,7 @@ namespace Avalonia.UnitTests
{
_pressedButton = mouseButton;
_pointer.Capture((IInputElement)target);
target.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
source.RaiseEvent(new PointerPressedEventArgs(source, _pointer, (IVisual)source, position, Timestamp(), props,
GetModifiers(modifiers), clickCount));
}
}

2
tests/Avalonia.Visuals.UnitTests/Avalonia.Visuals.UnitTests.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk" ToolsVersion="15.0">
<PropertyGroup>
<TargetFrameworks>netcoreapp2.0;net47</TargetFrameworks>
<TargetFrameworks>netcoreapp3.1;net47</TargetFrameworks>
<OutputType>Library</OutputType>
<IsTestProject>true</IsTestProject>
</PropertyGroup>

Loading…
Cancel
Save